Skip to content

feat(notifications): persistent "sticky until resolved" notifications (OS-471)#2033

Open
elibosley wants to merge 29 commits into
mainfrom
feat/persistent-notifications
Open

feat(notifications): persistent "sticky until resolved" notifications (OS-471)#2033
elibosley wants to merge 29 commits into
mainfrom
feat/persistent-notifications

Conversation

@elibosley

@elibosley elibosley commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Adds a persistent notification capability so condition-style alerts (reboot required, boot device corrupt, …) can live in the notification bell and stay until their condition resolves — the API foundation for retiring the legacy webgui floating yellow banner (OS-471).

Model

  • Notification / NotificationData gain persistent: Boolean and key: String (a stable producer key).
  • Severity stays INFO/WARNING/ALERT — ALERT already drives the red bell, so persistence is orthogonal to severity (no new enum, minimal UI churn).
  • Persistence semantics:
    • Not user-archivablearchiveNotification rejects persistent ones (409) and bulk archiveAll skips them.
    • Idempotent raise — creating with an existing key replaces the prior instance (latest wins, no dupes).
    • Resolve by key — new clearNotificationByKey(key) mutation removes matching unread notifications when the condition clears.

Changes

API

  • notifications.model.tspersistent + key on Notification and NotificationData.
  • core/types/states/notification.ts + notifications.service.ts — read/write the new fields in the .notify file; idempotent keyed create; clearNotificationsByKey; archive protection; legacy notify script args gain -k/-p.
  • notifications.resolver.tsclearNotificationByKey mutation.
  • Regenerated generated-schema.graphql.

Web

  • NotificationFragment exposes persistent; the bell Item and the legacy-embedded CriticalNotifications hide the dismiss/archive control for persistent alerts and show "Clears automatically when resolved."
  • Regenerated web GraphQL codegen.

Validation

  • API tsc --noEmit ✅, web vue-tsc --noEmit ✅, GraphQL codegen ✅ (both api + web).
  • key intentionally not in the web fragment (it collides with Vue's reserved key attribute and the UI doesn't need it — clearing is producer-side).

Pairs with

The webgui PR (unraid/webgui) that adds -k/-p to webGui/scripts/notify and routes the yellow-banner conditions (reboot-required, boot-device-corrupt) through persistent notifications. Combine via the diff-based PR test plugins.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added persistent, condition-style notifications that auto-clear when their condition resolves.
    • Added stable notification keys, plus a new “clear by key” action for unread condition notifications.
  • Bug Fixes / Behavior Changes

    • Persistent notifications are no longer bulk-archived or bulk-deleted.
    • Malformed notifications now default to non-persistent.
  • UI Updates

    • Persistent notifications render as “Active,” appear first, and can be toggled via a persistent/active filter in the sidebar.
    • Dismiss behavior updates: persistent items show non-interactive status text; non-persistent items can be dismissed/archived.

Adds a persistent notification capability so condition-style alerts (reboot
required, boot device corrupt, etc.) can be surfaced in the notification bell
and stay until their condition resolves — replacing the legacy webgui floating
yellow banner.

API:
- Notification/NotificationData gain `persistent` (Boolean) and `key` (stable
  producer key). Severity stays INFO/WARNING/ALERT (ALERT already drives the
  red bell), so persistence is orthogonal to severity.
- .notify file format + service read/write the new fields.
- Keyed creates are idempotent (raising with an existing key replaces it), and
  `clearNotificationByKey(key)` resolves a condition (removes matching unread).
- Persistent notifications are not user-archivable: archiveNotification rejects
  them and bulk archiveAll skips them.

Web:
- NotificationFragment exposes `persistent`; the bell Item and the legacy-embedded
  CriticalNotifications hide the dismiss/archive control for persistent alerts and
  show "Clears automatically when resolved".
- Regenerated GraphQL schema + web codegen.

Part of OS-471. Pairs with the webgui PR that routes the yellow-banner conditions
through persistent notifications.

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

Walkthrough

