Skip to content

feat(notifications): in-app update notifications with release notes#1166

Merged
jamcalli merged 11 commits into
jamcalli:developfrom
bommerts:feature/update-notify-v2
Jun 1, 2026
Merged

feat(notifications): in-app update notifications with release notes#1166
jamcalli merged 11 commits into
jamcalli:developfrom
bommerts:feature/update-notify-v2

Conversation

@bommerts
Copy link
Copy Markdown

@bommerts bommerts commented Apr 30, 2026

Adds an opt-in toggle so Pulsarr sends a one-time Discord webhook + Apprise system notification when a new GitHub release is detected. Replaces the old browser-side fetch with a server-side hourly cron + cached backend route, and surfaces release notes inline via a popover next to the version label.

This is the notifier slice of #1153 only — no auto-installer.

Server

  • New UpdateCheckService (cached, in-flight refresh dedup, semver normalisation, GitHub fetch with timeout / rate-limit / error handling)
  • New update-check plugin: hourly cron, baseline-on-enable, single-flight notification dispatch, watermark-advance-after-attempt to prevent retry storms when a webhook URL is permanently broken
  • New GET /v1/system/update-status route returning the cached payload
  • New sendUpdateAvailableNotification on AppriseService + sendUpdateAvailable orchestrator + facade method on NotificationService
  • New createUpdateAvailableNotificationHtml (Apprise) and createUpdateAvailableEmbed (Discord) templates — dedicated, NOT routed through the approval-card system template
  • notifyOnUpdate added to Config / ConfigFull / ConfigUpdate schemas. lastNotifiedVersion is internal-only — dedicated DB accessors that bypass ALLOWED_COLUMNS. Re-enabling notifyOnUpdate resets the watermark so users only get notified for releases newer than the moment they enabled the toggle.
  • Migration 090_20260430_add_update_notify_columns.ts adds notifyOnUpdate (boolean default false) and lastNotifiedVersion (nullable string) columns

Client

  • useVersionCheck rewritten to call /v1/system/update-status; no more direct GitHub fetches from the browser
  • VersionDisplay replaces the tooltip with a popover showing release name, truncated body, and a "View on GitHub" link
  • New "Notify me when a Pulsarr update is available" Switch in general notification settings

