Skip to content

feat(notify): persistent/keyed notifications; reboot notice -> bell (OS-471)#2676

Open
elibosley wants to merge 10 commits into
masterfrom
feat/banner-to-persistent-notifications
Open

feat(notify): persistent/keyed notifications; reboot notice -> bell (OS-471)#2676
elibosley wants to merge 10 commits into
masterfrom
feat/banner-to-persistent-notifications

Conversation

@elibosley

@elibosley elibosley commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Producer side of persistent ("sticky until resolved") notifications — the webgui half of replacing the floating yellow banner with the API notification bell (OS-471). Pairs with unraid/api#2033 (which adds the persistent/key fields, idempotent keyed create, and clearNotificationByKey).

Changes

  • webGui/scripts/notify (canonical file; webGui/scripts/notify is a symlink to it):
    • add gains -k <key> — a stable key; keyed notices use the key as the filename so re-raising is idempotent (no dupes).
    • add gains -ppersistent: written as persistent=true, and not copied to the archive folder (it isn't an archival event).
    • new notify clear -k <key> — resolves a keyed notification (removes it from the bell).
    • usage() updated.
  • PluginAPI.php — the reboot-required notice now also raises a persistent, keyed (reboot-required) ALERT notification, and clears it once no reboot reasons remain. It also clears naturally on reboot (RAM-backed /tmp is wiped and the bell repopulates from files).

How it combines with the API PR

The API watches /tmp/notifications/unread/*.notify; these new fields flow straight through:
notify add -p -k reboot-required … → a sticky ALERT in the bell that the user can't dismiss and that auto-resolves. The API's getLegacyScriptArgs already passes -k/-p, so API-originated persistent notifications use this same script.

Validation

  • php -l clean on both files.
  • End-to-end (reboot notice → bell, clear on resolve) is best verified on hardware with both PR test plugins installed (the diff-based stacking work makes that possible).

Scope / follow-ups

This wires the clearest condition (reboot-required) end to end. Remaining banner call sites — boot-device-corrupt → persistent, and transient ones (popup blocked, etc.) → toaster — plus removing the .upgrade_notice element, are the documented next step.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added stable-key persistent and conditional notifications, plus notify clear -k to dismiss specific keyed alerts.
    • Updated reboot-required and PR test build messaging to use keyed notification-bell persistence.
    • Replaced the legacy upgrade banner with a unified banner/notification system (persistent bell warnings and transient toast notifications).
  • Bug Fixes
    • Reboot notifications no longer stack; repeated notices update the same bell entry.
    • Reboot notices automatically clear once all reboot reasons are resolved.

…the bell

Adds the producer side of persistent ("sticky until resolved") notifications so
the API notification bell can replace the legacy floating yellow banner.

- webGui/scripts/notify:
  - `add` gains `-k <key>` (stable key; keyed notices use the key as the filename
    so re-raising is idempotent) and `-p` (persistent: written as persistent=true,
    not user-archivable, and not copied to the archive folder).
  - new `notify clear -k <key>` resolves a keyed notification.
- PluginAPI.php: the reboot-required notice now also raises a persistent,
  keyed (`reboot-required`) ALERT notification, and clears it once no reboot
  reasons remain. It also clears naturally on reboot (RAM-backed /tmp is wiped).

Pairs with unraid/api#2033 (persistent + key fields, clearNotificationByKey).
Part of OS-471. Follow-up: migrate the remaining banner call sites
(boot-device-corrupt -> persistent; transient -> toaster) and remove the
.upgrade_notice element.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ff5d4c1f-97cb-443a-9f7e-639ff61b5e4b

📥 Commits

Reviewing files that changed from the base of the PR and between ecacf04 and 20fa7a6.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/scripts/notify

Walkthrough

The notify shell script gains -k (stable key) and -p (persistent) options for notify add, changing filename selection and archive behavior, plus a new clear subcommand that safely deletes keyed notifications. Notify.php translates these via POST parameters. HeadInlineJS.php refactors the banner system from legacy HTML cycling to keyed persistent notifications and transient toasts. PluginAPI.php maintains a single idempotent reboot-required notification. PR plugin generation adopts keyed notifications for test build warnings.

Changes

Stable-key persistent notifications integration

Layer / File(s) Summary
notify script: stable-key and persistent modes
emhttp/plugins/dynamix/scripts/notify
New -k and -p options parsed by getopt; filename selection uses the stable key when provided; archive write skipped for persistent notifications; unread metadata records key and persistent='true'; new clear subcommand for keyed notification deletion with path-traversal protection via realpath prefix checks; usage() updated; init and cron-init create and manage the new active directory for persistent notifications.
Notify.php: POST parameter translation to CLI
emhttp/plugins/dynamix/include/Notify.php
POST parameter handling extended to translate -k, -l, -g options with escaped values, and -p flag without value; new cmd=clear branch invokes notify clear -k <key>.
HeadInlineJS.php: banner system refactor
emhttp/plugins/dynamix/include/DefaultPageLayout/HeadInlineJS.php
Replaces old bannerWarnings, cookie-based dismissal, and HTML upgrade-notice cycling with new bannerRegistry system; transient banners emit via window.toast with action-link extraction; persistent banners post to Notify.php keyed by stable text hash and generation timestamp; addRebootNotice() solely posts to PluginAPI.php instead of creating client banners; removes client-side pre-existing reboot notice re-display.
PluginAPI.php: reboot notice lifecycle
emhttp/plugins/dynamix.plugin.manager/scripts/PluginAPI.php
addRebootNotice executes notify add -k reboot-required -p to surface a persistent web UI notification; removeRebootNotice executes notify clear -k reboot-required when /tmp/reboot_notifications becomes empty.
PR plugin generation: keyed notification UI
.github/scripts/generate-pr-plugin.sh
Replaces old non-dismissible banner + uninstall link with new raisePrNotice() function that posts keyed notification via Notify.php; uses caPluginUpdateCheck to detect plugin version and either raise or clear the notification; removal script clears the keyed notification.

Sequence Diagram

sequenceDiagram
    participant Plugin
    participant PluginAPI as PluginAPI.php
    participant HeadJS as HeadInlineJS.php
    participant NotifyPHP as Notify.php
    participant NotifyScript as notify script
    participant ActiveDir as active dir
    participant WebUI as Web UI Toast

    Plugin->>PluginAPI: addRebootNotice(message)
    PluginAPI->>NotifyScript: notify add -k reboot-required -p
    NotifyScript->>ActiveDir: write reboot-required.notify

    HeadJS->>HeadJS: addBannerWarning(text, noDismiss)
    alt persistent banner (noDismiss)
        HeadJS->>NotifyPHP: POST action:add<br/>-k keyHash -p
        NotifyPHP->>NotifyScript: notify add -k keyHash -p
        NotifyScript->>ActiveDir: write keyHash.notify
    else transient banner
        HeadJS->>WebUI: window.toast(text, actions)
    end

    Plugin->>PluginAPI: removeRebootNotice(message)
    PluginAPI->>NotifyScript: notify clear -k reboot-required
    NotifyScript->>ActiveDir: delete reboot-required.notify
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • unraid/webgui#2674: Both PRs modify .github/scripts/generate-pr-plugin.sh, particularly the embedded PR test plugin UI/removal logic; this PR adds keyed notify clear on uninstall while the related PR rewrites the embedded removal workflow.

Poem

🐇 With -k for keys and -p to persist,
Notices stay steady, no duplicates missed.
Toast for the fleeting, bell for the staying,
One reboot-required ballet now displaying!
The rabbit refactors with mathematical grace,
Keyed notifications now light up the place. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: implementing persistent/keyed notifications and migrating the reboot notice to the notification bell system as part of OS-471.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
  • Commit unit tests in branch feat/banner-to-persistent-notifications

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

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.06.23.1724
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2676/webgui-pr-2676.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates
  • Post-merge behavior: This preview stays available after merge until preview storage expires or it is manually cleaned up

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix.plugin.manager/scripts/PluginAPI.php
emhttp/plugins/dynamix/include/DefaultPageLayout/HeadInlineJS.php
emhttp/plugins/dynamix/include/Notify.php
emhttp/plugins/dynamix/scripts/notify

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2676, or run:

plugin remove webgui-pr-2676

🤖 This comment is automatically generated and will be updated with each new push to this PR.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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 `@emhttp/plugins/dynamix/scripts/notify`:
- Line 270: The early break statement when checking if an archive file exists
prevents the processing of new unread notifications for keyed notifications that
already have an archived version. Instead of breaking immediately when a file
exists, modify the logic to continue processing when dealing with keyed
notifications that may have new unread content. The archive existence check
should not short-circuit the entire operation for keyed re-raise scenarios where
the same key may need to raise new notifications even if a previous archive
exists. Restructure the conditional logic to differentiate between keyed and
non-keyed notifications, ensuring that keyed notifications can proceed through
the notification handling flow regardless of existing archive files.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6da37b25-77fe-44e1-ab4e-8a026b4073ef

📥 Commits

Reviewing files that changed from the base of the PR and between 644ab82 and 8da3c14.

📒 Files selected for processing (2)
  • emhttp/plugins/dynamix.plugin.manager/scripts/PluginAPI.php
  • emhttp/plugins/dynamix/scripts/notify

Comment thread emhttp/plugins/dynamix/scripts/notify Outdated
elibosley and others added 7 commits June 19, 2026 17:06
Two bugs found while deploying to a live server:
- PluginAPI.php called `notify add -p -k ...`; PHP getopt() stops at the first
  non-option arg, so the literal `add` made it parse zero options. The script
  dispatches to 'add' whenever the first arg is an option, so call it
  options-first (no `add` literal).
- `notify clear` parsed its key with getopt(), which likewise stops at the
  'clear' sub-command. Parse argv directly to support `clear <key>` and
  `clear -k <key>`.

Verified on hardware: `notify -p -k reboot-required ...` writes a persistent
keyed notification and `notify clear -k reboot-required` removes it
(safe_filename maps the key symmetrically for add + clear).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ved copy

The legacy "if archived copy exists, skip" dedup blocked re-raising a keyed
condition after the user dismissed (archived) it - so a still-true condition
could never re-pin. Keyed notifications now clear any stale archived copy and
proceed, so re-raising re-pins and keeps the archive clean. Non-keyed
notifications keep the legacy dedup behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retire the fixed yellow .upgrade_notice bar by routing addBannerWarning by intent:
- Persistent conditions (noDismiss, not a transient "forced" in-progress banner)
  -> a keyed notification in the bell via Notify.php (idempotent re-raise,
  header-reserved, survives reloads, clears when resolved). Boot-device-corrupt
  now routes here too.
- Everything else -> a transient toast (globalThis.toast), with any banner link
  preserved as a toast action button.

Notify.php add gains -p/-k/-l passthrough plus a clear (clear-by-key) command.
Reboot notices are no longer double-shown: they rely solely on the PluginAPI
"reboot-required" bell notification (dropped the client banner + on-load re-display).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Banner page raises a persistent, keyed bell notification
(key=webgui-pr-PR_PLACEHOLDER) instead of a banner. Clear it in the
plugin removal script so uninstalling the PR test plugin also removes
the notification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Legacy banners (CA's "Action Centre Enabled", boot checks, ...) routed to the
bell have no explicit clear: they re-render on every page load while active
and simply stop when resolved, so a persistent bell notification would stick
forever.

Stamp each banner -> bell notification with a per-page-load generation, and
sweep any 'banner-' keyed notification not re-raised in the current generation
once the page settles. Resolved banners now clear on the next navigation.
Non-banner keys (PR plugins, reboot-required) own their lifecycle and are left
untouched.

- notify: add -g <gen> on add; add `banner-sweep <gen>` subcommand
- Notify.php: forward -g on add; add cmd=banner-sweep
- HeadInlineJS: stamp bell POST with a per-load bannerGen; sweep 3s after load

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move banner reconciliation to the backend (authoritative, triggered when the
notification drawer loads). The page's role is reduced to:
  - stamp each banner -> bell notification with a per-page-load generation
    (notify add -g <gen>), so the API can tell which banners were re-raised
    this load
  - expose window.bannerGen for the drawer to hand to the API

Drop the page-side banner-sweep (notify subcommand, Notify.php command, and the
HeadInlineJS post-load timer) in favor of the API's reconcileBannerNotifications
mutation (unraid/api). The -g stamp on `notify add` is retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generic "This stays in your notifications until the condition is resolved"
description was filler - the "Active" badge already conveys persistence - and
rendered as a redundant second line. Send an empty description so the bell card
shows only the banner message (paired with the web fix that hides empty lines).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@elibosley

Copy link
Copy Markdown
Member Author

CodeRabbit fix-loop resolution:

  • notify early break blocks keyed re-raise when an archive twin exists (~270) — already addressed. The archive-exists check no longer short-circuits keyed notifications:

    if (file_exists($archive)) {
      if ($key !== '') @unlink($archive); else break;
    }

    For a keyed (condition-style) notification it clears the stale archived copy and proceeds to write the fresh unread entry; only non-keyed legacy notifications break for dedup. So a keyed condition can re-raise even when a previous archived copy exists.

elibosley and others added 2 commits June 22, 2026 16:32
Persistent (-p) notifications now write to $path/active/ instead of unread/, so
they can be retrieved cheaply and are never swept by bulk archive/delete. To
keep the legacy nchan poller working, `notify get` unions active/ with unread/.
`clear -k` and `init` sweep active/ too, and a raise clears any stale twin left
in the other store. No migration needed — persistent notifications regenerate
on page load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Condition-style ("active") notifications are derived state, re-raised on every
page load. They were written to $path/active, but $path follows the user-facing
"Store notifications to boot drive" setting -- so on systems with that enabled,
a condition notice was persisted to flash and stranded forever once its
condition was gone (e.g. the plugin that raised it was removed).

Hardcode the active dir to /tmp/notifications/active, decoupled from $path.
A reboot now wipes condition notifications and the next page load re-raises only
those still true -- making "stuck forever" structurally impossible without any
generation-sweep reconciliation. Event notifications (unread/archive) still
honor the flash setting via $path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.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.

1 participant