Adds key and persistent fields to the notification data model across schema, TypeScript interfaces, NestJS models, and generated frontend types. The service gains idempotent create-by-key behavior, a new clearNotificationsByKey method, and guards preventing persistent notifications from being user-archived. The frontend conditionally hides dismiss/archive controls for persistent notifications, reorders the list to display persistent notifications first with improved animations, redesigns the sidebar with session-persisted filter state and updated icons, and adds localization strings for persistent notification UI.

Changes

Persistent / Condition-style Notifications

Layer / File(s) Summary
Schema contracts and data model
api/generated-schema.graphql, api/src/core/types/states/notification.ts, api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
Notification output type and NotificationData input gain optional key and persistent fields; clearNotificationByKey mutation is declared in the schema; NotificationIni interface and NestJS model classes add matching fields with validation decorators.
Service: create/clear/archive logic
api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
createNotification clears existing same-key unread notifications before writing; makeNotificationFileData and getLegacyScriptArgs conditionally serialize key and persistent; notificationFileToGqlNotification maps INI persistent='true' to boolean; new clearNotificationsByKey deletes matching unread entries; archiveNotification guards against persistent; archiveAll skips persistent notifications; malformed notification fallback sets persistent: false.
clearNotificationByKey resolver mutation
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
New @Mutation method clearNotificationByKey accepts a key string and delegates to notificationsService.clearNotificationsByKey(key).
Frontend types and persistent notification Item UI
web/src/components/Notifications/Item.vue, web/src/components/Notifications/CriticalNotifications.standalone.vue, web/src/components/Notifications/graphql/notification.query.ts, web/src/composables/gql/gql.ts, web/src/composables/gql/graphql.ts, web/__test__/store/notifications.test.ts
NOTIFICATION_FRAGMENT includes persistent; generated typings add clearNotificationByKey, MutationClearNotificationByKeyArgs, and persistent on all relevant types/fragments; Item.vue gates the archive button on !persistent, renders a pinned badge for persistent notifications, and reworks header layout with timestamp in header right-side block; CriticalNotifications.standalone.vue shows a static "clears automatically" message for persistent notifications instead of a dismiss button; store tests updated with persistent: false mock data.
Persistent-first list reordering and animations
web/src/components/Notifications/List.vue
New displayNotifications computed property reorders notifications to show persistent ones first while preserving API ordering within each group; template iteration updated to render from displayNotifications instead of notifications; new leave animation handler uses Element.animate with reduced-motion support and scoped CSS transitions.
Sidebar UI redesign and localization
web/src/components/Notifications/Sidebar.vue, web/src/locales/en.json
Sidebar.vue uses session-persisted filter state for importance and persistent visibility toggle; redesigns header/tabs with compact count badges and ghost-style action buttons that include Archive and Trash2 icons; updates NotificationsList props in both tabs; en.json adds strings for persistent notification labels and filter options.
Service tests
api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts
Test isolation changed to directly wipe filesystem directories with emptyDir to preserve persistent notifications across runs; createNotification helper forwards persistent and key fields; new integration tests verify archiveAll never archives persistent notifications and deleteNotifications preserves persistent notifications even after explicit archiving.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant NotificationsResolver
  participant NotificationsService
  participant FileSystem

  rect rgba(135, 206, 235, 0.5)
    Note over Client,FileSystem: Create persistent notification (idempotent)
    Client->>NotificationsResolver: createNotification(data { key, persistent: true })
    NotificationsResolver->>NotificationsService: createNotification(data)
    NotificationsService->>FileSystem: clearNotificationsByKey(data.key)
    FileSystem-->>NotificationsService: deleted matching unread files
    NotificationsService->>FileSystem: write new .notify file
    NotificationsService-->>Client: NotificationOverview
  end

  rect rgba(255, 165, 0, 0.5)
    Note over Client,FileSystem: Clear persistent notification by key
    Client->>NotificationsResolver: clearNotificationByKey(key)
    NotificationsResolver->>NotificationsService: clearNotificationsByKey(key)
    NotificationsService->>FileSystem: load unread, filter by key, delete matches
    NotificationsService-->>Client: NotificationOverview
  end

  rect rgba(220, 100, 100, 0.5)
    Note over Client,FileSystem: Archive blocked for persistent
    Client->>NotificationsResolver: archiveNotification(id)
    NotificationsResolver->>NotificationsService: archiveNotification(id)
    NotificationsService->>FileSystem: read .notify (persistent=true)
    NotificationsService-->>Client: 409 Conflict
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hoppity hop through the notify queue,
Some alerts stay persistent until conditions are through!
A key stamps each message with idempotent flair,
No archiving allowed — they'll resolve on their own with care.
The dismiss button hides when persistent is set,
A bunny's best feature: no lingering regret! 🔔

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 title accurately describes the main feature: persistent notifications that remain until their condition resolves, matching the comprehensive changes across API models, service logic, and web 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
  • Commit unit tests in branch feat/persistent-notifications

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 863661de40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts Outdated