Tested

  • 1418 / 1472 vitest pass (the failing files are unrelated network-dependent integration tests for tmdb.service and plex-label-sync)
  • tsc --noEmit server + client clean, biome clean
  • End-to-end on a real Windows native install (Pulsarr v0.15.3 build comparing against GitHub's v0.15.4):
    • Migration 090 applied cleanly
    • Plugin loaded; update-check cron registered in schedules table
    • Boot-time refresh succeeded; baseline-on-enable correctly suppressed the first observation as designed
    • With watermark forced to v0.15.3, dispatch fired, watermark advanced to v0.15.4 BEFORE delivery, Apprise → Telegram delivery succeeded with the rich card

Note on <hr> for Apprise → Telegram

While testing Apprise → Telegram delivery, found that Telegram's HTML parser rejects <hr> tags, returning Bad Request: can't parse entities: Unsupported start tag "hr". This is a pre-existing issue in the shared htmlWrapper (apprise-html.ts:41) that affects all Pulsarr Apprise → Telegram notifications (delete-sync, media, etc.). To keep PR scope tight, this PR addresses it ONLY for createUpdateAvailableNotificationHtml by inlining the wrapper styling without the <hr>. Other notification templates are unaffected — fixing them would be a separate PR.

Refs

Summary by CodeRabbit

  • New Features
    • New database fields to track update notification preference and last-notified version.
    • Opt-in "Update Notifications" setting (All/Discord/Apprise/Webhook/DM/None) in General settings.
    • Rich version popover with release title, published date, HTML release notes, and "View release on GitHub" link.
    • Server-backed update-status endpoint, background update checker with hourly scheduling and cached status.
    • Out-of-app alert delivery: orchestrated sends to Discord webhook, Discord DM, and Apprise with persisted dedupe.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds server-side update checking and caching, a /v1/system/update-status API, DB migration and helpers for notify/dedup state, an update-check Fastify plugin that schedules refreshes and conditionally dispatches notifications, notification orchestration and templates (Discord/Apprise), NotificationService plumbing, and client hook/UI and settings to consume/control notifications.

Changes

Update-check & notification feature

Layer / File(s) Summary
DB migration & config schema/types
migrations/migrations/090_20260430_add_update_notify_columns.ts, src/schemas/config/config.schema.ts, src/types/config.types.ts
Adds migration to add notifyOnUpdate and lastNotifiedVersion; adds UpdateNotifyEnum and notifyOnUpdate to config schemas and Config type.
Database methods & types
src/services/database/methods/config.ts, src/services/database/types/config-methods.ts
Persist notifyOnUpdate, include it in allowed update columns, and add getLastNotifiedVersion / setLastNotifiedVersion with type augmentation.
UpdateCheck service
src/services/update-check.service.ts
New UpdateCheckService that normalizes versions, fetches GitHub latest release, renders release HTML, caches status (ok/pending/rate_limited/error), and serializes concurrent refreshes.
API Route & Schema
src/routes/v1/system/update-status.ts, src/schemas/system/update-status.schema.ts
Adds GET /v1/system/update-status with OpenAPI/Zod response schema exposing cached update status and release metadata; handler logs and returns 500 on read failures.
Fastify Plugin: update-check
src/plugins/custom/update-check.ts
New plugin that decorates fastify.updateCheck, triggers refresh onReady, ensures hourly DB cron schedule, and conditionally dispatches notifications using persisted watermark and semver checks.
Notification Orchestration
src/services/notifications/orchestration/update-available.ts, src/services/notifications/orchestration/index.ts
Adds orchestration sendUpdateAvailable, data contracts, channel selection logic, and helpers for Discord webhook, Discord DM, and Apprise dispatch, aggregating results.
Notification Templates
src/services/notifications/templates/apprise-html.ts, src/services/notifications/templates/discord-embeds.ts
Adds Apprise HTML/text and Discord embed builders including truncation, published date handling, and layout for release notes.
Apprise channel & service wiring
src/services/notifications/channels/apprise.ts, src/services/notifications/channels/apprise.service.ts
Adds channel-level sendUpdateAvailableNotification that resolves system target, builds templated payload, and sends via Apprise; service method delegates into channel helper.
NotificationService plumbing
src/services/notification.service.ts
Adds sendUpdateAvailableNotification wrapper that forwards release and runtime deps into orchestration and returns aggregated delivery result.
Discord DM helpers & bot
src/services/notifications/channels/discord-dm.ts, src/services/notifications/discord-bot/bot.service.ts
Adds sendDirectMessageEmbed helper, refactors sendDirectMessage to delegate, and exposes sendDirectMessageEmbed on DiscordBotService.
Discord payload types
src/types/discord.types.ts
Adds DiscordAllowedMentions and optional allowed_mentions on DiscordWebhookPayload.
Frontend: Hook, UI & Settings
src/client/hooks/useVersionCheck.tsx, src/client/components/ui/version-display.tsx, src/client/features/notifications/components/general/general-settings-form.tsx
Refactors useVersionCheck to call server /v1/system/update-status (adds releaseName/bodyHtml/publishedAt, polling cadence change, sessionStorage dedupe); VersionDisplay shows a popover with release details; settings form adds notifyOnUpdate Select persisted to config.

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler as Cron Scheduler
  participant Plugin as update-check Plugin
  participant UpdateCheck as UpdateCheckService
  participant GitHub as GitHub API
  participant DB as Database
  participant NotificationSvc as NotificationService
  participant Discord as Discord Webhook
  participant Apprise as Apprise

  Scheduler->>Plugin: trigger cron handler
  Plugin->>UpdateCheck: refresh()
  UpdateCheck->>GitHub: fetch latest release
  GitHub-->>UpdateCheck: release metadata / status
  UpdateCheck->>DB: cache status (lastCheckedAt, latestVersion, metadata, status)
  Plugin->>DB: read `notifyOnUpdate`, read `lastNotifiedVersion`
  alt updateAvailable && notifyOnUpdate && semver.gt(latest, watermark)
    Plugin->>NotificationSvc: sendUpdateAvailableNotification(release)
    NotificationSvc->>Discord: send Discord embed (if configured)
    NotificationSvc->>Apprise: send Apprise notification (if configured)
    Discord-->>NotificationSvc: success/failure
    Apprise-->>NotificationSvc: success/failure
    NotificationSvc-->>Plugin: aggregated result
    Plugin->>DB: setLastNotifiedVersion(latestVersion) after success
  end
Loading
sequenceDiagram
  participant Client as Browser UI
  participant Hook as useVersionCheck
  participant API as GET /v1/system/update-status
  participant UpdateCheck as UpdateCheckService
  participant UI as VersionDisplay

  Client->>Hook: mount
  Hook->>API: fetch cached status
  API->>UpdateCheck: getStatus()
  UpdateCheck-->>API: cached payload
  API-->>Hook: update payload (updateAvailable, latestVersion, metadata)
  Hook-->>UI: deliver data
  alt updateAvailable
    UI->>UI: format date, render notes
    UI->>Client: render popover with release details + GitHub link
  else no update
    UI->>Client: render version label
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • jamcalli/Pulsarr#1188: Related — aligns with Apprise readiness/probe behavior used by the update-check plugin.
  • jamcalli/Pulsarr#818: Related — earlier NotificationService refactor that this PR extends.
  • jamcalli/Pulsarr#820: Related — additional NotificationService work connected to update notifications.

Suggested labels

feature

Suggested reviewers

  • jamcalli
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(notifications): in-app update notifications with release notes' directly and accurately summarizes the main changes—adding in-app update notifications with release notes display.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/hooks/useVersionCheck.tsx (1)

52-87: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the per-mount toast gate.

toastShownRef never resets, so this hook will only ever toast once per component lifetime. After the first update notification, later releases in the same tab are silently skipped even though sessionStorage already dedupes per version.

🔧 Proposed fix
-import { useEffect, useRef } from 'react'
+import { useEffect } from 'react'
@@
 export function useVersionCheck(): VersionCheckResult {
-  const toastShownRef = useRef(false)
-
   const {
     data: status,
@@
     if (
       versionInfo.updateAvailable &&
       versionInfo.latestVersion &&
-      !toastShownRef.current &&
       notifiedVersion !== versionInfo.latestVersion
     ) {
-      toastShownRef.current = true
       sessionStorage.setItem(VERSION_TOAST_KEY, versionInfo.latestVersion)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/hooks/useVersionCheck.tsx` around lines 52 - 87, The hook
currently uses toastShownRef to block further toasts for the component lifetime,
causing newer releases in the same tab to be skipped; remove the per-mount gate
by deleting the toastShownRef declaration and all uses of toastShownRef.current
in the useEffect condition and body, and rely solely on sessionStorage
(VERSION_TOAST_KEY) + versionInfo.latestVersion to dedupe toasts; keep the
existing sessionStorage.setItem(VERSION_TOAST_KEY, versionInfo.latestVersion)
and the rest of the notification logic inside the useEffect so each new version
triggers a toast even within the same mounted component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/schemas/system/update-status.schema.ts`:
- Around line 43-45: Update the 500-response schema for the update-status route
so it matches Fastify Sensible's HttpError shape instead of only { message }:
replace or refactor UpdateStatusErrorSchema to reuse the shared ErrorSchema (or
mirror its fields { statusCode, code, error, message }) and ensure the route's
response schema references that ErrorSchema for the 500 status; update any
imports to use ErrorSchema and update the responses map for the route that
references UpdateStatusErrorSchema.

In `@src/services/notifications/templates/discord-embeds.ts`:
- Around line 338-390: The webhook payload currently allows raw release notes
(from createUpdateAvailableEmbed via release.releaseBody) which can trigger
pings; fix by adding an explicit allowed_mentions field to the webhook payload
in update-available.ts (set "allowed_mentions": {"parse": []}) or sanitize
release.releaseBody inside createUpdateAvailableEmbed by escaping or stripping
occurrences of `@everyone`, `@here` and user/role mention patterns (e.g.
/@everyone|@here|<@!?\d+>|<@&\d+>/) before building description; reference
createUpdateAvailableEmbed, release.releaseBody, and the webhook payload in
update-available.ts and ensure the final payload contains allowed_mentions to
suppress mentions.

---

Outside diff comments:
In `@src/client/hooks/useVersionCheck.tsx`:
- Around line 52-87: The hook currently uses toastShownRef to block further
toasts for the component lifetime, causing newer releases in the same tab to be
skipped; remove the per-mount gate by deleting the toastShownRef declaration and
all uses of toastShownRef.current in the useEffect condition and body, and rely
solely on sessionStorage (VERSION_TOAST_KEY) + versionInfo.latestVersion to
dedupe toasts; keep the existing sessionStorage.setItem(VERSION_TOAST_KEY,
versionInfo.latestVersion) and the rest of the notification logic inside the
useEffect so each new version triggers a toast even within the same mounted
component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: bd4b85f0-d75b-47d0-93df-27ac3a214e04

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5ff66 and ab9f0fc.

📒 Files selected for processing (19)
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
  • src/client/components/ui/version-display.tsx
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/client/hooks/useVersionCheck.tsx
  • src/plugins/custom/update-check.ts
  • src/routes/v1/system/update-status.ts
  • src/schemas/config/config.schema.ts
  • src/schemas/system/update-status.schema.ts
  • src/services/database/methods/config.ts
  • src/services/database/types/config-methods.ts
  • src/services/notification.service.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/services/notifications/orchestration/index.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/notifications/templates/apprise-html.ts
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts

Comment thread src/schemas/system/update-status.schema.ts Outdated
Comment thread src/services/notifications/templates/discord-embeds.ts
@bommerts
Copy link
Copy Markdown
Author

bommerts commented May 1, 2026

Force-pushed 2a746dcdd (was ab9f0fc6f) to address three findings from CodeRabbit:

  • useVersionCheck: removed the toastShownRef per-mount gate that would have blocked toasts for every release after the first one in a long-lived tab — sessionStorage already dedupes per version
  • update-status.schema.ts: re-export the shared ErrorSchema instead of { message }, so the 500 response matches the Fastify Sensible HttpError shape ({ statusCode, code, error, message }) and passes response validation. Mirrors the metadata.schema.ts pattern
  • update-available.ts (Discord): added allowed_mentions: { parse: [] } to the webhook payload so an @everyone/@here/role mention sneaking into a GitHub release body can't ping the admin channel. Required a small additive change to DiscordWebhookPayload (new optional allowed_mentions field)

Single commit; typecheck + biome clean.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/notifications/templates/apprise-html.ts`:
- Around line 412-418: The current stripMarkdown replacements in apprise-html.ts
are too greedy and eat underscores/asterisks inside words or across lines;
update the emphasis regexes in the .replace chain (the patterns handling **, *,
__, _) to only match markers that are not adjacent to word characters and not
spanning newlines (use lookarounds like (?<!\w) and (?!\w) and forbid \n inside
the capture), and ensure the list-bullet replacement (.replace(/^\s*[-*+]\s+/gm,
'• ')) runs before single-asterisk/single-underscore emphasis so item lines like
"* item" are not consumed by the emphasis regexes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b6470d14-c267-40d5-9ff7-1483dfed90c0

📥 Commits

Reviewing files that changed from the base of the PR and between ab9f0fc and 2a746dc.

📒 Files selected for processing (20)
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
  • src/client/components/ui/version-display.tsx
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/client/hooks/useVersionCheck.tsx
  • src/plugins/custom/update-check.ts
  • src/routes/v1/system/update-status.ts
  • src/schemas/config/config.schema.ts
  • src/schemas/system/update-status.schema.ts
  • src/services/database/methods/config.ts
  • src/services/database/types/config-methods.ts
  • src/services/notification.service.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/services/notifications/orchestration/index.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/notifications/templates/apprise-html.ts
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts
  • src/types/discord.types.ts
✅ Files skipped from review due to trivial changes (3)
  • src/services/notifications/orchestration/index.ts
  • src/types/discord.types.ts
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/services/database/types/config-methods.ts
  • src/schemas/config/config.schema.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/plugins/custom/update-check.ts
  • src/client/components/ui/version-display.tsx
  • src/services/database/methods/config.ts

Comment thread src/services/notifications/templates/apprise-html.ts Outdated
Adds an opt-in toggle so Pulsarr sends a one-time Discord webhook + Apprise system notification when a new GitHub release is detected. Replaces the old browser-side fetch with a server-side hourly cron + cached backend route, and surfaces release notes inline via a popover next to the version label.

Server

- New 'UpdateCheckService' (cached, in-flight refresh dedup, semver normalisation, GitHub fetch with timeout/rate-limit handling)

- New 'update-check' plugin: hourly cron, baseline-on-enable, single-flight notification dispatch, watermark-advance-after-attempt to prevent retry storms

- New GET '/v1/system/update-status' route returning the cached payload

- New 'sendUpdateAvailableNotification' on AppriseService + 'sendUpdateAvailable' orchestrator + facade method on NotificationService

- New 'createUpdateAvailableNotificationHtml' (Apprise) and 'createUpdateAvailableEmbed' (Discord) templates - dedicated, NOT routed through the approval-card system template

- 'notifyOnUpdate' added to Config / ConfigFull / ConfigUpdate schemas. 'lastNotifiedVersion' is internal-only - dedicated DB accessors that bypass ALLOWED_COLUMNS. Re-enabling 'notifyOnUpdate' resets the watermark so users only get notified for releases newer than the moment they enabled the toggle.

- Migration 090 adds 'notifyOnUpdate' (boolean default false) and 'lastNotifiedVersion' (nullable string) columns

Client

- 'useVersionCheck' rewritten to call '/v1/system/update-status'; no more direct GitHub fetches from the browser

- 'VersionDisplay' replaces the tooltip with a popover showing release name, truncated body, and a 'View on GitHub' link

- New 'Notify me when a Pulsarr update is available' Switch in general notification settings

Supersedes jamcalli#1154 and jamcalli#1155. Refs jamcalli#1153.
@bommerts bommerts force-pushed the feature/update-notify-v2 branch from 2a746dc to 987912b Compare May 1, 2026 10:28
@bommerts
Copy link
Copy Markdown
Author

bommerts commented May 1, 2026

Force-pushed 987912b5d (was 2a746dcdd) to address CodeRabbit's latest finding on stripMarkdown.

The previous regex was too greedy on single-marker emphasis and would have mangled common GitHub release-note content:

  • notify_on_updatenotifyonupdate (single-_ rule consuming inside snake_case)
  • * item one\n* item two → collapsed across lines (single-* rule eating bullet lists)

Fixed by reordering bullet conversion before single-marker emphasis and making the single-marker patterns boundary-aware ((^|[^\w])...(?=[^\w]|$)) and line-scoped ([^\n*] / [^\n_] instead of [^*] / [^_]).

Verified against 9 cases: snake_case, mixed-case identifiers, bullet lists, mid-sentence emphasis, bold across newlines, and a realistic release-notes excerpt — all pass.

stripMarkdown was introduced in this PR (not pre-existing on develop), so this is in-scope.

Single commit; typecheck + biome clean.

@jamcalli
Copy link
Copy Markdown
Owner

jamcalli commented May 5, 2026

@bommerts

I'll have to do some work to get this in cleanly.

Some of your assumptions are misaligned with the behavior and I will fix upstream before merging. For example, the <hr> issue with telegram notifications are already handled, but apprise isn't actually ready until 5 seconds after the plugin initializes. This has never surfaced as nothing has been sending notifications in an onReady hook, before the apprise caches have properly gated content types by channel. I'll rework the apprise plugin to not actually be ready until after the caches are set etc.

Will also need to add a markdown formatter / renderer. Otherwise there is risk that future maintainers could use syntax that's perfectly allowed as per standard md conventions, but causes bugs within the Pulsarr UI / notificaitons.

I'll get this looked at sometime this week.

Also you should figure out why your tests aren't passing. They all work in CI and within my dev environment.

@bommerts
Copy link
Copy Markdown
Author

bommerts commented May 5, 2026

Thanks @jamcalli — appreciated, and happy to defer.

The apprise-not-actually-ready-for-5s detail is news to me; that fully explains the <hr> behavior I was attributing elsewhere, and a proper markdown renderer is clearly the right call over my regex band-aid in stripMarkdown. I'll stop pushing further fixes to this PR so I don't churn the area while you're reworking it.

On the tests: CI on 987912b5d came back green for me (the Build and Test job ran at 06:23 UTC and passed all suites), and my diff against develop doesn't touch test/, so I'm not sure which failure(s) you saw. If you can share a test name or a snippet of output I'll dig in immediately; otherwise I'll run the full bun run test locally and report back.

Happy to iterate on whatever lands after your rework if useful, and no rush from my end.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@jamcalli
Copy link
Copy Markdown
Owner

@bommerts

I finally had some time to go through this and make some corrects / adjustments.

Let me know your thoughts.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/services/notifications/templates/apprise-html.ts (1)

307-314: Add a trust-boundary comment (and optional sanitizer) for releaseBodyHtml injection

  • src/services/update-check.service.ts generates releaseBodyHtml by calling GitHub’s POST https://api.github.com/markdown and returns response.text() with no additional sanitization in this repo.
  • src/services/notifications/templates/apprise-html.ts then injects ${release.releaseBodyHtml} directly into the Apprise HTML.
  • GitHub’s Markdown-to-HTML output is sanitized (e.g., it strips dangerous/script content), but the template still relies on that guarantee—add a brief comment documenting the trust boundary and consider a lightweight allowlist/sanitization step as defense-in-depth.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/notifications/templates/apprise-html.ts` around lines 307 - 314,
The template injects release.releaseBodyHtml directly into the HTML (in the
notesCard construction in apprise-html.ts), so add a trust-boundary comment next
to that interpolation describing the provenance (generated via GitHub POST
/markdown and currently returned as response.text()) and the assumption that
GitHub sanitizes output; also add an optional defense-in-depth sanitization step
before interpolation (e.g., call a small allowlist sanitizer or DOMPurify-like
function such as sanitizeHtmlIfNeeded(release.releaseBodyHtml)) and reference
that sanitizer in the comment so future reviewers know the mitigation and where
to change it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugins/custom/update-check.ts`:
- Around line 40-41: The code advances the watermark by calling
fastify.db.setLastNotifiedVersion(targetVersion) where targetVersion =
status.latestVersion before attempting delivery, which prevents retries if
delivery (the logic that sets sent) fails; move the call to
fastify.db.setLastNotifiedVersion(targetVersion) so it runs only after
successful delivery (i.e., when the variable sent is truthy in the delivery
block), ensuring the watermark is updated only on success and failed
notifications can be retried on the next cron run.

---

Nitpick comments:
In `@src/services/notifications/templates/apprise-html.ts`:
- Around line 307-314: The template injects release.releaseBodyHtml directly
into the HTML (in the notesCard construction in apprise-html.ts), so add a
trust-boundary comment next to that interpolation describing the provenance
(generated via GitHub POST /markdown and currently returned as response.text())
and the assumption that GitHub sanitizes output; also add an optional
defense-in-depth sanitization step before interpolation (e.g., call a small
allowlist sanitizer or DOMPurify-like function such as
sanitizeHtmlIfNeeded(release.releaseBodyHtml)) and reference that sanitizer in
the comment so future reviewers know the mitigation and where to change it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ee0b7706-3873-4a2d-b55a-8ba70eadf742

📥 Commits

Reviewing files that changed from the base of the PR and between 3550a88 and f2fadd5.

📒 Files selected for processing (20)
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
  • src/client/components/ui/version-display.tsx
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/client/hooks/useVersionCheck.tsx
  • src/plugins/custom/update-check.ts
  • src/routes/v1/system/update-status.ts
  • src/schemas/config/config.schema.ts
  • src/schemas/system/update-status.schema.ts
  • src/services/database/methods/config.ts
  • src/services/database/types/config-methods.ts
  • src/services/notification.service.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/services/notifications/channels/discord-dm.ts
  • src/services/notifications/discord-bot/bot.service.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/notifications/templates/apprise-html.ts
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts
💤 Files with no reviewable changes (2)
  • src/services/database/types/config-methods.ts
  • src/routes/v1/system/update-status.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/client/hooks/useVersionCheck.tsx
  • src/services/notifications/templates/discord-embeds.ts
  • src/schemas/system/update-status.schema.ts

Comment thread src/plugins/custom/update-check.ts Outdated
@jamcalli jamcalli force-pushed the feature/update-notify-v2 branch from f2fadd5 to 53afbe8 Compare May 29, 2026 00:09
@bommerts
Copy link
Copy Markdown
Author

Thanks @jamcalli — this looks great. The apprise.whenReady() refactor and the GitHub /markdown rendering are both much better than what I had, and the multi-channel expansion is a nice bonus. Happy with the direction.

I have a working install running on a server here that I've been using to validate the feature end-to-end; happy to rebuild against 53afbe8 and confirm the rework behaves as expected if useful, otherwise no concerns from my side.

@jamcalli
Copy link
Copy Markdown
Owner

jamcalli commented Jun 1, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/hooks/useVersionCheck.tsx`:
- Around line 41-47: The refetch interval needs to be dynamic: in the
useAppQuery call that uses versionCheckKeys.status() and fetchUpdateStatus,
replace the fixed refetchInterval: FIFTEEN_MINUTES with a function that checks
the returned data (e.g., data?.status) and returns a short cadence (e.g., a few
seconds) while status === 'pending', and returns FIFTEEN_MINUTES once a
non-'pending' response is seen; implement this by changing refetchInterval to a
function (or callback) that inspects the query data and returns the appropriate
milliseconds, ensuring retry remains false and other options (staleTime) are
unchanged.

In `@src/services/notifications/templates/apprise-html.ts`:
- Around line 307-314: Add a short inline comment next to the
release.releaseBodyHtml interpolation in the notesCard template explaining that
the HTML is trusted because it is rendered via GitHub's markdown API
(renderReleaseBody → response.text()) and therefore not escaped here; also note
that if the rendering source changes or is replaced, the code must instead
escape/sanitize release.releaseBodyHtml (e.g., via escapeHtml or a sanitizer)
before injecting into the Apprise HTML.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ec69bb02-e000-4c9e-b883-730b9cc277b9

📥 Commits

Reviewing files that changed from the base of the PR and between 018ccfc and 98b9201.

⛔ Files ignored due to path filters (5)
  • docs/docs/notifications/apprise.md is excluded by !docs/**
  • docs/docs/notifications/discord.md is excluded by !docs/**
  • docs/docs/notifications/general-settings.md is excluded by !docs/**
  • docs/docs/notifications/native-webhooks.md is excluded by !docs/**
  • docs/docs/notifications/plex-mobile.md is excluded by !docs/**
📒 Files selected for processing (22)
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
  • src/client/components/ui/version-display.tsx
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/client/hooks/useVersionCheck.tsx
  • src/plugins/custom/update-check.ts
  • src/routes/v1/system/update-status.ts
  • src/schemas/config/config.schema.ts
  • src/schemas/system/update-status.schema.ts
  • src/services/database/methods/config.ts
  • src/services/database/types/config-methods.ts
  • src/services/notification.service.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/services/notifications/channels/discord-dm.ts
  • src/services/notifications/discord-bot/bot.service.ts
  • src/services/notifications/orchestration/index.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/notifications/templates/apprise-html.ts
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts
  • src/types/discord.types.ts

Comment thread src/client/hooks/useVersionCheck.tsx
Comment thread src/services/notifications/templates/apprise-html.ts
@jamcalli
Copy link
Copy Markdown
Owner

jamcalli commented Jun 1, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/database/methods/config.ts (1)

586-609: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset the dedupe watermark when notifyOnUpdate is re-enabled.

The PR contract says re-enabling this setting should clear the watermark, but updateConfig() only writes the new mode and setLastNotifiedVersion() cannot store null. As written, a user who already received the notification for the current latest release, turns notifications off, then turns them back on before upgrading will stay suppressed by the stale lastNotifiedVersion.

💡 Suggested fix
 export async function updateConfig(
   this: DatabaseService,
   config: Partial<Config>,
 ): Promise<boolean> {
   try {
+    const current =
+      config.notifyOnUpdate !== undefined
+        ? await this.knex('configs').where({ id: 1 }).first('notifyOnUpdate')
+        : undefined
+
     const updateData: Record<string, unknown> = {
       updated_at: this.timestamp,
     }

     for (const [key, value] of Object.entries(config)) {
       if (value !== undefined && ALLOWED_COLUMNS.has(key)) {
         if (JSON_COLUMNS.has(key)) {
           updateData[key] =
             value !== undefined && value !== null ? JSON.stringify(value) : null
         } else {
           updateData[key] = value
         }
       }
     }
+
+    if (
+      config.notifyOnUpdate !== undefined &&
+      config.notifyOnUpdate !== 'none' &&
+      (current?.notifyOnUpdate ?? 'none') === 'none'
+    ) {
+      updateData.lastNotifiedVersion = null
+    }

     const updated = await this.knex('configs')
       .where({ id: 1 })
       .update(updateData)
     return updated > 0
@@
 export async function setLastNotifiedVersion(
   this: DatabaseService,
-  version: string,
+  version: string | null,
 ): Promise<boolean> {

Also applies to: 643-656

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/database/methods/config.ts` around lines 586 - 609, When
re-enabling notifyOnUpdate you must clear the dedupe watermark; modify
updateConfig so if the incoming config contains notifyOnUpdate === true and the
current service config (this.config.notifyOnUpdate) is false, add
last_notified_version: null into updateData before performing the knex update
(bypass setLastNotifiedVersion since it cannot store null). Update the logic
inside updateConfig (refer to the updateConfig function and the updateData
object) to detect this transition and set updateData['last_notified_version'] =
null so the DB row is cleared when notifications are re-enabled.
🧹 Nitpick comments (1)
src/client/components/ui/version-display.tsx (1)

63-68: 💤 Low value

Consider sanitizing releaseBodyHtml before rendering with dangerouslySetInnerHTML

src/client/components/ui/version-display.tsx injects releaseBodyHtml directly; there’s no existing HTML-sanitizer dependency in the project (e.g., DOMPurify isn’t present). On the server, releaseBodyHtml is produced by forwarding GitHub’s /markdown output without any additional local sanitization, so the UI relies on GitHub always emitting safe HTML. Add explicit sanitization (preferably on the server so both notifications and the UI share it) or document the safety assumption.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/components/ui/version-display.tsx` around lines 63 - 68, The
releaseBodyHtml is being injected via dangerouslySetInnerHTML in the
VersionDisplay component without sanitization; update the pipeline to sanitize
HTML server-side where the GitHub /markdown output is forwarded (the function
that fetches/forwards release notes, e.g., getReleaseBodyHtml or
fetchReleaseNotes) using a well-known sanitizer (DOMPurify server build,
sanitize-html, or similar) and return a sanitized string, then change the
VersionDisplay consumer to expect and render sanitized releaseBodyHtml (still
using dangerouslySetInnerHTML but only with the sanitized value) or, if you
prefer client-side hardening, run the same sanitizer in the component before
setting dangerouslySetInnerHTML; ensure tests or a comment document the chosen
sanitizer and that releaseBodyHtml passed to VersionDisplay is sanitized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/notifications/orchestration/update-available.ts`:
- Around line 96-102: The db lookup for the primary Discord user
(deps.db.getPrimaryUser()) must be moved inside the try block in sendDiscordDM
so any thrown error is caught and the DM channel degrades to false instead of
bubbling up and aborting sendUpdateAvailable's Promise.all; update sendDiscordDM
to perform await deps.db.getPrimaryUser() inside its try, handle missing or
invalid primaryUser?.discord_id the same way as now, and return false on any
exception so webhook/Apprise paths can still run.

In `@src/services/notifications/templates/apprise-html.ts`:
- Around line 324-330: The plain-text fallback variable textBody currently omits
the release notes; append release.releaseBody (truncated if needed) to textBody
so Apprise-only text targets receive the notes too. Locate the textBody
construction (variable textBody) and after adding Published (or before "View
release"), append a sanitized/truncated version of release.releaseBody (e.g.,
strip HTML, limit length) and then continue with the existing "View release" and
signature lines.

---

Outside diff comments:
In `@src/services/database/methods/config.ts`:
- Around line 586-609: When re-enabling notifyOnUpdate you must clear the dedupe
watermark; modify updateConfig so if the incoming config contains notifyOnUpdate
=== true and the current service config (this.config.notifyOnUpdate) is false,
add last_notified_version: null into updateData before performing the knex
update (bypass setLastNotifiedVersion since it cannot store null). Update the
logic inside updateConfig (refer to the updateConfig function and the updateData
object) to detect this transition and set updateData['last_notified_version'] =
null so the DB row is cleared when notifications are re-enabled.

---

Nitpick comments:
In `@src/client/components/ui/version-display.tsx`:
- Around line 63-68: The releaseBodyHtml is being injected via
dangerouslySetInnerHTML in the VersionDisplay component without sanitization;
update the pipeline to sanitize HTML server-side where the GitHub /markdown
output is forwarded (the function that fetches/forwards release notes, e.g.,
getReleaseBodyHtml or fetchReleaseNotes) using a well-known sanitizer (DOMPurify
server build, sanitize-html, or similar) and return a sanitized string, then
change the VersionDisplay consumer to expect and render sanitized
releaseBodyHtml (still using dangerouslySetInnerHTML but only with the sanitized
value) or, if you prefer client-side hardening, run the same sanitizer in the
component before setting dangerouslySetInnerHTML; ensure tests or a comment
document the chosen sanitizer and that releaseBodyHtml passed to VersionDisplay
is sanitized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 871f99e0-42e0-48e1-8b0a-a04050700d7b

📥 Commits

Reviewing files that changed from the base of the PR and between 018ccfc and 74b5eaa.

⛔ Files ignored due to path filters (5)
  • docs/docs/notifications/apprise.md is excluded by !docs/**
  • docs/docs/notifications/discord.md is excluded by !docs/**
  • docs/docs/notifications/general-settings.md is excluded by !docs/**
  • docs/docs/notifications/native-webhooks.md is excluded by !docs/**
  • docs/docs/notifications/plex-mobile.md is excluded by !docs/**
📒 Files selected for processing (22)
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
  • src/client/components/ui/version-display.tsx
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/client/hooks/useVersionCheck.tsx
  • src/plugins/custom/update-check.ts
  • src/routes/v1/system/update-status.ts
  • src/schemas/config/config.schema.ts
  • src/schemas/system/update-status.schema.ts
  • src/services/database/methods/config.ts
  • src/services/database/types/config-methods.ts
  • src/services/notification.service.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/services/notifications/channels/discord-dm.ts
  • src/services/notifications/discord-bot/bot.service.ts
  • src/services/notifications/orchestration/index.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/notifications/templates/apprise-html.ts
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts
  • src/types/discord.types.ts

Comment thread src/services/notifications/orchestration/update-available.ts Outdated
Comment thread src/services/notifications/templates/apprise-html.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/hooks/useVersionCheck.tsx (1)

72-93: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move the session toast watermark into the delayed callback.

Line 72 records the version as notified before the 3-second timer fires. If this hook unmounts before the timeout runs, the cleanup cancels the toast but the session watermark stays set, so the user won't see the notification again in that session.

Suggested fix
     ) {
-      sessionStorage.setItem(VERSION_TOAST_KEY, versionInfo.latestVersion)
-
       const url = versionInfo.releaseUrl
       const timeoutId = setTimeout(() => {
+        sessionStorage.setItem(VERSION_TOAST_KEY, versionInfo.latestVersion)
         toast(
           `A new version (v${versionInfo.latestVersion}) is available. You're running v${versionInfo.currentVersion}.`,
           {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/hooks/useVersionCheck.tsx` around lines 72 - 93, The session
watermark (sessionStorage.setItem(VERSION_TOAST_KEY, versionInfo.latestVersion))
is being set immediately instead of only when the toast actually appears, so
move that call inside the delayed callback used to show the toast (the
setTimeout handler that calls toast) so the watermark is only written if the
timeout fires; keep the existing clearTimeout(timeoutId) in the cleanup to
prevent writing when the hook unmounts early and reference versionInfo,
VERSION_TOAST_KEY, timeoutId, and the toast action when making the change.
♻️ Duplicate comments (1)
src/services/notifications/templates/apprise-html.ts (1)

324-330: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include release notes in the plain-text fallback.

The textBody currently omits release.releaseBody, so Apprise targets that only render plain text lose the release notes entirely. This was flagged in an earlier review and remains unaddressed.

🔧 Suggested fix
+ const releaseNotes = release.releaseBody?.trim()
+
  let textBody = 'Pulsarr update available\n\n'
  textBody += `${displayName}\n`
  textBody += `Current: v${release.currentVersion}\n`
  textBody += `Latest: v${release.latestVersion}\n`
  if (publishedAt) textBody += `Published: ${publishedAt}\n`
+ if (releaseNotes) textBody += `\nRelease Notes:\n${releaseNotes}\n`
  textBody += `\nView release: ${release.releaseUrl}\n`
  textBody += '\n- Pulsarr'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/notifications/templates/apprise-html.ts` around lines 324 - 330,
The plain-text fallback in the Apprise template builds textBody but omits the
release notes; update the code that constructs textBody (variable textBody in
this file) to include release.releaseBody (if present) between the version lines
and the "View release" link so plain-text targets receive the release notes; use
the existing
publishedAt/displayName/release.currentVersion/release.latestVersion/release.releaseUrl
context and ensure you prepend/append appropriate newlines so the
release.releaseBody is readable in the plain-text output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugins/custom/update-check.ts`:
- Around line 49-55: The code logs a successful dispatch even when persisting
the watermark fails; after a successful send (when variable sent is true), call
fastify.db.setLastNotifiedVersion(targetVersion) and check its return value — if
it returns false or throws, treat this as an error: log an error via
fastify.log.error (include targetVersion and the failure) and do not log the
'Update-available notification dispatched' info; only log success when
setLastNotifiedVersion succeeds. Ensure you handle exceptions from
setLastNotifiedVersion (try/catch) and reference the sent, targetVersion,
fastify.db.setLastNotifiedVersion and fastify.log symbols.

In `@src/services/database/methods/config.ts`:
- Around line 624-635: The catch in getLastNotifiedVersion is swallowing DB
errors and returning null (which dispatchNotification treats as "no prior
notification"); instead of returning null on error, log the error with
this.log.error as is and re-throw the caught error so callers can skip dispatch;
modify the catch in getLastNotifiedVersion
(DatabaseService.getLastNotifiedVersion) to throw the original error after
logging rather than returning null.
- Around line 643-657: The setLastNotifiedVersion function currently only
accepts a non-null string which prevents clearing the nullable DB column; change
the signature and any related type augmentation to accept version: string |
null, update the update payload in setLastNotifiedVersion to write
lastNotifiedVersion: version (allowing null) and keep updated_at behavior, and
adjust its callers (notably update-check.ts) to call
setLastNotifiedVersion(null) when you need to clear the watermark so the dedupe
state can be reset.

---

Outside diff comments:
In `@src/client/hooks/useVersionCheck.tsx`:
- Around line 72-93: The session watermark
(sessionStorage.setItem(VERSION_TOAST_KEY, versionInfo.latestVersion)) is being
set immediately instead of only when the toast actually appears, so move that
call inside the delayed callback used to show the toast (the setTimeout handler
that calls toast) so the watermark is only written if the timeout fires; keep
the existing clearTimeout(timeoutId) in the cleanup to prevent writing when the
hook unmounts early and reference versionInfo, VERSION_TOAST_KEY, timeoutId, and
the toast action when making the change.

---

Duplicate comments:
In `@src/services/notifications/templates/apprise-html.ts`:
- Around line 324-330: The plain-text fallback in the Apprise template builds
textBody but omits the release notes; update the code that constructs textBody
(variable textBody in this file) to include release.releaseBody (if present)
between the version lines and the "View release" link so plain-text targets
receive the release notes; use the existing
publishedAt/displayName/release.currentVersion/release.latestVersion/release.releaseUrl
context and ensure you prepend/append appropriate newlines so the
release.releaseBody is readable in the plain-text output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 10b821a3-19ff-4b2e-ab54-c279b64dfac8

📥 Commits

Reviewing files that changed from the base of the PR and between 018ccfc and 74b5eaa.

⛔ Files ignored due to path filters (5)
  • docs/docs/notifications/apprise.md is excluded by !docs/**
  • docs/docs/notifications/discord.md is excluded by !docs/**
  • docs/docs/notifications/general-settings.md is excluded by !docs/**
  • docs/docs/notifications/native-webhooks.md is excluded by !docs/**
  • docs/docs/notifications/plex-mobile.md is excluded by !docs/**
📒 Files selected for processing (22)
  • migrations/migrations/090_20260430_add_update_notify_columns.ts
  • src/client/components/ui/version-display.tsx
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/client/hooks/useVersionCheck.tsx
  • src/plugins/custom/update-check.ts
  • src/routes/v1/system/update-status.ts
  • src/schemas/config/config.schema.ts
  • src/schemas/system/update-status.schema.ts
  • src/services/database/methods/config.ts
  • src/services/database/types/config-methods.ts
  • src/services/notification.service.ts
  • src/services/notifications/channels/apprise.service.ts
  • src/services/notifications/channels/apprise.ts
  • src/services/notifications/channels/discord-dm.ts
  • src/services/notifications/discord-bot/bot.service.ts
  • src/services/notifications/orchestration/index.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/notifications/templates/apprise-html.ts
  • src/services/notifications/templates/discord-embeds.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts
  • src/types/discord.types.ts

Comment thread src/plugins/custom/update-check.ts
Comment on lines +624 to +635
export async function getLastNotifiedVersion(
this: DatabaseService,
): Promise<string | null> {
try {
const row = await this.knex('configs')
.where({ id: 1 })
.first('lastNotifiedVersion')
return (row?.lastNotifiedVersion as string | null) ?? null
} catch (error) {
this.log.error({ error }, 'Error reading lastNotifiedVersion')
return null
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't map a failed watermark read to null.

dispatchNotification() treats null as “no prior notification”, so returning null here on a transient query failure can resend the current latest-release notification. Re-throw after logging so callers skip dispatch instead of losing the dedupe state.

Suggested fix
 export async function getLastNotifiedVersion(
   this: DatabaseService,
 ): Promise<string | null> {
   try {
     const row = await this.knex('configs')
       .where({ id: 1 })
       .first('lastNotifiedVersion')
     return (row?.lastNotifiedVersion as string | null) ?? null
   } catch (error) {
     this.log.error({ error }, 'Error reading lastNotifiedVersion')
-    return null
+    throw error
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/database/methods/config.ts` around lines 624 - 635, The catch in
getLastNotifiedVersion is swallowing DB errors and returning null (which
dispatchNotification treats as "no prior notification"); instead of returning
null on error, log the error with this.log.error as is and re-throw the caught
error so callers can skip dispatch; modify the catch in getLastNotifiedVersion
(DatabaseService.getLastNotifiedVersion) to throw the original error after
logging rather than returning null.

Comment on lines +643 to +657
export async function setLastNotifiedVersion(
this: DatabaseService,
version: string,
): Promise<boolean> {
try {
const updated = await this.knex('configs').where({ id: 1 }).update({
lastNotifiedVersion: version,
updated_at: this.timestamp,
})
return updated > 0
} catch (error) {
this.log.error({ error, version }, 'Error setting lastNotifiedVersion')
return false
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "notifyOnUpdate|setLastNotifiedVersion|getLastNotifiedVersion" src -C3
fd 'config-methods.ts' src --exec sed -n '1,120p' {}

Repository: jamcalli/Pulsarr

Length of output: 12374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find all call sites of the watermark helpers
rg -n "setLastNotifiedVersion\s*\(" src -S
rg -n "getLastNotifiedVersion\s*\(" src -S

# 2) Find where notifyOnUpdate is updated / toggle handled server-side
rg -n "notifyOnUpdate" src -S

# 3) Inspect the DB methods + update-check logic around the watermark usage
for f in \
  src/services/database/methods/config.ts \
  src/plugins/custom/update-check.ts
do
  echo "===== $f (relevant excerpt) ====="
  # Print around known line anchors
  case "$f" in
    *config.ts)
      sed -n '430,760p' "$f" | nl -ba | sed -n '1,220p'
      ;;
    *update-check.ts)
      sed -n '1,140p' "$f" | nl -ba
      ;;
  esac
done

Repository: jamcalli/Pulsarr

Length of output: 2393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "lastNotifiedVersion" src -S

# Inspect the watermark read/write logic
echo "===== src/plugins/custom/update-check.ts ====="
sed -n '1,120p' src/plugins/custom/update-check.ts

# Inspect DB helpers around get/set + updateConfig exclusion list
echo "===== src/services/database/methods/config.ts (relevant) ====="
sed -n '430,520p' src/services/database/methods/config.ts
sed -n '600,690p' src/services/database/methods/config.ts

# Inspect config update path (client) that toggles notifyOnUpdate
echo "===== src/client/features/notifications/components/general/general-settings-form.tsx (relevant) ====="
sed -n '1,220p' src/client/features/notifications/components/general/general-settings-form.tsx
sed -n '220,360p' src/client/features/notifications/components/general/general-settings-form.tsx

# Inspect public DB interface augmentation
echo "===== src/services/database/types/config-methods.ts ====="
sed -n '1,120p' src/services/database/types/config-methods.ts

Repository: jamcalli/Pulsarr

Length of output: 22080


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "lastNotifiedVersion" . -S

# Find other potential watermark-clearing paths
rg -n "setLastNotifiedVersion\s*\(" . -S
rg -n "getLastNotifiedVersion\s*\(" . -S

# Look for config update handling that reacts specifically to notifyOnUpdate changes
rg -n "notifyOnUpdate.*(none|re-?enable|enable)|\\bnotifyOnUpdate\\b" src -S

Repository: jamcalli/Pulsarr

Length of output: 3255


Add a way to clear the update notification watermark

lastNotifiedVersion is internal/nullable at the DB level (migrations/...090_20260430_add_update_notify_columns.ts sets table.string('lastNotifiedVersion').nullable()), but setLastNotifiedVersion (and its type augmentation) only accepts version: string, and the only caller (src/plugins/custom/update-check.ts) only ever advances it after a successful send—there’s no path to clear it when notifyOnUpdate is toggled back on. As a result, the dedupe state persists across disable/re-enable because the update-check plugin always uses storedLastNotified if present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/database/methods/config.ts` around lines 643 - 657, The
setLastNotifiedVersion function currently only accepts a non-null string which
prevents clearing the nullable DB column; change the signature and any related
type augmentation to accept version: string | null, update the update payload in
setLastNotifiedVersion to write lastNotifiedVersion: version (allowing null) and
keep updated_at behavior, and adjust its callers (notably update-check.ts) to
call setLastNotifiedVersion(null) when you need to clear the watermark so the
dedupe state can be reset.

@jamcalli jamcalli merged commit c5cc1e1 into jamcalli:develop Jun 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants