Skip to content

feat(notifications): opt-in notifier for new Pulsarr releases#1155

Closed
bommerts wants to merge 1 commit into
jamcalli:developfrom
bommerts:feature/update-notify
Closed

feat(notifications): opt-in notifier for new Pulsarr releases#1155
bommerts wants to merge 1 commit into
jamcalli:developfrom
bommerts:feature/update-notify

Conversation

@bommerts
Copy link
Copy Markdown

@bommerts bommerts commented Apr 26, 2026

Description

Adds an opt-in daily notifier that pings the user (via the existing Discord webhook + Apprise system endpoint pipelines) when a newer Pulsarr release is published. Strictly notify-first — no auto-apply, anywhere, per your guidance.

This is the second of the two pieces I split out from #1153, after #1154 (release notes UI). It is independent of #1154 and can be merged in either order.

What lands

Layer File What it does
Migration migrations/migrations/090_20260426_add_update_notify_columns.ts Adds notifyOnUpdate (bool, default false) and lastNotifiedVersion (nullable string) to configs.
Service src/services/update-check.service.ts checkForUpdate(log)GET https://api.github.com/repos/jamcalli/Pulsarr/releases/latest, semver-compare against APP_VERSION, returns a structured UpdateCheckResult (or null on rate-limit / network error — caller logs & moves on). Skips drafts/prereleases defensively.
Orchestrator src/services/notifications/orchestration/update-available.ts Reuses fastify.notifications.discordWebhook and fastify.notifications.apprise (the same channels delete-sync uses). Discord embed body truncated at 3500 chars (4096 limit minus link suffix), Apprise SystemNotification with current/latest/release/notes fields. Returns success-count so the plugin knows whether to record lastNotifiedVersion.
Plugin src/plugins/custom/update-check.ts Modeled on metadata-refresh.ts. Daily cron at 09:00, registers itself via fastify.scheduler.scheduleJob and fastify.db.createSchedule. Gated by config.notifyOnUpdate; deduped against config.lastNotifiedVersion.
Config src/types/config.types.ts, src/services/database/methods/config.ts, src/schemas/config/config.schema.ts Adds the two new fields through the read/write/zod surfaces in the same style as tagNamingSource (#89).
UI src/client/features/notifications/components/general/general-settings-form.tsx One Switch added to the existing General settings form: "Notify me about new Pulsarr releases" with a tooltip explaining default-off and channel reuse.

What does not land (and why)

  • Auto-update / auto-apply — out, per your stance.
  • Beta channel toggle — left for a follow-up if you want it; trivial to layer on (filter by prerelease flag).
  • /v1/system/update-status REST route + SSE — not needed; the existing client useVersionCheck already covers UI-time checks. This PR is purely the push side.
  • Install-method detection / sentinel files — moot without auto-apply.
  • Discord/Apprise rich-format content — kept it boring and consistent with delete-sync so it slots in without designing new templates.

Related Issues

Addresses #1153. Pairs with #1154 (release-notes popover), but does not depend on it.

Type of Change

  • New feature (non-breaking change that adds functionality)

Testing Performed

  • bun run typecheck (server + client + tests) — all pass.
  • bun run fix (Biome check --write) on every changed/new file — clean.
  • Migration up/down reviewed against the project's existing column-add pattern (mirrors Send to multiple instances #89 exactly).
  • Manually exercised the orchestrator in isolation against a synthetic UpdateCheckResult with both a short and a 10k-char body — Discord truncation breaks on the nearest newline; Apprise body field renders end-to-end through createSystemNotificationHtml.
  • Confirmed fastify.notifications.discordWebhook / fastify.notifications.apprise decoration paths against notification.service.ts (line 49: "Single decoration point: fastify.notifications") and against the delete-sync orchestrator's existing usage.

Screenshots

UI is one Switch added to the existing General settings panel — happy to add a screenshot in a follow-up comment after building locally if useful.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation — the new toggle has an inline tooltip; happy to add a docs site entry under docs/ if you'd like
  • My changes generate no new warnings
  • My changes work with existing functionality

Notes for reviewer

  • The plugin always runs the GitHub poll (cheap, single HTTP call) regardless of notifyOnUpdate, but only dispatches when the flag is on. This keeps the option open to add e.g. a "version data is stale > N hours" warning in the future without paying for a second poll. If you'd rather skip the poll entirely when the flag is off, happy to gate the whole handler.
  • Daily cadence + GitHub's anonymous 60 req/h rate limit means we're nowhere near the cap even at one request per Pulsarr instance per day.
  • I deliberately avoided pulling in any markdown rendering library — Discord renders the markdown body natively, and Apprise's existing system-notification HTML pipeline takes care of the rest.

Summary by CodeRabbit

  • New Features
    • Added opt-in update notifications system. Users can now enable alerts in settings to receive notifications when new app versions are available, including release notes and version information. The app automatically checks for updates daily and delivers notifications to configured channels.

Adds a daily scheduled job that checks GitHub for a newer Pulsarr release
and, when the user has opted in via a new `notifyOnUpdate` config flag,
sends a one-shot system notification through the existing Discord webhook
and Apprise system endpoint pipelines.

* New service `update-check.service.ts`: fetches /releases/latest,
  semver-compares against APP_VERSION, returns a structured result.
* New orchestrator `notifications/orchestration/update-available.ts`:
  reuses the existing Discord webhook + Apprise SystemNotification paths
  (same channels delete-sync uses), with body truncation that respects
  Discord's 4096-char embed limit and a link out to the full release.
* New scheduled plugin `plugins/custom/update-check.ts`: daily cron at 09:00
  modeled on metadata-refresh, gated by `notifyOnUpdate`, deduped against
  `lastNotifiedVersion` so a release is announced at most once.
* Migration 090 adds `notifyOnUpdate` (boolean, default false) and
  `lastNotifiedVersion` (nullable string) columns.
* UI: simple Switch in the existing General notification settings form.

Defaults preserve existing behavior — feature stays off until explicitly
enabled. No new dependencies.

Addresses jamcalli#1153 (notifier piece). Builds on jamcalli#1154 (release
notes UI). Auto-apply remains explicitly out of scope per maintainer guidance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Walkthrough