@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

Caution

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

⚠️ Outside diff range comments (1)
api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (1)

633-638: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

archiveAll (no importance filter) still attempts persistent notifications.

This branch archives every unread ID, so persistent notifications hit the 409 path in archiveNotification instead of being skipped up front.

Proposed fix
  public async archiveAll(importance?: NotificationImportance) {
    const { UNREAD } = this.paths();

    if (!importance) {
-     await readdir(UNREAD).then((ids) => this.archiveIds(ids));
+     const unreadPaths = await this.listFilesInFolder(UNREAD);
+     const [loaded] = await this.loadNotificationsFromPaths(unreadPaths, {
+       type: NotificationType.UNREAD,
+     });
+     const archivableIds = loaded.filter((n) => !n.persistent).map((n) => n.id);
+     await this.archiveIds(archivableIds);
      return { overview: NotificationsService.overview };
    }
🤖 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 `@api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts`
around lines 633 - 638, In the archiveAll method of NotificationsService, when
importance is not provided, the method currently archives all unread
notification IDs without filtering out persistent notifications. This causes
persistent notifications to reach the archiveNotification method where they
encounter a 409 error instead of being skipped upfront. Filter the ids array
obtained from readdir(UNREAD) to exclude persistent notifications before passing
them to archiveIds. You can check if a notification is persistent by examining
its properties before including it in the archive operation.
🤖 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 `@api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts`:
- Around line 302-307: The createNotification method performs a
clear-then-create operation for keyed notifications that is not atomic, allowing
concurrent requests with the same key to bypass idempotency and create
duplicates. Refactor this method to wrap the clearNotificationsByKey and
notification creation operations in a single atomic database transaction, or
better yet, replace the separate clear and create calls with a single atomic
upsert operation at the database level that ensures only one notification with a
given key exists after the operation completes.

---

Outside diff comments:
In `@api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts`:
- Around line 633-638: In the archiveAll method of NotificationsService, when
importance is not provided, the method currently archives all unread
notification IDs without filtering out persistent notifications. This causes
persistent notifications to reach the archiveNotification method where they
encounter a 409 error instead of being skipped upfront. Filter the ids array
obtained from readdir(UNREAD) to exclude persistent notifications before passing
them to archiveIds. You can check if a notification is persistent by examining
its properties before including it in the archive operation.
🪄 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: f785abf8-da18-4d80-a7cb-f98beed713ee

📥 Commits

Reviewing files that changed from the base of the PR and between 3e8f61f and 863661d.

📒 Files selected for processing (11)
  • api/generated-schema.graphql
  • api/src/core/types/states/notification.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • web/__test__/store/notifications.test.ts
  • web/src/components/Notifications/CriticalNotifications.standalone.vue
  • web/src/components/Notifications/Item.vue
  • web/src/components/Notifications/graphql/notification.query.ts
  • web/src/composables/gql/gql.ts
  • web/src/composables/gql/graphql.ts

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.41985% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.86%. Comparing base (3e8f61f) to head (f155a56).

Files with missing lines Patch % Lines
...Notifications/CriticalNotifications.standalone.vue 0.00% 6 Missing ⚠️
.../resolvers/notifications/notifications.resolver.ts 55.55% 4 Missing ⚠️
...h/resolvers/notifications/notifications.service.ts 97.14% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2033      +/-   ##
==========================================
+ Coverage   52.77%   52.86%   +0.09%     
==========================================
  Files        1035     1035              
  Lines       72060    72232     +172     
  Branches     8293     8305      +12     
==========================================
+ Hits        38027    38183     +156     
- Misses      33907    33923      +16     
  Partials      126      126              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR2033/dynamix.unraid.net.plg

elibosley and others added 11 commits June 19, 2026 17:28
Persistent (condition-style) notifications now sort above the rest in the
notification list, regardless of timestamp, so sticky alerts stay visible.
Display-only stable sort; the seen-tracking watcher still uses the unsorted
list so it keys off the genuinely latest notification.

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

The .notify ini reader coerces persistent="true" into a boolean true, so the
strict string compare (persistent === 'true') always evaluated false and every
notification came back persistent:false. Normalize via String(persistent) so
both the coerced boolean and a literal "true" string resolve correctly.

Found on hardware: the bell rendered the archive button and skipped the
sticky-to-top sort because persistent was always false over GraphQL.

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

- Persistent notifications now render as a pinned card: orange left-accent
  border, subtle tint, and an uppercase "Pinned" badge in the header.
- Tighten the item typography to feel native to the webgui: text-sm base,
  muted/condensed description, smaller timestamp, less vertical gap.

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

Persistent notifications are no longer hard-locked. A single, explicit dismiss
is allowed but gated behind a confirmation that makes clear it only hides the
reminder (acknowledge) and does not resolve the underlying condition - which
re-pins if the producer raises it again (idempotent key).

- API: archiveNotification no longer rejects persistent notifications; bulk
  archiveAll still skips them so a casual "Archive all" can't hide pinned alerts.
- Web: pinned items show a subdued "Dismiss" (X) action that opens a warning
  dialog (useConfirm) before archiving.

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

- clearNotificationsByKey now removes matching entries from both unread AND
  archive, so dismissing then re-raising a keyed condition no longer leaves
  stale archived copies behind.
- Give every notification the card treatment (rounded, bordered, padded), with
  normal items as a neutral variant and persistent ones as the orange pinned
  variant. Dropped the list divider in favor of spaced cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…large solid buttons

Archive and Delete used the default large solid Button; switch them to the same
ghost + sm + muted styling as Dismiss so the row actions are proportionate and
the panel reads lighter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the janky leave animation (card snapped to full width then slid) with a
FLIP-based transition: the dismissed card slides out to the right and fades while
the remaining cards glide up to close the gap. Leaving card is taken out of flow
keeping its width + vertical position so siblings animate instead of jumping.
Respects prefers-reduced-motion.

Persistent items already render pinned (styling + top sort) in the archive tab
too, since both key off `persistent`, not the tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Cleaner title (xl, semibold, tracking-tight).
- Real segmented Unread/Archived tabs with an active pill (bg + shadow) and
  counts shown as subtle rounded pills instead of "(n)".
- Divider under the controls to separate them from the list; consistent px-4
  padding across header, tabs, filters, and list.

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

Replace the absolute/FLIP leave (card floated out of flow while siblings jumped)
with a Web Animations API leave hook: the dismissed card slides right and
collapses its own height/margins/padding/borders in one motion, so the rows
below flow up continuously. Honors prefers-reduced-motion. CSS still handles
enter + reorder (move) transitions.

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

Replace the bare red-hover text links with muted ghost buttons carrying an
Archive / Trash icon, matching the per-item actions. Delete all keeps the
destructive hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unfiltered "Archive all" path called archiveIds() over every unread
file with no persistent filter, so persistent (pinned) notifications were
swept into the archive. Only the per-importance path skipped them.

Unify both paths to load and filter out persistent notifications before
archiving. Add a regression test covering filtered and unfiltered
archiveAll.

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

@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.

🧹 Nitpick comments (2)
web/src/components/Notifications/List.vue (1)

123-125: ⚡ Quick win

Avoid as HTMLElement in transition hook.

This cast is avoidable; narrow the type at runtime before using element-specific APIs.

Proposed change
 function onLeave(el: Element, done: () => void) {
-  const node = el as HTMLElement;
+  if (!(el instanceof HTMLElement)) {
+    done();
+    return;
+  }
+  const node = el;

As per coding guidelines, “Avoid using casting whenever possible, prefer proper typing from the start.”

🤖 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 `@web/src/components/Notifications/List.vue` around lines 123 - 125, The
onLeave function uses an unsafe type cast with `as HTMLElement` to convert the
Element parameter to HTMLElement. Remove this cast and instead narrow the type
at runtime by checking if the element is an instance of HTMLElement before using
any HTMLElement-specific APIs on it. This approach ensures proper typing without
relying on casting, following the coding guideline to avoid casts whenever
possible.

Source: Coding guidelines

api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (1)

478-493: 💤 Low value

Consider batching deletes for consistency with other bulk operations.

The sequential await in the for-loop works but differs from the batchProcess pattern used elsewhere (e.g., archiveAll). While condition-style notifications are typically few, batching would improve consistency and handle edge cases with many keyed notifications more efficiently.

♻️ Suggested refactor using batchProcess
     public async clearNotificationsByKey(key: string): Promise<NotificationOverview> {
         // Sweep both states: a keyed condition may have been dismissed (archived) by
         // the user, so clearing/resolving it - or re-raising it idempotently - should
         // not leave stale copies behind in either unread or archive.
         for (const type of [NotificationType.UNREAD, NotificationType.ARCHIVE]) {
             const list = await this.getNotifications({
                 type,
                 offset: 0,
                 limit: Number.MAX_SAFE_INTEGER,
             });
-            for (const notification of list.filter((n) => n.key === key)) {
-                await this.deleteNotification({ id: notification.id, type });
-            }
+            const toDelete = list.filter((n) => n.key === key);
+            await batchProcess(toDelete, (notification) =>
+                this.deleteNotification({ id: notification.id, type })
+            );
         }
         return this.getOverview();
     }
🤖 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 `@api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts`
around lines 478 - 493, The clearNotificationsByKey method currently deletes
notifications sequentially with individual awaits in a for-loop, which differs
from the batchProcess pattern used elsewhere in the codebase like in archiveAll.
Instead of awaiting each deleteNotification call individually, refactor the
method to collect all notification IDs that need deletion (from both UNREAD and
ARCHIVE types where key matches), then use the batchProcess pattern to delete
them in batches for consistency and better efficiency.
🤖 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.

Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts`:
- Around line 478-493: The clearNotificationsByKey method currently deletes
notifications sequentially with individual awaits in a for-loop, which differs
from the batchProcess pattern used elsewhere in the codebase like in archiveAll.
Instead of awaiting each deleteNotification call individually, refactor the
method to collect all notification IDs that need deletion (from both UNREAD and
ARCHIVE types where key matches), then use the batchProcess pattern to delete
them in batches for consistency and better efficiency.

In `@web/src/components/Notifications/List.vue`:
- Around line 123-125: The onLeave function uses an unsafe type cast with `as
HTMLElement` to convert the Element parameter to HTMLElement. Remove this cast
and instead narrow the type at runtime by checking if the element is an instance
of HTMLElement before using any HTMLElement-specific APIs on it. This approach
ensures proper typing without relying on casting, following the coding guideline
to avoid casts whenever possible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9af5efba-f4cb-4b16-b00f-09053235fbba

📥 Commits

Reviewing files that changed from the base of the PR and between 9cb3126 and 48a00b3.

📒 Files selected for processing (6)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • web/src/components/Notifications/Item.vue
  • web/src/components/Notifications/List.vue
  • web/src/components/Notifications/Sidebar.vue
  • web/src/locales/en.json

elibosley and others added 9 commits June 19, 2026 21:46
The pinned card's heavy orange treatment (orange wash, thick orange border,
orange badge) read as alarming. Replace with a neutral elevated card: subtle
left accent, muted background, and a quiet "Pinned" pill with a Pin icon.

Rename the persistent action from "Dismiss" to "Archive" (it archives, gated
behind a confirm) and use the archive-box icon for consistency with the
regular archive action. Rename confirmDismiss i18n keys to confirmArchive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restore the orange pinned-notification treatment (preferred) and instead
recolor the per-notification "View" link from text-primary (orange) to a
neutral secondary-foreground with a foreground hover, on all notifications.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deleteNotifications() emptied the entire directory and zeroed the overview,
so the "Delete all" action (deleteArchivedNotifications) wiped persistent
notifications too. Make it selective: load entries, delete only the
non-persistent ones, and recompute stats from what remains.

Tests previously relied on deleteAllNotifications() for isolation; since
that now preserves persistent entries, switch setup/teardown to a hard
emptyDir wipe so persistent test data can't leak between tests. Add a
regression test covering Delete-all on the archive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persistent (condition-style) notifications are re-raised by their producer
on every page load, so archiving them is futile — the keyed re-raise clears
the archived copy and re-pins them. Remove the archive action entirely for
persistent items; they clear only when their condition resolves (by key).

Drops the now-unused confirm-archive flow and its i18n keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pinned (persistent) notifications re-fire on every page load and can't be
archived, so a long-running condition (e.g. multiple PR test plugins) can
crowd the unread list. Add a "Pinned" toggle next to the importance filter,
on by default, that filters persistent notifications out of the list (both
unread and archive tabs) when switched off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the Pinned toggle in the same segmented container and use the same
neutral active/inactive button styles as the importance filter, instead of
the standalone orange chip, so the two filters read as one consistent row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The importance filters are text-only, so remove the Pin icon (and its
gap) from the Pinned toggle to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the user-facing badge and filter from "Pinned" to "Active" (clearer
that these track an ongoing condition, not a user-pinned item). Keep
`persistent` as the internal data/API term; rename the UI state/prop to
showPersistent to map directly to the field it filters on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Importance and "Active" filter selections were plain refs, so they reset on
every webgui page navigation (each a full reload). Back them with
sessionStorage so the chosen filters survive page changes within the tab.
'' is the All-Types sentinel, mapped back to undefined for the query.

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

@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

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/Notifications/Item.vue (1)

118-127: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Timestamp text should use the same fallback as the tooltip.

Line 126 renders only reformattedTimestamp. If timestamp is missing but formattedTimestamp exists, the visible timestamp is empty even though Line 118 has a fallback.

Suggested fix
-        <p class="text-secondary-foreground text-xs whitespace-nowrap">{{ reformattedTimestamp }}</p>
+        <p class="text-secondary-foreground text-xs whitespace-nowrap">
+          {{ formattedTimestamp ?? reformattedTimestamp }}
+        </p>
🤖 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 `@web/src/components/Notifications/Item.vue` around lines 118 - 127, The
timestamp display text on line 126 uses only reformattedTimestamp and will be
empty if timestamp is missing but formattedTimestamp exists, even though the
tooltip attribute on line 118 has a proper fallback pattern. Update the
paragraph element that displays the timestamp to use the same fallback logic as
the title attribute: change the template binding from reformattedTimestamp alone
to formattedTimestamp ?? reformattedTimestamp to ensure consistent behavior
between the visible timestamp and the tooltip.
🤖 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 `@web/src/components/Notifications/Sidebar.vue`:
- Around line 48-53: Remove the two explanatory comments in the filter state
setup section in Sidebar.vue. The first comment beginning with "Filter
selections persist for the browser session" above the importance
useSessionStorage declaration and the second comment starting with "Persistent
("Active") notifications are shown by default" above the showPersistent
useSessionStorage declaration should both be deleted. Keep the variable
declarations themselves (importance and showPersistent) intact, removing only
the accompanying comments to align with the repository standards that discourage
unnecessary explanatory comments.

---

Outside diff comments:
In `@web/src/components/Notifications/Item.vue`:
- Around line 118-127: The timestamp display text on line 126 uses only
reformattedTimestamp and will be empty if timestamp is missing but
formattedTimestamp exists, even though the tooltip attribute on line 118 has a
proper fallback pattern. Update the paragraph element that displays the
timestamp to use the same fallback logic as the title attribute: change the
template binding from reformattedTimestamp alone to formattedTimestamp ??
reformattedTimestamp to ensure consistent behavior between the visible timestamp
and the tooltip.
🪄 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: 662628d2-3094-4b3d-a20a-6bd3df480a22

📥 Commits

Reviewing files that changed from the base of the PR and between 84db37c and 3aabb66.

📒 Files selected for processing (6)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • web/src/components/Notifications/Item.vue
  • web/src/components/Notifications/List.vue
  • web/src/components/Notifications/Sidebar.vue
  • web/src/locales/en.json
✅ Files skipped from review due to trivial changes (1)
  • web/src/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/components/Notifications/List.vue
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts

Comment thread web/src/components/Notifications/Sidebar.vue
Make the status tab controlled via v-model backed by sessionStorage so the
selected Unread/Archived view sticks across webgui page navigations, matching
the importance and Active filters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
elibosley and others added 7 commits June 19, 2026 23:17
- Make Archive-all / Delete-all icon-only (title + sr-only) so it no longer
  dominates the tab row.
- Merge the importance chips and the Active toggle into a single segmented
  bar with a divider; shorten "All Types" to "All" and scroll horizontally
  on narrow widths so filters never wrap to a second line.
- Move the set-once notification Settings link out of the filter row into a
  slim footer pinned at the bottom of the panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Restore the text label on Archive-all / Delete-all (still compact:
  text-xs, h-8, tight padding) so the action is self-explanatory.
- Move the notification Settings gear out of the footer and back into the
  header title row (offset left of the sheet close button); drop the footer
  to reclaim the vertical space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gear floated in the top-right near the sheet's X close button without
aligning to it, looking cluttered. Place it inline right after the
"Notifications" title (size-7, muted) so it aligns with the title and stays
clear of the close button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add reconcileBannerNotifications(currentGeneration) mutation. It clears any
unread "banner-" keyed notification whose stored generation no longer matches
the page's current page-load generation - i.e. a legacy JS banner (CA, boot
checks, ...) whose producer stopped rendering it. Non-banner keys (PR plugins,
reboot-required) own their lifecycle and are untouched.

The notification drawer calls it on open with the page's window.bannerGen (set
by the webgui), so reconciliation happens authoritatively in the backend when
notifications are loaded, instead of via a page-side timer. Reads the new `gen`
ini field. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A notification with no description (or no subject) rendered an empty line,
leaving an awkward blank row. Guard both with v-if so only populated fields
take vertical space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drop @isnotempty from the description field (Notification + NotificationData).
  Banner/condition notifications legitimately have no description, and requiring
  one made them fail validation and render as "invalid". Empty is now valid; the
  UI hides the line.
- Reconcile stale banner notifications on the indicator's mount (deferred until
  the page settles), so the badge self-corrects on every navigation, not only
  when the bell is opened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- List.vue onLeave: narrow the transition element via `instanceof HTMLElement`
  instead of an `as HTMLElement` cast (per coding guidelines).
- Item.vue: render `reformattedTimestamp || formattedTimestamp` so the visible
  timestamp isn't empty when `timestamp` is missing but `formattedTimestamp`
  exists, matching the tooltip fallback.

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

Copy link
Copy Markdown
Member Author

CodeRabbit fix-loop resolutions (review-body / outside-diff findings):

  • archiveAll (no-importance) still attempts persistent (~633) — already fixed. archiveAll was unified so both the unfiltered and per-importance paths loadNotificationsFromPaths + filter((n) => !n.persistent) before archiving, so persistent notifications are skipped up front and never hit the 409 path.
  • as HTMLElement cast in List.vue onLeave (123) — fixed in 7dff141df: narrowed via el instanceof HTMLElement.
  • Item.vue timestamp fallback (126) — fixed in 7dff141df: now renders reformattedTimestamp || formattedTimestamp.
  • Batch deletes in clearNotificationsByKey (478, low value) — skipping. The deletes run sequentially on purpose: each deleteNotification mutates the shared in-memory overview counter, so parallelizing via batchProcess would race on that state. Keyed/condition sets are tiny, so sequential is both safe and clearer.

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