Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Nov 22, 2025

Summary

  • introduce a reusable BackgroundJobsService for tracking long-running operations
  • use the shared job helper for notification archive/delete jobs and status lookup
  • register the background job service with the resolver module for future reuse

Testing

  • pnpm lint-staged

Codex Task

Summary by CodeRabbit

  • New Features
    • Archive and delete operations now run as background jobs with live progress/status updates.
    • UI polls job status and shows real-time processing (e.g., "Processing X / Y").
    • Delete button shown for all notifications.
    • New "delete unread notifications" flow with confirmation dialog.
    • Controls disable while jobs are active to prevent duplicate actions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 2025

Walkthrough

Introduces background job support for notifications: new BackgroundJobsService, NotificationJob types, async mutations (startArchiveAll/startDeleteAll) and polling-based frontend UI updates; existing delete mutation was nested under notifications.delete.

Changes

Cohort / File(s) Summary
GraphQL Mutation Wiring
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts, api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
Added NotificationMutations type and exposed notifications on root mutations; resolver returns NotificationMutations instance.
Notification Job Models
api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
Added NotificationJobState and NotificationJobOperation enums, NotificationJob type, and optional job field on Notifications; registered enums for GraphQL.
Notification Mutations Resolver
api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
New NotificationMutationsResolver with mutations: delete(id,type): NotificationOverview, startArchiveAll(importance?): NotificationJob, startDeleteAll(type?): NotificationJob; guarded by DELETE_ANY permission.
Notification Resolvers & Service
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts, api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
Added job resolve field; NotificationsService now depends on BackgroundJobsService and exposes startDeleteAllJob, startArchiveAllJob, and getJob with job lifecycle handling.
Background Jobs Core
api/src/unraid-api/graph/services/background-jobs.service.ts
New BackgroundJobsService implementing in-memory BackgroundJob management: create/update/get/run job APIs, generic types, logging and error handling.
Module Registration
api/src/unraid-api/graph/resolvers/resolvers.module.ts
Registered NotificationMutationsResolver and BackgroundJobsService in module providers.
Frontend: Components
web/src/components/Notifications/Item.vue, web/src/components/Notifications/Sidebar.vue
Item.vue: removed v-if that hid delete button for ARCHIVE. Sidebar.vue: replaced direct archive/delete calls with job-based mutations, added polling (jobPollers, activeJobs, activeStates), job lifecycle helpers, button disabling, and cleanup on unmount.
Frontend: GraphQL
web/src/components/Notifications/graphql/notification.query.ts
Added NOTIFICATION_JOB_FRAGMENT; replaced archiveAllNotificationsStartArchiveAllNotifications, deleteArchivedNotificationsStartDeleteNotifications; nested deleteNotification under notifications.delete; added notificationJobStatus query.
Frontend: Localization
web/src/locales/en.json
Added translation keys for unread deletion confirmation and processing status (notifications.sidebar.confirmDeleteUnread.*, notifications.sidebar.processingStatus).

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend (Sidebar.vue)
    participant Apollo as Apollo (GraphQL)
    participant Resolver as Notifications Mutation Resolver
    participant Service as Notifications Service
    participant BgJobs as BackgroundJobsService

    User->>Frontend: Trigger "Archive All"
    Frontend->>Apollo: startArchiveAll(importance?)
    Apollo->>Resolver: startArchiveAll mutation
    Resolver->>Service: startArchiveAllJob(importance?)
    Service->>BgJobs: createJob(...) & runJob(...)
    BgJobs-->>Service: Job (QUEUED / RUNNING updates)
    Service-->>Resolver: NotificationJob
    Resolver-->>Apollo: NotificationJob (response)
    Apollo-->>Frontend: Job returned (id,state,processed,total)
    Frontend->>Frontend: start polling (notificationJobStatus(jobId))
    loop Poll until SUCCEEDED/FAILED
        Frontend->>Apollo: notificationJobStatus(jobId)
        Apollo->>BgJobs: getJob(jobId)
        BgJobs-->>Apollo: current job state
        Apollo-->>Frontend: updated job
        Frontend->>Frontend: update UI, stop when complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to: BackgroundJobsService async behavior and state transitions; NotificationsService job integration and race conditions; permission guards and error propagation in NotificationMutationsResolver; polling lifecycle and interval cleanup in Sidebar.vue; GraphQL schema changes that alter existing mutation paths.

Poem

🐰✨ I hop and queue the jobs tonight,
States progress steady, green to bright.
Polling beats keep tasks in sight,
Archiving, deleting — all done right.
The rabbit rests; the inbox light. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: introducing a shared BackgroundJobsService and refactoring notification jobs to use it, which aligns with the primary objectives and file changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/update-notifications-view-for-bulk-actions

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

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/mutation/mutation.resolver.ts (1)

3-12: New notifications mutation namespace matches existing pattern; fix Prettier import issue

The new notifications() mutation follows the existing namespaced style (array/docker/vm) and cleanly exposes NotificationMutations.

CI is failing on a Prettier check complaining about NotificationMutations in the import block. Ensure the import list includes it with the expected formatting (and then rerun pnpm lint-staged):

-import {
-    ApiKeyMutations,
-    ArrayMutations,
-    DockerMutations,
-    ParityCheckMutations,
-    RCloneMutations,
-    RootMutations,
-    VmMutations,
-} from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
+import {
+    ApiKeyMutations,
+    ArrayMutations,
+    DockerMutations,
+    NotificationMutations,
+    ParityCheckMutations,
+    RCloneMutations,
+    RootMutations,
+    VmMutations,
+} from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';

Also applies to: 46-49

🧹 Nitpick comments (11)
api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (2)

62-90: Job helper wrappers around BackgroundJobsService look good; consider nullability alignment

createJob, updateJob, and runJob cleanly encapsulate the interaction with BackgroundJobsService, and getJob provides a simple lookup surface.

If backgroundJobs.getJob can return undefined for unknown IDs, consider changing the return type of getJob (and any callers) to NotificationJob | null to better reflect actual behavior and avoid surprises at call sites.


359-381: Clarify queued IDs vs paths and make fire‑and‑forget intent explicit

The async job runners look correct, but two small refactors would make them less error‑prone:

  1. Use IDs instead of full paths in the delete queue

listFilesInFolder returns absolute paths; pushing those into queue.id relies on path.join’s behavior with absolute paths in deleteNotification. To avoid this subtle coupling, derive IDs from the paths before queueing:

-        for (const targetType of targets) {
-            const ids = await this.listFilesInFolder(this.paths()[targetType]);
-            ids.forEach((id) => queue.push({ id, type: targetType }));
-        }
+        for (const targetType of targets) {
+            const paths = await this.listFilesInFolder(this.paths()[targetType]);
+            paths.forEach((path) =>
+                queue.push({ id: this.getIdFromPath(path), type: targetType })
+            );
+        }
  1. Make ignored job Promise explicit

Since you’re intentionally fire‑and‑forget, prefix runJob with void in both starters to avoid unhandled‑promise‑lint noise:

-        setImmediate(() =>
-            this.runJob(job, async () => {
+        setImmediate(() =>
+            void this.runJob(job, async () => {
                 // ...
             })
         );

Apply analogous void usage in startArchiveAllJob.

Also applies to: 545-572

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

14-18: Align job field nullability between GraphQL and TypeScript

The new job resolve field cleanly exposes NotificationsService.getJob, but the decorator marks it as nullable: true while the TS return type is NotificationJob. If an unknown ID results in undefined/null from the service (or if you later change it to do so), this will conflict with the non‑nullable TS type.

Consider updating both the resolver and service signatures to return NotificationJob | null (or making the schema non‑nullable) so the implementation, TypeScript types, and GraphQL schema all agree.

Also applies to: 45-48

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

18-60: Resolver shape and permissions for notification mutations look appropriate

The NotificationMutationsResolver correctly targets NotificationMutations with @Resolver, and exposes delete, startArchiveAll, and startDeleteAll as @ResolveFields. Each field is guarded with UsePermissions({ action: AuthAction.DELETE_ANY, resource: Resource.NOTIFICATIONS }), matching the expected authorization semantics for destructive notification operations. Delegation to NotificationsService keeps business logic out of the resolver.

One note: given the background job implementation, the startDeleteAll check for job.state === NotificationJobState.FAILED will currently never trigger because the work runs asynchronously after the job is returned. It’s harmless, but you may consider either letting the service throw on synchronous failures or documenting that job failure is surfaced via polling the job state, not via this mutation.

api/src/unraid-api/graph/services/background-jobs.service.ts (1)

5-93: BackgroundJobsService implementation is sound and matches notification job usage

The generic job model (BackgroundJob), in‑memory storage via jobs Map, and createJob/updateJob/getJob/runJob APIs all line up with how NotificationsService is using them (create + async runJob + later getJob). State transitions and error logging in runJob are handled cleanly, and AppError 404 on unknown job IDs is appropriate for consumers.

If you expect a high volume of background jobs over the lifetime of a node, consider adding an optional eviction mechanism (e.g., TTL-based cleanup of completed/failed jobs or a max-size LRU) to keep the in‑memory jobs map from growing unbounded.

web/src/components/Notifications/graphql/notification.query.ts (1)

81-89: Delete-all mutation wiring looks correct, but confirm argument semantics

StartDeleteNotifications calls notifications.startDeleteAll(type: $type) and returns a NotificationJob. This matches the job-based delete flow; just make sure:

  • NotificationType is indeed the correct enum for the type argument.
  • Passing null (if ever done) is supported by the server, since $type is not marked non-null here.

If the server requires a type, consider making $type: NotificationType! to catch mistakes at compile time.

web/src/components/Notifications/Sidebar.vue (5)

2-3: Explicit Vue imports are acceptable but not required

Given Nuxt’s auto-import setup, importing computed, onBeforeUnmount, reactive, and ref is optional. Keeping them is fine for clarity and doesn’t introduce issues.


57-72: Job state containers are well-structured

jobPollers and activeJobs track polling intervals and job metadata per operation (archiveAll, deleteArchived, deleteUnread), and activeStates centralizes what counts as “in progress”. This layout makes the subsequent helpers straightforward.


109-123: Job activity and label helpers correctly drive UI state

isJobActive and jobLabel both derive from activeStates and activeJobs, and jobLabel delegates to the new notifications.sidebar.processingStatus string when in progress, falling back to static button text otherwise. Once the i18n placeholder syntax is fixed, this will give clear live feedback.


246-248: Unmount hook correctly cleans up polling intervals

onBeforeUnmount iterates over jobPollers and clears any existing intervals, preventing timer leaks when the sidebar is closed or the component is destroyed. This is sufficient; no need to also null out the entries at this point.


287-316: Buttons correctly reflect job state and show progress labels

In both tabs:

  • Buttons are disabled when the corresponding mutation is loading or isJobActive(...) indicates an in-progress job.
  • Labels use jobLabel(...) to swap from static text to “Processing …” with live counts.

Once the i18n string is fixed, users will see accurate progress feedback and won’t be able to start overlapping jobs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31af99e and 53851e4.

📒 Files selected for processing (12)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (3 hunks)
  • api/src/unraid-api/graph/services/background-jobs.service.ts (1 hunks)
  • web/src/components/Notifications/Item.vue (0 hunks)
  • web/src/components/Notifications/Sidebar.vue (7 hunks)
  • web/src/components/Notifications/graphql/notification.query.ts (3 hunks)
  • web/src/locales/en.json (1 hunks)
💤 Files with no reviewable changes (1)
  • web/src/components/Notifications/Item.vue
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
Learnt from: pujitm
Repo: unraid/api PR: 941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.
📚 Learning: 2025-01-31T22:01:22.708Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:24-27
Timestamp: 2025-01-31T22:01:22.708Z
Learning: The Unraid UI uses a modern notification system with a custom `unraid-toaster` component replacing the legacy jGrowl notifications. The system is backed by a comprehensive GraphQL API with real-time subscription support for notification updates.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2025-01-30T19:56:42.055Z
Learnt from: pujitm
Repo: unraid/api PR: 1075
File: web/components/Notifications/Sidebar.vue:52-57
Timestamp: 2025-01-30T19:56:42.055Z
Learning: Notification links in the Unraid API are validated at creation time on the server side, making additional client-side validation unnecessary when receiving notifications through GraphQL subscriptions.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • web/src/components/Notifications/graphql/notification.query.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-01T17:52:55.311Z
Learnt from: pujitm
Repo: unraid/api PR: 941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.

Applied to files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
  • web/src/components/Notifications/Sidebar.vue
  • web/src/components/Notifications/graphql/notification.query.ts
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.

Applied to files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-06T20:59:06.096Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:87-89
Timestamp: 2024-11-06T20:59:06.096Z
Learning: Error handling in mutations like `addRoleForUser` in `auth.resolver.ts` is performed at the service level, so additional error handling in the resolver is unnecessary.

Applied to files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
📚 Learning: 2025-08-29T19:48:45.517Z
Learnt from: elibosley
Repo: unraid/api PR: 1618
File: web/components/Logs/SingleLogViewer.vue:248-299
Timestamp: 2025-08-29T19:48:45.517Z
Learning: In Vue Apollo's composition API, subscribeToMore automatically handles subscription cleanup when the related query is stopped or the component unmounts, unlike React Apollo where manual cleanup is required. Manual unsubscribe is only needed when changing subscription variables, not for lifecycle management.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
Repo: unraid/api PR: 1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-09T15:47:29.325Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-24
Timestamp: 2024-12-09T15:47:29.325Z
Learning: In our Nuxt setup using Vue.js, components defined within the codebase are autoloaded and do not require explicit import statements in the script section. For example, components like `TabsList` and `TabsTrigger` used in `web/components/Notifications/TabList.vue` are automatically available without imports.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-06T14:43:04.876Z
Learnt from: pujitm
Repo: unraid/api PR: 974
File: web/components/Notifications/List.vue:31-31
Timestamp: 2024-12-06T14:43:04.876Z
Learning: In the Vue.js application using `useQuery` from Vue Apollo, the `error` reactive variable captures errors from both the initial query and any `fetchMore` calls, so wrapping `fetchMore` in a try-catch block is unnecessary.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2025-08-23T16:27:30.015Z
Learnt from: elibosley
Repo: unraid/api PR: 1609
File: web/components/ApiKey/ApiKeyCreate.vue:414-418
Timestamp: 2025-08-23T16:27:30.015Z
Learning: In Vue 3 templates, refs are automatically unwrapped for reading but NOT for assignment. Writing `formData = newValue` in a template replaces the ref object and breaks reactivity, while `formData.value = newValue` in script setup properly updates the ref's value.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
🧬 Code graph analysis (6)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (1)
api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts (5)
  • ObjectType (99-120)
  • ObjectType (122-131)
  • ObjectType (133-174)
  • ObjectType (176-190)
  • ObjectType (192-211)
api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts (3)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (14-50)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3)
  • Resolver (21-172)
  • overview (41-43)
  • job (46-48)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/services/background-jobs.service.ts (1)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (1)
  • job (46-48)
api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (1)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (2)
  • job (46-48)
  • notifications (34-38)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
web/composables/gql/graphql.ts (1)
  • Mutation (1199-1239)
web/src/components/Notifications/graphql/notification.query.ts (2)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (1)
  • deleteNotification (71-79)
api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (1)
  • deleteNotification (325-340)
🪛 GitHub Actions: CI - Main (API)
api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts

[error] 12-12: "NotificationMutations" is not exported by "src/unraid-api/graph/resolvers/notifications/notifications.model.ts", imported by "src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts".

web/src/components/Notifications/Sidebar.vue

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts

[error] 7-7: Prettier formatting failed. Insert NotificationMutations, (prettier/prettier).

🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts

[failure] 212-212:
Delete

🔇 Additional comments (12)
api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts (1)

17-27: Notification job enums and GraphQL registration look consistent

Enum values and registerEnumType wiring match the existing notification enums pattern and should integrate cleanly with the new job service and schema.

Also applies to: 38-45

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

18-21: DI wiring for notification mutations and background jobs looks correct

Registering NotificationMutationsResolver and BackgroundJobsService here matches how other mutation namespaces and shared services are exposed and should give NotificationsService and related resolvers access to the shared job helper.

Also applies to: 33-35, 62-66

api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (2)

43-47: NotificationMutations grouping type looks consistent

The new NotificationMutations object type and description align with the existing mutation namespaces (array/docker/vm/etc.). No issues from a schema/typing standpoint.


68-69: RootMutations.notifications field wiring is correct

The notifications field follows the same pattern as other grouped mutation fields (type, description, default instance). This will integrate cleanly with RootMutationsResolver.notifications().

web/src/locales/en.json (1)

307-309: Confirm-delete-unread copy looks consistent with existing patterns

Title, description, and confirm text are clear and match the destructive nature and scope (“all unread notifications”) of the action. No changes needed.

web/src/components/Notifications/graphql/notification.query.ts (3)

26-35: Job fragment fields cover polling needs

NotificationJobFragment exposes id, operation, state, processed, total, and error – everything the client-side poller needs without overfetching. Looks good.


71-79: I'll check the generated GraphQL schema to finalize the verification:

Archive-all mutation correctly returns a job

In GraphQL inputs such as field arguments, nullable types are always optional, and the resolver confirms this: importance is marked as nullable: true in the NestJS @Args() decorator, and the TypeScript parameter is optional (importance?: NotificationImportance). The client-side mutation correctly expresses this nullability—$importance: NotificationImportance without a ! suffix allows the argument to be omitted or null. The mutation calls notifications.startArchiveAll and returns NotificationJobFragment, matching the background job model.


110-118: Fix missing GraphQL type specification on job resolver argument

The job field resolver at api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts:46 is missing the GraphQL type specification for the id argument.

Currently:

public job(@Args('id') jobId: string): NotificationJob

Should be:

public job(@Args('id', { type: () => ID }) jobId: string): NotificationJob

This inconsistency with other @Args('id', { type: () => ... }) usages in the same file (lines 72, 90, 116) may cause incorrect schema generation or type inference issues. The query contract expects ID! type, which must be explicitly declared in the decorator.

⛔ Skipped due to learnings
Learnt from: elibosley
Repo: unraid/api PR: 942
File: api/src/unraid-api/auth/api-key.service.ts:62-70
Timestamp: 2024-11-05T14:49:07.308Z
Learning: In `api/src/unraid-api/auth/api-key.service.ts`, when handling read errors in the `findById` method, throw a `GraphQLError` instead of an `InternalServerErrorException`.
Learnt from: elibosley
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.
Learnt from: pujitm
Repo: unraid/api PR: 1219
File: api/src/unraid-api/main.ts:18-25
Timestamp: 2025-03-07T19:29:12.540Z
Learning: When handling type mismatches between dependencies in TypeScript (such as between NestJS and Fastify), prefer using `ts-expect-error` with a detailed explanation comment instead of `as any` type assertions. This better documents the reason for the type bypass and makes the issue more visible for future maintenance.
web/src/components/Notifications/Sidebar.vue (4)

50-56: Perfect! The project uses @vue/apollo-composable: 4.2.2. Now let me verify the loading ref semantics for that specific version.

Loading ref semantics verified—code is correct

The loading ref tracks the progress of the mutation in @vue/apollo-composable, and your code correctly destructures and uses it. The pattern is standard and consistent with the documented API for version 4.2.2.


134-149: Archive-all confirmation correctly handles importance filtering

confirmAndArchiveAll:

  • Uses the existing confirm dialog with archive-all translations.
  • Calls startArchiveAllJob with the current importance filter value.
  • When importance is null or omitted, the mutation passes an empty filter object {} to loadNotificationsFromPaths, which processes all notification importances as intended.
  • Seeds job tracking via setJob('archiveAll', job) and starts polling.

This matches the background job model and disables the button while the job is active via other helpers.


151-183: Verification confirms consistent enum usage

The enum values match exactly across frontend and backend:

  • Backend: NotificationType enum defined in api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts with UNREAD = 'UNREAD' and ARCHIVE = 'ARCHIVE'
  • Frontend: Generated from GraphQL schema in web/src/composables/gql/graphql.ts with identical values
  • The startDeleteAll resolver in api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts accepts type?: NotificationType, and both function calls pass the correct enum members
  • Job state tracking with distinct keys (deleteArchived, deleteUnread) aligns with the implementations

1-248: Request manual verification of Prettier formatting

The sandbox environment lacks the necessary dependencies to run Prettier, so I cannot verify the formatting issue. Please run the formatter locally in your development environment using one of these commands:

pnpm prettier --write web/src/components/Notifications/Sidebar.vue

Then commit any formatting changes that result. This will ensure your code complies with the project's Prettier configuration before merging.

Comment on lines +8 to +15
import {
NotificationImportance,
NotificationJob,
NotificationJobState,
NotificationMutations,
NotificationOverview,
NotificationType,
} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix NotificationMutations import to resolve CI failure

CI error shows NotificationMutations is not exported from notifications.model.ts, and in this file it is imported from that module. The type is actually defined in mutation.model.ts, so this import is invalid and breaks the build.

Update the imports to pull NotificationMutations from the correct module:

-import {
-    NotificationImportance,
-    NotificationJob,
-    NotificationJobState,
-    NotificationMutations,
-    NotificationOverview,
-    NotificationType,
-} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
+import {
+    NotificationImportance,
+    NotificationJob,
+    NotificationJobState,
+    NotificationOverview,
+    NotificationType,
+} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
+import { NotificationMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
🧰 Tools
🪛 GitHub Actions: CI - Main (API)

[error] 12-12: "NotificationMutations" is not exported by "src/unraid-api/graph/resolvers/notifications/notifications.model.ts", imported by "src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts".

🤖 Prompt for AI Agents
In
api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
around lines 8 to 15, the import list incorrectly pulls NotificationMutations
from notifications.model.js (which doesn't export it) causing CI to fail; remove
NotificationMutations from the import statement here and add an import of
NotificationMutations from the correct module (mutation.model.ts) using the
appropriate relative path, then run type-check/build to verify the error is
resolved.

Comment on lines +56 to 69
export const deleteNotification = graphql(/* GraphQL */ `
mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {
notifications {
delete(id: $id, type: $type) {
archive {
total
}
unread {
total
}
}
}
}
`);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Ensure deleteNotification matches new schema and all call sites

This mutation now goes through the nested notifications { delete(...) } field and only fetches archive/unread totals. That’s fine as long as:

  • The GraphQL schema actually exposes Mutation.notifications.delete(id, type) with an object that has archive.total and unread.total.
  • All TS types and call sites that previously expected a flat deleteNotification payload (e.g., a NotificationFragment or different shape) have been updated.

Please double-check codegen output and usages of deleteNotification in the web app to ensure there are no runtime GraphQL errors or type mismatches.


🏁 Script executed:

#!/bin/bash
# Inspect all usages of the deleteNotification mutation to ensure they match the new shape.
rg -n "deleteNotification\(" web -S -C3

Length of output: 9084


I need to inspect the actual source file to verify the current state of the code.The web search didn't provide specific information about the file in the repository. Let me directly inspect the file to check the actual code.

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Directly inspect the file mentioned in the review comment
cat "web/src/components/Notifications/graphql/notification.query.ts" | head -100

Length of output: 2090


I've found a critical discrepancy. The source mutation definition uses notifications { delete(...) } with both archive.total and unread.total, but the generated codegen in web/src/composables/gql/gql.ts shows a flat structure with only archive.total. Let me find where this mutation is actually called to assess the impact.

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find actual usage of deleteNotification in Vue/TypeScript components and composables
rg -n "deleteNotification" web/src -A5 -B5 --type ts --type vue -g '!gql.ts' | head -150

Length of output: 27


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Search for deleteNotification usage excluding the codegen file, broader search
rg -n "deleteNotification" web/src -A3 -B3 --type ts -g '!gql.ts' 2>/dev/null || rg -n "deleteNotification" web/src -A3 -B3 -g '!gql.ts' -g '!*.d.ts'

Length of output: 11568


Now let me find where this mutation is actually called in components:


🏁 Script executed:

#!/bin/bash
# Search for where deleteNotification is used in Vue/component files
rg -n "deleteNotification" web/src/components -A5 -B5

Length of output: 4219


Let me see the complete source mutation definition:


🏁 Script executed:

#!/bin/bash
# Get the complete deleteNotification mutation from source file
sed -n '56,70p' "web/src/components/Notifications/graphql/notification.query.ts"

Length of output: 300


Regenerate codegen to match the updated mutation schema

The source mutation in web/src/components/Notifications/graphql/notification.query.ts (lines 56–69) has been updated to use a nested notifications { delete(...) } structure with both archive.total and unread.total fields. However, the generated TypeScript types in web/src/composables/gql/graphql.ts still reflect an older flat structure with only archive.total. This type mismatch will cause runtime GraphQL errors when the component in web/src/components/Notifications/Item.vue executes the mutation.

The codegen must be regenerated to produce types that match the actual GraphQL source mutation, and the Apollo cache handler at web/src/helpers/apollo-cache/index.ts:176 should be reviewed to ensure it properly handles the updated response structure.

Comment on lines 24 to 33
import ConfirmDialog from '~/components/ConfirmDialog.vue';
import {
archiveAllNotifications,
deleteArchivedNotifications,
startDeleteNotifications,
NOTIFICATION_FRAGMENT,
NOTIFICATION_JOB_FRAGMENT,
notificationsOverview,
notificationJobStatus,
resetOverview,
} from '~/components/Notifications/graphql/notification.query';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

GraphQL and Apollo imports align with new job-based flow

Pulling in startDeleteNotifications, NOTIFICATION_JOB_FRAGMENT, and notificationJobStatus plus NotificationImportance, NotificationJobState, NotificationType, and useApolloClient matches how the component now manages background jobs and polling. Import structure looks coherent.


🌐 Web query:

Confirm that the generated GraphQL typings expose `NotificationJobState`, `NotificationType`, and the new NotificationJob-related operations used here.

💡 Result:

I don’t have the generated typings or the code you’re referring to. Please either

  • paste the generated typings (or the file path / repo link), or
  • paste the GraphQL schema and the client-side code that uses NotificationJob-related operations,

and I’ll confirm whether NotificationJobState, NotificationType, and the new operations are exposed and match the usages.


I need to search the codebase for the GraphQL schema and generated typings to verify the exports. Let me investigate.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find GraphQL query files and typings in the notifications directory
find . -path "*/Notifications/*" -type f \( -name "*.query.*" -o -name "*.graphql*" -o -name "*.gql" -o -name "*types*" \) 2>/dev/null | head -20

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Search for the notification.query file and related typings
fd -e ts -e js -e graphql "notification" --type f | grep -i "query\|type\|schema" | head -20

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Look for generated GraphQL types directory
fd -t d "generated\|__generated__\|types" --max-depth 3 | head -20

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the notification.query.ts file to see actual exports
cat ./web/src/components/Notifications/graphql/notification.query.ts

Length of output: 2689


🏁 Script executed:

#!/bin/bash
# Look for TypeScript declaration files (.d.ts) and type definitions
fd -e "d.ts" --type f | head -20

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Search for NotificationJobState and NotificationType definitions in the codebase
rg -n "NotificationJobState|NotificationType" --type ts --type js -B2 -A2 | head -40

Length of output: 4459



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for NotificationJobState with case-insensitive matching
rg -in "NotificationJobState" --type ts --type js -B1 -A1

Length of output: 5018


🏁 Script executed:

#!/bin/bash
# Search for NotificationImportance
rg -n "NotificationImportance" --type ts --type js -B1 -A1 | head -30

Length of output: 1966


🏁 Script executed:

#!/bin/bash
# Check what types are exported from graphql.ts
grep -n "^export" ./web/composables/gql/graphql.ts | head -50

Length of output: 2267


🏁 Script executed:

#!/bin/bash
# Look for the state field type in NOTIFICATION_JOB_FRAGMENT context
rg -n "NotificationJob\s*{" ./web/composables/gql/graphql.ts -A 10

Length of output: 101



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for NotificationJobState in the generated graphql.ts file
grep -n "NotificationJobState" ./web/composables/gql/graphql.ts | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if useApolloClient is available (may be from Vue Apollo or local composable)
rg -n "useApolloClient" --type ts --type js | head -10

Length of output: 556



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the exact import in Sidebar.vue
sed -n '40,50p' ./web/src/components/Notifications/Sidebar.vue

Length of output: 482


🏁 Script executed:

#!/bin/bash
# Search for any enum ending with State in graphql.ts
grep -n "export enum.*State" ./web/composables/gql/graphql.ts

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Look for NotificationJobState anywhere in web/src and composables directories
find ./web -name "*.ts" -o -name "*.vue" | xargs grep -l "export.*NotificationJobState" 2>/dev/null

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check graphql codegen configuration
find . -maxdepth 2 -type f \( -name "codegen.yml" -o -name "codegen.yaml" -o -name "codegen.ts" \) 2>/dev/null

Length of output: 33



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the web codegen configuration
cat ./web/codegen.ts

Length of output: 834


🏁 Script executed:

#!/bin/bash
# Look for any reference to NotificationJobState in schema files
find . -name "*.graphql" -o -name "*.gql" | xargs grep -l "NotificationJobState" 2>/dev/null

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the tail of graphql.ts to see if NotificationJobState is at the end
tail -100 ./web/composables/gql/graphql.ts | grep -A5 -B5 "Notification"

Length of output: 29228



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the generated schema file exists and contains NotificationJobState
if [ -f ./api/generated-schema.graphql ]; then
  grep -n "NotificationJobState" ./api/generated-schema.graphql
else
  echo "File not found"
fi

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any graphql schema files
find . -name "*.graphql" -type f | head -20

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Check if there's a schema or sdl directory
find . -type d -name "schema" -o -type d -name "sdl" -o -type d -name "graphql" | head -10

Length of output: 239



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the size and contents of the generated schema to see if it has notification types
wc -l ./api/generated-schema.graphql
echo "---"
grep -i "notification" ./api/generated-schema.graphql | head -20

Length of output: 737


🏁 Script executed:

#!/bin/bash
# Check what NotificationJobState references exist across the codebase
rg "NotificationJobState" --type ts --type vue -l

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Get exact line from Sidebar.vue showing how NotificationJobState is used
sed -n '40,80p' ./web/src/components/Notifications/Sidebar.vue

Length of output: 1501


Missing GraphQL type definition for NotificationJobState

The import of NotificationJobState will fail: it doesn't exist in web/composables/gql/graphql.ts because the backend GraphQL schema (api/generated-schema.graphql) doesn't expose it. While the backend defines NotificationJobState in the API layer, it hasn't been registered in the GraphQL schema or queries. The component uses it at lines 64 and 71 but the type is unavailable.

Verify that:

  • NotificationJobState enum is registered and exposed in the backend GraphQL schema
  • The schema is regenerated and graphql.ts is updated to include the new types
🤖 Prompt for AI Agents
In web/src/components/Notifications/Sidebar.vue around lines 24 to 33,
Sidebar.vue imports and uses NotificationJobState but the backend GraphQL schema
does not expose that enum so graphql.ts has no generated type; fix by
adding/registering the NotificationJobState enum to the backend GraphQL schema
(ensure the enum is exported/declared in api/generated-schema.graphql or the
server schema definitions), re-run the schema generation and GraphQL Code
Generator to regenerate web/composables/gql/graphql.ts so NotificationJobState
is present, then update the import in Sidebar.vue if the generated module path
changed.

Comment on lines +73 to +107
const stopPolling = (key: keyof typeof jobPollers) => {
const interval = jobPollers[key];
if (interval) {
clearInterval(interval);
jobPollers[key] = null;
}
};
const setJob = (key: keyof typeof jobPollers, job?: unknown) => {
const parsed = job ? useFragment(NOTIFICATION_JOB_FRAGMENT, job) : null;
activeJobs[key] = parsed;
if (!parsed || !activeStates.includes(parsed.state)) {
stopPolling(key);
}
};
const pollJob = (key: keyof typeof jobPollers) => {
const job = activeJobs[key];
if (!job) return;
stopPolling(key);
if (!activeStates.includes(job.state)) return;
jobPollers[key] = setInterval(async () => {
const { data } = await client.query({
query: notificationJobStatus,
variables: { id: job.id },
fetchPolicy: 'network-only',
});
const updated = data?.notifications.job;
if (updated) {
setJob(key, updated);
}
}, 750);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Strengthen polling robustness: handle errors and missing jobs

The polling helpers mostly look solid, but there are a couple of edge cases worth tightening:

  • The setInterval callback is async and any rejected client.query will surface as an unhandled promise rejection. That can spam the console or trigger global handlers.
  • If the server ever starts returning job: null (e.g., job expired/purged), updated will be falsy, setJob won’t be called, and the interval keeps running indefinitely against a missing job.

Consider something like:

-  jobPollers[key] = setInterval(async () => {
-    const { data } = await client.query({
+  jobPollers[key] = setInterval(async () => {
+    try {
+      const { data } = await client.query({
         query: notificationJobStatus,
         variables: { id: job.id },
         fetchPolicy: 'network-only',
-    });
-    const updated = data?.notifications.job;
-    if (updated) {
-      setJob(key, updated);
-    }
+      });
+      const updated = data?.notifications.job;
+      // Treat a missing job as terminal to avoid polling forever
+      setJob(key, updated ?? undefined);
+    } catch {
+      // Optional: stop polling on repeated errors, or surface a toast
+      stopPolling(key);
+    }
   }, 750);

This keeps the interval from running forever on a missing job and prevents unhandled rejections.


🌐 Web query:

Check how Apollo Client surfaces network and GraphQL errors in `client.query` for the version used here, to confirm whether unhandled rejections may be thrown from this polling loop.

💡 Result:

Short answer: Yes — client.query returns a Promise that will reject on errors (network errors and, with the default errorPolicy "none", GraphQL errors), so if your polling loop calls client.query and you don't catch/reject-handler those promises you can get unhandled promise rejections. [1][2]

Key points and options:

  • Promise-based APIs (client.query / client.mutate) either reject or resolve depending on errorPolicy. With errorPolicy: "none" (the default) the promise rejects for errors; with errorPolicy: "all" the promise resolves and the returned result contains an error field. [1][2]
  • Network errors always cause the promise to reject. [1][3]
  • Apollo notes that aborted queries are handled so they don't produce an unhandled rejection if you don't attach a handler, but that does not apply to ordinary network/GraphQL errors — you must handle them yourself (try/catch or .catch). [2]

Practical fixes:

  • Add .catch(...) or try/catch around every client.query used by the polling loop; or
  • Use errorPolicy: "all" and check result.error instead of relying on promise rejection.

Sources:
[1] Apollo Client — Handling operation errors / Error behavior for promise-based APIs.
[2] Apollo Client — Queries docs (client.query behavior and error handling).
[3] Apollo Client migration notes (network errors always reject).



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the Sidebar.vue file and show the polling code section
fd -type f -name "Sidebar.vue" --path "*/Notifications/*" | head -5

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Examine lines 73-107 of the Sidebar.vue file to see current state
find . -type f -name "Sidebar.vue" -path "*/Notifications/*" -exec sed -n '73,107p' {} +

Length of output: 916


🏁 Script executed:

#!/bin/bash
# Also check the entire polling-related functions in Sidebar.vue
find . -type f -name "Sidebar.vue" -path "*/Notifications/*" -exec sed -n '50,150p' {} +

Length of output: 3299


🏁 Script executed:

#!/bin/bash
# Check package.json to see which Apollo Client version is used
fd -type f -name "package.json" | head -1 | xargs grep -A2 -B2 "@apollo/client" || echo "Could not find @apollo/client version"

Length of output: 208


🏁 Script executed:

#!/bin/bash
# Search for all uses of setInterval with async callbacks in the codebase to understand pattern
rg -n "setInterval\s*\(\s*async" --type vue --type ts --type js | head -20

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Check if try/catch is already around client.query in the file
grep -n "try\|catch" web/src/components/Notifications/Sidebar.vue 2>/dev/null || echo "File not found or no try/catch blocks"

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Alternative: just show the whole Sidebar.vue file to assess state
cat "web/src/components/Notifications/Sidebar.vue" 2>/dev/null | head -200

Length of output: 6643


🏁 Script executed:

#!/bin/bash
# Look for jobPollers in the file
rg -A 30 "jobPollers\[key\]\s*=\s*setInterval" web/src/components/Notifications/Sidebar.vue

Length of output: 787


🏁 Script executed:

#!/bin/bash
# Simple web search to understand the latest Apollo Client behavior docs
echo "Checking Apollo Client error handling patterns..."

Length of output: 49


Add error handling to polling loop to prevent unhandled rejections and infinite polling on missing jobs

The polling helpers have two edge cases that need tightening:

  • The setInterval callback is async but does not catch errors from client.query(). Apollo Client rejects promises on network errors and GraphQL errors (with default errorPolicy), so any rejection will surface as an unhandled promise rejection and may spam the console or trigger global error handlers.
  • If the server returns job: null (e.g., job expired/purged), updated will be falsy, setJob won't be called, and the interval will run indefinitely against a missing job.

Wrap the query in try/catch and call stopPolling(key) on error; also treat a missing job response as terminal:

  jobPollers[key] = setInterval(async () => {
+   try {
      const { data } = await client.query({
        query: notificationJobStatus,
        variables: { id: job.id },
        fetchPolicy: 'network-only',
      });
      const updated = data?.notifications.job;
-     if (updated) {
-       setJob(key, updated);
-     }
+     setJob(key, updated ?? undefined);
+   } catch {
+     stopPolling(key);
+   }
  }, 750);
🤖 Prompt for AI Agents
In web/src/components/Notifications/Sidebar.vue around lines 73 to 107, the
polling callback can cause unhandled promise rejections and may poll forever
when the server returns job: null; wrap the async setInterval callback in a
try/catch, call stopPolling(key) in the catch (and optionally log the error with
context), and also treat a falsy updated response as terminal by calling
stopPolling(key) before returning instead of leaving the interval running.

"notifications.sidebar.confirmDeleteUnread.description": "Are you sure you want to permanently delete all unread notifications? This cannot be undone.",
"notifications.sidebar.confirmDeleteUnread.title": "Delete unread notifications",
"notifications.sidebar.deleteAllAction": "Delete All",
"notifications.sidebar.processingStatus": "Processing {{processed}} / {{total}}",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix interpolation syntax in processingStatus

vue-i18n expects single-brace placeholders (e.g. {processed}), but this string uses {{processed}} / {{total}}, which will likely render the braces literally instead of substituting values.

Recommend:

-  "notifications.sidebar.processingStatus": "Processing {{processed}} / {{total}}",
+  "notifications.sidebar.processingStatus": "Processing {processed} / {total}",
🤖 Prompt for AI Agents
In web/src/locales/en.json around line 311 the translation uses double-brace
interpolation "Processing {{processed}} / {{total}}", but vue-i18n expects
single-brace placeholders; update the string to use single braces "Processing
{processed} / {total}" and confirm any calls to $t/$tc pass named params {
processed, total } so values are substituted correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
web/src/components/Notifications/Sidebar.vue (2)

2-2: Imports align with the new job-based polling implementation.

The additions support:

  • Lifecycle cleanup with onBeforeUnmount
  • Manual GraphQL queries via useApolloClient
  • Job fragments and status queries
  • Type safety with enum imports

Note: The past review comment about NotificationJobState being missing from generated types should be addressed first.

Also applies to: 4-4, 24-46


100-118: Polling implementation needs error handling as flagged in previous review.

The previous review comment correctly identifies that the polling loop lacks error handling and can run indefinitely if the server returns job: null. The suggested fix adds try-catch for error handling and treats missing jobs as terminal.

🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts (2)

17-22: Consider adding a CANCELLED state for user-initiated job termination.

The current state machine covers the happy path and failure scenarios, but lacks support for user-initiated cancellation. If users can cancel long-running archive/delete operations, adding a CANCELLED state would improve the state model completeness.

Would you like me to propose the addition of a CANCELLED state and the corresponding cancellation mutation?


192-211: Consider adding validation decorators for consistency with other ObjectTypes.

While validation decorators are not strictly required for output types (ObjectType), other classes in this file (Notification, NotificationCounts, etc.) include them (@IsEnum, @IsInt, @IsString, etc.). Adding them to NotificationJob would maintain consistency and provide an extra layer of type safety.

Apply this diff to add validation decorators:

 @ObjectType()
 export class NotificationJob {
     @Field(() => ID)
+    @IsString()
+    @IsNotEmpty()
     id!: string;
 
     @Field(() => NotificationJobOperation)
+    @IsEnum(NotificationJobOperation)
+    @IsNotEmpty()
     operation!: NotificationJobOperation;
 
     @Field(() => NotificationJobState)
+    @IsEnum(NotificationJobState)
+    @IsNotEmpty()
     state!: NotificationJobState;
 
     @Field(() => Int)
+    @IsInt()
+    @Min(0)
+    @IsNotEmpty()
     processed!: number;
 
     @Field(() => Int)
+    @IsInt()
+    @Min(0)
+    @IsNotEmpty()
     total!: number;
 
     @Field({ nullable: true })
+    @IsString()
+    @IsOptional()
     error?: string | null;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53851e4 and 291fb24.

📒 Files selected for processing (3)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts (4 hunks)
  • web/src/components/Notifications/Sidebar.vue (7 hunks)
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: pujitm
Repo: unraid/api PR: 941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.
📚 Learning: 2025-01-31T22:01:22.708Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:24-27
Timestamp: 2025-01-31T22:01:22.708Z
Learning: The Unraid UI uses a modern notification system with a custom `unraid-toaster` component replacing the legacy jGrowl notifications. The system is backed by a comprehensive GraphQL API with real-time subscription support for notification updates.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2025-01-30T19:56:42.055Z
Learnt from: pujitm
Repo: unraid/api PR: 1075
File: web/components/Notifications/Sidebar.vue:52-57
Timestamp: 2025-01-30T19:56:42.055Z
Learning: Notification links in the Unraid API are validated at creation time on the server side, making additional client-side validation unnecessary when receiving notifications through GraphQL subscriptions.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.

Applied to files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts
📚 Learning: 2024-11-01T17:52:55.311Z
Learnt from: pujitm
Repo: unraid/api PR: 941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-06T14:43:04.876Z
Learnt from: pujitm
Repo: unraid/api PR: 974
File: web/components/Notifications/List.vue:31-31
Timestamp: 2024-12-06T14:43:04.876Z
Learning: In the Vue.js application using `useQuery` from Vue Apollo, the `error` reactive variable captures errors from both the initial query and any `fetchMore` calls, so wrapping `fetchMore` in a try-catch block is unnecessary.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2025-08-29T19:48:45.517Z
Learnt from: elibosley
Repo: unraid/api PR: 1618
File: web/components/Logs/SingleLogViewer.vue:248-299
Timestamp: 2025-08-29T19:48:45.517Z
Learning: In Vue Apollo's composition API, subscribeToMore automatically handles subscription cleanup when the related query is stopped or the component unmounts, unlike React Apollo where manual cleanup is required. Manual unsubscribe is only needed when changing subscription variables, not for lifecycle management.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-09T15:47:29.325Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-24
Timestamp: 2024-12-09T15:47:29.325Z
Learning: In our Nuxt setup using Vue.js, components defined within the codebase are autoloaded and do not require explicit import statements in the script section. For example, components like `TabsList` and `TabsTrigger` used in `web/components/Notifications/TabList.vue` are automatically available without imports.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
Repo: unraid/api PR: 1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
📚 Learning: 2024-12-17T13:55:42.068Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/components/ColorSwitcher.ce.vue:1-2
Timestamp: 2024-12-17T13:55:42.068Z
Learning: In this Nuxt.js project, components used in templates are automatically imported by Nuxt, so explicit import statements for components are unnecessary.

Applied to files:

  • web/src/components/Notifications/Sidebar.vue
🧬 Code graph analysis (1)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
web/composables/gql/graphql.ts (1)
  • Mutation (1199-1239)
🔇 Additional comments (18)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2)

7-7: LGTM!

The import follows the established pattern and is correctly placed alphabetically within the imports block.


46-49: LGTM!

The implementation correctly follows the established nested mutation pattern used throughout this resolver. The notifications() method appropriately creates a namespace for notification-related mutations, consistent with how array, docker, vm, and other domain mutations are structured.

api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts (4)

1-1: LGTM - ID import supports the new job identifier field.

The addition of ID to the imports is necessary for typing the id field in the NotificationJob class.


24-27: LGTM - Operation enum covers the job types introduced in this PR.

The enum values align with the archive and delete operations mentioned in the PR objectives.


38-44: LGTM - Enum registrations follow the established pattern.

The registerEnumType calls properly expose the new enums to the GraphQL schema.


188-189: LGTM - Optional job field enables status queries through the notifications resolver.

Making the field nullable is appropriate since a job may not be active at query time.

web/src/components/Notifications/Sidebar.vue (12)

49-51: LGTM - Mutation bindings and Apollo client setup support the job-based workflow.

The renamed startArchiveAllJob mutation and new startDeleteAll mutation align with the background job pattern. The Apollo client instance enables manual polling queries.


56-80: LGTM - Job tracking state is well-structured and type-safe.

The separate tracking of polling intervals (jobPollers) and job state (activeJobs) with explicit keys for each operation provides clear separation of concerns and type safety.


82-82: LGTM - Active states correctly identify non-terminal job states.

Limiting active states to QUEUED and RUNNING ensures polling stops once jobs reach terminal states (SUCCEEDED or FAILED).


84-90: LGTM - Polling cleanup is correctly implemented.

The function properly clears the interval and resets the reference to null.


92-98: LGTM - Job state management includes automatic polling cleanup.

The function correctly handles job updates and automatically stops polling when the job reaches a terminal state or is missing.


120-123: LGTM - Job activity check is straightforward and correct.

The helper correctly determines if a job is in progress by checking its state against the active states array.


125-134: LGTM - Dynamic job labels enhance user experience with progress feedback.

The helper provides real-time progress updates when jobs are active and falls back to static labels otherwise.


145-160: LGTM - Archive operation properly initiates job tracking after confirmation.

The function follows a good pattern: confirm → mutate → track job → start polling.


162-177: LGTM - Delete archives operation follows the established job pattern.

The function correctly uses NotificationType.ARCHIVE to target archived notifications and initiates proper job tracking.


179-194: LGTM - Unread deletion feature follows the consistent job-based pattern.

The new function extends deletion capabilities to unread notifications with proper job tracking.


257-259: LGTM - Lifecycle cleanup prevents resource leaks.

The onBeforeUnmount hook properly clears all active polling intervals when the component is destroyed.


300-327: LGTM - UI properly integrates job state with button states and labels.

The template changes correctly:

  • Disable buttons during mutations and active jobs
  • Display dynamic progress labels via jobLabel
  • Maintain separate job tracking for archive and delete operations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor Notifications to Use a Shared Background Job for Archiving and Deleting

2 participants