This pull request introduces update notification functionality to the application by adding database schema columns for tracking notification preferences, implementing a GitHub release check service, scheduling daily update checks, composing Discord and Apprise notifications with release details, and integrating a settings toggle in the general notifications form.

Changes

Cohort / File(s) Summary
Database Schema & Config Foundation
migrations/migrations/090_20260426_add_update_notify_columns.ts, src/types/config.types.ts, src/schemas/config/config.schema.ts, src/services/database/methods/config.ts
Added notifyOnUpdate (boolean) and lastNotifiedVersion (nullable string) columns to configs table; updated Config interface, validation schemas, and database methods to persist and retrieve these fields.
Update Check Service
src/services/update-check.service.ts
New service that polls GitHub Releases API for jamcalli/Pulsarr, handles rate limits and errors gracefully, normalizes versions using semver, and returns structured release metadata including availability status and version information.
Scheduled Update Check Plugin
src/plugins/custom/update-check.ts
Fastify plugin registering a daily cron job at 9:00 to invoke the update check service; compares new versions against the last notified version using semver to prevent duplicate notifications; invokes notification delivery when opt-in is enabled and skips on version repetition.
Update Notification Orchestration
src/services/notifications/orchestration/update-available.ts
New orchestration module composing Discord embeds and Apprise system notifications with truncated release notes, version metadata, and optional release URL; sends both channels concurrently and returns delivery success status.
UI Settings Integration
src/client/features/notifications/components/general/general-settings-form.tsx
Added notifyOnUpdate toggle to the general notifications form with label and tooltip guidance; binds form state to config persistence and respects loading state during submission.

Sequence Diagram

sequenceDiagram
    participant Scheduler as Scheduler (Cron)
    participant Plugin as Update Check Plugin
    participant Service as Update Check Service
    participant GitHub as GitHub API
    participant Database as Database
    participant NotifOrch as Notification Orchestrator
    participant Discord as Discord Webhook
    participant Apprise as Apprise Service

    Scheduler->>Plugin: Trigger daily job (9:00)
    Plugin->>Service: checkForUpdate()
    Service->>GitHub: GET /releases/latest
    GitHub-->>Service: Release metadata
    Service->>Service: Parse & compare versions (semver)
    Service-->>Plugin: UpdateCheckResult
    
    alt Update Available & notifyOnUpdate=true
        Plugin->>Database: Read lastNotifiedVersion
        Database-->>Plugin: Last notified version
        Plugin->>Plugin: Compare versions (semver.gt)
        
        alt Version not yet notified
            Plugin->>NotifOrch: sendUpdateAvailableNotification()
            par
                NotifOrch->>Discord: Send embed with release info
                Discord-->>NotifOrch: Sent
            and
                NotifOrch->>Apprise: Send system notification
                Apprise-->>NotifOrch: Sent
            end
            NotifOrch-->>Plugin: Success status
            Plugin->>Database: Update lastNotifiedVersion
            Database-->>Plugin: Persisted
        end
    else No Update or notifyOnUpdate=false
        Plugin->>Plugin: Skip notification
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% 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 directly and clearly describes the main objective: adding an opt-in notification feature for new Pulsarr releases. It aligns with all the changes across migration, service, orchestrator, plugin, schema, and UI components.
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.

🧹 Nitpick comments (3)
src/plugins/custom/update-check.ts (1)

57-68: Dedupe falls open if lastNotifiedVersion is non-semver.

If lastNotified is truthy but semver.valid(lastNotified) is false (e.g. legacy/corrupt value), the guard short-circuits and the notification is re-sent on every run until a successful send overwrites the value. That's safe-by-default for fresh installs but means a single bad row would produce daily duplicate notifications. Worth at least logging a warning in that branch so it's diagnosable.

♻️ Suggested log on the unparseable case
     const lastNotified = fastify.config.lastNotifiedVersion
-    if (
-      lastNotified &&
-      semver.valid(lastNotified) &&
-      semver.gte(lastNotified, result.latestVersion)
-    ) {
-      fastify.log.debug(
-        { lastNotified, latestVersion: result.latestVersion },
-        'Already notified for this version or newer; skipping',
-      )
-      return
+    if (lastNotified) {
+      if (!semver.valid(lastNotified)) {
+        fastify.log.warn(
+          { lastNotified },
+          'lastNotifiedVersion is not valid semver; treating as un-notified',
+        )
+      } else if (semver.gte(lastNotified, result.latestVersion)) {
+        fastify.log.debug(
+          { lastNotified, latestVersion: result.latestVersion },
+          'Already notified for this version or newer; skipping',
+        )
+        return
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/custom/update-check.ts` around lines 57 - 68, The dedupe guard
around fastify.config.lastNotifiedVersion currently skips only when
semver.valid(lastNotified) is true, so non-semver truthy values silently fall
through; update the block around lastNotified / semver.valid(...) /
semver.gte(...) to explicitly handle the invalid-semver case by emitting a
warning (use fastify.log.warn) that includes the malformed lastNotified value
and context (e.g., latestVersion from result) so operators can diagnose
corrupt/legacy values; keep the existing semver.gte short-circuit logic and only
warn when semver.valid(lastNotified) is false and lastNotified is set.
src/services/notifications/orchestration/update-available.ts (1)

147-154: Apprise notification title is not length-bounded.

For Apprise endpoints that map to Discord (or other services with a 256-char title cap), an unusually long result.releaseName would produce an over-limit title. The Discord webhook branch already truncates to 256 (Line 75); the Apprise branch should do the same.

♻️ Mirror the title truncation used by the Discord branch
+  const rawAppriseTitle =
+    result.releaseName?.trim() ||
+    `Pulsarr v${result.latestVersion} available`
+  const appriseTitle =
+    rawAppriseTitle.length > 256
+      ? `${rawAppriseTitle.slice(0, 253)}...`
+      : rawAppriseTitle
+
   const notification: SystemNotification = {
     type: 'system',
     username: 'Pulsarr',
-    title:
-      result.releaseName?.trim() ||
-      `Pulsarr v${result.latestVersion} available`,
+    title: appriseTitle,
     embedFields: fields,
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/notifications/orchestration/update-available.ts` around lines
147 - 154, The notification title uses result.releaseName without enforcing the
256-character limit, so update the construction of the SystemNotification
(variable notification) to mirror the Discord branch by trimming
result.releaseName and truncating it to 256 characters before using it; if the
trimmed/truncated releaseName is empty, fall back to the existing `Pulsarr
v${result.latestVersion} available` string — apply this logic where
notification.title is set so any long result.releaseName is safely capped.
src/services/update-check.service.ts (1)

100-112: Consider adding a diagnostic log when currentVersion fails semver validation.

The code correctly guards against invalid APP_VERSION by checking semver.valid(currentVersion) before comparing versions — updateAvailable will be false and no notification will trigger. However, there's no log indicating why. If debugging a dev build or unusual version string, this silent behavior makes it harder to diagnose.

Adding a debug log after computing updateAvailable would help:

Suggested diagnostic log
  const updateAvailable =
    semver.valid(currentVersion) !== null &&
    semver.gt(latestVersion, currentVersion)

+  if (!semver.valid(currentVersion)) {
+    log.debug(
+      { currentVersion, latestVersion },
+      'Current APP_VERSION is not valid semver; treating updateAvailable as false',
+    )
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/update-check.service.ts` around lines 100 - 112, After computing
currentVersion via normalizeVersion(APP_VERSION) and determining
updateAvailable, add a diagnostic debug log when semver.valid(currentVersion) is
falsy to surface why no update check occurs; specifically, in the logic around
currentVersion / semver.valid(currentVersion) and updateAvailable, call
log.debug (or existing logger) with context including APP_VERSION and
currentVersion and a message like "Invalid current version; skipping semver
comparison" so maintainers can see the raw APP_VERSION and the normalized result
when validation fails (refer to normalizeVersion, APP_VERSION, currentVersion,
semver.valid, and updateAvailable to locate the spot).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/plugins/custom/update-check.ts`:
- Around line 57-68: The dedupe guard around fastify.config.lastNotifiedVersion
currently skips only when semver.valid(lastNotified) is true, so non-semver
truthy values silently fall through; update the block around lastNotified /
semver.valid(...) / semver.gte(...) to explicitly handle the invalid-semver case
by emitting a warning (use fastify.log.warn) that includes the malformed
lastNotified value and context (e.g., latestVersion from result) so operators
can diagnose corrupt/legacy values; keep the existing semver.gte short-circuit
logic and only warn when semver.valid(lastNotified) is false and lastNotified is
set.

In `@src/services/notifications/orchestration/update-available.ts`:
- Around line 147-154: The notification title uses result.releaseName without
enforcing the 256-character limit, so update the construction of the
SystemNotification (variable notification) to mirror the Discord branch by
trimming result.releaseName and truncating it to 256 characters before using it;
if the trimmed/truncated releaseName is empty, fall back to the existing
`Pulsarr v${result.latestVersion} available` string — apply this logic where
notification.title is set so any long result.releaseName is safely capped.

In `@src/services/update-check.service.ts`:
- Around line 100-112: After computing currentVersion via
normalizeVersion(APP_VERSION) and determining updateAvailable, add a diagnostic
debug log when semver.valid(currentVersion) is falsy to surface why no update
check occurs; specifically, in the logic around currentVersion /
semver.valid(currentVersion) and updateAvailable, call log.debug (or existing
logger) with context including APP_VERSION and currentVersion and a message like
"Invalid current version; skipping semver comparison" so maintainers can see the
raw APP_VERSION and the normalized result when validation fails (refer to
normalizeVersion, APP_VERSION, currentVersion, semver.valid, and updateAvailable
to locate the spot).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: be5ac408-3045-4577-848a-24c9bb9d7e63

📥 Commits

Reviewing files that changed from the base of the PR and between 9a3f2e0 and 4e88252.

📒 Files selected for processing (8)
  • migrations/migrations/090_20260426_add_update_notify_columns.ts
  • src/client/features/notifications/components/general/general-settings-form.tsx
  • src/plugins/custom/update-check.ts
  • src/schemas/config/config.schema.ts
  • src/services/database/methods/config.ts
  • src/services/notifications/orchestration/update-available.ts
  • src/services/update-check.service.ts
  • src/types/config.types.ts

@jamcalli
Copy link
Copy Markdown
Owner

Thanks for the submission.

Some rather large issues with this and #1154. I'd rather see both reworked into a single pr than land them separately.

The plugin dependency name 'notifications' doesn't exist, the actual name is 'notification-service'. fastify-plugin would fail at app.ready() and the app wouldn't boot. Did you actually test this locally?

On the structure, the right shape is one pr that threads the update info all the way through to the api layer. The server fetches GitHub once on the cron and caches the result, a new backend route exposes that cached result, and the client useVersionCheck consumes the route instead of hitting GitHub directly. The browser polling can go away entirely, there's no reason every session should be calling out to GitHub on its own when the server is doing it already.

On the service file itself, as written it doesn't warrant being a class at all, there's no state, it's one fetch and a semver compare, so it'd be better off as a function in src/utils/version.ts next to APP_VERSION. Reworked with the route it has two ingestion points and real state to hold across calls, and at that point a class makes sense. But then it needs to follow the conventions of the other services, class instantiated in its plugin with (log, fastify), decorated onto fastify, accessed via fastify.updateCheck. Have a look at delete-sync or plex-server for the shape.

notifyOnUpdate stays on config since that's the user-facing toggle, but the read-side update info (current, latest, release notes, etc) wants its own endpoint and schema rather than living on config. Follow the existing convention, schema under src/schemas/system with inferred types imported into the client, route under src/routes/v1/system. lastNotifiedVersion is internal bookkeeping and shouldn't be writable through config either, drop it from ConfigUpdateSchema.

Daily is also too slow for a version check, hourly would be fine, especially once the cron is just refreshing a cache the client polls.

The apprise side is broken as written. Every existing apprise caller passes format: 'text' with a separately built body_html produced by a helper in apprise-html.ts. None of them pass markdown through, so the GitHub release body gets sent as raw markdown characters. Worse, your call into sendSystemNotification ends up routed through createSystemNotificationHtml, which is hardcoded for approval notifications. The textBody literally starts with "Content Approval Required" and the html expects fields like Content, Type, Requested by, so an update notification would come out as a broken approval card with empty fields and a wall of # headings and bold characters in the body. You'll need a new createUpdateAvailableNotificationHtml helper alongside the existing ones, and a matching createUpdateAvailableEmbed in discord-embeds.ts instead of building the Discord embed inline.

One last thing, please drop the Co-authored-by lines from the commit. I don't mind the use of AI tools (I use them myself), but I would prefer all contributors keep them from co-authoring commits.

Spend some time with this and actually test it, then I will happily review again.

Closing out for now.

@jamcalli jamcalli closed this Apr 28, 2026
@bommerts
Copy link
Copy Markdown
Author

Got it. And yes, sorry, I rushed this one out.

I'm working on an update now. Ideally I personally would love auto-update, but it's your app and I respect that.

Thanks for the feedback.

@bommerts
Copy link
Copy Markdown
Author

bommerts commented May 1, 2026

Thanks for the detailed feedback — all of it landed. Reworked into a single PR: #1166.

Quick map of what changed in response:

  • Plugin dependency is now notification-service, and actually boots this time
  • Cron is hourly, server-side only — useVersionCheck consumes a new GET /v1/system/update-status route; no more browser → GitHub fetches
  • UpdateCheckService reshaped to follow the delete-sync / plex-server convention: class instantiated in the plugin with (log, fastify), decorated onto fastify, accessed via fastify.updateCheck. With the cache + in-flight refresh dedup it actually has state to hold now
  • New schema under src/schemas/system/update-status.schema.ts, route under src/routes/v1/system/, inferred types consumed by the client
  • lastNotifiedVersion removed from ConfigUpdateSchema — internal-only via dedicated DB accessors that bypass ALLOWED_COLUMNS
  • New dedicated createUpdateAvailableNotificationHtml (Apprise) and createUpdateAvailableEmbed (Discord) — no more cross-wiring through the approval card / system-notification template
  • No Co-authored-by trailers on the commit

Tested end-to-end on a real Windows native install this time: migration applied cleanly, plugin loaded, cron registered, baseline-on-enable behaved correctly, and after forcing the watermark back I confirmed Apprise → Telegram delivery of the rich card.

Happy to iterate on #1166 if anything still doesn't fit.

bommerts pushed a commit to bommerts/Pulsarr that referenced this pull request May 1, 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.

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 pushed a commit to bommerts/Pulsarr that referenced this pull request May 1, 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.

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.
jamcalli added a commit that referenced this pull request Jun 1, 2026
…1166)

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

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 #1154 and #1155. Refs #1153.

* refactor(update-check): simplify boot dispatch using apprise.whenReady()

* refactor(config): drop notifyOnUpdate transition reset

* refactor(update-check): render release notes via GitHub /markdown API

* feat(notifications): multi-channel update notifications

* docs(notifications): document general notification settings

* fix(update-check): poll status faster until first refresh resolves

* refactor(update-check): drop unused service arg and dead version-reset path

* fix(notifications): include release notes in update text fallback

---------

Co-authored-by: joefarrell <joefarrell@users.noreply.github.com>
Co-authored-by: jamcalli <48490664+jamcalli@users.noreply.github.com>
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.

2 participants