Skip to content

Comments

feat: replace docker overview table with web component (7.3+)#1764

Merged
pujitm merged 159 commits intomainfrom
feat/docker-overview-table
Dec 18, 2025
Merged

feat: replace docker overview table with web component (7.3+)#1764
pujitm merged 159 commits intomainfrom
feat/docker-overview-table

Conversation

@pujitm
Copy link
Member

@pujitm pujitm commented Oct 27, 2025

Summary

Introduces a new Vue-based Docker container management interface replacing the legacy webgui table.

Container Management

  • Start, stop, pause, resume, and remove containers via GraphQL mutations
  • Bulk actions for managing multiple containers at once
  • Container update detection with one-click updates
  • Real-time container statistics (CPU, memory, I/O)

Organization & Navigation

  • Folder-based container organization with drag-and-drop support
  • Accessible reordering via keyboard controls
  • Customizable column visibility with persistent preferences
  • Column resizing and reordering
  • Filtering and search across container properties

Auto-start Configuration

  • Dedicated autostart view with delay configuration
  • Drag-and-drop reordering of start/stop sequences

Logs & Console

  • Integrated log viewer with filtering and download
  • Persistent console sessions with shell selection
  • Slideover panel for quick access

Networking

  • Port conflict detection and alerts
  • Tailscale integration for container networking status
  • LAN IP and port information display

Additional Features

  • Orphaned container detection and cleanup
  • Template mapping management
  • Critical notifications system
  • WebUI visit links with Tailscale support

PR Summary by Claude Opus 4.5

Summary by CodeRabbit

  • New Features

    • Docker controls: pause/unpause/remove/update containers, batch updates, autostart configuration, template sync, port‑conflict reporting, container logs (since/tail) and live stats, per‑container Tailscale status, deduplicated warning/alert notifications (notifyIfUnique).
  • UI Improvements

    • New Docker management UI: overview, standalone container pages, sidebar tree, draggable folders, rich containers table, log & console viewers, autostart settings, container sizes modal, orphaned & port‑conflict alerts, critical notifications widget, resizable/persistent columns.
  • Tests & Documentation

    • Expanded unit/integration tests and new developer Docker documentation.

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

@pujitm
Copy link
Member Author

pujitm commented Oct 27, 2025

@coderabbitai pause

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a large Docker feature set (template scanning, autostart, logs, ports, networks, stats, tailscale), notifications, organizer flat view and folder operations, many backend services and GraphQL surface expansions, extensive web UI components/composables, tests, docs, and environment/tooling updates.

Changes

Cohort / File(s) Change Summary
Env & Packaging
\api/.env.development`, `api/.env.staging`, `api/.env.production`, `api/package.json`, `web/package.json``
Added PATHS_DOCKER_TEMPLATES and ENABLE_NEXT_DOCKER_RELEASE env entries; added dependency fast-xml-parser and web deps @formkit/auto-animate, @tanstack/vue-table.
Ignore / Formatting / Tooling
\api/.gitignore`, `api/.prettierignore`, `web/.prettierignore`, `api/.eslintrc.ts`, `api/justfile``
Updated ignore lists (dist/, build outputs), added watch justfile target, adjusted ESLint/Prettier ignores.
GraphQL Schema & Surface
\api/generated-schema.graphql``
Large schema expansion: Docker types (logs, stats, portConflicts, autostart inputs, template sync result), Notifications types, organizer FlatOrganizerEntry, many queries/mutations/subscriptions (pause/unpause/remove/update, notifyIfUnique, dockerContainerStats, notificationsWarningsAndAlerts, organizer mutations).
Docker Module Wiring
\api/src/unraid-api/graph/resolvers/docker/docker.module.ts`, `api/src/unraid-api/graph/resolvers/resolvers.module.ts``
Registered many new Docker providers/services; added NotificationsModule and ServicesModule imports and adjusted test wiring.
New API Services
\api/src/unraid-api/graph/resolvers/docker/{docker-template-scanner, docker-template-icon, docker-log, docker-port, docker-network, docker-stats, docker-autostart, docker-tailscale, docker-manifest}.service.ts``
Added services: template scanning & icon extraction, log sizes & retrieval, port deduplication/conflict detection, network listing with caching, live docker stats streamer, autostart management, tailscale status resolution, and manifest helpers.
Docker Service & Resolvers
\api/src/unraid-api/graph/resolvers/docker/*.ts` (service, resolver, mutations, container resolver, organizer service)`
Expanded DockerService/resolvers: new methods (pause/unpause/remove/update/updateAll/updateAutostartConfiguration, getPortConflicts, getContainerLogSizes/getContainerLogs, getNetworks), enrichment (templatePath,isOrphaned,autostart), stats pubsub wiring, organizer mutations, and resolver field implementations.
Template Mapping & Paths
\api/src/environment.ts`, `api/.env.development`, `api/src/store/modules/paths.ts`, `api/src/test/store/modules/paths.test.ts``
Added PATHS_DOCKER_TEMPLATES parsing and defaults; added docker-userprefs path; tests updated.
Organizer Flattening & Ops
\api/src/unraid-api/organizer/*`, `api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts``
Replaced nested root with rootId + flatEntries (FlatOrganizerEntry); added removeStaleRefsFromView/enrichFlatEntries and folder operations: moveItemsToPosition, renameFolderById, createFolderWithItems, updateViewPreferences.
Notifications
\api/src/unraid-api/graph/resolvers/notifications/*`, `packages/unraid-shared/src/pubsub/graphql.pubsub.ts``
New NotificationsModule/service with fingerprint-based deduplication, notifyIfUnique mutation, warningsAndAlerts resolve field and subscription; added pubsub channel constants.
File Modifier & Version Utils
\api/src/unraid-api/unraid-file-modifier/*`, `api/src/common/compare-semver-version.ts`, `api/src/common/get-unraid-version-sync.ts``
New DockerContainersPageModification; FileModification.shouldApply option for OS version check; new compareVersions and synchronous Unraid version helpers + tests.
Docker Client Utility
\api/src/unraid-api/graph/resolvers/docker/utils/docker-client.ts``
Added singleton getDockerClient wrapper for dockerode.
Log / Error / Network Helpers
\api/src/core/utils/misc/catch-handlers.ts`, `api/src/core/utils/network.ts`, `api/src/core/log.ts``
DockerError.address made optional; added getLanIp helper; introduced LOG_TRANSPORT env handling for log destination/format.
Web: Components (Docker / Notifications / Common / Logs)
\web/src/components/**/*`, `web/components.d.ts`, `web/public/test-pages/all-components.html``
Added 30+ components (management UI, containers table, autostart settings, overview, sidebar, log viewers, console viewer, tailscale indicator, orphaned/port-conflict alerts, modals, ContainerSizesModal, ContainerOverviewCard, etc.), removed legacy Logs/Console components, updated component registry and typings, added component gallery page.
Web: GraphQL Documents & Typings
\web/src/components/Docker/docker-.{query,mutation,subscription}.ts`, `web/src/components/Notifications/graphql/`, `web/src/composables/gql/gql.ts`, `web/src/composables/gql/graphql.ts``
Added many GraphQL operation documents and client overload typings for the new Docker/Notification operations (containers, logs, stats, autostart, template mappings, organizer flatEntries, etc.).
Web: Composables & UI Utilities
\web/src/composables/*`, `web/src/utils/{docker,unapiScope,tableRenderers}.ts`, `web/src/helpers/env.ts``
Added composables: tree data/expansion/filter/selection/drag-drop/drop-projection, folder operations/tree, entry reordering, context menu, container actions/bulk/update/log sessions/console sessions, persistent column visibility, container-stats subscription; added Docker utils, unapi scope utils, table renderers; exported featureFlags.
Web: Table & Renderer Helpers
\web/src/utils/tableRenderers.ts`, `web/src/composables/useDockerTableColumns.ts`, `web/src/components/Common/BaseTreeTable.vue`, `web/src/components/Common/TableColumnMenu.vue``
New table render helpers, Docker-specific column builder, BaseTreeTable component, and column menu with drag/drop ordering and visibility persistence.
Web: Log Viewing Stack
\web/src/components/Logs/BaseLogViewer.vue`, `web/src/components/Logs/SingleLogViewer.vue`, `web/src/components/Docker/SingleDockerLogViewer.vue`, `web/src/components/Docker/DockerLogViewerModal.vue``
Added BaseLogViewer, adapted SingleLogViewer to use it, added Docker-specific single-log viewer & modal with polling, filtering, tail/cursor support.
Tests
many \*.spec.ts``
New and extended tests across template scanner, autostart, log service, network/port services, stats, notifications, organizer resolution, DockerService unit & integration tests and test wiring updates.
Docs & README
\api/docs/developer/docker.md`, `api/README.md``
New developer documentation for Docker feature and README additions.
Misc
\api/dev/configs/api.json`, `api/scripts/build.ts`, `web/eslint.config.mjs`, `web/src/main.ts`, `web/src/utils/unapiScope.ts``
Dev config version bump, build import path normalization, updated eslint Vue version constraint, ensureUnapiScope integration in main and mount-engine portal/UNAPI scoping changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant GraphQL as DockerResolver
  participant DockerService
  participant TemplateScanner as DockerTemplateScannerService
  participant FS as Filesystem
  participant Config as DockerConfigService

  Client->>GraphQL: query containers / call syncDockerTemplatePaths
  GraphQL->>DockerService: getContainers(skipCache?)
  DockerService->>TemplateScanner: syncMissingContainers(containers)
  TemplateScanner->>FS: read PATHS_DOCKER_TEMPLATES/*.xml
  FS-->>TemplateScanner: XML contents
  TemplateScanner->>TemplateScanner: parseTemplate(), matchContainerToTemplate()
  TemplateScanner->>Config: updateMappings(mappings)
  Config-->>TemplateScanner: ack
  TemplateScanner-->>DockerService: sync result
  DockerService-->>GraphQL: enriched containers (templatePath, iconUrl, isOrphaned)
  GraphQL-->>Client: response
Loading
sequenceDiagram
  autonumber
  participant DockerStats as DockerStatsService
  participant PubSub as PUBSUB (DOCKER_STATS)
  participant GraphQLSub as DockerResolver.Subscription
  participant Client

  DockerStats->>PubSub: publish DockerContainerStats
  PubSub->>GraphQLSub: event
  GraphQLSub->>Client: push subscription update
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Files/areas needing focused review:
    • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts — XML parsing, directory traversal, matching heuristics, PATHS handling, and I/O error aggregation.
    • api/src/unraid-api/graph/resolvers/docker/docker.service.ts & api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts — execa calls, pause/unpause/remove/update flows, cache invalidation, pubsub events, lifecycle/start-stop behavior, and state transitions.
    • api/src/unraid-api/graph/resolvers/docker/docker-tailscale.service.ts & api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts — tailscale command exec/parse, DERP mapping, caching, URL templating and placeholder expansion (security).
    • Web table/tree stack and drag/drop/composables (useDropProjection, useTreeExpansion, tableRenderers, BaseTreeTable, DockerContainersTable) — DOM measurements, drop projection correctness, drag/drop accessibility and edge cases.
    • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts and resolver integration — fingerprinting/deduplication correctness and pubsub publishing cadence.
    • Integration points: getDockerClient singleton, cache manager usage, and feature-flag gating (ENABLE_NEXT_DOCKER_RELEASE) across resolvers.

Possibly related PRs

Suggested reviewers

  • zackspear
  • elibosley

Poem

🐰 I hopped through templates, logs, and ports so bright,
I sniffed for icons deep into the night.
I nudged autostarts, chased stats on the run,
Flat entries and alerts — the warren's work is done.
Rejoice, little warren: containers gleam in light! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: replace docker overview table with web component' is directly related to the primary change in the changeset, which implements a new Vue-based Docker container management web component.

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f54e53 and 970876e.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (2 hunks)

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

@coderabbitai coderabbitai bot requested review from elibosley and zackspear October 27, 2025 16:52
await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
this.logger.debug(`Invalidated container cache after pausing ${id}`);

let containers = await this.getContainers({ skipCache: true });

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

The initial value of containers is unused, since it is always overwritten.

Copilot Autofix

AI 2 months ago

To fix the problem, simply remove the initial assignment of containers at line 324, as its value is never used. Instead, just declare the variable with let containers;, which allows the same variable name to be re-assigned in the loop as currently happens. No further code or logic changes are needed, as this does not affect the existing functionality. Only a single line needs to be modified in docker.service.ts, within the pause method, by removing the initialization part of the containers declaration.


Suggested changeset 1
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
--- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
+++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
@@ -321,7 +321,7 @@
         await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
         this.logger.debug(`Invalidated container cache after pausing ${id}`);
 
-        let containers = await this.getContainers({ skipCache: true });
+        let containers;
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
EOF
@@ -321,7 +321,7 @@
await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
this.logger.debug(`Invalidated container cache after pausing ${id}`);

let containers = await this.getContainers({ skipCache: true });
let containers;
let updatedContainer: DockerContainer | undefined;
for (let i = 0; i < 5; i++) {
await sleep(500);
Copilot is powered by AI and may make mistakes. Always verify output.
await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
this.logger.debug(`Invalidated container cache after unpausing ${id}`);

let containers = await this.getContainers({ skipCache: true });

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

The initial value of containers is unused, since it is always overwritten.

Copilot Autofix

AI 2 months ago

To fix the problem, simply remove the initial assignment:

  • Remove the declaration and initial assignment of containers on line 352.
  • Declare the containers variable in the for-loop with let containers; before the loop.
  • The for-loop assignments to containers will then operate as intended.
  • Optionally, ensure TypeScript is happy with possible type inference (the type can be inferred from the assignment in the loop, but you may use a type annotation if required: let containers: DockerContainer[];).

Only lines within the body of the unpause method are to be changed.

Suggested changeset 1
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
--- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
+++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
@@ -349,7 +349,7 @@
         await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
         this.logger.debug(`Invalidated container cache after unpausing ${id}`);
 
-        let containers = await this.getContainers({ skipCache: true });
+        let containers: DockerContainer[];
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
EOF
@@ -349,7 +349,7 @@
await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
this.logger.debug(`Invalidated container cache after unpausing ${id}`);

let containers = await this.getContainers({ skipCache: true });
let containers: DockerContainer[];
let updatedContainer: DockerContainer | undefined;
for (let i = 0; i < 5; i++) {
await sleep(500);
Copilot is powered by AI and may make mistakes. Always verify output.
* Combines moveEntriesToFolder with position-based insertion.
*/
export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
const { view, sourceEntryIds, destinationFolderId, position, resources } = params;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable resources.

Copilot Autofix

AI 3 months ago

To fix the problem, simply remove resources from the object destructuring in the moveItemsToPosition function. This means changing the line from:

const { view, sourceEntryIds, destinationFolderId, position, resources } = params;

to:

const { view, sourceEntryIds, destinationFolderId, position } = params;

No other changes are needed, as resources is not referenced elsewhere in the function; this fix will not affect any existing functionality.


Suggested changeset 1
api/src/unraid-api/organizer/organizer.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/organizer/organizer.ts b/api/src/unraid-api/organizer/organizer.ts
--- a/api/src/unraid-api/organizer/organizer.ts
+++ b/api/src/unraid-api/organizer/organizer.ts
@@ -683,7 +683,7 @@
  * Combines moveEntriesToFolder with position-based insertion.
  */
 export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
-    const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
+    const { view, sourceEntryIds, destinationFolderId, position } = params;
 
     const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });
 
EOF
@@ -683,7 +683,7 @@
* Combines moveEntriesToFolder with position-based insertion.
*/
export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
const { view, sourceEntryIds, destinationFolderId, position } = params;

const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });

Copilot is powered by AI and may make mistakes. Always verify output.
* Combines createFolder + moveItems + positioning in a single atomic operation.
*/
export function createFolderWithItems(params: CreateFolderWithItemsParams): OrganizerView {
const { view, folderId, folderName, parentId, sourceEntryIds = [], position, resources } = params;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable resources.

Copilot Autofix

AI 3 months ago

To fix this issue, remove the unused resources variable from the destructuring assignment in the function moveItemsToPosition. Since the function does not use resources, its presence only adds unnecessary clutter. You may also review whether the resources property is needed in the MoveItemsToPositionParams interface; if it is not used elsewhere (within shown code), it should be removed for clarity. In this case, remove resources?: OrganizerV1['resources']; from the interface to keep the API clean.

All changes should be restricted to the code regions shown, specifically to lines 678 in the interface definition, and the function parameter destructuring on line 686.


Suggested changeset 1
api/src/unraid-api/organizer/organizer.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/organizer/organizer.ts b/api/src/unraid-api/organizer/organizer.ts
--- a/api/src/unraid-api/organizer/organizer.ts
+++ b/api/src/unraid-api/organizer/organizer.ts
@@ -675,7 +675,7 @@
     sourceEntryIds: Set<string>;
     destinationFolderId: string;
     position: number;
-    resources?: OrganizerV1['resources'];
+
 }
 
 /**
@@ -683,7 +683,7 @@
  * Combines moveEntriesToFolder with position-based insertion.
  */
 export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
-    const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
+    const { view, sourceEntryIds, destinationFolderId, position } = params;
 
     const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });
 
EOF
@@ -675,7 +675,7 @@
sourceEntryIds: Set<string>;
destinationFolderId: string;
position: number;
resources?: OrganizerV1['resources'];

}

/**
@@ -683,7 +683,7 @@
* Combines moveEntriesToFolder with position-based insertion.
*/
export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
const { view, sourceEntryIds, destinationFolderId, position } = params;

const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });

Copilot is powered by AI and may make mistakes. Always verify output.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

✅ Actions performed

Reviews paused.

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: 19

Caution

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

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

845-851: Log the original timestamp string (not the null date object).

Current log prints "null" instead of the bad input.

Apply this diff:

-        if (!date) {
-            this.logger.warn(`[formatTimestamp] Could not parse date from timestamp: ${date}`);
-            return timestamp;
-        }
+        if (!date) {
+            this.logger.warn(
+                `[formatTimestamp] Could not parse date from timestamp: ${timestamp}`
+            );
+            return timestamp;
+        }
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (1)

29-37: Normalize resource IDs to match UI/organizer expectations.

Docker container names include a leading "/" from the Docker API. The UI strips this for display (as seen in docker-form.service.ts line 19), but containerToResource doesn't normalize it, causing ID mismatches between organizer resources and UI rows.

 export function containerToResource(container: DockerContainer): OrganizerContainerResource {
-    const stableRef = container.names[0] || container.image;
+    const stableRef =
+        (container.names?.[0]?.replace(/^\//, '') ||
+         container.image ||
+         container.id);
     return {
         id: stableRef,
         type: 'container',
         name: stableRef,
         meta: container,
     };
 }

This normalizes IDs to match the slash-less format already used elsewhere in the codebase (docker-form.service.ts, docker-template-scanner.service.ts) and adds container.id as a stable fallback. Verify DnD/move flows work correctly after applying this change.

🧹 Nitpick comments (50)
api/src/unraid-api/organizer/organizer.test.ts (1)

267-300: Consider adding assertions to verify resource entries and nested folder contents remain unchanged.

The test correctly validates that resources already referenced in nested folders are not duplicated in the root. To make it more comprehensive, consider also asserting that:

  1. The ref entries for resourceA and resourceB still exist in the result
  2. The stuff folder's children remain ['resourceA', 'resourceB']

This would guard against accidental removal or modification of existing entries.

Example additional assertions:

 const rootChildren = (result.entries['root1'] as OrganizerFolder).children;
 expect(rootChildren).toEqual(['stuff']);
+
+// Verify resource entries still exist
+expect(result.entries['resourceA']).toBeDefined();
+expect(result.entries['resourceB']).toBeDefined();
+
+// Verify nested folder contents unchanged
+const stuffChildren = (result.entries['stuff'] as OrganizerFolder).children;
+expect(stuffChildren).toEqual(['resourceA', 'resourceB']);
api/src/unraid-api/organizer/organizer.model.ts (1)

260-310: Add boolean validation for hasChildren

Strengthen validation with IsBoolean on hasChildren.

Apply:

@@
-import {
-    Equals,
-    IsArray,
-    IsIn,
-    IsNumber,
-    IsObject,
-    IsOptional,
-    IsString,
-    ValidateNested,
-} from 'class-validator';
+import {
+    Equals,
+    IsArray,
+    IsBoolean,
+    IsIn,
+    IsNumber,
+    IsObject,
+    IsOptional,
+    IsString,
+    ValidateNested,
+} from 'class-validator';
@@
     @Field()
-    hasChildren!: boolean;
+    @IsBoolean()
+    hasChildren!: boolean;
web/src/components/Wrapper/mount-engine.ts (2)

39-61: Cache portal selector to avoid repeated DOM/style work

Memoize ensurePortalRoot’s result; subsequent calls can return the cached selector.

+let cachedPortalSelector: string | undefined;
 function ensurePortalRoot(): string | undefined {
-  if (typeof document === 'undefined') {
+  if (cachedPortalSelector) {
+    return cachedPortalSelector;
+  }
+  if (typeof document === 'undefined') {
     return undefined;
   }
@@
-  return `#${PORTAL_ROOT_ID}`;
+  cachedPortalSelector = `#${PORTAL_ROOT_ID}`;
+  return cachedPortalSelector;
 }

14-15: ESM import extension for internal modules

Per web TypeScript ESM guideline, include .js on internal specifiers.

-import { ensureUnapiScope, ensureUnapiScopeForSelectors, observeUnapiScope } from '~/utils/unapiScope';
+import { ensureUnapiScope, ensureUnapiScopeForSelectors, observeUnapiScope } from '~/utils/unapiScope.js';
web/src/composables/useTreeData.ts (3)

41-52: Eliminate unsafe casts by typing flatEntries with meta generics.

Avoid as T by typing entries so entry.meta is already T. This aligns with “avoid type casting” guidance. Minimal change:

Apply:

@@
-import type { FlatOrganizerEntry } from '@/composables/gql/graphql';
+import type { FlatOrganizerEntry } from '@/composables/gql/graphql';
+
+type FlatEntryWithMeta<M> = Omit<FlatOrganizerEntry, 'meta'> & { meta?: M | null };
@@
-export interface TreeDataOptions<T> {
-  flatEntries?: MaybeRef<FlatOrganizerEntry[] | undefined>;
+export interface TreeDataOptions<T> {
+  flatEntries?: MaybeRef<FlatEntryWithMeta<T>[] | undefined>;
@@
-        const builtFromMeta =
-          entry.meta && buildFlatRow ? buildFlatRow(entry.meta as T, entry.name) : null;
+        const builtFromMeta =
+          entry.meta && buildFlatRow ? buildFlatRow(entry.meta, entry.name) : null;
@@
-          meta: (entry.meta as T) ?? builtFromMeta?.meta,
+          meta: entry.meta ?? builtFromMeta?.meta,

This removes the casts and preserves behavior. As per coding guidelines.

Also applies to: 53-64


66-71: Guard against accidental cycles in childrenIds.

If backend data ever contains a cycle or self-reference, recursion will blow the stack. Add a visited set to short‑circuit repeats.

@@
-      function buildTreeFromFlat(entry: FlatOrganizerEntry): TreeRow<T> {
+      const visited = new Set<string>();
+      function buildTreeFromFlat(entry: FlatEntryWithMeta<T>): TreeRow<T> {
+        if (visited.has(entry.id)) {
+          return { id: entry.id, type: entry.type, name: entry.name, children: [], icon: entry.icon };
+        }
+        visited.add(entry.id);
@@
-          row.children = entry.childrenIds
+          row.children = entry.childrenIds
             .map((childId) => entriesById.get(childId))
             .filter(Boolean)
             .map((child) => buildTreeFromFlat(child!));

132-141: Avoid duplicate “flatten rows” logic across composables.

flattenRows here and flattenSelectableRows in useRowSelection are near‑duplicates. Extract a shared utility to prevent drift.

web/src/utils/unapiScope.ts (2)

111-119: Clarify multi‑root behavior; new roots aren’t observed after first call.

Subsequent calls add selectors (good) but early‑return prevents observing a different root. Either document that only the first root is observed, or support multiple roots.

Option A (doc): add JSDoc noting only initial root is observed. Option B (code): maintain a Map<root, observer> and observe each unique root.


137-146: Harden against invalid selectors.

node.matches(observerState.selectorList) and querySelectorAll can throw on malformed selectors. Wrap in try/catch to avoid breaking the observer loop.

web/src/composables/useRowSelection.ts (1)

24-33: DRY: Share flattening logic with useTreeData.

flattenSelectableRows mirrors flattenRows. Consider extracting a shared utility (e.g., flattenTreeRows(rows, filterType?)) to keep one source of truth.

web/src/composables/gql/graphql.ts (1)

22-22: Avoid any for GraphQL JSON scalar (update codegen mapping).

Generated types use any for JSON. Map JSON to unknown (or a precise Record) in codegen to align with our TS guidelines.

Add to your codegen config (e.g., web/codegen.ts):

 export default defineConfig({
   generates: {
     'web/src/composables/gql/': {
       preset: 'client',
       presetConfig: { /* ... */ },
+      config: {
+        scalars: {
+          JSON: 'unknown',
+        },
+      },
     },
   },
 });

As per coding guidelines.

web/package.json (1)

107-107: Pin version for reproducible builds.

Use an exact version for @tanstack/vue-table to avoid surprise upgrades in CI.

-    "@tanstack/vue-table": "^8.21.3",
+    "@tanstack/vue-table": "8.21.3",
api/package.json (1)

107-107: Pin fast-xml-parser and verify safe parser options.

  • Pin the version to reduce supply-chain drift.
  • Ensure parser options disable dangerous entity expansion.
-        "fast-xml-parser": "^5.3.0",
+        "fast-xml-parser": "5.3.0",

Please confirm you initialize XMLParser with conservative options (e.g., ignoreAttributes as needed, no external entity expansion).

web/src/assets/main.css (1)

12-12: Import Nuxt UI into a cascade layer to minimize bleed.

Anchor Nuxt UI under components (keeps cascade predictable with Tailwind v4 layers).

-@import "@nuxt/ui";
+@import "@nuxt/ui" layer(components);

Please verify no unintended style overrides in the legacy WebGUI (spot-check buttons/links/modals).

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

68-77: Nice wiring of new providers in tests; consider DRY setup.

Repeated provider stubs can live in a small factory to reduce duplication across specs.

api/src/environment.ts (1)

114-117: Harden PATHS_DOCKER_TEMPLATES parsing (trim/filter; safe fallback).

split(',') can yield empty/whitespace paths, breaking scanners. Trim, filter falsy, and fall back to defaults if env resolves to empty.

-export const PATHS_DOCKER_TEMPLATES = process.env.PATHS_DOCKER_TEMPLATES?.split(',') ?? [
-    '/boot/config/plugins/dockerMan/templates-user',
-    '/boot/config/plugins/dockerMan/templates',
-];
+const DEFAULT_DOCKER_TEMPLATE_PATHS = [
+  '/boot/config/plugins/dockerMan/templates-user',
+  '/boot/config/plugins/dockerMan/templates',
+] as const;
+
+const envPathsRaw = process.env.PATHS_DOCKER_TEMPLATES;
+export const PATHS_DOCKER_TEMPLATES: readonly string[] =
+  envPathsRaw
+    ? envPathsRaw
+        .split(',')
+        .map((p) => p.trim())
+        .filter(Boolean)
+    : DEFAULT_DOCKER_TEMPLATE_PATHS;
+
+// If provided env resolves to empty, fall back to defaults
+export const _PATHS_DOCKER_TEMPLATES =
+  PATHS_DOCKER_TEMPLATES.length > 0 ? PATHS_DOCKER_TEMPLATES : DEFAULT_DOCKER_TEMPLATE_PATHS;

If consumers import PATHS_DOCKER_TEMPLATES directly, swap usages to _PATHS_DOCKER_TEMPLATES.

web/src/components/Docker/docker-create-folder.mutation.ts (1)

1-25: Mutation doc looks good; consider fragments and typed DocumentNode.

Location and naming conform. To reduce duplication across Docker mutations, extract a fragment for flatEntries and export a typed DocumentNode for better TS inference.

As per coding guidelines.

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

292-357: Solid coverage for dedupe and limit; add ordering assertion.

Great tests. Consider asserting deterministic ordering (e.g., ALERTs before WARNINGs, or newest first) so UI expectations are locked and regressions are caught.


359-396: Good uniqueness tests; clarify scope across ARCHIVE vs UNREAD.

Add a case: create UNREAD, archive it, then call notifyIfUnique with same payload and expect creation → this documents uniqueness being scoped to UNREAD only.

web/src/components/Docker/DockerContainerOverview.vue (1)

2-2: Consider removing explicit computed import.

Nuxt provides auto-imports for Vue composition API functions like computed. While explicit imports aren't incorrect and may be required in test files, they're typically unnecessary in component files.

Apply this diff if auto-imports are working in your setup:

-import { computed } from 'vue';
 import { useQuery } from '@vue/apollo-composable';
web/src/components/Docker/DockerSidebarTree.vue (2)

6-9: Narrow emit payload types to what you actually emit.

You only emit for container rows; tighten typing to reflect that and avoid accidental folder handling later.

 interface Emits {
-  (e: 'item:click', item: { id: string; type: string; name: string }): void;
-  (e: 'item:select', item: { id: string; type: string; name: string; selected: boolean }): void;
+  (e: 'item:click', item: { id: string; type: 'container'; name: string }): void;
+  (e: 'item:select', item: { id: string; type: 'container'; name: string; selected: boolean }): void;
 }
 
 function onRowClick(payload: {
   id: string;
   type: 'container' | 'folder';
   name: string;
   containerId?: string;
 }) {
   if (payload.type === 'container') {
-    emit('item:click', { id: payload.id, type: payload.type, name: payload.name });
+    emit('item:click', { id: payload.id, type: 'container', name: payload.name });
   }
 }
 
 function onRowSelect(payload: {
   id: string;
   type: 'container' | 'folder';
   name: string;
   containerId?: string;
   selected: boolean;
 }) {
   if (payload.type === 'container') {
     emit('item:select', {
       id: payload.id,
-      type: payload.type,
+      type: 'container',
       name: payload.name,
       selected: payload.selected,
     });
   }
 }

Also applies to: 31-40, 42-57


63-69: Use a consistent prop access style in the template.

You mix props.* and bare prop keys. Pick one for readability; simplest is to use props.* everywhere here.

 <DockerContainersTable
-  :containers="props.containers"
-  :flat-entries="props.flatEntries"
-  :root-folder-id="props.rootFolderId"
+  :containers="props.containers"
+  :flat-entries="props.flatEntries"
+  :root-folder-id="props.rootFolderId"
   compact
-  :active-id="activeId"
-  :selected-ids="selectedIds"
+  :active-id="props.activeId"
+  :selected-ids="props.selectedIds"
   :loading="props.disabled"
web/src/components/Docker/docker-rename-folder.mutation.ts (1)

3-44: Right-size the selection and include __typename for better cache updates.

Consider extracting a shared fragment for flatEntries and adding __typename fields. This reduces drift across mutations/queries and helps Apollo cache normalization.

 export const RENAME_DOCKER_FOLDER = gql`
   mutation RenameDockerFolder($folderId: String!, $newName: String!) {
     renameDockerFolder(folderId: $folderId, newName: $newName) {
+      __typename
       version
       views {
+        __typename
         id
         name
         rootId
         flatEntries {
+          __typename
           id
           type
           name
           parentId
           depth
           position
           path
           hasChildren
           childrenIds
           meta {
+            __typename
             id
             names
             state
             status
             image
             ports {
+              __typename
               privatePort
               publicPort
               type
             }
             autoStart
             hostConfig {
+              __typename
               networkMode
             }
             created
             isUpdateAvailable
             isRebuildReady
           }
         }
       }
     }
   }
 `;

If multiple documents repeat this shape, extract a FlatEntry fragment and spread it here to avoid duplication.

web/src/components/Notifications/CriticalNotifications.standalone.vue (1)

26-31: Avoid casting empty variables object when the query has no variables.

If the query truly has no variables, prefer passing undefined and adjusting types/codegen so variables are optional to eliminate {} as ... casting.

->(warningsAndAlerts, {} as WarningAndAlertNotificationsQueryVariables, {
+>(warningsAndAlerts, undefined, {
   fetchPolicy: 'network-only',
 });

As per coding guidelines.

api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.spec.ts (2)

12-15: Use a unique temp directory per test run to avoid collisions.

Hard-coding /tmp/test-templates risks cross-run interference. Use a unique path and feed it into the env mock.

-vi.mock('@app/environment.js', () => ({
-    PATHS_DOCKER_TEMPLATES: ['/tmp/test-templates'],
+const TEST_TEMPLATES_DIR = `/tmp/test-templates-${process.pid}-${Date.now()}`;
+vi.mock('@app/environment.js', () => ({
+    PATHS_DOCKER_TEMPLATES: [TEST_TEMPLATES_DIR],
     ENABLE_NEXT_DOCKER_RELEASE: true,
 }));
 
 describe('DockerTemplateScannerService', () => {
     let service: DockerTemplateScannerService;
     let dockerConfigService: DockerConfigService;
     let dockerService: DockerService;
-    const testTemplateDir = '/tmp/test-templates';
+    const testTemplateDir = TEST_TEMPLATES_DIR;
 
     beforeEach(async () => {
         await mkdir(testTemplateDir, { recursive: true });

Also applies to: 21-25, 55-57


270-282: Avoid asserting on exact error wording unless that format is the subject.

This assertion is brittle; prefer checking that an error exists or that an expected code/path was taken.

-            expect(result.errors.length).toBeGreaterThan(0);
-            expect(result.errors[0]).toContain('Failed to get containers');
+            expect(result.errors.length).toBeGreaterThan(0);
+            expect(result.errors[0]).toBeTruthy();

As per coding guidelines.

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

20-21: Wiring looks solid; consider exporting new services if needed externally.

If other modules inject DockerTemplateScannerService or DockerTemplateIconService, re-export them from this module; otherwise this is fine as-is.

Also applies to: 24-24, 30-31

api/src/unraid-api/graph/resolvers/docker/docker-template-icon.service.ts (1)

38-56: Avoid unbounded parallel file reads; batch to cap concurrency.

Large fleets can exhaust file descriptors. Batch reads to, e.g., 25 at a time.

Apply this refactor to getIconsForContainers:

-        const results = await Promise.all(iconPromises);
+        const results: Array<{ id: string; icon: string } | null> = [];
+        const batchSize = 25;
+        for (let i = 0; i < iconPromises.length; i += batchSize) {
+            const batch = iconPromises.slice(i, i + batchSize);
+            results.push(...(await Promise.all(batch)));
+        }
api/src/unraid-api/organizer/organizer.resolution.test.ts (1)

75-112: Reduce index-coupling in tests; prefer id-based selection.

Use flatEntries.find(e => e.id === '...') over fixed indices to avoid brittleness if traversal order changes.

Also applies to: 232-251, 336-347, 390-410, 516-529

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

115-124: Avoid enum re-indexing; pass the enum value directly.

NotificationType[type] is redundant; type is already the enum value.

Apply this diff:

-        const notification = await this.loadNotificationFile(path, NotificationType[type]);
+        const notification = await this.loadNotificationFile(path, type);

145-157: Nice: centralized snapshot publisher for warnings/alerts. Consider coalescing bursts.

On heavy churn, debounce/throttle publishWarningsAndAlerts (e.g., 200–500 ms) to reduce redundant broadcasts.

api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1)

113-116: Improve error logging context/stack.

Pass message + stack to Logger.error for clearer diagnostics.

Apply this diff:

-            const errorMsg = `Failed to get containers: ${error instanceof Error ? error.message : 'Unknown error'}`;
-            this.logger.error(error, 'Failed to get containers');
+            const errorMsg = `Failed to get containers: ${
+                error instanceof Error ? error.message : 'Unknown error'
+            }`;
+            this.logger.error(errorMsg, (error as Error)?.stack);
             result.errors.push(errorMsg);
web/src/composables/useContainerActions.ts (2)

53-55: Remove unnecessary casts; use TreeRow fields directly.

TreeRow already includes containerId and state. Casting to inline shapes is avoidable and risks drift. Read row.containerId and row.state directly.

-const containerId = (row as { containerId?: string }).containerId || row.id;
-const state = (row as { state?: string }).state;
+const containerId = row.containerId ?? row.id;
+const state = row.state;

Also applies to: 68-70, 147-153, 167-173


77-109: Add error handling and user feedback for partial failures.

Batches await each mutation but swallow errors. Provide onError callback (peer to onSuccess) or surface failed items; otherwise users get “Action completed” even when some ops fail.

-export interface ContainerActionOptions<T = unknown> {
+export interface ContainerActionOptions<T = unknown> {
   ...
   onSuccess?: (message: string) => void;
+  onError?: (message: string, detail?: unknown) => void;
 }
@@
   try {
     await runStartStopBatch(toStart, toStop);
     onSuccess?.('Action completed');
   } finally {
+    // optionally: catch and call onError
   }

Also consider try/catch within each loop to collect failures and refetch once at end.

Also applies to: 111-143, 196-202, 229-235, 204-216, 237-249

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

185-192: Subscription shape looks good. Add brief description for SDL clarity.

Optional: add { description } to @subscription decorator to document the stream.

api/src/unraid-api/graph/resolvers/docker/docker.service.ts (2)

2-9: Remove unused OnModuleInit import.

Imported but not used; trim to satisfy static analysis.

-import {
-    forwardRef,
-    Inject,
-    Injectable,
-    Logger,
-    OnApplicationBootstrap,
-    OnModuleInit,
-} from '@nestjs/common';
+import { forwardRef, Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';

289-295: Drop redundant initial containers assignment before polling.

The first assignment is overwritten inside the loop. Initialize updatedContainer directly.

-        let containers = await this.getContainers({ skipCache: true });
-        let updatedContainer: DockerContainer | undefined;
+        let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
-            updatedContainer = containers.find((c) => c.id === id);
+            const containers = await this.getContainers({ skipCache: true });
+            updatedContainer = containers.find((c) => c.id === id);

(Apply similarly to unpause.)

Also applies to: 317-323

web/src/components/Docker/DockerContainersTable.vue (1)

447-458: Avoid blocking window.prompt for folder creation UX.

Inline prompt is jarring and inconsistent with rest of the UI modals. Prefer a small input modal within the existing "Move to folder" dialog or a dedicated modal.

I can provide a small UModal-based "New folder" prompt component if helpful.

web/src/composables/useDragDrop.ts (2)

51-57: Simplify selection logic at dragstart.

The isDraggingId(row.id) check is redundant at dragstart (draggingIds is set later). Clean up for clarity.

-    const ids =
-      selected.length && isDraggingId(row.id)
-        ? selected
-        : selected.length && rowSelection?.value?.[row.id]
-          ? selected
-          : [row.id];
+    const ids = selected.length && rowSelection?.value?.[row.id] ? selected : [row.id];

73-82: Optional: guard RAF in non-DOM environments.

In SSR unit tests without RAF, add a fallback to avoid ReferenceErrors.

-    rafId = requestAnimationFrame(() => {
+    const raf = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (fn: FrameRequestCallback) => {
+      fn(performance?.now?.() ?? 0);
+      return 0 as unknown as number;
+    };
+    rafId = raf(() => {
       const area = computeDropArea(e, element);
       dragOverState.value = { rowId: row.id, area };
       rafId = null;
     });
web/src/composables/useFolderOperations.ts (5)

3-3: ESM import specifier should end with .js

TypeScript files must import with .js extensions for ESM compatibility. Update the path.

-import type { FlatFolderRow } from '@/composables/useFolderTree';
+import type { FlatFolderRow } from '@/composables/useFolderTree.js';

As per coding guidelines.


18-33: Replace explicit any in mutation options with a minimal typed shape

Avoid any. Model the subset you use (refetchQueries, awaitRefetchQueries).

   deleteFolderMutation: (
     args: { entryIds: string[] },
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    options?: any
+    options?: { refetchQueries?: unknown[]; awaitRefetchQueries?: boolean }
   ) => Promise<unknown>;
   setFolderChildrenMutation: (
     args: { folderId: string; childrenIds: string[] },
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    options?: any
+    options?: { refetchQueries?: unknown[]; awaitRefetchQueries?: boolean }
   ) => Promise<unknown>;
   moveEntriesMutation: (
     args: { destinationFolderId: string; sourceEntryIds: string[] },
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    options?: any
+    options?: { refetchQueries?: unknown[]; awaitRefetchQueries?: boolean }
   ) => Promise<unknown>;

As per coding guidelines.


147-163: Interactive rename should also call rename mutation

Mirror the non-interactive path; avoid create+delete.

-  const parentId = parentById.value[id] || rootFolderId.value;
-  const children = folderChildrenIds.value[id] || [];
-  await createFolderMutation({ name: proposed, parentId, childrenIds: children });
-  await setFolderChildrenMutation({ folderId: id, childrenIds: [] });
-  await deleteFolderMutation(
-    { entryIds: [id] },
-    {
-      refetchQueries: [refetchQuery],
-      awaitRefetchQueries: true,
-    }
-  );
+  await options.renameFolderMutation?.(
+    { folderId: id, newName: proposed },
+    { refetchQueries: [refetchQuery], awaitRefetchQueries: true }
+  );
   onSuccess?.('Folder renamed');

74-87: Add basic error handling around move operation

Prevents stuck UI when mutation fails; surfaces errors.

-  await moveEntriesMutation(
+  try {
+    await moveEntriesMutation(
       { destinationFolderId: selectedFolderId.value, sourceEntryIds: ids },
       {
         refetchQueries: [refetchQuery],
         awaitRefetchQueries: true,
       }
     );
     onSuccess?.('Moved to folder');
     close();
+  } catch (err) {
+    console.error('confirmMove failed', err);
+  }

10-12: Remove unused visibleFolders from the public API

It’s not used in this composable; trimming avoids confusion and churn for callers.

-  visibleFolders: Ref<FlatFolderRow[]>;
api/src/unraid-api/organizer/organizer.ts (3)

126-142: Avoid any for meta; use precise type

meta is a DockerContainer (via OrganizerContainerResource.meta). Type it explicitly.

-        let meta: any = undefined;
+        let meta: OrganizerContainerResource['meta'] | undefined = undefined;

As per coding guidelines.


648-654: Unused resources param in MoveItemsToPosition; either use or prefix to silence

It’s not used and triggers CodeQL notices.

-export interface MoveItemsToPositionParams {
+export interface MoveItemsToPositionParams {
   view: OrganizerView;
   sourceEntryIds: Set<string>;
   destinationFolderId: string;
   position: number;
-  resources?: OrganizerV1['resources'];
+  resources?: OrganizerV1['resources'];
 }
@@
-export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
-    const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
+export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
+    const { view, sourceEntryIds, destinationFolderId, position, resources: _resources } = params;

Alternatively, remove the field and update call sites.

Also applies to: 660-678


720-751: Materialize missing refs when creating a folder with items

When sourceEntryIds include resources not yet in entries, ensure ref entries exist before move. Also resolves CodeQL “unused variable resources”.

 export function createFolderWithItems(params: CreateFolderWithItemsParams): OrganizerView {
     const { view, folderId, folderName, parentId, sourceEntryIds = [], position, resources } = params;

-    let newView = createFolderInView({
+    let newView = createFolderInView({
         view,
         folderId,
         folderName,
         parentId,
-        childrenIds: sourceEntryIds,
+        childrenIds: [],
     });

-    if (sourceEntryIds.length > 0) {
-        newView = moveEntriesToFolder({
-            view: newView,
-            sourceEntryIds: new Set(sourceEntryIds),
-            destinationFolderId: folderId,
-        });
-    }
+    if (sourceEntryIds.length > 0) {
+        // Ensure refs exist for resources (if provided)
+        if (resources) {
+            newView = setFolderChildrenInView({
+                view: newView,
+                folderId,
+                childrenIds: sourceEntryIds,
+                resources,
+            });
+        }
+        newView = moveEntriesToFolder({
+            view: newView,
+            sourceEntryIds: new Set(sourceEntryIds),
+            destinationFolderId: folderId,
+        });
+    }

To keep API stable, callers already pass resources (see docker-organizer.service.ts). No other changes required.

web/src/components/Common/BaseTreeTable.vue (2)

458-472: Tighten row typing to avoid any in table callbacks

Leverage TreeRow instead of any for get-row-id/sub-rows/can-select to preserve type safety.

-      :get-row-id="(row: any) => row.id"
-      :get-sub-rows="(row: any) => row.children"
-      :get-row-can-select="(row: any) => row.original.type === selectableType"
+      :get-row-id="(row: TreeRow<T>) => row.id"
+      :get-sub-rows="(row: TreeRow<T>) => row.children"
+      :get-row-can-select="(row: { original: TreeRow<T> }) => row.original.type === selectableType"

224-240: Column-visibility on compact: also reset when expanding back

Currently only sets when compact=true; consider restoring defaults when compact=false to avoid stale hidden columns if props.columns change.

   (isCompact) => {
     if (isCompact) {
       // existing code...
       columnVisibility.value = hideColumns;
+    } else {
+      columnVisibility.value = {};
     }
   },

Ensure this matches UTable’s column-visibility contract in @nuxt/ui.

Comment on lines 198 to 200
private normalizeRepository(repository: string): string {
return repository.split(':')[0].toLowerCase();
}
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

Repository normalization breaks images with ports/digests (false matches).

Split only on a tag colon that appears after the last slash, and strip digests. Current split(':')[0] truncates e.g. registry:5000/repo:tag -> 'registry'.

Apply this diff:

-    private normalizeRepository(repository: string): string {
-        return repository.split(':')[0].toLowerCase();
-    }
+    private normalizeRepository(repository: string): string {
+        // strip digest if present
+        const [noDigest] = repository.split('@');
+        // remove tag only if the colon is after the last slash (i.e., it's a tag, not a port)
+        const lastColon = noDigest.lastIndexOf(':');
+        const lastSlash = noDigest.lastIndexOf('/');
+        const withoutTag = lastColon > lastSlash ? noDigest.slice(0, lastColon) : noDigest;
+        return withoutTag.toLowerCase();
+    }

Comment on lines +35 to +50
@ResolveField(() => DockerContainer, { description: 'Pause (Suspend) a container' })
@UsePermissions({
action: AuthAction.UPDATE_ANY,
resource: Resource.DOCKER,
})
public async pause(@Args('id', { type: () => PrefixedID }) id: string) {
return this.dockerService.pause(id);
}
@ResolveField(() => DockerContainer, { description: 'Unpause (Resume) a container' })
@UsePermissions({
action: AuthAction.UPDATE_ANY,
resource: Resource.DOCKER,
})
public async unpause(@Args('id', { type: () => PrefixedID }) id: string) {
return this.dockerService.unpause(id);
}
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

Pause/unpause resolvers look consistent; please verify service wiring and idempotency.

Looks good with correct @ResolveField and permissions. Two asks:

  • Confirm DockerService exposes pause/unpause and GraphQL schema includes these fields.
  • Ensure pause/unpause are idempotent (calling on already paused/running containers returns 200/OK state vs throwing), and map engine errors to typed GraphQL errors.

Run to verify:

Optional: unify descriptions ("Pause (Suspend)" vs "Unpause (Resume)") for symmetry.


🏁 Script executed:

#!/bin/bash
# 1) Service methods exist
rg -nP --type=ts -C2 '\bclass\s+DockerService\b' api | sed -n '1,200p'
rg -nP --type=ts -n '\bpause\s*\(' api
rg -nP --type=ts -n '\bunpause\s*\(' api

# 2) Schema contains mutations
rg -nP -n '(type|extend\s+type)\s+DockerMutations' api | head -n 5
rg -nP -n '\bpause\s*\(' api | head -n 5
rg -nP -n '\bunpause\s*\(' api | head -n 5

# 3) ContainerState has PAUSED
rg -nP -n 'enum\s+ContainerState\b.*?PAUSED' -U api

Length of output: 3254


🏁 Script executed:

#!/bin/bash
# Get full pause/unpause method implementations
echo "=== pause method implementation ==="
sed -n '283,310p' api/src/unraid-api/graph/resolvers/docker/docker.service.ts

echo -e "\n=== unpause method implementation ==="
sed -n '311,338p' api/src/unraid-api/graph/resolvers/docker/docker.service.ts

# Check for error handling patterns in the file
echo -e "\n=== Error handling patterns in docker.service ==="
rg -nP --type=ts '(catch|throw|GraphQL|error)' api/src/unraid-api/graph/resolvers/docker/docker.service.ts | head -n 20

# Check ContainerState enum
echo -e "\n=== ContainerState enum ==="
rg -nP --type=ts 'enum\s+ContainerState' -A 15 api/src/unraid-api/graph/resolvers/docker/

# Check DockerContainer response type
echo -e "\n=== DockerContainer type/interface ==="
rg -nP --type=ts '(interface|type|class)\s+DockerContainer' -A 5 api/src/unraid-api/graph/

Length of output: 6877


Critical: pause/unpause lack error handling and idempotency guarantees.

Service methods confirmed to exist with correct schema wiring. However, verification reveals two critical gaps:

  1. Missing error handling: container.pause() and container.unpause() calls are unprotected. If already paused/running or on Docker errors, exceptions bubble up as generic errors instead of typed GraphQL errors (inconsistent with error handling patterns elsewhere in the service like handleDockerListError).

  2. Not idempotent: Methods call container.pause()/unpause() without checking current state first. Calling pause on already-paused containers will likely throw, violating idempotency expectations.

Required fixes at api/src/unraid-api/graph/resolvers/docker/docker.service.ts:

  • Lines 283–310 (pause): Wrap container.pause() in try/catch; map errors to GraphQL errors; optionally check current state first.
  • Lines 311–338 (unpause): Same pattern—wrap call, map errors, check state.

Optional: Unify resolver descriptions ("Pause (Suspend)" vs "Unpause (Resume)") for consistency.

Comment on lines +59 to 67
async getResources(
opts?: Partial<ContainerListOptions> & { skipCache?: boolean }
): Promise<OrganizerV1['resources']> {
const { skipCache = false, ...listOptions } = opts ?? {};
const containers = await this.dockerService.getContainers({
skipCache,
...(listOptions as any),
});
return containerListToResourcesObject(containers);
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

Avoid any spread; keep typed options.

Minor typing issue: (listOptions as any) breaks type safety.

-        const containers = await this.dockerService.getContainers({
-            skipCache,
-            ...(listOptions as any),
-        });
+        const containers = await this.dockerService.getContainers({
+            skipCache,
+            ...(listOptions as Partial<ContainerListOptions>),
+        });

If DockerService has a narrow options type, consider introducing a shared GetContainersOptions interface to avoid casting altogether.

🤖 Prompt for AI Agents
In
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
around lines 59 to 67, the code spreads (listOptions as any) into getContainers
which breaks type safety; replace the any cast by using a properly typed options
interface: create or import a shared GetContainersOptions (e.g., a narrow
interface that matches DockerService.getContainers parameter), type the local
opts/listOptions to Partial<GetContainersOptions> & { skipCache?: boolean }, and
spread listOptions without casting so the call to
this.dockerService.getContainers remains fully typed; if necessary adjust
DockerService.getContainers signature to accept GetContainersOptions to keep
types aligned.

Comment on lines 889 to 892
private sortLatestFirst(a: Notification, b: Notification) {
const defaultTimestamp = 0;
return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);
}
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 sort comparator: ISO timestamps -> NaN with Number(); breaks ordering.

Compare by parsed time to keep latest-first ordering.

Apply this diff:

-    private sortLatestFirst(a: Notification, b: Notification) {
-        const defaultTimestamp = 0;
-        return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);
-    }
+    private sortLatestFirst(a: Notification, b: Notification) {
+        const toMs = (t?: string | null) => (t ? Date.parse(t) : 0);
+        return toMs(b.timestamp) - toMs(a.timestamp);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private sortLatestFirst(a: Notification, b: Notification) {
const defaultTimestamp = 0;
return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);
}
private sortLatestFirst(a: Notification, b: Notification) {
const toMs = (t?: string | null) => (t ? Date.parse(t) : 0);
return toMs(b.timestamp) - toMs(a.timestamp);
}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
around lines 889 to 892, the comparator uses Number(...) on ISO timestamp
strings which yields NaN and breaks ordering; replace it with parsed epoch
values (e.g., Date.parse(...) or new Date(...).getTime()) and fall back to
defaultTimestamp (0) when parsing fails or timestamp is missing so the function
reliably returns bTime - aTime for latest-first ordering.

Comment on lines +132 to +145
async function handleDeleteFolder() {
const id = selectedFolderId.value;
if (!id || id === rootFolderId.value) return;
if (!confirm('Delete this folder? Contents will move to root.')) return;
await deleteFolderMutation(
{ entryIds: [id] },
{
refetchQueries: [refetchQuery],
awaitRefetchQueries: true,
}
);
selectedFolderId.value = rootFolderId.value;
onSuccess?.('Folder deleted');
}
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

Confirmation text is misleading; backend deletes contents, not “move to root”

deleteDockerEntries cascades delete; telling users “Contents will move to root” risks data loss.

-  if (!confirm('Delete this folder? Contents will move to root.')) return;
+  if (!confirm('Delete this folder? This will also delete all its contents.')) return;

If you actually intend “move to root”, implement a dedicated mutation to reparent children before deleting, and call that here.

Also applies to: 165-176

🤖 Prompt for AI Agents
In web/src/composables/useFolderOperations.ts around lines 132 to 145 (and apply
the same change to 165–176), the confirmation prompt falsely states “Contents
will move to root” even though the backend cascades deletes; update the
confirmation text to accurately warn that deleting the folder will also delete
its contents (e.g., “Delete this folder and all its contents?”) OR, if you
intend to move children to the root instead of deleting, implement and call a
dedicated reparenting mutation to move child entries to root before calling the
deleteFolderMutation and adjust the UI flow to await that mutation and only then
delete the now-empty folder.

Comment on lines +64 to +71
function expandFolder(id: string) {
expandedFolders.value.add(id);
}

function collapseFolder(id: string) {
expandedFolders.value.delete(id);
}

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 non-reactive Set mutations in expand/collapse.

Mutating Set in place won’t trigger reactivity. Reassign a new Set after updates (like toggle does).

 function expandFolder(id: string) {
-  expandedFolders.value.add(id);
+  const set = new Set(expandedFolders.value);
+  set.add(id);
+  expandedFolders.value = set;
 }
 
 function collapseFolder(id: string) {
-  expandedFolders.value.delete(id);
+  const set = new Set(expandedFolders.value);
+  set.delete(id);
+  expandedFolders.value = set;
 }
🤖 Prompt for AI Agents
In web/src/composables/useFolderTree.ts around lines 64 to 71, expandFolder and
collapseFolder mutate the Set in place which does not trigger Vue reactivity;
change them to create and assign a new Set instead (for expandFolder: build a
new Set from the existing values plus the id, then assign to
expandedFolders.value; for collapseFolder: create a copy of the current Set,
remove the id from the copy, then assign that new Set to expandedFolders.value)
so updates are reactive (match the toggle implementation pattern).

import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';

import { globalPinia } from '~/store/globalPinia';
import { ensureUnapiScope } from '~/utils/unapiScope';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use .js extension in TS ESM import.

For ESM compatibility, import internal TS modules with .js in specifiers.

-import { ensureUnapiScope } from '~/utils/unapiScope';
+import { ensureUnapiScope } from '~/utils/unapiScope.js';

As per coding guidelines. Mount logic is fine; no other issues noted.

Also applies to: 31-37

🤖 Prompt for AI Agents
In web/src/main.ts around lines 15 and also for imports at lines 31-37, the
review notes that TypeScript ESM imports of internal modules must include the
.js extension in the import specifiers; update each import statement (including
the one for ~/utils/unapiScope and the imports on lines 31-37) to append .js to
the module path so the compiled ESM output resolves correctly at runtime.

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: 2

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/app/__test__/app.module.integration.spec.ts (1)

69-113: Add mock cleanup between tests.

The guidelines require clearing mocks between tests to prevent state pollution. Add an afterEach hook to reset mock state.

As per coding guidelines.

Apply this diff to add mock cleanup:

     afterAll(async () => {
         if (app) {
             await app.close();
         }
     });
+
+    afterEach(() => {
+        vi.clearAllMocks();
+    });
🧹 Nitpick comments (4)
api/src/core/utils/misc/catch-handlers.ts (1)

4-6: Type change looks good, but consider explicit undefined handling below.

Making address optional better reflects the actual error structure. However, the usage at line 16 would benefit from an explicit check.

Consider applying this defensive check at line 16 for clarity:

-        if (error.code === 'ENOENT' && error.address === socketPath) {
+        if (error.code === 'ENOENT' && error.address && error.address === socketPath) {

This makes the undefined handling explicit and improves readability.

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

52-57: LGTM! Consider exposing an optional limit parameter.

The implementation is clean and follows existing resolver patterns. The service method signature suggests it accepts a limit parameter—consider exposing this as an optional argument to give clients control over the result size, or confirm the default behavior is intentional.

api/src/unraid-api/app/__test__/app.module.integration.spec.ts (1)

183-189: Clarify test description to match actual behavior.

The test description claims to verify "service-to-service communication," but it only checks that AuthService can be resolved and has the expected method. No actual inter-service communication is tested.

Apply this diff to align the description with the actual test:

-        it('should have working service-to-service communication', () => {
-            // Test that the module can resolve its services without errors
-            // This validates that dependency injection is working correctly
+        it('should resolve services with correct method signatures', () => {
             const authService = moduleRef.get(AuthService);
             expect(authService).toBeDefined();
             expect(typeof authService.validateCookiesWithCsrfToken).toBe('function');
api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts (1)

125-131: Consider resetting all mock methods for consistency.

Currently, only some mock methods are reset in beforeEach. For better test isolation and to prevent potential state leakage in future tests, consider also resetting:

  • mockDockerConfigService.replaceConfig
  • mockDockerConfigService.validate
  • mockDockerTemplateScannerService.scanTemplates
  • mockNotificationsService.notifyIfUnique

Example:

     mockDockerTemplateScannerService.bootstrapScan.mockResolvedValue(undefined);
     mockDockerTemplateScannerService.syncMissingContainers.mockResolvedValue(false);
+    mockDockerTemplateScannerService.scanTemplates.mockResolvedValue({
+        scanned: 0,
+        matched: 0,
+        skipped: 0,
+        errors: [],
+    });
+    mockDockerConfigService.replaceConfig.mockReset();
+    mockDockerConfigService.validate.mockResolvedValue(undefined);
+    mockNotificationsService.notifyIfUnique.mockResolvedValue(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 b4526b6 and 7b8a4ff.

📒 Files selected for processing (6)
  • api/src/core/utils/misc/catch-handlers.ts (1 hunks)
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/core/utils/misc/catch-handlers.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

api/**/*.{test,spec}.{ts,tsx}: API test suite is Vitest; do not use Jest
Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/core/utils/misc/catch-handlers.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
🧠 Learnings (10)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to {api,web}/**/*.{test,spec}.{ts,tsx} : Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Let stores initialize with natural default state; don’t mock the store under test

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store under test; allow the store to initialize with its natural default state

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly

Applied to files:

  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts
🧬 Code graph analysis (1)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3)
web/src/composables/gql/graphql.ts (4)
  • Notification (1459-1472)
  • Mutation (1264-1311)
  • NotificationData (1482-1488)
  • Subscription (2113-2126)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/core/pubsub.ts (1)
  • createSubscription (19-23)
🔇 Additional comments (11)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (2)

5-5: LGTM!

The import statement correctly includes the .js extension for ESM compatibility and is necessary for providing the mock dependency in the test setup.


220-225: LGTM!

The mock provider for DockerTemplateIconService is appropriately structured and follows the established pattern of other service mocks in this test suite. Mocking this dependency is suitable since icon extraction is a non-trivial operation, and the mock correctly provides a minimal implementation that returns an empty Map for isolated testing of DockerOrganizerService.

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

106-116: Formatting issue resolved.

The Prettier formatting issue flagged in the previous review has been addressed—the description is now properly split across lines 108-109 with correct indentation. The mutation logic and types are correct.


186-193: LGTM!

The subscription is properly implemented with correct permissions, follows the established pattern for other subscriptions in the file, and correctly delegates to the pub/sub infrastructure.

api/src/unraid-api/app/__test__/app.module.integration.spec.ts (2)

13-63: Mock setup is well-structured.

The centralized store mock with factory functions aligns with testing guidelines. The mock provides comprehensive test data for CSRF tokens, Docker state, and filesystem paths.


115-126: LGTM!

The tests correctly validate module compilation and dependency injection with overridden providers.

api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts (5)

10-11: LGTM! Imports follow ESM conventions.

The new service imports correctly use .js extensions for ESM compatibility.

Also applies to: 14-14


85-94: LGTM! Mock structure follows existing patterns.

The mockDockerConfigService is properly structured with sensible default return values for testing.


96-106: LGTM! Mock implementation is well-structured.

The mockDockerTemplateScannerService provides appropriate default return values for all methods.


108-111: LGTM! Minimal mock is appropriate.

The mockNotificationsService follows the established pattern.


140-151: LGTM! Provider injection follows NestJS testing patterns.

The new service mocks are properly injected into the TestingModule, enabling isolated unit testing of the DockerService.

Comment on lines +187 to +193
beforeAll(() => {
if (!isQemuAvailable()) {
throw new Error(
'QEMU not available - skipping VM integration tests. Please install QEMU to run these tests.'
);
}
});
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

Clarify error message - throwing doesn't skip tests.

The error message states "skipping VM integration tests", but throwing an error in beforeAll fails the entire test suite rather than skipping it. The fail-fast approach is reasonable, but the message should reflect the actual behavior.

Apply this diff to clarify the message:

-            throw new Error(
-                'QEMU not available - skipping VM integration tests. Please install QEMU to run these tests.'
-            );
+            throw new Error(
+                'QEMU not available. Please install QEMU to run VM integration tests.'
+            );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeAll(() => {
if (!isQemuAvailable()) {
throw new Error(
'QEMU not available - skipping VM integration tests. Please install QEMU to run these tests.'
);
}
});
beforeAll(() => {
if (!isQemuAvailable()) {
throw new Error(
'QEMU not available. Please install QEMU to run VM integration tests.'
);
}
});
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts around lines 187
to 193, the beforeAll currently throws an Error saying "QEMU not available -
skipping VM integration tests..." which is inaccurate because throwing in
beforeAll fails the suite rather than skipping; update the thrown Error message
to accurately state the behavior (e.g., "QEMU not available - failing VM
integration tests. Install QEMU to run these tests.") so the message reflects
that the tests will fail when QEMU is missing.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 7b8a4ff to 79f874a Compare October 27, 2025 18:07
@codecov
Copy link

codecov bot commented Oct 27, 2025

Codecov Report

❌ Patch coverage is 18.17858% with 6900 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.40%. Comparing base (d13a1f6) to head (970876e).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...eb/src/components/Docker/DockerContainersTable.vue 0.16% 604 Missing ⚠️
...rc/components/Docker/DockerContainerManagement.vue 0.17% 572 Missing ⚠️
...eb/src/components/Docker/ContainerOverviewCard.vue 0.20% 493 Missing ⚠️
web/src/components/Common/BaseTreeTable.vue 0.21% 459 Missing ⚠️
.../src/components/Docker/DockerAutostartSettings.vue 0.29% 338 Missing ⚠️
web/src/composables/useDockerTableColumns.ts 0.00% 331 Missing and 1 partial ⚠️
...graph/resolvers/docker/docker-tailscale.service.ts 8.55% 246 Missing ⚠️
web/src/composables/useContainerActions.ts 0.00% 243 Missing and 1 partial ⚠️
...src/components/Docker/DockerTailscaleIndicator.vue 0.42% 233 Missing ⚠️
...Notifications/CriticalNotifications.standalone.vue 0.45% 218 Missing ⚠️
... and 82 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1764      +/-   ##
==========================================
- Coverage   52.89%   46.40%   -6.50%     
==========================================
  Files         868      954      +86     
  Lines       50051    59770    +9719     
  Branches     5079     5541     +462     
==========================================
+ Hits        26473    27734    +1261     
- Misses      23503    31917    +8414     
- Partials       75      119      +44     

☔ View full report in Codecov by Sentry.
📢 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.

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: 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/auth/api-key.service.ts (1)

232-236: Avoid using any type.

The explicit any type annotation violates the coding guidelines. Define a proper type for the parsed permission structure or use unknown with appropriate type narrowing.

As per coding guidelines.

Consider defining an interface for the parsed content:

+            interface ParsedPermission {
+                resource?: string;
+                actions?: string[];
+            }
+
             // Normalize permission actions to AuthAction enum values
             // Uses shared helper to handle all legacy formats
             if (parsedContent.permissions) {
-                parsedContent.permissions = parsedContent.permissions.map((permission: any) => ({
+                parsedContent.permissions = parsedContent.permissions.map((permission: ParsedPermission) => ({
                     ...permission,
                     actions: normalizeLegacyActions(permission.actions || []),
                 }));
🧹 Nitpick comments (3)
api/src/core/utils/misc/catch-handlers.ts (1)

16-16: Consider adding explicit undefined check for defensive programming.

While the current logic works correctly in typical scenarios, explicitly checking for error.address before comparison would guard against the edge case where both address and socketPath are undefined.

Apply this diff for more explicit defensive handling:

-        if (error.code === 'ENOENT' && error.address === socketPath) {
+        if (error.code === 'ENOENT' && error.address && error.address === socketPath) {
api/src/unraid-api/auth/api-key.service.ts (2)

186-190: Use a lower log level for non-fatal ENOENT case.

The code treats ENOENT as non-fatal (returns empty array), but logs it as an error. Since the constructor ensures the directory exists and this scenario represents a valid state (no API keys created yet), consider using this.logger.debug() or this.logger.warn() instead of this.logger.error() to avoid alarming users or operators with error-level logs for expected conditions.

Apply this diff:

     if (error.code === 'ENOENT') {
         // Directory doesn't exist, which means no API keys have been created yet
-        this.logger.error(`API key directory does not exist: ${this.basePath}`);
+        this.logger.debug(`API key directory does not exist: ${this.basePath}`);
         return [];
     }

66-70: Correct the chokidar callback signature.

The 'all' event handler receives (event, path) parameters, but the callback only declares (path). While this doesn't cause a runtime error due to JavaScript's parameter handling, it's imprecise and prevents logging which event occurred (add, change, unlink).

Apply this diff:

-    watch(this.basePath, { ignoreInitial: false }).on('all', async (path) => {
-        this.logger.debug(`API key changed: ${path}`);
+    watch(this.basePath, { ignoreInitial: false }).on('all', async (event, path) => {
+        this.logger.debug(`API key ${event}: ${path}`);
         this.memoryApiKeys = [];
         this.memoryApiKeys = await this.loadAllFromDisk();
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b8a4ff and 79f874a.

📒 Files selected for processing (7)
  • api/src/core/utils/misc/catch-handlers.ts (1 hunks)
  • api/src/unraid-api/app/__test__/app.module.integration.spec.ts (3 hunks)
  • api/src/unraid-api/auth/api-key.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
  • api/src/unraid-api/app/test/app.module.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts
🧰 Additional context used
📓 Path-based instructions (3)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/auth/api-key.service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/core/utils/misc/catch-handlers.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/auth/api-key.service.ts
  • api/src/core/utils/misc/catch-handlers.ts
🧠 Learnings (2)
📚 Learning: 2024-11-05T14:49:07.308Z
Learnt from: elibosley
PR: unraid/api#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`.

Applied to files:

  • api/src/unraid-api/auth/api-key.service.ts
📚 Learning: 2024-11-04T20:41:22.303Z
Learnt from: mdatelle
PR: unraid/api#942
File: api/src/unraid-api/auth/auth.service.ts:0-0
Timestamp: 2024-11-04T20:41:22.303Z
Learning: In `api/src/unraid-api/auth/auth.service.ts`, the `addRoleToApiKey` function operates on API keys stored as JSON files in a directory, not a database, so concurrency is not a concern for modifying `apiKey.roles`.

Applied to files:

  • api/src/unraid-api/auth/api-key.service.ts
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3)
web/src/composables/gql/graphql.ts (4)
  • Notification (1459-1472)
  • Mutation (1264-1311)
  • NotificationData (1482-1488)
  • Subscription (2113-2126)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/core/pubsub.ts (1)
  • createSubscription (19-23)
api/src/unraid-api/auth/api-key.service.ts (1)
api/src/unraid-api/cli/log.service.ts (1)
  • error (65-69)
🔇 Additional comments (3)
api/src/core/utils/misc/catch-handlers.ts (1)

4-6: LGTM! Correct defensive typing improvement.

Making the address property optional accurately models the behavior of NodeJS.ErrnoException, which doesn't always include an address. This prevents type errors when handling Docker errors that lack this property.

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

52-57: LGTM!

The warningsAndAlerts ResolveField is well-structured with a clear description and proper typing. The delegation to the service layer follows the pattern used throughout the resolver.


185-193: Code implementation verified; external package definition cannot be inspected in this environment.

The subscription correctly imports PUBSUB_CHANNEL from @app/core/pubsub.js and uses PUBSUB_CHANNEL.NOTIFICATION_WARNINGS_AND_ALERTS consistently with the service layer (which publishes to the same channel at line 148 of notifications.service.ts). The resolver has proper @Subscription and @UsePermissions decorators with correct type annotations. However, the actual NOTIFICATION_WARNINGS_AND_ALERTS constant definition resides in the external @unraid/shared package, which cannot be inspected in the sandbox environment. The fact that the code compiles and both service and resolver use the same channel consistently indicates the constant is properly defined.

@pujitm
Copy link
Member Author

pujitm commented Oct 27, 2025

@coderabbitai pause

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

✅ Actions performed

Reviews paused.

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: 1

🧹 Nitpick comments (1)
web/src/__tests__/scopeTailwindToUnapi.spec.ts (1)

70-77: LGTM! Test validates the core root element scoping behavior.

The test correctly verifies that html and body selectors are transformed to the scope element (.unapi), with suffixes properly preserved.

Optional: Consider additional test cases for comprehensive coverage.

The current test covers the primary scenarios. For more thorough validation, you could add cases for:

  • Combinators: html > div.unapi > div
  • Pseudo-selectors: body:hover.unapi:hover
  • Multiple suffixes: body.dark.theme-custom.unapi.dark.theme-custom
  • Negative cases: verify htmldiv is treated as a regular class (not specially scoped)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79f874a and 5d6bda0.

📒 Files selected for processing (3)
  • web/postcss/scopeTailwindToUnapi.ts (2 hunks)
  • web/src/__tests__/scopeTailwindToUnapi.spec.ts (1 hunks)
  • web/src/assets/main.css (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/src/__tests__/scopeTailwindToUnapi.spec.ts
  • web/postcss/scopeTailwindToUnapi.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/src/__tests__/scopeTailwindToUnapi.spec.ts
🧠 Learnings (2)
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Tailwind v4 is used; no tailwind config file is needed

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/src/assets/main.css
🧬 Code graph analysis (1)
web/src/__tests__/scopeTailwindToUnapi.spec.ts (1)
web/postcss/scopeTailwindToUnapi.ts (1)
  • scopeTailwindToUnapi (133-168)
🔇 Additional comments (4)
web/postcss/scopeTailwindToUnapi.ts (2)

39-39: LGTM! Pattern correctly identifies root element selectors.

The regex properly matches html or body only when followed by valid selector boundaries, preventing false positives like htmldiv or bodybuilder.


108-112: LGTM! Root element transformation logic is sound.

The implementation correctly:

  • Strips html or body from the selector
  • Preserves any suffix (classes, pseudo-selectors, combinators)
  • Returns just the scope when no suffix exists

Examples: html.unapi, body.dark.unapi.dark, html:hover.unapi:hover

web/src/assets/main.css (2)

12-12: ✓ Nuxt UI import re-enabled for Docker web component feature.

Re-enabling the Nuxt UI import is appropriate for the Docker web component implementation. This aligns with the PR objectives.


38-40: Dialog button resets look good but verify styling consistency.

The simplified button resets (removing !important flags and using standard properties) are cleaner. Ensure Nuxt UI modal/slideover buttons maintain proper styling across the dialog variants. This assumes Nuxt UI v3 buttons don't rely on inherited button styles that the preflight previously provided globally.

Test that Nuxt UI modal close buttons and dialog controls render correctly with the scoped preflight approach.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 5d6bda0 to 234c2e2 Compare October 27, 2025 19:44
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: 2

Caution

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

⚠️ Outside diff range comments (1)
web/src/__tests__/scopeTailwindToUnapi.spec.ts (1)

5-5: Missing .js extension in import specifier.

The import path should include the .js extension for ESM compatibility.

As per coding guidelines

Apply this diff:

-import scopeTailwindToUnapi from '../../postcss/scopeTailwindToUnapi';
+import scopeTailwindToUnapi from '../../postcss/scopeTailwindToUnapi.js';
♻️ Duplicate comments (1)
web/src/assets/main.css (1)

27-28: Preflight scoping approach remains problematic.

As previously flagged, importing tailwindcss/preflight.css inside @layer unapi-base has fundamental issues—preflight targets document root elements (html, body) which cannot be scoped to a container selector like .unapi.

Refer to the earlier review comment for detailed solutions: either omit preflight and provide custom scoped resets, or use a PostCSS nesting plugin with :where(.unapi) wrapper.

🧹 Nitpick comments (1)
web/src/__tests__/scopeTailwindToUnapi.spec.ts (1)

70-77: LGTM: Test correctly validates root element scoping.

The test appropriately verifies both bare root elements (html.unapi) and root elements with suffixes (body.dark.unapi.dark).

Consider adding edge case coverage for selectors like html.dark, body > div, or html body in future test iterations to ensure comprehensive validation of the ROOT_ELEMENT_PATTERN logic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6bda0 and 234c2e2.

📒 Files selected for processing (4)
  • web/postcss/scopeTailwindToUnapi.ts (2 hunks)
  • web/src/__tests__/scopeTailwindToUnapi.spec.ts (1 hunks)
  • web/src/assets/main.css (2 hunks)
  • web/vite.config.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/postcss/scopeTailwindToUnapi.ts
  • web/vite.config.ts
  • web/src/__tests__/scopeTailwindToUnapi.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/src/__tests__/scopeTailwindToUnapi.spec.ts
🧠 Learnings (2)
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Tailwind v4 is used; no tailwind config file is needed

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/src/assets/main.css
🧬 Code graph analysis (1)
web/src/__tests__/scopeTailwindToUnapi.spec.ts (1)
web/postcss/scopeTailwindToUnapi.ts (1)
  • scopeTailwindToUnapi (133-168)
🪛 GitHub Actions: CI - Main (API)
web/src/assets/main.css

[error] 1-1: [@tailwindcss/vite:generate:build] Package path ./.nuxt/ui.css is not exported from package /home/runner/work/api/api/web/node_modules/@nuxt/ui (see exports field in /home/runner/work/api/api/web/node_modules/@nuxt/ui/package.json)

🔇 Additional comments (5)
web/src/assets/main.css (1)

39-42: LGTM! Improved button reset specificity.

Removing !important from these button resets improves maintainability while still achieving the necessary style resets for dialog and Radix UI buttons.

web/postcss/scopeTailwindToUnapi.ts (3)

38-38: LGTM: Light mode pattern addition is consistent.

The addition of /^\.light\b/ mirrors the existing dark mode pattern and correctly uses a word boundary to ensure precise matching.


39-39: LGTM: Regex pattern correctly identifies root elements.

The lookahead pattern ensures html and body are matched only when followed by valid CSS syntax, and the character class appropriately covers common selectors and combinators.


108-112: LGTM: Root element transformation logic is correct.

The implementation properly transforms root elements (html/body) to the scope element with any suffix preserved (e.g., body.dark.unapi.dark, html.unapi).

web/src/__tests__/scopeTailwindToUnapi.spec.ts (1)

79-86: LGTM: Test correctly validates theme mode merging.

The test properly verifies that both .light and .dark selectors are merged onto the unapi scope (.unapi.light, .unapi.dark), confirming the updated MERGE_WITH_SCOPE_PATTERNS behavior.

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

🧹 Nitpick comments (1)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1)

9-9: Consider using readonly for the id property.

The id property could be simplified by adding the readonly modifier, and the explicit type annotation is optional when initialized with a string literal.

-    id: string = 'docker-containers-page';
+    readonly id = 'docker-containers-page';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62fea59 and c13d395.

📒 Files selected for processing (1)
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
🧬 Code graph analysis (1)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
  • ShouldApplyWithReason (14-23)
🔇 Additional comments (4)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (4)

1-6: LGTM: Imports follow ESM guidelines.

The imports correctly use .js extensions for local modules as required for ESM compatibility.


32-37: LGTM: Patch generation logic is correct.

The method appropriately reads the current file content, generates new content, and creates a diff-based patch. The override path support is a good pattern for testing.


19-19: No issues found—method exists and is correctly used.

The isUnraidVersionGreaterThanOrEqualTo method is properly defined as a protected async method on the FileModification base class (api/src/unraid-api/unraid-file-modifier/file-modification.ts, line 298) and is correctly called on line 19 of the docker-containers-page.modification.ts file.


39-52: No issues found—web component is properly registered and complete file replacement is intentional.

The modification follows the FileModification pattern to replace the entire Unraid Docker Containers page template. The web component unraid-docker-container-overview is correctly registered in component-registry.ts and imports DockerContainerOverview.standalone.vue (confirmed to exist). The complete replacement is the intended behavior to upgrade the UI for Unraid versions < 7.3, with a version check that disables the modification for Unraid 7.3+ where Docker integration is native.

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: 3

♻️ Duplicate comments (2)
web/src/components/Docker/DockerContainersTable.vue (2)

95-114: CRITICAL: Row ID must use stable organizer identifier (container name), not Docker runtime ID.

This issue was flagged in a previous review but remains unresolved. Line 104 uses meta?.id || name, which sets the row ID to the Docker runtime ID. However, the useTreeData composable keys entryParentById and positionById maps by organizer entry IDs (stable container names). This mismatch causes silent failures in drag-and-drop, folder moves, and position lookups because the row ID won't match the keys in these maps.

Apply this diff to fix the issue:

 function toContainerTreeRow(
   meta: DockerContainer | null | undefined,
   fallbackName?: string
 ): TreeRow<DockerContainer> {
   const name = meta?.names?.[0]?.replace(/^\//, '') || fallbackName || 'Unknown';
   const updatesParts: string[] = [];
   if (meta?.isUpdateAvailable) updatesParts.push('Update');
   if (meta?.isRebuildReady) updatesParts.push('Rebuild');
   return {
-    id: meta?.id || name,
+    id: name,
     type: 'container',
     name,
     state: meta?.state ?? '',
     ports: formatPorts(meta || undefined),
     autoStart: meta?.autoStart ? 'On' : 'Off',
     updates: updatesParts.join(' / ') || '—',
     containerId: meta?.id,
     meta: meta || undefined,
   };
 }

232-249: Normalize container state before color mapping.

This issue was flagged in a previous review but remains unresolved. The meta.state value may be a lowercase string (e.g., "running", "paused", "exited"), but the colorMap is keyed by ContainerState enum values which are typically uppercase. Without normalization, the lookup at line 243 will silently fail and default to 'neutral', causing incorrect badge colors.

Apply this diff to normalize the state:

       cell: ({ row }) => {
         if (row.original.type === 'folder') return '';
-        const state = row.original.state ?? '';
+        const state = String(row.original.state ?? '').toUpperCase();
         const isBusy = busyRowIds.value.has(row.original.id);
         const colorMap: Record<string, 'success' | 'warning' | 'neutral'> = {
           [ContainerState.RUNNING]: 'success',
           [ContainerState.PAUSED]: 'warning',
           [ContainerState.EXITED]: 'neutral',
         };
         const color = colorMap[state] || 'neutral';
         if (isBusy) {
           return h(USkeleton, { class: 'h-5 w-20' });
         }
         return h(UBadge, { color }, () => state);
       },
🧹 Nitpick comments (10)
web/src/helpers/env.ts (1)

7-10: Feature flag default semantics: confirm intent.

This enables navigation by default and only disables when env is exactly 'false'. If that’s intended, LGTM. Optionally mark as readonly for safer typing.

-export const featureFlags = {
+export const featureFlags = {
   /** Navigate to legacy edit page when clicking docker containers instead of showing compacted view */
   DOCKER_EDIT_PAGE_NAVIGATION: String(import.meta.env.VITE_DOCKER_EDIT_PAGE_NAVIGATION) !== 'false',
-};
+} as const;
web/src/components/Docker/DockerContainerManagement.vue (1)

235-249: Avoid 1970-01-01T... when created is missing.

Show a placeholder instead of epoch when created is falsy.

-    creationDate: new Date((c.created || 0) * 1000).toISOString(),
+    creationDate: c.created ? new Date(c.created * 1000).toISOString() : '—',
web/src/composables/useDockerEditNavigation.ts (2)

1-1: ESM import extension for TS runtime imports.

Per guidelines, use .js extension in TS import specifiers for ESM.

-import { featureFlags } from '@/helpers/env';
+import { featureFlags } from '@/helpers/env.js';

As per coding guidelines.


18-23: Simplify basePath derivation.

pathname never includes '?'. This can be simplified; also avoid trailing slash duplication.

-    const currentPath = window.location.pathname;
-    const basePath = currentPath.substring(
-      0,
-      currentPath.indexOf('?') === -1 ? currentPath.length : currentPath.indexOf('?')
-    );
-    window.location.href = `${basePath}/UpdateContainer?xmlTemplate=edit:${encodeURIComponent(templatePath)}`;
+    const basePath = window.location.pathname.replace(/\/$/, '');
+    window.location.href = `${basePath}/UpdateContainer?xmlTemplate=edit:${encodeURIComponent(templatePath)}`;
web/src/components/Docker/DockerContainersTable.vue (3)

143-177: Consider simplifying the bidirectional columnVisibility sync.

The current implementation uses a computed property with getter/setter plus two watchers to maintain bidirectional sync between baseTableRef.columnVisibility and columnVisibilityFallback. While functional, this pattern is complex and could lead to subtle timing issues or unnecessary re-renders.

Consider using a single source of truth with a simpler fallback mechanism:

-const columnVisibilityFallback = ref<Record<string, boolean>>({});
-const columnVisibility = computed({
-  get: (): Record<string, boolean> => {
-    if (baseTableRef.value?.columnVisibility) {
-      return baseTableRef.value.columnVisibility.value;
-    }
-    return columnVisibilityFallback.value;
-  },
-  set: (value: Record<string, boolean>) => {
-    if (baseTableRef.value?.columnVisibility) {
-      baseTableRef.value.columnVisibility.value = value;
-    }
-    columnVisibilityFallback.value = value;
-  },
-});
-
-watch(
-  () => baseTableRef.value?.columnVisibility?.value,
-  (value) => {
-    if (value) {
-      columnVisibilityFallback.value = value;
-    }
-  },
-  { immediate: true, deep: true }
-);
-
-watch(
-  baseTableRef,
-  (instance) => {
-    if (instance?.columnVisibility && Object.keys(columnVisibilityFallback.value).length) {
-      instance.columnVisibility.value = { ...columnVisibilityFallback.value };
-    }
-  },
-  { immediate: true }
-);
+const columnVisibility = computed(() => 
+  baseTableRef.value?.columnVisibility?.value ?? {}
+);
+
+function setColumnVisibility(value: Record<string, boolean>) {
+  if (baseTableRef.value?.columnVisibility) {
+    baseTableRef.value.columnVisibility.value = value;
+  }
+}

Then update line 366 from columnVisibility.value = { ...current, [id]: next }; to setColumnVisibility({ ...current, [id]: next });


385-396: Consider using a proper toast service instead of global window.toast.

The current implementation augments the global Window interface and directly accesses window.toast. While functional, this approach is fragile and doesn't provide type safety or testability.

Consider creating a composable like useToast() that provides a strongly-typed toast API:

// composables/useToast.ts
export function useToast() {
  return {
    success: (message: string, options?: { description?: string }) => {
      // Implementation
    },
    error: (message: string, options?: { description?: string }) => {
      // Implementation
    },
  };
}

Then use it in the component:

-declare global {
-  interface Window {
-    toast?: {
-      success: (title: string, options: { description?: string }) => void;
-      error?: (title: string, options: { description?: string }) => void;
-    };
-  }
-}
-
-function showToast(message: string) {
-  window.toast?.success(message);
-}
+const toast = useToast();
+
+function showToast(message: string) {
+  toast.success(message);
+}

445-465: Consider replacing window.prompt with a proper modal dialog.

Line 448 uses window.prompt() to collect the new folder name. While functional, this provides a poor user experience with limited styling and validation capabilities.

Consider using the existing UModal component pattern (similar to the move/rename modals) to create a dedicated "Create Folder" modal with proper validation and styling. This would provide:

  • Better visual consistency with the rest of the UI
  • Input validation before submission
  • Ability to cancel without dismissing via keyboard shortcuts
  • Accessibility improvements

Example structure:

<UModal v-model:open="createFolderModalOpen" title="Create folder">
  <template #body>
    <UInput 
      v-model="newFolderName" 
      placeholder="Folder name"
      @keydown.enter="confirmCreateFolder"
    />
  </template>
  <template #footer="{ close }">
    <UButton variant="outline" @click="close">Cancel</UButton>
    <UButton :disabled="!newFolderName.trim()" @click="confirmCreateFolder">Create</UButton>
  </template>
</UModal>
web/src/composables/gql/graphql.ts (3)

839-853: FlatOrganizerEntry structure looks solid; minor type nit

Schema is coherent for a flattened model. If positions are always integers, prefer Int; if fractional indexing is intended, Float is fine.


17-23: Safer scalar mappings: avoid number for BigInt and any for JSON

  • BigInt → number risks precision loss; prefer string (or bigint with custom parsing).
  • JSON → any violates the TS guideline; prefer unknown (or a JsonValue type).

Update scalar mappings (ideally in codegen config) and re-generate.

 export type Scalars = {
   ID: { input: string; output: string; }
   String: { input: string; output: string; }
   Boolean: { input: boolean; output: boolean; }
   Int: { input: number; output: number; }
   Float: { input: number; output: number; }
-  /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */
-  BigInt: { input: number; output: number; }
+  /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */
+  BigInt: { input: string; output: string; }
   /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
   DateTime: { input: string; output: string; }
   /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
-  JSON: { input: any; output: any; }
+  JSON: { input: JSONValue; output: JSONValue; }

Add once near the top (outside this block):

type JSONValue = unknown;

As per coding guidelines.


1-5: Generated file note

This appears code-generated. Please apply fixes in the source GraphQL ops/codegen config and re-run generation; avoid manual edits here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c13d395 and 8aa0178.

📒 Files selected for processing (7)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
  • web/src/composables/gql/graphql.ts (20 hunks)
  • web/src/composables/useDockerEditNavigation.ts (1 hunks)
  • web/src/helpers/env.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/src/helpers/env.ts
  • web/src/composables/useDockerEditNavigation.ts
  • web/src/composables/gql/graphql.ts
  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Docker/DockerContainersTable.vue
web/**/*.query.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

GraphQL queries must be placed in .query.ts files

Place GraphQL queries in .query.ts files under web/

Files:

  • web/src/components/Docker/docker-containers.query.ts
web/**/*.{query,mutation}.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Docker/docker-containers.query.ts
🧠 Learnings (1)
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#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:

  • web/src/composables/gql/gql.ts
🧬 Code graph analysis (2)
web/src/composables/useDockerEditNavigation.ts (1)
web/src/helpers/env.ts (1)
  • featureFlags (7-10)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (11)
  • Scalars (11-75)
  • Maybe (3-3)
  • DockerContainerOverviewForm (725-731)
  • DockerMutations (733-739)
  • DockerContainer (703-723)
  • ResolvedOrganizerV1 (1914-1918)
  • Notification (1388-1401)
  • NotificationOverview (1432-1436)
  • InputMaybe (4-4)
  • QueryDockerContainerOverviewFormArgs (1743-1745)
  • Exact (5-5)
🔇 Additional comments (15)
web/src/composables/gql/gql.ts (1)

31-44: Generated GraphQL additions look good.

The new Docker/Notifications documents and overloads are consistent with the codegen pattern; no runtime logic changes. Please ensure the codegen Babel/SWC plugin is enabled for production to avoid bundle bloat from the in-memory documents map. Based on learnings.

Also applies to: 51-61, 93-106, 113-123, 211-266, 291-294, 319-334

web/src/composables/useDockerEditNavigation.ts (1)

11-16: Remove unused variable.

name is computed but unused; clean it up.

-    const name = containerName || (container?.names?.[0] || '').replace(/^\//, '');
     const templatePath = container?.templatePath;
 
-    if (!name || !templatePath) {
+    if (!templatePath) {
       return false;
     }

Likely an incorrect or invalid review comment.

web/src/composables/gql/graphql.ts (13)

526-530: Adding ContainerState.PAUSED looks good

Enum extension is consistent with Docker semantics. Ensure UI state handling accounts for PAUSED distinctly from RUNNING/EXITED.


727-733: DockerContainerOverviewForm type: OK

Matches the new overview-form query usage; no issues spotted.


1736-1737: dockerContainerOverviewForm query: OK

Type and args align with usage; looks good.


1800-1803: Query args for overview form: OK

skipCache arg is consistent.


1968-1974: Organizer view migration (rootId + flatEntries): fully propagated downstream

Verification confirms all downstream consumers have been updated. No references to the old .root property remain, and all components, composables, and GraphQL operations consistently use rootId and flatEntries.


1515-1522: warningsAndAlerts field is fully implemented on the server — review comment is incorrect

The warningsAndAlerts field exists in the server GraphQL schema (api/generated-schema.graphql:1204), has a resolver implementation (notifications.resolver.ts:55), a model definition (notifications.model.ts:172), and is actively published in subscriptions. No coordination or removal is needed; the client type is correct.

Likely an incorrect or invalid review comment.


786-793: DockerTemplateSyncResult type matches server implementation

Verification confirms the mutation syncDockerTemplatePaths in the server GraphQL schema (api/generated-schema.graphql:2466) returns DockerTemplateSyncResult! with matching field definitions: scanned, matched, skipped, and errors. All field types align between the server schema and the client TypeScript type definition.


3066-3066: I need to search the codebase directly for GraphQL schema definitions and the codegen configuration to determine if templatePath is actually defined server-side.

Now let me search the codebase for GraphQL schema files and query definitions to verify if templatePath is actually supported by the server.

Verify that the server exposes templatePath before shipping. This field is present in GetDockerContainers but may cause runtime errors if unsupported.

The verification script confirms that templatePath is indeed included in the GetDockerContainersDocument at line 3066. However, this only confirms the field is present in the generated client query document—it does not verify whether the server's GraphQL schema actually exposes this field. If the server does not support it, queries will fail at runtime with a GraphQL validation error.

Before shipping:

  • Confirm that the server schema defines meta.templatePath
  • Test against the backend to ensure queries don't return errors
  • If unsupported, remove templatePath from the query and re-generate

699-703: Schema support for organizer(skipCache) argument is confirmed

The API schema at api/generated-schema.graphql (line 1144) explicitly defines the organizer field on the Docker type with the skipCache argument: organizer(skipCache: Boolean! = false): ResolvedOrganizerV1!. The DockerOrganizerArgs type is correctly defined and supported by the server. Queries will not fail.

Likely an incorrect or invalid review comment.


3083-3083: Field is properly defined and non-nullable; review concern is incorrect

The field warningsAndAlerts is defined as non-nullable in the GraphQL schema ([Notification!]!) and implemented in the resolver. Since the query is generated from schema, type consistency is guaranteed. The client code safely uses optional chaining as defensive programming. If the field were missing from the server, this would manifest as a schema definition error during code generation, not a runtime failure. This is standard API versioning, not a code defect.

Likely an incorrect or invalid review comment.


3090-3090: No API validation needed—mutation is properly implemented and generated

The notifyIfUnique mutation is already implemented server-side with the @Mutation decorator and full deduplication logic. The web-side generated types (NotifyIfUniqueMutation, NotifyIfUniqueDocument) confirm the schema was properly generated. No risk of a 400 error.


724-725: Based on the script execution results, I can confirm the client-side usage pattern but cannot fully verify the server-side schema status. The evidence shows:

Confirmed findings:

  • templatePath is actively selected in web/src/components/Docker/docker-containers.query.ts:42
  • templatePath is used in web/src/composables/useDockerEditNavigation.ts (lines 12, 14, 23)
  • Generated types include templatePath?: Maybe<Scalars['String']['output']> in the DockerContainer metadata type
  • Multiple GraphQL documents in web/src/composables/gql/gql.ts reference templatePath field

Unable to verify:
The server schema verification failed because the path api/src/**/generated/graphql.ts doesn't exist. Without access to the actual server GraphQL schema definition, I cannot confirm whether DockerContainer type currently exposes this field.

Key observation:
The fact that generated types (graphql.ts) already include templatePath as an optional field suggests it was present in the schema during codegen. However, if the server schema has been modified since codegen was last run, there could be a mismatch.


Verify server schema before landing this change. Confirm that the server's DockerContainer type currently exposes the templatePath field. You can:

  1. Check api/src/schema/ directory for server GraphQL type definitions
  2. Run GraphQL introspection against the running server endpoint
  3. Re-run client codegen and verify no validation errors occur

If templatePath is not exposed by the server, either remove it from the client queries or land the server schema addition first.


737-745: Regenerate server GraphQL schema before deployment—resolver implementations exist but generated types are stale

The server resolvers for pause() and unpause() are implemented in git/api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts, but the generated GraphQL schema in git/api/src/unraid-api/cli/generated/graphql.ts is outdated and does not include these fields in the DockerMutations type.

Server resolvers have:

  • pause() resolver with @ResolveField decorator
  • unpause() resolver with @ResolveField decorator

Server generated schema is missing:

  • pause: DockerContainer
  • unpause: DockerContainer

The client code is correct and complete. Ensure the server's GraphQL schema is regenerated as part of the build process before deployment to prevent runtime failures when the client calls these mutations.

Comment on lines 157 to 174
const { result, loading, refetch } = useQuery<{
docker: {
id: string;
organizer: {
views: Array<{
id: string;
name: string;
rootId: string;
flatEntries: FlatOrganizerEntry[];
}>;
};
containers: DockerContainer[];
};
}>(GET_DOCKER_CONTAINERS, {
fetchPolicy: 'cache-and-network',
variables: { skipCache: true },
});

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

Query/data mismatch: docker.containers is not fetched but is used.

GET_DOCKER_CONTAINERS (as defined) returns organizer/views only, so result.value.docker.containers will be undefined and your containers computed becomes [] always. Fetch containers in the query or stop relying on it.

Apply this fix (paired with the change in docker-containers.query.ts below):

-const { result, loading, refetch } = useQuery<{
-  docker: {
-    id: string;
-    organizer: {
-      views: Array<{
-        id: string;
-        name: string;
-        rootId: string;
-        flatEntries: FlatOrganizerEntry[];
-      }>;
-    };
-    containers: DockerContainer[];
-  };
-}>(GET_DOCKER_CONTAINERS, {
+const { result, loading, refetch } = useQuery(GET_DOCKER_CONTAINERS, {
   fetchPolicy: 'cache-and-network',
   variables: { skipCache: true },
 });

Optional (strongly recommended): use generated types for safety.

-import { GET_DOCKER_CONTAINERS } from '@/components/Docker/docker-containers.query';
+import { GetDockerContainersDocument, type GetDockerContainersQuery, type GetDockerContainersQueryVariables } from '@/composables/gql/graphql';

-const { result, loading, refetch } = useQuery(GET_DOCKER_CONTAINERS, {
+const { result, loading, refetch } = useQuery<GetDockerContainersQuery, GetDockerContainersQueryVariables>(GetDockerContainersDocument, {
   fetchPolicy: 'cache-and-network',
   variables: { skipCache: true },
});

Also applies to: 178-179

🤖 Prompt for AI Agents
In web/src/components/Docker/DockerContainerManagement.vue around lines 157-174
(also check lines 178-179), the GraphQL hook expects docker.containers but
GET_DOCKER_CONTAINERS currently only returns organizer/views so
result.value.docker.containers is undefined and your computed containers is
always empty; update the GET_DOCKER_CONTAINERS query to include the containers
selection (id, name, status, etc.) or remove usage of docker.containers and
fetch containers with a separate query, then adjust the useQuery type to the
generated TypeScript types (or at minimum include containers in the local type)
so the hook returns the containers field and the computed property works
correctly.

Comment on lines 2734 to 2736
id: Scalars['PrefixedID']['input'];
}>;

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

GetDockerActiveContainer: unused $id variable breaks GraphQL validation

The operation declares $id but never uses it. Many servers reject unused variables. Fix by removing the variable or using it (if a single‑container query exists).

- query GetDockerActiveContainer($id: PrefixedID!) {
-   docker {
+ query GetDockerActiveContainer {
+   docker {
      id
      containers {
        id
        names
        image
        created
        state
        status
        autoStart
        ports { privatePort publicPort type }
        hostConfig { networkMode }
        networkSettings
        labels
      }
    }
  }

Follow-up: update the source .graphql/.gql for this op and re-run codegen so the QueryVariables type drops id.

Also applies to: 3065-3065

🤖 Prompt for AI Agents
In web/src/composables/gql/graphql.ts around lines 2734-2736 (and also at line
3065), the generated GetDockerActiveContainer operation declares a $id variable
that is never used which breaks GraphQL validation; edit the original
.graphql/.gql operation to remove the unused $id parameter (or modify the
operation to actually use $id if the intent was to query a single container),
then re-run your GraphQL codegen so the generated QueryVariables type no longer
includes `id`.

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 (4)
web/src/composables/gql/graphql.ts (1)

2733-2738: Unresolved: GetDockerActiveContainer still has unused $id variable.

The past review comment about the unused $id variable breaking GraphQL validation remains unresolved. The operation declares $id: PrefixedID! on line 2734 but never uses it in the query body to filter containers.

As noted in the previous review, edit the original .graphql/.gql operation to either:

  1. Remove the unused $id parameter, OR
  2. Use $id to query a single container if that was the intent

Then re-run GraphQL codegen to regenerate this file.

Also applies to: 3065-3065

web/src/components/Docker/docker-containers.query.ts (1)

3-51: Query structure may not match consumer expectations.

The past review comment flagged that the query doesn't include a docker.containers field, which consumers might expect. The DockerContainersTable component accepts both containers: DockerContainer[] and flatEntries?: FlatOrganizerEntry[] as props. If this query is used to populate those props, you'll need to either:

  1. Add a docker.containers field to this query, or
  2. Transform flatEntries.meta into the containers array at the call site

Verify that consumers using this query can properly populate the containers prop.

#!/bin/bash
# Find components that use GET_DOCKER_CONTAINERS and check how they populate containers prop
rg -n "GET_DOCKER_CONTAINERS" -g "*.vue" -g "*.ts" -A 10
web/src/components/Docker/DockerContainersTable.vue (2)

163-188: Row ID must use stable organizer entry ID, not Docker runtime ID.

Line 172 sets id: meta?.id || name, using the Docker runtime ID. As flagged in the past review, this causes failures because entryParentById and positionById (built by useTreeData) are keyed by organizer entry IDs (the stable container name), not Docker runtime IDs. This breaks drag-and-drop, move operations, and busy state management.

Change to use the stable container name:

   return {
-    id: meta?.id || name,
+    id: name,
     type: 'container',
     name,
     state: meta?.state ?? '',
     version: formatImage(meta || undefined),
     network: formatNetwork(meta || undefined),
     containerIp: formatContainerIp(meta || undefined),
     containerPort: formatInternalPorts(meta || undefined),
     lanPort: formatExternalPorts(meta || undefined),
     volumes: formatVolumes(meta || undefined),
     autoStart: meta?.autoStart ? 'On' : 'Off',
     updates: updatesParts.join(' / ') || '—',
     uptime: formatUptime(meta || undefined),
     containerId: meta?.id,
     meta: meta || undefined,
   };

299-316: Consider normalizing container state for reliable color mapping.

The colorMap is keyed by ContainerState enum values (line 306-310), but row.original.state (from meta?.state at line 175) may have different casing than the enum. If there's a mismatch, the mapping silently falls back to 'neutral'.

Consider normalizing the state before lookup to ensure correct badge colors:

   const state = row.original.state ?? '';
+  const normalizedState = String(state).toUpperCase();
   const isBusy = busyRowIds.value.has(row.original.id);
   const colorMap: Record<string, 'success' | 'warning' | 'neutral'> = {
     [ContainerState.RUNNING]: 'success',
     [ContainerState.PAUSED]: 'warning',
     [ContainerState.EXITED]: 'neutral',
   };
-  const color = colorMap[state] || 'neutral';
+  const color = colorMap[normalizedState] || 'neutral';
🧹 Nitpick comments (3)
web/src/composables/gql/gql.ts (1)

2-2: Update code generator configuration to align with TypeScript guidelines.

This generated file has two coding guideline violations:

  • Line 2: Import missing .js extension (required for ESM compatibility)
  • Line 397: Uses any type and type casting (guidelines require precise typing)

Since this is auto-generated code, update the GraphQL CodeGen configuration to emit compliant output rather than manually editing this file.

As per coding guidelines.

Also applies to: 397-397

web/src/components/Common/BaseTreeTable.vue (2)

37-37: Generic component uses Docker-specific default searchable keys.

The default searchableKeys includes Docker-specific fields like 'ports', 'containerId', and 'updates'. For a generic tree table component under Common/, prefer an empty array or generic keys like ['id', 'name', 'type'], and let consumers provide domain-specific keys.

-  searchableKeys: () => ['id', 'name', 'type', 'state', 'ports', 'autoStart', 'updates', 'containerId'],
+  searchableKeys: () => ['id', 'name', 'type'],

465-467: Use proper typing instead of any.

Replace any with the correct type for row parameter.

-      :get-row-id="(row: any) => row.id"
-      :get-sub-rows="(row: any) => row.children"
-      :get-row-can-select="(row: any) => row.original.type === selectableType"
+      :get-row-id="(row: TreeRow<T>) => row.id"
+      :get-sub-rows="(row: TreeRow<T>) => row.children"
+      :get-row-can-select="(row: { original: TreeRow<T> }) => row.original.type === selectableType"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa0178 and f8c0653.

📒 Files selected for processing (6)
  • web/src/components/Common/BaseTreeTable.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
  • web/src/composables/gql/graphql.ts (20 hunks)
  • web/src/composables/useTreeData.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/composables/useTreeData.ts
🧰 Additional context used
📓 Path-based instructions (4)
web/**/*.query.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

GraphQL queries must be placed in .query.ts files

Place GraphQL queries in .query.ts files under web/

Files:

  • web/src/components/Docker/docker-containers.query.ts
web/**/*.{query,mutation}.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Docker/docker-containers.query.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/graphql.ts
  • web/src/composables/gql/gql.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Common/BaseTreeTable.vue
🧠 Learnings (12)
📓 Common learnings
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/**/*.query.ts : Place GraphQL queries in .query.ts files under web/

Applied to files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#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:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/graphql.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-01-30T19:56:42.055Z
Learnt from: pujitm
PR: unraid/api#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:

  • web/src/composables/gql/graphql.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: All GraphQL under web must follow the .query.ts/.mutation.ts naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.{query,mutation}.ts : All GraphQL under `web/` must follow this naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
PR: unraid/api#1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/**/*.mutation.ts : Place GraphQL mutations in .mutation.ts files under web/

Applied to files:

  • web/src/composables/gql/gql.ts
🧬 Code graph analysis (1)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (11)
  • Scalars (11-75)
  • Maybe (3-3)
  • DockerContainerOverviewForm (725-731)
  • DockerMutations (733-739)
  • DockerContainer (703-723)
  • ResolvedOrganizerV1 (1914-1918)
  • Notification (1388-1401)
  • NotificationOverview (1432-1436)
  • InputMaybe (4-4)
  • QueryDockerContainerOverviewFormArgs (1743-1745)
  • Exact (5-5)
🔇 Additional comments (11)
web/src/composables/gql/gql.ts (1)

31-44: LGTM! Generated GraphQL operations follow the established pattern.

The additions correctly extend the GraphQL surface with Docker operations (container management, folder operations, pause/unpause/start/stop) and notification operations (warnings/alerts stream, unique notifications). The structure is consistent: type definitions in Documents, runtime entries in documents, and corresponding function overloads.

Based on learnings.

Also applies to: 51-51, 58-58, 61-61, 93-106, 113-113, 120-120, 123-123, 211-266, 291-294, 319-322, 331-334

web/src/composables/gql/graphql.ts (8)

22-22: The any type for JSON scalar is acceptable in generated GraphQL code.

This is standard practice for GraphQL JSON scalars in code generated by GraphQL CodeGen, where the actual JSON structure is validated at runtime by the GraphQL schema rather than at compile time.


528-528: LGTM: Container pause/unpause functionality properly typed.

The addition of the PAUSED state to the ContainerState enum and the corresponding pause and unpause mutations in DockerMutations are well-structured and correctly typed.

Also applies to: 737-765


727-733: LGTM: New Docker types are well-defined.

The new types for container overview forms, template sync results, and flattened organizer entries follow GraphQL conventions and are properly typed:

  • DockerContainerOverviewForm for JSON-Forms integration
  • DockerTemplateSyncResult for tracking template scanning results
  • FlatOrganizerEntry with comprehensive fields for the flattened tree structure (depth, position, path, parentId, childrenIds)

Also applies to: 786-792, 839-852


969-973: LGTM: ResolvedOrganizerView refactored to use flattened structure.

The update from root to rootId + flatEntries is a cleaner approach for representing the hierarchical organizer data in a flattened format, which should improve performance for large folder structures.


1278-1303: LGTM: New Docker organizer mutations properly defined.

The new mutations for folder management and template synchronization are well-structured:

  • createDockerFolderWithItems supports creating folders with initial items at specific positions
  • moveDockerItemsToPosition enables precise positioning in the organizer
  • renameDockerFolder for folder renaming
  • syncDockerTemplatePaths for template path synchronization

All mutations have corresponding Args types properly defined.

Also applies to: 1351-1411


1291-1292: LGTM: Notification deduplication system properly typed.

The additions support a deduplication system for notifications:

  • notifyIfUnique mutation creates notifications only if no equivalent unread notification exists
  • warningsAndAlerts field provides deduplicated warning/alert notifications
  • notificationsWarningsAndAlerts subscription for real-time updates

This properly prevents duplicate notification spam.

Also applies to: 1398-1400, 1520-1521, 2119-2119


2740-2839: LGTM: New Docker operation documents are properly structured.

The following operations are well-defined:

  • GetDockerContainers properly uses the skipCache parameter
  • CreateDockerFolderWithItems supports folder creation with items and positioning
  • MoveDockerItemsToPosition enables precise organizer positioning
  • GetDockerContainerOverviewForm retrieves JSON-Forms schema
  • PauseDockerContainer, UnpauseDockerContainer for container lifecycle
  • RenameDockerFolder, SetDockerFolderChildren for folder management
  • StartDockerContainer, StopDockerContainer for container control

2876-2882: LGTM: Notification deduplication operations properly implemented.

The new operations for notification deduplication are correctly structured:

  • WarningAndAlertNotifications query retrieves deduplicated warning/alert notifications
  • NotifyIfUnique mutation conditionally creates notifications (returns null if duplicate exists)
  • NotificationsWarningsAndAlertsSub subscription provides real-time updates for warnings/alerts

This system should effectively prevent notification spam while keeping users informed of critical issues.

Also applies to: 2928-2936, 2957-2963

web/src/components/Common/BaseTreeTable.vue (1)

1-441: LGTM - well-designed generic tree table component.

The component provides a comprehensive API with search, filtering, row selection, drag-and-drop, and column visibility features. The exposed API via defineExpose enables parent components to interact with table state effectively.

web/src/components/Docker/DockerContainersTable.vue (1)

1-777: Comprehensive implementation with robust feature set.

The component integrates multiple concerns effectively: tree data management, drag-and-drop, folder operations, container actions with confirmations, and a rich column set with formatters. The composable-based architecture (useTreeData, useFolderTree, useFolderOperations, useContainerActions) promotes good separation of concerns.

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: 3

♻️ Duplicate comments (4)
web/src/components/Docker/DockerContainersTable.vue (2)

165-190: Row ID must use stable organizer identifier, not Docker runtime ID.

The row id should use the stable container name instead of meta?.id to ensure consistency with entryParentById and positionById lookups. This critical issue was previously flagged and remains unresolved.

Apply this diff:

 function toContainerTreeRow(
   meta: DockerContainer | null | undefined,
   fallbackName?: string
 ): TreeRow<DockerContainer> {
   const name = meta?.names?.[0]?.replace(/^\//, '') || fallbackName || 'Unknown';
   const updatesParts: string[] = [];
   if (meta?.isUpdateAvailable) updatesParts.push('Update');
   if (meta?.isRebuildReady) updatesParts.push('Rebuild');
   return {
-    id: meta?.id || name,
+    id: name,
     type: 'container',
     name,
     state: meta?.state ?? '',
     version: formatImage(meta || undefined),
     network: formatNetwork(meta || undefined),
     containerIp: formatContainerIp(meta || undefined),
     containerPort: formatInternalPorts(meta || undefined),
     lanPort: formatExternalPorts(meta || undefined),
     volumes: formatVolumes(meta || undefined),
     autoStart: meta?.autoStart ? 'On' : 'Off',
     updates: updatesParts.join(' / ') || '—',
     uptime: formatUptime(meta || undefined),
     containerId: meta?.id,
     meta: meta || undefined,
   };
 }

370-387: Normalize container state before color mapping.

The state value from meta.state may be lowercase, which prevents matching the colorMap keyed by enum-cased values. This minor issue was previously flagged.

Apply this diff to normalize the state:

 cell: ({ row }) => {
   if (row.original.type === 'folder') return '';
   const state = row.original.state ?? '';
+  const stateKey = typeof state === 'string' ? state.toUpperCase() : state;
   const isBusy = busyRowIds.value.has(row.original.id);
   const colorMap: Record<string, 'success' | 'warning' | 'neutral'> = {
     [ContainerState.RUNNING]: 'success',
     [ContainerState.PAUSED]: 'warning',
     [ContainerState.EXITED]: 'neutral',
   };
-  const color = colorMap[state] || 'neutral';
+  const color = colorMap[stateKey] || 'neutral';
   if (isBusy) {
     return h(USkeleton, { class: 'h-5 w-20' });
   }
   return h(UBadge, { color }, () => state);
 },
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)

284-310: Invalidate both container caches after pause (duplicate of prior feedback)

Only deleting CONTAINER_CACHE_KEY leaves CONTAINER_WITH_SIZE_CACHE_KEY stale. Use clearContainerCache().

-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after pausing ${id}`);
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after pausing ${id}`);
web/src/composables/gql/graphql.ts (1)

2741-2745: GetDockerActiveContainer: unused $id variable breaks validation (source op needs fix)

$id is declared but not used in the query selection. Remove the var from the source .graphql or use it, then re-run codegen.

- query GetDockerActiveContainer($id: PrefixedID!) {
+ query GetDockerActiveContainer {
   docker {
     id
     containers {
       id
       names
       image
       created
       state
       status
       autoStart
       ports { privatePort publicPort type }
       hostConfig { networkMode }
       networkSettings
       labels
     }
   }
 }

Also applies to: 3079-3080

🧹 Nitpick comments (6)
web/src/components/Docker/DockerContainersTable.vue (1)

579-590: Consider extracting toast utility to a composable.

Extending the global Window interface with a toast property works but couples the component to global state. For better testability and maintainability, consider creating a useToast composable that encapsulates this functionality.

api/generated-schema.graphql (1)

1151-1156: Align DockerContainerOverviewForm with FormSchema for consistency

Other forms implement FormSchema with values instead of data. Consider either implementing FormSchema here or renaming data→values to keep client patterns uniform.

web/src/components/Docker/docker-update-container.mutation.ts (2)

3-15: Reuse generated DocumentNode to avoid duplicate ops

Prefer the codegen export UpdateDockerContainerDocument from web/src/composables/gql/graphql.ts for type safety and to prevent op drift.

Apply:

-import { gql } from '@apollo/client';
-
-export const UPDATE_DOCKER_CONTAINER = gql`
-  mutation UpdateDockerContainer($id: PrefixedID!) {
-    docker {
-      updateContainer(id: $id) {
-        id
-        names
-        state
-        isUpdateAvailable
-        isRebuildReady
-      }
-    }
-  }
-`;
+export { UpdateDockerContainerDocument as UPDATE_DOCKER_CONTAINER } from '@/composables/gql/graphql';

7-12: Consider selecting __typename for better Apollo cache normalization

Helps policies and cache diffs; cheap to include.

api/src/unraid-api/graph/resolvers/docker/docker.service.ts (2)

2-9: Remove unused OnModuleInit import

Not implemented/used. Trim the import list.

-    OnApplicationBootstrap,
-    OnModuleInit,
+    OnApplicationBootstrap,

354-363: Harden external update call (timeout and avoid shell if not required)

Add a reasonable timeout and, if the script is executable, drop shell to reduce risk.

-            await execa(
-                '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
-                [encodeURIComponent(containerName)],
-                { shell: 'bash' }
-            );
+            await execa(
+                '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
+                [encodeURIComponent(containerName)],
+                { timeout: 60_000 } // 60s, tune as needed
+            );

Note: If the script truly requires a shell, keep shell but still set timeout.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8c0653 and e29e8d1.

📒 Files selected for processing (10)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (7 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (4 hunks)
  • web/components.d.ts (4 hunks)
  • web/src/assets/main.css (4 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/docker-update-container.mutation.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
  • web/src/composables/gql/graphql.ts (20 hunks)
✅ Files skipped from review due to trivial changes (1)
  • api/dev/configs/api.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/assets/main.css
🧰 Additional context used
📓 Path-based instructions (6)
web/**/*.mutation.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

GraphQL mutations must be placed in .mutation.ts files

Place GraphQL mutations in .mutation.ts files under web/

Files:

  • web/src/components/Docker/docker-update-container.mutation.ts
web/**/*.{query,mutation}.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Docker/docker-update-container.mutation.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/src/components/Docker/docker-update-container.mutation.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/components.d.ts
  • web/src/composables/gql/graphql.ts
  • web/src/composables/gql/gql.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainersTable.vue
🧠 Learnings (29)
📓 Common learnings
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
PR: unraid/api#1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • web/src/components/Docker/docker-update-container.mutation.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
PR: unraid/api#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/docker/docker.mutations.resolver.ts
📚 Learning: 2024-11-06T20:59:06.096Z
Learnt from: mdatelle
PR: unraid/api#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/docker/docker.mutations.resolver.ts
📚 Learning: 2025-04-23T20:22:36.001Z
Learnt from: pujitm
PR: unraid/api#1367
File: api/src/unraid-api/app/pubsub.module.ts:24-28
Timestamp: 2025-04-23T20:22:36.001Z
Learning: In the Unraid API project, string literals are intentionally used for dependency injection tokens instead of Symbols to enable "collision" behavior across modules that don't directly import from each other. This is a temporary approach during refactoring until a shared library for constants is implemented.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-04-07T14:34:47.255Z
Learnt from: elibosley
PR: unraid/api#1334
File: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts:63-66
Timestamp: 2025-04-07T14:34:47.255Z
Learning: In DockerEventService, the chokidar file watcher is configured with `ignoreInitial: false` to ensure that existing files (like the Docker socket) are detected and processed at application startup, not just when files change after the watcher is initialized.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2024-12-09T15:47:29.325Z
Learnt from: pujitm
PR: unraid/api#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/components.d.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/components/**/*.vue : Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-17T13:55:42.068Z
Learnt from: elibosley
PR: unraid/api#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/components.d.ts
📚 Learning: 2024-12-06T17:38:40.999Z
Learnt from: elibosley
PR: unraid/api#974
File: web/components/Loading/Error.vue:1-3
Timestamp: 2024-12-06T17:38:40.999Z
Learning: In Nuxt.js projects, components are automatically imported, so explicit import statements for components like `LoadingSpinner` in `web/components/Loading/Error.vue` are not necessary.

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-06T17:34:16.133Z
Learnt from: pujitm
PR: unraid/api#974
File: web/components/Loading/Error.vue:50-50
Timestamp: 2024-12-06T17:34:16.133Z
Learning: In this project, the `Button` component from `~/components/shadcn/Button.vue` is autoloaded and does not need to be imported manually in components like `web/components/Loading/Error.vue`.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/components.d.ts
  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/components.d.ts
  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-02-18T17:36:22.670Z
Learnt from: pujitm
PR: unraid/api#1143
File: web/components/DummyServerSwitcher.vue:16-19
Timestamp: 2025-02-18T17:36:22.670Z
Learning: The DummyServerSwitcher component in web/components/DummyServerSwitcher.vue is a development tool for testing different server states, and includes intentional debug elements to aid development.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#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/components.d.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#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:

  • web/src/composables/gql/graphql.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-01-30T19:56:42.055Z
Learnt from: pujitm
PR: unraid/api#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/generated-schema.graphql
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.{query,mutation}.ts : All GraphQL under `web/` must follow this naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: All GraphQL under web must follow the .query.ts/.mutation.ts naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
PR: unraid/api#1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/**/*.query.ts : Place GraphQL queries in .query.ts files under web/

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/**/*.mutation.ts : Place GraphQL mutations in .mutation.ts files under web/

Applied to files:

  • web/src/composables/gql/gql.ts
🧬 Code graph analysis (4)
web/src/components/Docker/docker-update-container.mutation.ts (1)
api/src/unraid-api/cli/generated/gql.ts (1)
  • gql (96-98)
api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts (2)
web/src/composables/gql/graphql.ts (1)
  • DockerContainer (704-725)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (3)
web/src/composables/gql/graphql.ts (2)
  • Docker (680-687)
  • DockerContainer (704-725)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (51-59)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (10)
  • DockerOrganizerArgs (699-701)
  • Scalars (11-75)
  • Maybe (3-3)
  • DockerContainerOverviewForm (725-731)
  • DockerMutations (733-739)
  • DockerContainer (703-723)
  • ResolvedOrganizerV1 (1914-1918)
  • Notification (1388-1401)
  • NotificationOverview (1432-1436)
  • QueryDockerContainerOverviewFormArgs (1743-1745)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[notice] 2-9: Unused variable, import, function or class
Unused import OnModuleInit.


[warning] 290-290: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.


[warning] 318-318: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.

🔇 Additional comments (8)
web/src/components/Docker/DockerContainersTable.vue (6)

1-29: LGTM! Proper import approach for testability.

The explicit imports from Vue align with coding guidelines, ensuring the component is testable even when Nuxt auto-imports don't apply in tests.


81-163: LGTM! Well-structured format utilities with proper error handling.

The format functions handle null/undefined inputs gracefully and include try-catch blocks for complex operations like IP and volume extraction.


270-368: LGTM! Complex name column with excellent UX details.

The name column cell renderer properly handles indentation, icons, and update notifications with a popover. Good use of stopPropagation to prevent unintended row clicks on interactive elements.


592-613: Well-structured update handler with proper state management.

The function correctly manages busy state, handles errors, and triggers refetches. Note that the busy state keying on row.id depends on the fix for line 174 to work correctly with organizer lookups.


684-715: LGTM! Comprehensive drop handling logic.

The function covers all drop scenarios (folder, container, before/after) with appropriate validation and position calculations. Proper mutation selection and refetch behavior.


866-1116: LGTM! Well-structured template with good component composition.

The template effectively uses slots, modals, and event handlers to create a comprehensive container management UI. The folder tree modal and confirmation modals provide clear user workflows.

web/components.d.ts (1)

1-144: LGTM! Auto-generated component type declarations.

This file is auto-generated by unplugin-vue-components and correctly adds type declarations for the new Docker components (BaseTreeTable, DockerContainersTable, DockerSidebarTree, etc.) and Nuxt UI components (UModal, UPopover, USkeleton, UTable) introduced in this PR.

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

35-59: Resolvers look correct and permissions match existing pattern

Wiring + @ResolveField usage and authorization align with start/stop. LGTM.

Comment on lines +312 to +338
public async unpause(id: string): Promise<DockerContainer> {
const container = this.client.getContainer(id);
await container.unpause();
await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
this.logger.debug(`Invalidated container cache after unpausing ${id}`);

let containers = await this.getContainers({ skipCache: true });
let updatedContainer: DockerContainer | undefined;
for (let i = 0; i < 5; i++) {
await sleep(500);
containers = await this.getContainers({ skipCache: true });
updatedContainer = containers.find((c) => c.id === id);
this.logger.debug(
`Container ${id} state after unpause attempt ${i + 1}: ${updatedContainer?.state}`
);
if (updatedContainer?.state === ContainerState.RUNNING) {
break;
}
}

if (!updatedContainer) {
throw new Error(`Container ${id} not found after unpausing`);
}
const appInfo = await this.getAppInfo();
await pubsub.publish(PUBSUB_CHANNEL.INFO, appInfo);
return updatedContainer;
}
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

Mirror the same fixes for unpause

  • Invalidate both caches via clearContainerCache().
  • Add idempotency (if already running) + try/catch.
  • Remove initial unnecessary containers assignment.
-        await container.unpause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after unpausing ${id}`);
+        try {
+            const inspect = await container.inspect();
+            if (inspect?.State && !inspect.State.Paused) {
+                await this.clearContainerCache();
+                const existing = (await this.getContainers({ skipCache: true })).find(c => c.id === id);
+                if (existing) return existing;
+            }
+            await container.unpause();
+        } catch (error) {
+            catchHandlers.docker(error as any);
+            throw error;
+        }
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after unpausing ${id}`);
-        let containers = await this.getContainers({ skipCache: true });
-        let updatedContainer: DockerContainer | undefined;
+        let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
-            updatedContainer = containers.find((c) => c.id === id);
+            const containers = await this.getContainers({ skipCache: true });
+            updatedContainer = containers.find((c) => c.id === id);

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 318-318: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.

🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/docker/docker.service.ts around lines 312
to 338, replace the current unpause flow: remove the initial unnecessary
assignment to containers, call this.clearContainerCache() instead of only
deleting CONTAINER_CACHE_KEY to invalidate both caches, add an idempotency check
before unpausing (if the container is already RUNNING return it immediately),
wrap the unpause call in try/catch to handle and log errors without crashing,
and keep the retry loop to poll getContainers({ skipCache: true }) until state
becomes RUNNING; ensure errors in the catch bubble as a handled error and that
cache invalidation, pubsub.publish, and the final return use the
updatedContainer found by the polling loop.

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: 2

♻️ Duplicate comments (3)
web/src/composables/gql/graphql.ts (1)

2748-2750: GetDockerActiveContainer: unused $id variable still present (breaks validation)

Remove the variable or use it; many servers reject unused vars. Update the source .graphql op and re-run codegen.

Apply at the source (illustrative diff against generated doc):

- query GetDockerActiveContainer($id: PrefixedID!) {
+ query GetDockerActiveContainer {
   docker {
     id
     containers { id names image created state status autoStart
       ports { privatePort publicPort type }
       hostConfig { networkMode }
       networkSettings
       labels
     }
   }
 }

Verification script to locate the op in repo and confirm removal:

#!/bin/bash
# Find the source .graphql/.gql defining GetDockerActiveContainer and show its header and variables
fd -t f -a -e graphql -e gql | xargs -I{} rg -nC2 -e 'query\s+GetDockerActiveContainer' {}
# Ensure no '$id' remains in the op signature
rg -nC1 -e 'query\s+GetDockerActiveContainer\((.*?)\)' || echo "No variables declared — OK"

Also applies to: 3094-3094

web/src/composables/gql/gql.ts (1)

33-37: Add templatePath (and other overview meta fields) to organizer mutation payloads

All of the new organizer mutations (create/move/rename/set children) return the full views.flatEntries.meta tree, but unlike GetDockerContainers they stop short of selecting templatePath (and the other new overview fields). When Apollo writes these responses back into the cache, the affected entries immediately lose meta.templatePath, so the Docker overview icons disappear until we force a refetch. Please align each mutation document's selection set with the query (at minimum include templatePath, and consider networkSettings/mounts if the UI relies on them) so the cache stays coherent after organizer operations.

Also applies to: 40-41, 46-46

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

59-67: Replace any cast with proper typing

The as any cast breaks type safety and violates the coding guidelines that mandate never using the any type.

As per coding guidelines

 async getResources(
     opts?: Partial<ContainerListOptions> & { skipCache?: boolean }
 ): Promise<OrganizerV1['resources']> {
     const { skipCache = false, ...listOptions } = opts ?? {};
     const containers = await this.dockerService.getContainers({
         skipCache,
-        ...(listOptions as any),
+        ...listOptions,
     });
     return containerListToResourcesObject(containers);
 }

The spread should work without casting since listOptions is already typed as Partial<ContainerListOptions>. If there's a type mismatch, adjust the getContainers signature to accept Partial<ContainerListOptions> or create a shared options interface.

🧹 Nitpick comments (1)
api/generated-schema.graphql (1)

1158-1216: Notifications surface: consider pagination/criteria docs

  • warningsAndAlerts returns an unpaged array; if counts grow, add optional limit/offset to avoid large payloads.
  • Document “equivalence” criteria used by notifyIfUnique to prevent client surprises.

Also applies to: 2443-2447, 2669-2673

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e29e8d1 and 3bf110c.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (11)
  • api/generated-schema.graphql (7 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (5 hunks)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
  • web/src/components/Docker/DockerContainerOverview.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/components/Docker/docker-update-view-prefs.mutation.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
  • web/src/composables/gql/graphql.ts (21 hunks)
  • web/src/composables/useDockerColumnVisibility.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Docker/DockerContainerOverview.vue
🧰 Additional context used
📓 Path-based instructions (7)
web/**/*.mutation.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

GraphQL mutations must be placed in .mutation.ts files

Place GraphQL mutations in .mutation.ts files under web/

Files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
web/**/*.{query,mutation}.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
  • web/src/components/Docker/docker-containers.query.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
  • web/src/composables/useDockerColumnVisibility.ts
  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
  • web/src/composables/gql/graphql.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/DockerContainerManagement.vue
web/**/*.query.ts

📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)

GraphQL queries must be placed in .query.ts files

Place GraphQL queries in .query.ts files under web/

Files:

  • web/src/components/Docker/docker-containers.query.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
🧠 Learnings (20)
📓 Common learnings
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/composables/useDockerColumnVisibility.ts
  • web/src/components/Docker/docker-containers.query.ts
  • web/src/components/Docker/DockerContainerManagement.vue
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
PR: unraid/api#1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/**/*.mutation.ts : Place GraphQL mutations in .mutation.ts files under web/

Applied to files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
PR: unraid/api#1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/docker-containers.query.ts
  • web/src/components/Docker/DockerContainerManagement.vue
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/**/*.query.ts : Place GraphQL queries in .query.ts files under web/

Applied to files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
PR: unraid/api#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:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • web/src/composables/gql/graphql.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.{query,mutation}.ts : All GraphQL under `web/` must follow this naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: All GraphQL under web must follow the .query.ts/.mutation.ts naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
PR: unraid/api#1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
PR: unraid/api#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/docker/docker.resolver.ts
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
PR: unraid/api#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/docker/docker.resolver.ts
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-03-07T19:29:12.540Z
Learnt from: pujitm
PR: unraid/api#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.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Never use the any type; prefer precise typing

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Avoid type casting; model proper types from the start

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-09-09T14:34:07.189Z
Learnt from: pujitm
PR: unraid/api#1582
File: packages/unraid-shared/src/util/processing.ts:69-71
Timestamp: 2025-09-09T14:34:07.189Z
Learning: In TypeScript code, pujitm considers it acceptable to use `any` type for private/internal fields when the type is not exposed through the public API and is masked by properly typed method declarations. The focus is on ensuring the public interface maintains type safety rather than enforcing strict typing on all internal implementation details.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-01-30T19:56:42.055Z
Learnt from: pujitm
PR: unraid/api#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/generated-schema.graphql
🧬 Code graph analysis (6)
web/src/components/Docker/docker-update-view-prefs.mutation.ts (1)
api/src/unraid-api/cli/generated/gql.ts (1)
  • gql (96-98)
web/src/composables/useDockerColumnVisibility.ts (1)
web/src/components/Docker/docker-update-view-prefs.mutation.ts (1)
  • UPDATE_DOCKER_VIEW_PREFERENCES (3-45)
web/src/components/Docker/docker-containers.query.ts (1)
api/src/unraid-api/cli/generated/gql.ts (1)
  • gql (96-98)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (4)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
web/src/composables/gql/graphql.ts (4)
  • DockerContainerOverviewForm (727-733)
  • ResolvedOrganizerV1 (1975-1979)
  • Mutation (1271-1319)
  • DockerTemplateSyncResult (793-799)
api/src/unraid-api/decorators/use-feature-flag.decorator.ts (1)
  • UseFeatureFlag (20-22)
api/src/unraid-api/organizer/organizer.ts (1)
  • DEFAULT_ORGANIZER_ROOT_ID (17-17)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (3)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2)
  • containers (52-60)
  • organizer (90-94)
web/src/composables/gql/graphql.ts (1)
  • ResolvedOrganizerV1 (1975-1979)
api/src/unraid-api/organizer/organizer.ts (7)
  • resolveOrganizer (214-228)
  • addMissingResourcesToView (50-87)
  • moveItemsToPosition (660-678)
  • renameFolder (690-704)
  • DEFAULT_ORGANIZER_ROOT_ID (17-17)
  • createFolderWithItems (720-751)
  • DEFAULT_ORGANIZER_VIEW_ID (16-16)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (5)
  • Scalars (11-75)
  • Maybe (3-3)
  • Notification (1473-1486)
  • InputMaybe (4-4)
  • Exact (5-5)
🪛 GitHub Actions: CI - Main (API)
web/src/composables/useDockerColumnVisibility.ts

[error] 1-1: TS6192 All imports in import declaration are unused. (vue-tsc --noEmit). Command failed with exit code 1.

🪛 GitHub Check: CodeQL
web/src/composables/useDockerColumnVisibility.ts

[notice] 1-1: Unused variable, import, function or class
Unused imports ref, watch.

🔇 Additional comments (8)
api/generated-schema.graphql (4)

866-874: Guard new Docker mutations with permissions

pause, unpause, and updateContainer mutate runtime; ensure they’re gated with @usePermissions (e.g., resource: DOCKER, action: UPDATE_OWN/ANY) or equivalent server-side checks. Schema is generated, so add guards at the source before generation.


1114-1116: templatePath exposure may leak filesystem details

templatePath: String is now public. Confirm it’s sanitized (no absolute host paths or sensitive locations) and safe to show to remote users. If needed, expose a relative or redacted path.


1120-1123: New ContainerState.PAUSED requires client handling

Clients that switch on ContainerState must handle PAUSED (icons, actions, tooltips). Verify no exhaustive switches default to error/unknown.


1147-1149: Organizer API shape changed (rootId + flatEntries) and overview form added

This is a breaking surface change for consumers expecting tree/unions. Confirm all frontend queries/mappers migrated, and add release notes/deprecations for external clients if applicable.

Also applies to: 1151-1156, 1225-1231, 1237-1249, 2394-2395

web/src/composables/gql/graphql.ts (3)

526-530: Ensure UI handles ContainerState.PAUSED

Audit any switch/if ladders mapping RUNNING/EXITED only; add PAUSED styling and actions (resume/stop).


737-747: LGTM: generated types/docs match schema changes

Pause/unpause/update mutations, template sync result, flat organizer entries + overview form, notifications queries/subscriptions all align. No TypeScript import/path issues in this generated file.

Also applies to: 770-773, 793-799, 846-859, 1982-1988, 727-733, 1750-1751, 1814-1817, 1533-1536, 2986-2993, 3114-3114, 3124-3124


2905-2912: Notifications warnings/alerts: client usage

Use the query for initial load and the subscription for live updates; avoid polling both. Links are validated server-side, so no extra client validation is required. Based on learnings

Also applies to: 2986-2993

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

1-254: LGTM! Well-structured resolver additions

The resolver changes are well-implemented:

  • Proper use of UseFeatureFlag and UsePermissions decorators
  • Consistent error handling via service layer
  • Template scanner integration in containers resolver
  • New mutations follow established patterns

Comment on lines +22 to +40
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
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

Include templatePath in mutation response to maintain cache coherence

The mutation returns meta fields but omits templatePath, networkSettings, and mounts that are present in GetDockerContainers. When Apollo writes this mutation response into the cache, it will drop those fields from affected entries, causing Docker overview icons to disappear.

Add the missing fields to the meta selection:

           meta {
             id
             names
             state
             status
             image
             ports {
               privatePort
               publicPort
               type
             }
             autoStart
             hostConfig {
               networkMode
             }
+            networkSettings
+            mounts
             created
             isUpdateAvailable
             isRebuildReady
+            templatePath
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
networkSettings
mounts
created
isUpdateAvailable
isRebuildReady
templatePath
}
🤖 Prompt for AI Agents
In web/src/components/Docker/docker-update-view-prefs.mutation.ts around lines
22 to 40, the mutation's meta selection omits templatePath, networkSettings, and
mounts which exist on GetDockerContainers; add templatePath, networkSettings
(with the same subfields as GetDockerContainers), and mounts (with the same
subfields) to the meta selection so Apollo will write the full shape back into
the cache and not drop those fields; ensure the selection exactly matches the
fields used by GetDockerContainers and update any TypeScript types/interfaces if
needed.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 3bf110c to 772c364 Compare November 3, 2025 17:29
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: 3

♻️ Duplicate comments (5)
web/src/components/Docker/DockerContainerManagement.vue (1)

157-174: Query/data mismatch: verify docker.containers is fetched.

The query type includes docker.containers, but based on previous reviews, GET_DOCKER_CONTAINERS may only return organizer.views. Line 180 expects result.value?.docker?.containers, which will be undefined if the query doesn't include the containers field.

Verify that GET_DOCKER_CONTAINERS (defined in docker-containers.query.ts) includes:

query GetDockerContainers {
  docker {
    id
    containers {
      id
      names
      state
      # ... other fields
    }
    organizer {
      views {
        # ...
      }
    }
  }
}

If the query doesn't fetch containers, either add the field or extract containers from flatEntries[].meta.

web/src/components/Docker/DockerContainersTable.vue (2)

176-176: Row ID uses Docker runtime ID instead of stable identifier.

Line 176 sets id: meta?.id || name, using the Docker runtime ID. However, useTreeData and organizer maps (entryParentById, positionById) are keyed by the stable entry ID (container name). This mismatch causes drag-and-drop, folder operations, and selection to fail when looking up rows in these maps.

Change the row ID to use the stable container name:

 return {
-  id: meta?.id || name,
+  id: name,
   type: 'container',
   name,
   state: meta?.state ?? '',
   version: formatImage(meta || undefined),
   network: formatNetwork(meta || undefined),
   containerIp: formatContainerIp(meta || undefined),
   containerPort: formatInternalPorts(meta || undefined),
   lanPort: formatExternalPorts(meta || undefined),
   volumes: formatVolumes(meta || undefined),
   autoStart: meta?.autoStart ? 'On' : 'Off',
   updates: updatesParts.join(' / ') || '—',
   uptime: formatUptime(meta || undefined),
   containerId: meta?.id,
   meta: meta || undefined,
 };

The Docker runtime ID is preserved in containerId for container operations.


374-381: Normalize container state for color mapping.

If meta.state is lowercase (e.g., "running"), it won't match the enum-keyed colorMap entries, causing the badge to always use the neutral fallback color.

Normalize the state before lookup:

 const state = row.original.state ?? '';
+const stateKey = String(state).toUpperCase();
 const isBusy = busyRowIds.value.has(row.original.id);
 const colorMap: Record<string, 'success' | 'warning' | 'neutral'> = {
   [ContainerState.RUNNING]: 'success',
   [ContainerState.PAUSED]: 'warning',
   [ContainerState.EXITED]: 'neutral',
 };
-const color = colorMap[state] || 'neutral';
+const color = colorMap[stateKey] || 'neutral';
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (1)

59-68: The any cast still breaks type safety.

This issue was previously flagged: the spread (listOptions as any) removes type checking when calling getContainers. Consider introducing a shared interface or using Partial<ContainerListOptions> to maintain type safety throughout the call chain.

web/src/composables/gql/graphql.ts (1)

2748-2753: Fix unused $id variable in GetDockerActiveContainer.

The generated query still declares $id but never uses it, so GraphQL servers will reject the operation for having unused variables (same issue noted previously). Please drop the variable in the source .graphql file and re-run codegen so clients don’t ship an invalid operation.

-query GetDockerActiveContainer($id: PrefixedID!) {
-  docker {
+query GetDockerActiveContainer {
+  docker {
     id
     containers {

Also applies to: 3094-3099

🧹 Nitpick comments (1)
web/src/components/Docker/DockerContainerOverview.vue (1)

12-27: Consider using generated GraphQL types.

The inline type definition for the query result is verbose. If your project has GraphQL code generation, use the generated types for better type safety and maintainability.

import { GetDockerContainersDocument, type GetDockerContainersQuery } from '@/composables/gql/graphql';

const { result, loading, error, refetch } = useQuery<GetDockerContainersQuery>(
  GetDockerContainersDocument,
  { fetchPolicy: 'cache-and-network' }
);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf110c and 772c364.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (11)
  • api/generated-schema.graphql (7 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (5 hunks)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
  • web/src/components/Docker/DockerContainerOverview.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/components/Docker/docker-update-view-prefs.mutation.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
  • web/src/composables/gql/graphql.ts (21 hunks)
  • web/src/composables/useDockerColumnVisibility.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/components/Docker/docker-update-view-prefs.mutation.ts
  • web/src/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/DockerContainerOverview.vue
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • web/src/composables/gql/graphql.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
  • web/src/composables/useDockerColumnVisibility.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
🧠 Learnings (23)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/DockerContainerOverview.vue
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/DockerContainerOverview.vue
📚 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/docker/docker.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/docker/docker.resolver.ts
  • web/src/composables/gql/graphql.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/docker/docker.resolver.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
Repo: unraid/api PR: 1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.{query,mutation}.ts : All GraphQL under `web/` must follow this naming convention

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: All GraphQL under web must follow the .query.ts/.mutation.ts naming convention

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.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/generated-schema.graphql
📚 Learning: 2025-03-07T19:29:12.540Z
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.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Never use the any type; prefer precise typing

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Avoid type casting; model proper types from the start

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-09-09T14:34:07.189Z
Learnt from: pujitm
Repo: unraid/api PR: 1582
File: packages/unraid-shared/src/util/processing.ts:69-71
Timestamp: 2025-09-09T14:34:07.189Z
Learning: In TypeScript code, pujitm considers it acceptable to use `any` type for private/internal fields when the type is not exposed through the public API and is masked by properly typed method declarations. The focus is on ensuring the public interface maintains type safety rather than enforcing strict typing on all internal implementation details.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Applied to files:

  • web/src/composables/useDockerColumnVisibility.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/components/**/*.vue : Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Applied to files:

  • web/src/composables/useDockerColumnVisibility.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files

Applied to files:

  • web/src/composables/useDockerColumnVisibility.ts
📚 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/composables/useDockerColumnVisibility.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Ensure Vue reactivity imports (computed, ref, watchEffect) are added to store source files

Applied to files:

  • web/src/composables/useDockerColumnVisibility.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • web/src/composables/useDockerColumnVisibility.ts
📚 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/composables/useDockerColumnVisibility.ts
🧬 Code graph analysis (4)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (4)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
web/src/composables/gql/graphql.ts (2)
  • DockerContainerOverviewForm (727-733)
  • DockerTemplateSyncResult (793-799)
api/src/unraid-api/decorators/use-feature-flag.decorator.ts (1)
  • UseFeatureFlag (20-22)
api/src/unraid-api/organizer/organizer.ts (1)
  • DEFAULT_ORGANIZER_ROOT_ID (17-17)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (6)
  • Scalars (11-75)
  • Maybe (3-3)
  • Notification (1473-1486)
  • NotificationOverview (1517-1521)
  • InputMaybe (4-4)
  • Exact (5-5)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (2)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2)
  • containers (52-60)
  • organizer (90-94)
api/src/unraid-api/organizer/organizer.ts (7)
  • resolveOrganizer (214-228)
  • addMissingResourcesToView (50-87)
  • moveItemsToPosition (660-678)
  • renameFolder (690-704)
  • DEFAULT_ORGANIZER_ROOT_ID (17-17)
  • createFolderWithItems (720-751)
  • DEFAULT_ORGANIZER_VIEW_ID (16-16)
web/src/composables/useDockerColumnVisibility.ts (1)
web/src/components/Docker/docker-update-view-prefs.mutation.ts (1)
  • UPDATE_DOCKER_VIEW_PREFERENCES (3-45)
🔇 Additional comments (22)
web/src/components/Docker/DockerContainerManagement.vue (2)

26-42: Good defensive pattern for router access.

The tryUseRoute and tryUseRouter helpers safely handle cases where the component is used outside a router context, returning null instead of throwing. This enables flexible usage across different contexts.


85-144: Well-implemented route synchronization.

The bidirectional route synchronization handles both Vue Router and direct URL manipulation with proper cycle prevention via the syncingFromRoute flag. This enables the component to work both within and outside a router context.

web/src/components/Docker/DockerContainersTable.vue (2)

278-366: Name column cell renderer is well-implemented.

The name column rendering handles indentation, icons (with fallback), folder vs container types, and update badges with a confirmation popover. The use of data-stop-row-click prevents event bubbling appropriately.


716-747: Drop handler comprehensively covers all drop scenarios.

The handleDropOnRow function properly handles:

  • Dropping into folders (area='inside')
  • Creating folders from container drops
  • Repositioning items before/after targets
  • Proper validation and guards
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (8)

1-57: LGTM! Clean dependency injection and imports.

The addition of DockerTemplateIconService and new organizer helper functions follows NestJS patterns correctly. Import paths use .js extensions as required by the coding guidelines.


88-95: Good: skipCache option properly threaded through.

The method correctly accepts and forwards the skipCache flag to getResources, maintaining the caching control flow.


97-113: Excellent icon integration with proper filtering.

The method correctly filters container resources, extracts template paths, and integrates icon mapping into the organizer resolution flow. The type narrowing with OrganizerContainerResource is appropriate here.


213-227: Proper view reassignment after deletion.

The explicit reassignment of newOrganizer.views.default after calling addMissingResourcesToView correctly handles the pure-function pattern, ensuring deleted but still-existing resources are properly restored to the view.


253-278: New mutation follows established patterns.

The moveItemsToPosition method correctly implements the sync-mutate-validate-save pattern used throughout the service, with proper Set conversion and resource passing to the helper function.


280-299: LGTM! Consistent folder rename implementation.

The method follows the established mutation pattern and correctly delegates validation to the renameFolder helper function.


301-346: Well-structured folder creation with items.

The method includes appropriate input validation, early-return optimization when the folder already exists, and correctly delegates to the helper function with all required parameters including position.


348-366: Preferences update correctly handles JSON data.

The method appropriately types preferences as Record<string, unknown> for JSON flexibility, validates the view exists, and safely mutates the cloned organizer before persisting.

api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (9)

1-34: Clean integration of new services and JSON scalar.

The imports and constructor correctly integrate DockerFormService, DockerTemplateScannerService, and GraphQLJSON scalar support. All imports use .js extensions as required.


52-60: Good pattern: sync templates and refresh when needed.

The flow correctly synchronizes missing container templates and conditionally re-fetches containers with cache bypass when synchronization occurs, ensuring consistency.


73-82: Properly secured form query with cache control.

The new query correctly applies READ_ANY permissions, accepts a skipCache parameter, and delegates to the service layer following GraphQL resolver best practices.


90-93: Cache control added to organizer query.

The skipCache parameter is properly threaded through to the service layer, allowing clients to bypass caching when needed.


162-179: LGTM! Proper mutation with authorization.

The mutation correctly applies feature flag and UPDATE_ANY permissions, delegates to the service layer, and returns the resolved organizer. The two-step pattern (mutate, then resolve) is appropriate.


181-196: Consistent folder rename mutation.

The mutation follows the established pattern with proper decorators and service delegation. Error handling is appropriately left to the service layer.


198-217: Well-handled optional parameters.

The mutation correctly handles nullable arguments with appropriate defaults, using DEFAULT_ORGANIZER_ROOT_ID for parent and empty array for sourceEntryIds when not provided.


219-234: Correct GraphQLJSON usage for preferences.

The mutation properly uses GraphQLJSON scalar for arbitrary preference data, matching the service layer's Record<string, unknown> type. The viewId default of 'default' is consistent with the system constant.


246-254: Template sync mutation properly secured.

The mutation correctly applies UPDATE_ANY permissions for the template scanning operation and returns the typed result from the scanner service.

api/generated-schema.graphql (1)

1-2680: Auto-generated schema correctly reflects resolver changes.

The GraphQL schema has been properly generated from the TypeScript resolvers and models. All new types (DockerContainerOverviewForm, DockerTemplateSyncResult, FlatOrganizerEntry, notification types) and operations (pause/unpause/update container, organizer mutations, template sync) are correctly defined with appropriate field types and nullability.

fetchPolicy: 'cache-and-network',
});

const containers = computed<DockerContainer[]>(() => []);
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

Containers computed property returns empty array.

The containers computed property always returns an empty array and never accesses result.value. The GraphQL query type doesn't include a docker.containers field, yet this empty array is passed to DockerContainersTable. If containers are meant to be derived from flatEntries[].meta, extract them; otherwise, update the query to fetch docker.containers.

Apply this diff if containers should be extracted from flatEntries:

-const containers = computed<DockerContainer[]>(() => []);
+const containers = computed<DockerContainer[]>(() => {
+  return flatEntries.value
+    .filter(entry => entry.type === 'container' && entry.meta)
+    .map(entry => entry.meta as DockerContainer);
+});

Or update GET_DOCKER_CONTAINERS query to include the containers field and use:

-const containers = computed<DockerContainer[]>(() => []);
+const containers = computed<DockerContainer[]>(() => result.value?.docker?.containers || []);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In web/src/components/Docker/DockerContainerOverview.vue around line 29 the
computed `containers` currently returns an empty array and never reads the
GraphQL `result.value`; fix by deriving containers from the query response: if
docker container data is stored in flatEntries[].meta, change the computed to
read `result.value` (guarding for undefined), iterate `result.value.flatEntries`
and extract/flatten the container objects from each entry.meta (filtering nulls)
and return that array; alternatively, if containers should come directly from
the API, update the GET_DOCKER_CONTAINERS GraphQL query to include `docker {
containers { ... } }` and change the computed to return
`result.value.docker.containers` with proper null checks.

import { useMutation } from '@vue/apollo-composable';
import { useDebounceFn, useStorage } from '@vueuse/core';

import { UPDATE_DOCKER_VIEW_PREFERENCES } from '@/components/Docker/docker-update-view-prefs.mutation';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use .js extension in ESM import.

TypeScript sources compile to ESM, so this import must include the .js extension or it will break at runtime/bundling. Please update the specifier.

-import { UPDATE_DOCKER_VIEW_PREFERENCES } from '@/components/Docker/docker-update-view-prefs.mutation';
+import { UPDATE_DOCKER_VIEW_PREFERENCES } from '@/components/Docker/docker-update-view-prefs.mutation.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { UPDATE_DOCKER_VIEW_PREFERENCES } from '@/components/Docker/docker-update-view-prefs.mutation';
import { UPDATE_DOCKER_VIEW_PREFERENCES } from '@/components/Docker/docker-update-view-prefs.mutation.js';
🤖 Prompt for AI Agents
In web/src/composables/useDockerColumnVisibility.ts around line 4, the ESM
import specifier lacks the required .js extension and will fail at runtime;
update the import to reference the compiled ESM file by adding the .js extension
(e.g. import from '@/components/Docker/docker-update-view-prefs.mutation.js') so
the module resolver can locate the built module.

Comment on lines +28 to +48
function mergeServerPreferences(serverPrefs?: Record<string, unknown> | null) {
if (!serverPrefs) return;

const merged: ViewPreferences = {};

if (serverPrefs.columnVisibility) {
merged.columnVisibility = serverPrefs.columnVisibility as Record<string, boolean>;
}

if (Object.keys(merged).length > 0) {
localPrefs.value = { ...localPrefs.value, ...merged };
}
}

function saveColumnVisibility(columnVisibility: Record<string, boolean>) {
localPrefs.value = {
...localPrefs.value,
columnVisibility,
};

saveToServer(localPrefs.value);
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

Narrow server prefs before using them.

serverPrefs is typed as Record<string, unknown>, so serverPrefs.columnVisibility is unknown and if (serverPrefs.columnVisibility) triggers TS2571 (“Object is of type 'unknown'”). Narrow the argument to the expected shape before using it so the composable compiles.

-  function mergeServerPreferences(serverPrefs?: Record<string, unknown> | null) {
-    if (!serverPrefs) return;
-
-    const merged: ViewPreferences = {};
-
-    if (serverPrefs.columnVisibility) {
-      merged.columnVisibility = serverPrefs.columnVisibility as Record<string, boolean>;
-    }
-
-    if (Object.keys(merged).length > 0) {
-      localPrefs.value = { ...localPrefs.value, ...merged };
-    }
+  function mergeServerPreferences(serverPrefs?: ViewPreferences | null) {
+    if (!serverPrefs?.columnVisibility) return;
+
+    localPrefs.value = {
+      ...localPrefs.value,
+      columnVisibility: serverPrefs.columnVisibility,
+    };
   }
🤖 Prompt for AI Agents
In web/src/composables/useDockerColumnVisibility.ts around lines 28 to 48, the
code reads serverPrefs as Record<string, unknown> and accesses
serverPrefs.columnVisibility directly which causes TS2571; add a narrow/type
guard that checks serverPrefs is a non-null object and that it has a
"columnVisibility" property whose value is an object mapping string->boolean
(e.g. typeof serverPrefs === 'object' && serverPrefs !== null &&
'columnVisibility' in serverPrefs && typeof (serverPrefs as
any).columnVisibility === 'object'), then assign merged.columnVisibility =
serverPrefs.columnVisibility as Record<string, boolean> only after that guard
(or implement a small type predicate function to validate the shape), so the
compiler knows columnVisibility is the expected Record<string, boolean> before
use.

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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 772c364 and ddaa9f1.

📒 Files selected for processing (1)
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-02-03T17:21:26.738Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php:203-207
Timestamp: 2025-02-03T17:21:26.738Z
Learning: The project uses patches to override existing Unraid pages rather than modifying them directly.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-01-29T16:35:43.699Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:39-41
Timestamp: 2025-01-29T16:35:43.699Z
Learning: The UnraidFileModifierService in the Unraid API provides comprehensive error handling for all FileModification implementations, including detailed error logging with stack traces and modification IDs. Individual FileModification implementations should focus on their core functionality without duplicating error handling.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-03T18:57:53.577Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:80-80
Timestamp: 2025-02-03T18:57:53.577Z
Learning: In the unraid/api codebase, patch files generated for file modifications always use the .patch extension, regardless of the source file type being modified.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
🧬 Code graph analysis (1)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (2)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
  • ShouldApplyWithReason (14-23)
api/src/environment.ts (1)
  • ENABLE_NEXT_DOCKER_RELEASE (120-120)

Comment on lines +41 to +45
const fileContent = await readFile(this.filePath, 'utf-8');
const newContent = this.applyToSource();

return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
}
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

Read the override file when generatePatch receives one.

When an overridePath is passed (which our tests and tooling do), we still read /usr/local/.../DockerContainers.page, so the call blows up with ENOENT instead of diffing against the override fixture. Use the override path for both reading and patch generation.

-        const fileContent = await readFile(this.filePath, 'utf-8');
+        const sourcePath = overridePath ?? this.filePath;
+        const fileContent = await readFile(sourcePath, 'utf-8');
         const newContent = this.applyToSource();
 
-        return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
+        return this.createPatchWithDiff(sourcePath, fileContent, newContent);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fileContent = await readFile(this.filePath, 'utf-8');
const newContent = this.applyToSource();
return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
}
const sourcePath = overridePath ?? this.filePath;
const fileContent = await readFile(sourcePath, 'utf-8');
const newContent = this.applyToSource();
return this.createPatchWithDiff(sourcePath, fileContent, newContent);
}
🤖 Prompt for AI Agents
In
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
around lines 41-45, the code always reads this.filePath then uses overridePath
only for createPatchWithDiff, which causes ENOENT when tests pass an override
fixture; change the logic to read from overridePath if provided (e.g. const
sourcePath = overridePath ?? this.filePath; const fileContent = await
readFile(sourcePath, 'utf-8')) and then call createPatchWithDiff(sourcePath,
fileContent, newContent) so both the file read and the patch generation use the
same override path when supplied.

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 (3)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (2)

270-296: Add error handling, idempotency, and fix cache invalidation.

The method has several issues:

  • Only invalidates CONTAINER_CACHE_KEY, missing the size cache. Use clearContainerCache() instead.
  • Initial assignment to containers (line 276) is unused per static analysis.
  • Missing idempotency check (if already paused, return early).
  • Docker API call lacks try-catch error handling.

Apply this fix:

 public async pause(id: string): Promise<DockerContainer> {
     const container = this.client.getContainer(id);
-    await container.pause();
-    await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-    this.logger.debug(`Invalidated container cache after pausing ${id}`);
+    try {
+        const inspect = await container.inspect();
+        if (inspect?.State?.Paused) {
+            await this.clearContainerCache();
+            const existing = (await this.getContainers({ skipCache: true })).find(c => c.id === id);
+            if (existing) return existing;
+        }
+        await container.pause();
+    } catch (error) {
+        catchHandlers.docker(error as any);
+        throw error;
+    }
+    await this.clearContainerCache();
+    this.logger.debug(`Invalidated container caches after pausing ${id}`);

-    let containers = await this.getContainers({ skipCache: true });
     let updatedContainer: DockerContainer | undefined;
     for (let i = 0; i < 5; i++) {
         await sleep(500);
-        containers = await this.getContainers({ skipCache: true });
+        const containers = await this.getContainers({ skipCache: true });
         updatedContainer = containers.find((c) => c.id === id);

298-324: Mirror the same fixes needed in pause method.

The method has identical issues to pause:

  • Only invalidates CONTAINER_CACHE_KEY, missing the size cache. Use clearContainerCache() instead.
  • Initial assignment to containers (line 304) is unused per static analysis.
  • Missing idempotency check (if already running, return early).
  • Docker API call lacks try-catch error handling.

Apply this fix:

 public async unpause(id: string): Promise<DockerContainer> {
     const container = this.client.getContainer(id);
-    await container.unpause();
-    await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-    this.logger.debug(`Invalidated container cache after unpausing ${id}`);
+    try {
+        const inspect = await container.inspect();
+        if (inspect?.State && !inspect.State.Paused) {
+            await this.clearContainerCache();
+            const existing = (await this.getContainers({ skipCache: true })).find(c => c.id === id);
+            if (existing) return existing;
+        }
+        await container.unpause();
+    } catch (error) {
+        catchHandlers.docker(error as any);
+        throw error;
+    }
+    await this.clearContainerCache();
+    this.logger.debug(`Invalidated container caches after unpausing ${id}`);

-    let containers = await this.getContainers({ skipCache: true });
     let updatedContainer: DockerContainer | undefined;
     for (let i = 0; i < 5; i++) {
         await sleep(500);
-        containers = await this.getContainers({ skipCache: true });
+        const containers = await this.getContainers({ skipCache: true });
         updatedContainer = containers.find((c) => c.id === id);
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1)

197-199: Fix repository normalization to handle registries with ports and digests.

The current implementation incorrectly truncates registries with ports (e.g., registry:5000/repo:tagregistry) and images with digests. The split on : should only remove tags that appear after the last slash.

Apply this fix:

 private normalizeRepository(repository: string): string {
-    return repository.split(':')[0].toLowerCase();
+    // Strip digest if present
+    const [noDigest] = repository.split('@');
+    // Remove tag only if the colon is after the last slash (i.e., it's a tag, not a port)
+    const lastColon = noDigest.lastIndexOf(':');
+    const lastSlash = noDigest.lastIndexOf('/');
+    const withoutTag = lastColon > lastSlash ? noDigest.slice(0, lastColon) : noDigest;
+    return withoutTag.toLowerCase();
 }
🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1)

34-54: Consider exponential backoff for retry logic.

The fixed 60-second retry interval is functional but exponential backoff (e.g., 5s, 10s, 20s, 40s) would be more resilient for transient failures while providing faster recovery.

-                setTimeout(() => this.bootstrapScan(attempt + 1, maxAttempts), 60_000);
+                const delayMs = Math.min(5_000 * Math.pow(2, attempt - 1), 60_000);
+                setTimeout(() => this.bootstrapScan(attempt + 1, maxAttempts), delayMs);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddaa9f1 and 18c6a04.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-04-23T20:22:36.001Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: api/src/unraid-api/app/pubsub.module.ts:24-28
Timestamp: 2025-04-23T20:22:36.001Z
Learning: In the Unraid API project, string literals are intentionally used for dependency injection tokens instead of Symbols to enable "collision" behavior across modules that don't directly import from each other. This is a temporary approach during refactoring until a shared library for constants is implemented.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (5)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)
  • Injectable (31-398)
api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (1)
  • Injectable (13-48)
api/src/unraid-api/graph/resolvers/docker/docker-template-icon.service.ts (1)
  • Injectable (6-61)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (52-60)
api/src/environment.ts (1)
  • PATHS_DOCKER_TEMPLATES (114-117)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (3)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • Docker (680-687)
  • DockerContainer (704-725)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (52-60)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[warning] 276-276: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.


[warning] 304-304: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.

🔇 Additional comments (13)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (5)

1-21: LGTM: Import changes align with new functionality.

The import additions support template path enrichment, notifications, and external command execution. Removal of unused OnModuleInit correctly addresses the static analysis warning.


40-48: LGTM: Constructor dependencies and cache TTL properly configured.

The new service dependencies enable template mapping and notification features. The explicit CACHE_TTL_SECONDS constant (60 seconds) is correctly converted to milliseconds (60,000) when passed to cacheManager.set() throughout the file, complying with cache-manager v7 requirements.


140-169: LGTM: Container enrichment and error handling properly implemented.

The try-catch wrapper delegates Docker list errors to centralized notification handling, and the templatePath enrichment correctly maps containers to their configuration templates. The cached data includes the enriched fields for consistent behavior.


326-363: Review shell command construction for injection risks.

The encodeURIComponent() call (line 343) is designed for URL encoding, not shell escaping. While container names from Docker are typically safe, using shell: 'bash' with URI-encoded arguments creates an inconsistency that could mask injection risks in future modifications.

Consider removing the shell option to let execa handle escaping:

-        await execa(
-            '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
-            [encodeURIComponent(containerName)],
-            { shell: 'bash' }
-        );
+        await execa(
+            '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
+            [containerName]
+        );

If the script genuinely requires URI encoding or shell features, document why and validate the encoding approach matches the script's expectations.


365-397: LGTM: Error notification flow is well-structured.

The centralized error handling gracefully notifies on Docker list failures while preventing notification errors from masking the original Docker error. Message truncation at 240 characters prevents notification size limits from being exceeded.

api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (8)

1-32: LGTM: Service structure and dependencies are well-configured.

The XMLParser configuration appropriately preserves attributes and trims values for template parsing. Dependencies are properly injected for config management and Docker interaction.


56-74: LGTM: Missing container detection triggers appropriate rescans.

The method correctly identifies containers without mappings (excluding those in the skip list) and triggers a rescan when needed, enabling the resolver to refresh cached data.


76-118: LGTM: Template scanning orchestration is robust.

Storing null for unmatched containers (line 106) prevents unnecessary rescans while maintaining awareness of containers without templates. Error aggregation provides comprehensive feedback on scan results.


120-150: LGTM: Template loading handles errors gracefully.

Per-file and per-directory error handling ensures individual failures don't abort the entire scan, with comprehensive error aggregation for diagnostics.


152-166: LGTM: Template parsing is straightforward and defensive.

Returning null for templates without a Container element allows the scanner to skip invalid templates without breaking the overall scan.


168-191: LGTM: Two-pass matching prioritizes specific over generic matches.

Matching by container name first, then falling back to repository matching, ensures more specific template associations take precedence.


193-195: LGTM: Container name normalization is correct and consistent.

Stripping the leading slash and lowercasing aligns with Docker naming conventions and matches the normalization in docker.service.ts.


201-209: LGTM: Mapping updates follow the correct validation and persistence pattern.

Validating the updated config before persistence ensures schema compliance and prevents invalid states from being written.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 23684fd to 3541f14 Compare November 5, 2025 16:07
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 (4)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1)

200-202: Repository normalization breaks images with ports/digests (false matches).

Split only on a tag colon that appears after the last slash, and strip digests. Current split(':')[0] truncates e.g. registry:5000/repo:tagregistry.

Apply this diff to handle ports and digests correctly:

-    private normalizeRepository(repository: string): string {
-        return repository.split(':')[0].toLowerCase();
-    }
+    private normalizeRepository(repository: string): string {
+        // strip digest if present
+        const [noDigest] = repository.split('@');
+        // remove tag only if the colon is after the last slash (i.e., it's a tag, not a port)
+        const lastColon = noDigest.lastIndexOf(':');
+        const lastSlash = noDigest.lastIndexOf('/');
+        const withoutTag = lastColon > lastSlash ? noDigest.slice(0, lastColon) : noDigest;
+        return withoutTag.toLowerCase();
+    }
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1)

40-45: Read the override file when generatePatch receives one.

When an overridePath is passed (which tests and tooling do), the code still reads /usr/local/.../DockerContainers.page, causing ENOENT instead of diffing against the override fixture. Use the override path for both reading and patch generation.

-        const fileContent = await readFile(this.filePath, 'utf-8');
+        const sourcePath = overridePath ?? this.filePath;
+        const fileContent = await readFile(sourcePath, 'utf-8');
         const newContent = this.applyToSource();
 
-        return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
+        return this.createPatchWithDiff(sourcePath, fileContent, newContent);
web/src/components/Docker/DockerContainersTable.vue (2)

176-176: Critical: Row ID must use stable container name, not Docker runtime ID.

This issue was previously flagged but remains unresolved. Setting id: meta?.id || name causes a mismatch with organizer maps (entryParentById, positionById) that are keyed by stable entry IDs (container names). This breaks drag-and-drop, moves, and folder operations.

Apply this fix:

-    id: meta?.id || name,
+    id: name,

The containerId field on line 189 already correctly stores meta?.id for Docker operations.


373-380: State normalization missing before color mapping.

This issue was previously flagged. The state value from meta.state may be lowercase (e.g., "running"), but the colorMap is keyed by enum values which are typically uppercase. This causes silent fallback to 'neutral' color.

Apply this fix:

         const state = row.original.state ?? '';
+        const stateKey = typeof state === 'string' ? state.toUpperCase() : state;
         const isBusy = busyRowIds.value.has(row.original.id);
         const colorMap: Record<string, 'success' | 'warning' | 'neutral'> = {
           [ContainerState.RUNNING]: 'success',
           [ContainerState.PAUSED]: 'warning',
           [ContainerState.EXITED]: 'neutral',
         };
-        const color = colorMap[state] || 'neutral';
+        const color = colorMap[stateKey] || 'neutral';
🧹 Nitpick comments (1)
web/src/components/Docker/DockerContainersTable.vue (1)

712-712: Consider using a modal instead of window.prompt for folder creation.

The window.prompt call blocks the UI and doesn't match the modern modal-based UX used elsewhere in the component (see lines 999-1107 for the move-to-folder modal).

Consider adding a similar modal dialog for folder name input with proper validation and consistent styling. This would improve user experience and align with the component's design patterns.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23684fd and 3541f14.

📒 Files selected for processing (5)
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/composables/useDockerColumnVisibility.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • web/src/composables/useDockerColumnVisibility.ts
🧰 Additional context used
📓 Path-based instructions (4)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainersTable.vue
🧠 Learnings (7)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-02-03T17:21:26.738Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php:203-207
Timestamp: 2025-02-03T17:21:26.738Z
Learning: The project uses patches to override existing Unraid pages rather than modifying them directly.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-01-29T16:35:43.699Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:39-41
Timestamp: 2025-01-29T16:35:43.699Z
Learning: The UnraidFileModifierService in the Unraid API provides comprehensive error handling for all FileModification implementations, including detailed error logging with stack traces and modification IDs. Individual FileModification implementations should focus on their core functionality without duplicating error handling.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-04T18:45:23.106Z
Learnt from: elibosley
Repo: unraid/api PR: 1111
File: api/src/unraid-api/unraid-file-modifier/file-modification.ts:182-187
Timestamp: 2025-02-04T18:45:23.106Z
Learning: In the FileModification class's patch handling:
- `results === false` indicates patch application failure
- Empty string (`results === ''`) is a valid patch result indicating the file should be deleted
- These are distinct conditions and should be handled differently

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-03T18:57:53.577Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:80-80
Timestamp: 2025-02-03T18:57:53.577Z
Learning: In the unraid/api codebase, patch files generated for file modifications always use the .patch extension, regardless of the source file type being modified.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (4)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)
  • Injectable (31-398)
api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (1)
  • Injectable (13-48)
api/src/environment.ts (2)
  • ENABLE_NEXT_DOCKER_RELEASE (120-120)
  • PATHS_DOCKER_TEMPLATES (114-117)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (52-60)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (2)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
  • ShouldApplyWithReason (14-23)
api/src/environment.ts (1)
  • ENABLE_NEXT_DOCKER_RELEASE (120-120)
🔇 Additional comments (12)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (9)

1-32: LGTM! Class setup and dependencies are well-structured.

The imports follow ESM conventions with .js extensions, XMLParser configuration is appropriate for parsing Docker templates, and the NestJS dependency injection pattern is correctly implemented.


34-57: LGTM! Bootstrap scan with retry logic is well-implemented.

The feature flag check, retry mechanism with backoff, and comprehensive logging provide a robust initialization flow.


59-77: LGTM! Efficient sync check prevents unnecessary rescans.

The method correctly identifies containers needing template mappings and triggers a sync only when necessary, with the boolean return value enabling intelligent cache management by callers.


79-121: LGTM! Scanning orchestration is clean and observable.

The method coordinates template loading, container matching, and mapping updates with proper error handling and detailed result tracking.


123-153: LGTM! Template loading is resilient to individual failures.

The per-file and per-directory error handling ensures that parse failures don't prevent the entire scan from completing, with comprehensive error aggregation for observability.


155-169: LGTM! Template parsing is straightforward and correctly structured.

The method properly parses Docker template XML files and extracts the necessary metadata, with appropriate null handling for invalid templates.


171-194: LGTM! Matching strategy is well-designed.

The two-pass approach (name first, then repository) provides both precision and fallback coverage. Note that effectiveness depends on the normalizeRepository method being correct (see separate comment).


196-198: LGTM! Container name normalization is correct.

Removing the leading slash and lowercasing ensures consistent, case-insensitive container name matching.


204-211: LGTM! Mapping persistence is properly validated.

The method correctly validates the updated configuration before persisting, ensuring data integrity through the DockerConfigService.

api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (3)

1-12: LGTM! Class structure and imports are correct.

The class properly extends FileModification with appropriate imports and targets the correct Unraid Docker Containers page.


14-38: LGTM! Conditional application logic is well-guarded.

The method correctly checks feature flags and Unraid version to ensure the modification only applies when needed (pre-7.3 with feature flag enabled), with clear reasoning for each condition.


47-60: LGTM! Page replacement content is correctly formatted.

The method returns properly structured Unraid page content with appropriate metadata (menu placement, icon, conditionals) and the Docker container overview web component.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 3541f14 to 2db9ac6 Compare November 5, 2025 17:47
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 (4)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1)

200-202: Repository normalization still breaks ports/digests

This is the same issue flagged earlier: repository.split(':')[0] chops valid hosts that include ports (e.g., registry:5000/repo:tag) and leaves digest suffixes (@sha256:...), so many images never match their templates. Strip digests first and only drop the tag when the colon appears after the last slash.

-    private normalizeRepository(repository: string): string {
-        return repository.split(':')[0].toLowerCase();
-    }
+    private normalizeRepository(repository: string): string {
+        const [withoutDigest] = repository.split('@');
+        const lastColon = withoutDigest.lastIndexOf(':');
+        const lastSlash = withoutDigest.lastIndexOf('/');
+        const withoutTag =
+            lastColon > lastSlash ? withoutDigest.slice(0, lastColon) : withoutDigest;
+        return withoutTag.toLowerCase();
+    }
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1)

41-45: Read from overridePath when provided to avoid ENOENT in tests.

When overridePath is passed (used by tests and tooling), the code still reads from this.filePath, causing ENOENT errors instead of reading the override fixture for patch generation.

-        const fileContent = await readFile(this.filePath, 'utf-8');
+        const sourcePath = overridePath ?? this.filePath;
+        const fileContent = await readFile(sourcePath, 'utf-8');
         const newContent = this.applyToSource();
 
-        return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
+        return this.createPatchWithDiff(sourcePath, fileContent, newContent);
web/src/components/Docker/DockerContainersTable.vue (2)

178-193: Row IDs must remain organizer entry IDs.
Line 178 still uses the Docker runtime ID for row.id, so BaseTreeTable lookups via entryParentById and positionById fail for rows built from flatData, which breaks drag-and-drop, selection, and folder moves. Restore the stable organizer ID (the entry name) here and keep the runtime ID in containerId.

-    id: meta?.id || name,
+    id: name,

354-370: Normalize container state before mapping badge color.
Line 359 reads the raw Docker state string (often lowercase like “running”), but the color map keys are uppercased enum values, so the badge silently falls back to neutral. Normalize the state value before looking it up so running/paused/exited all hit the right colors.

-        const state = row.original.state ?? '';
+        const rawState = row.original.state ?? '';
+        const stateKey =
+          typeof rawState === 'string' ? rawState.toUpperCase() : rawState;
         const colorMap: Record<string, 'success' | 'warning' | 'neutral'> = {
           [ContainerState.RUNNING]: 'success',
           [ContainerState.PAUSED]: 'warning',
           [ContainerState.EXITED]: 'neutral',
         };
-        const color = colorMap[state] || 'neutral';
+        const color = colorMap[stateKey as ContainerState] || 'neutral';
         if (isBusy) {
           return h(USkeleton, { class: 'h-5 w-20' });
         }
-        return h(UBadge, { color }, () => state);
+        return h(UBadge, { color }, () => rawState);
🧹 Nitpick comments (2)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (2)

10-10: Consider making id readonly for consistency.

The filePath property is marked readonly, but id is not. Since modification IDs shouldn't change after instantiation, marking id as readonly would improve consistency and prevent accidental mutation.

-    id: string = 'docker-containers-page';
+    readonly id: string = 'docker-containers-page';

14-38: Logic is sound; consider checking ENABLE_NEXT_DOCKER_RELEASE first for efficiency.

The conditional logic correctly gates the modification based on feature flag and version. However, checking the environment variable before calling super.shouldApply() would avoid unnecessary file system checks when the feature is disabled.

 async shouldApply(): Promise<ShouldApplyWithReason> {
+    if (!ENABLE_NEXT_DOCKER_RELEASE) {
+        return {
+            shouldApply: false,
+            reason: 'ENABLE_NEXT_DOCKER_RELEASE is not enabled, so Docker overview table modification is not applied',
+        };
+    }
+
     const baseCheck = await super.shouldApply({ checkOsVersion: false });
     if (!baseCheck.shouldApply) {
         return baseCheck;
     }

-    if (!ENABLE_NEXT_DOCKER_RELEASE) {
-        return {
-            shouldApply: false,
-            reason: 'ENABLE_NEXT_DOCKER_RELEASE is not enabled, so Docker overview table modification is not applied',
-        };
-    }
-
     if (await this.isUnraidVersionGreaterThanOrEqualTo('7.3.0')) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3541f14 and 2db9ac6.

📒 Files selected for processing (6)
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/composables/useDockerColumnVisibility.ts (1 hunks)
  • web/src/composables/usePersistentColumnVisibility.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • web/src/composables/useDockerColumnVisibility.ts
🧰 Additional context used
📓 Path-based instructions (4)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
  • web/src/composables/usePersistentColumnVisibility.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Docker/DockerContainersTable.vue
🧠 Learnings (7)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-02-03T17:21:26.738Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php:203-207
Timestamp: 2025-02-03T17:21:26.738Z
Learning: The project uses patches to override existing Unraid pages rather than modifying them directly.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-01-29T16:35:43.699Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:39-41
Timestamp: 2025-01-29T16:35:43.699Z
Learning: The UnraidFileModifierService in the Unraid API provides comprehensive error handling for all FileModification implementations, including detailed error logging with stack traces and modification IDs. Individual FileModification implementations should focus on their core functionality without duplicating error handling.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-04T18:45:23.106Z
Learnt from: elibosley
Repo: unraid/api PR: 1111
File: api/src/unraid-api/unraid-file-modifier/file-modification.ts:182-187
Timestamp: 2025-02-04T18:45:23.106Z
Learning: In the FileModification class's patch handling:
- `results === false` indicates patch application failure
- Empty string (`results === ''`) is a valid patch result indicating the file should be deleted
- These are distinct conditions and should be handled differently

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-03T18:57:53.577Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:80-80
Timestamp: 2025-02-03T18:57:53.577Z
Learning: In the unraid/api codebase, patch files generated for file modifications always use the .patch extension, regardless of the source file type being modified.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (2)
api/src/environment.ts (2)
  • ENABLE_NEXT_DOCKER_RELEASE (120-120)
  • PATHS_DOCKER_TEMPLATES (114-117)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (52-60)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (2)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
  • ShouldApplyWithReason (14-23)
api/src/environment.ts (1)
  • ENABLE_NEXT_DOCKER_RELEASE (120-120)
🔇 Additional comments (1)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1)

47-60: LGTM!

The page content format is correct for Unraid page modifications. The header fields are appropriate, the condition correctly checks for Docker daemon availability, and the web component integration aligns with the PR objective to replace the Docker overview table.

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: 3

♻️ Duplicate comments (12)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1)

40-45: Use overridePath for file reading when provided.

When overridePath is passed (e.g., in tests), the code still reads from this.filePath, causing ENOENT errors. Both the file read and patch generation should use the same source path.

Apply this diff:

     protected async generatePatch(overridePath?: string): Promise<string> {
-        const fileContent = await readFile(this.filePath, 'utf-8');
+        const sourcePath = overridePath ?? this.filePath;
+        const fileContent = await readFile(sourcePath, 'utf-8');
         const newContent = this.applyToSource();

-        return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
+        return this.createPatchWithDiff(sourcePath, fileContent, newContent);
     }
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2)

247-258: Use Int instead of Number for position arguments.

Line 250 uses Number for the position argument, which maps to GraphQL Float. Position semantics should use Int (signed 32-bit integer). The Int type is already imported on line 5.

-        @Args('position', { type: () => Number }) position: number
+        @Args('position', { type: () => Int }) position: number

283-296: Use Int for position argument.

Line 287 uses Number for the nullable position argument. For consistency and schema clarity, use Int.

-        @Args('position', { type: () => Number, nullable: true }) position?: number
+        @Args('position', { type: () => Int, nullable: true }) position?: number
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (6)

47-47: Rename constant to reflect milliseconds for clarity.

The constant is named CACHE_TTL_SECONDS but is multiplied by 1000 at call sites. This is error-prone. Rename to CACHE_TTL_MS with value 60_000 and remove the multiplication at call sites.

-    public static readonly CACHE_TTL_SECONDS = 60;
+    public static readonly CACHE_TTL_MS = 60_000;

Then update call sites (lines 194, etc.) to use DockerService.CACHE_TTL_MS directly without multiplication.


101-108: Guard port.Type before calling toUpperCase().

port.Type can be undefined. Calling toUpperCase() on undefined will throw. Default to 'tcp' first.

             type:
-                ContainerPortType[port.Type.toUpperCase() as keyof typeof ContainerPortType] ||
-                ContainerPortType.TCP,
+                ContainerPortType[
+                    ((port.Type ?? 'tcp') as string).toUpperCase() as keyof typeof ContainerPortType
+                ] ?? ContainerPortType.TCP,

180-189: Avoid indexing with potentially undefined container name.

If c.names[0] is undefined, containerName becomes undefined after the replace chain. Indexing templateMappings[undefined] is problematic and isOrphaned would incorrectly be true.

     const containersWithTemplatePaths = containers.map((c) => {
-        const containerName = c.names[0]?.replace(/^\//, '').toLowerCase();
-        const templatePath = config.templateMappings?.[containerName] || undefined;
+        const rawName = c.names?.[0]?.replace(/^\//, '');
+        const containerName = rawName ? rawName.toLowerCase() : undefined;
+        const templatePath = containerName ? config.templateMappings?.[containerName] : undefined;
         return {
             ...c,
             templatePath,
-            isOrphaned: !templatePath,
+            isOrphaned: containerName ? !templatePath : false,
         };
     });

319-345: Use clearContainerCache() for consistent cache invalidation and remove useless assignment.

Line 322 only deletes CONTAINER_CACHE_KEY, missing CONTAINER_WITH_SIZE_CACHE_KEY. Use clearContainerCache() for consistency. Additionally, line 325's initial containers assignment is overwritten in the loop without being used (flagged by static analysis).

     public async pause(id: string): Promise<DockerContainer> {
         const container = this.client.getContainer(id);
         await container.pause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after pausing ${id}`);
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after pausing ${id}`);

-        let containers = await this.getContainers({ skipCache: true });
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
+            const containers = await this.getContainers({ skipCache: true });
             updatedContainer = containers.find((c) => c.id === id);

347-373: Mirror cache and assignment fixes for unpause.

Same issues as pause: line 350 only deletes one cache key, and line 353 has a useless initial assignment.

     public async unpause(id: string): Promise<DockerContainer> {
         const container = this.client.getContainer(id);
         await container.unpause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after unpausing ${id}`);
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after unpausing ${id}`);

-        let containers = await this.getContainers({ skipCache: true });
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
+            const containers = await this.getContainers({ skipCache: true });

389-398: Avoid shell in execa and add timeout to prevent hangs.

Running with shell: 'bash' increases risk and is unnecessary. Pass args directly and set a reasonable timeout.

         try {
             await execa(
                 '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
-                [encodeURIComponent(containerName)],
-                { shell: 'bash' }
+                [containerName],
+                { timeout: 300_000 }
             );
         } catch (error) {

Note: If URL encoding is actually required by the script, keep encodeURIComponent, but shell execution is still unnecessary.

api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (3)

64-67: Guard against empty names array before normalizing.

c.names[0] is accessed without verifying the array is non-empty. If a container has an empty names array, normalizeContainerName will receive undefined and throw when calling .replace().

Apply this guard:

     const needsSync = containers.filter((c) => {
+        if (!c.names?.[0]) return false;
         const containerName = this.normalizeContainerName(c.names[0]);
-        return !mappings[containerName] && !skipSet.has(containerName);
+        const hasMapping = Object.prototype.hasOwnProperty.call(mappings, containerName);
+        return !hasMapping && !skipSet.has(containerName);
     });

This also uses key existence check to distinguish "no mapping" from "mapped to null" (already scanned, no match), avoiding unnecessary rescans.


97-111: Guard against empty names array in scanTemplates loop.

Line 98 accesses container.names[0] without checking if it exists. This will throw if a container has an empty names array.

Apply this guard:

     for (const container of containers) {
+        if (!container.names?.[0]) {
+            result.skipped++;
+            continue;
+        }
         const containerName = this.normalizeContainerName(container.names[0]);

246-269: Guard against empty names array in matchContainerToTemplate.

Line 250 accesses container.names[0] without checking if the array has elements.

Apply this guard:

     private matchContainerToTemplate(
         container: DockerContainer,
         templates: ParsedTemplate[]
     ): ParsedTemplate | null {
+        if (!container.names?.[0]) {
+            return null;
+        }
         const containerName = this.normalizeContainerName(container.names[0]);
🧹 Nitpick comments (4)
api/docs/developer/docker.md (1)

96-96: Specify full repo-relative paths for service files.

File paths (e.g., docker.service.ts, docker-network.service.ts) lack context. Per the coding guidelines, prefer files in api/src/unraid-api/. Providing full paths (e.g., api/src/unraid-api/graph/resolvers/docker/docker.service.ts) improves developer navigation and consistency.

Also applies to: 115-115, 528-528

web/postcss/scopeTailwindToUnapi.ts (2)

182-194: Consider more concise comments.

The comment block thoroughly explains the cleanup logic, but could be more concise. Per coding guidelines, avoid comments for obvious things.

Consider condensing to something like:

-    OnceExit(root) {
-      // Remove @layer at-rules after all rules have been scoped.
-      // Tailwind CSS v4 uses @layer directives (e.g., @layer utilities, @layer components)
-      // to organize CSS. After the Rule hook scopes all selectors within these layers,
-      // the @layer wrappers are no longer needed in the final output.
-      //
-      // This cleanup step:
-      // 1. Extracts all scoped rules from inside @layer blocks
-      // 2. Moves them to the parent container (outside the @layer)
-      // 3. Removes the now-empty @layer wrapper
-      //
-      // This produces cleaner CSS output, avoids potential browser compatibility issues
-      // with CSS layers, and ensures the final CSS only contains the scoped rules without
-      // the organizational layer structure.
+    OnceExit(root) {
+      // Remove @layer wrappers after scoping. Tailwind v4's @layer directives are
+      // organizational only; we extract and hoist their contents to avoid browser
+      // compatibility issues with CSS layers.
       root.walkAtRules('layer', (atRule: AtRule) => {

195-222: Consider improving type casting patterns.

The implementation uses multiple type casts including double-casts (as unknown as Container). Per coding guidelines, avoid casting when possible and prefer proper typing from the start.

Additionally, there's inconsistency between type-checking (line 206) and optional chaining (line 211).

Consider refactoring to reduce casts and improve consistency:

-      root.walkAtRules('layer', (atRule: AtRule) => {
-        const removableAtRule = atRule as RemovableAtRule;
-        const parent = atRule.parent as ParentContainer | undefined;
-        if (!parent) {
+      root.walkAtRules('layer', (atRule) => {
+        const parent = atRule.parent;
+        if (!parent || !('insertBefore' in parent)) {
           return;
         }
 
-        // Extract all nodes from the @layer and move them to the parent
-        if (
-          Array.isArray(removableAtRule.nodes) &&
-          removableAtRule.nodes.length > 0 &&
-          typeof (parent as ParentContainer).insertBefore === 'function'
-        ) {
-          const parentContainer = parent as ParentContainer;
-          while (removableAtRule.nodes.length) {
-            const node = removableAtRule.nodes[0]!;
-            parentContainer.insertBefore?.(atRule as unknown as Container, node);
+        if ('nodes' in atRule && Array.isArray(atRule.nodes)) {
+          while (atRule.nodes.length > 0) {
+            const node = atRule.nodes[0];
+            if (node && typeof parent.insertBefore === 'function') {
+              parent.insertBefore(atRule, node);
+            }
           }
         }
 
-        // Remove the empty @layer wrapper
-        if (typeof removableAtRule.remove === 'function') {
-          removableAtRule.remove();
+        if ('remove' in atRule && typeof atRule.remove === 'function') {
+          atRule.remove();
           return;
         }
 
-        (parent as ParentContainer).removeChild?.(atRule as unknown as Container);
+        if ('removeChild' in parent && typeof parent.removeChild === 'function') {
+          parent.removeChild(atRule);
+        }
       });
web/src/components/Common/BaseTreeTable.vue (1)

485-485: Simplify: wrapColumnHeaderRenderer always returns a value.

The nullish coalescing operator ?? originalHeader is unnecessary because wrapColumnHeaderRenderer always returns a function (lines 302-317).

Apply this diff:

-      const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
+      const header = wrapColumnHeaderRenderer(originalHeader);
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b123c30 and 3f8ad0b.

📒 Files selected for processing (13)
  • api/README.md (1 hunks)
  • api/docs/developer/docker.md (1 hunks)
  • api/src/common/compare-semver-version.spec.ts (1 hunks)
  • api/src/common/compare-semver-version.ts (1 hunks)
  • api/src/common/get-unraid-version-sync.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (8 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (7 hunks)
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts (3 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (1 hunks)
  • web/components.d.ts (6 hunks)
  • web/postcss/scopeTailwindToUnapi.ts (2 hunks)
  • web/src/components/Common/BaseTreeTable.vue (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • api/src/common/compare-semver-version.spec.ts
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/common/compare-semver-version.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/common/get-unraid-version-sync.ts
  • web/postcss/scopeTailwindToUnapi.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/components.d.ts
api/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Files:

  • api/src/common/compare-semver-version.spec.ts
  • api/README.md
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/common/compare-semver-version.ts
  • api/docs/developer/docker.md
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/common/get-unraid-version-sync.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • api/src/common/compare-semver-version.spec.ts
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/common/compare-semver-version.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/common/get-unraid-version-sync.ts
  • web/postcss/scopeTailwindToUnapi.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/components.d.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Files:

  • api/src/common/compare-semver-version.spec.ts
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/common/compare-semver-version.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/common/get-unraid-version-sync.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • api/src/common/compare-semver-version.spec.ts
  • api/README.md
  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/common/compare-semver-version.ts
  • api/docs/developer/docker.md
  • web/src/components/Common/BaseTreeTable.vue
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/common/get-unraid-version-sync.ts
  • web/postcss/scopeTailwindToUnapi.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/components.d.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/postcss/scopeTailwindToUnapi.ts
  • web/components.d.ts
🧠 Learnings (57)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Test what the code does, not implementation details like exact error message wording

Applied to files:

  • api/src/common/compare-semver-version.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • api/src/common/compare-semver-version.spec.ts
📚 Learning: 2025-11-24T17:51:46.348Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-11-24T17:51:46.348Z
Learning: Applies to api/**/*.test.{ts,tsx} : Use Vitest for the test suite, not Jest

Applied to files:

  • api/src/common/compare-semver-version.spec.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details

Applied to files:

  • api/src/common/compare-semver-version.spec.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use VITEST for test suite, not jest

Applied to files:

  • api/src/common/compare-semver-version.spec.ts
📚 Learning: 2025-11-24T17:51:46.348Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-11-24T17:51:46.348Z
Learning: Prefer adding new files to the Nest.js repository located at api/src/unraid-api/ instead of legacy code

Applied to files:

  • api/README.md
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to api/**/* : Prefer adding new files to the NestJS repo located at `api/src/unraid-api/` instead of the legacy code

Applied to files:

  • api/README.md
📚 Learning: 2025-08-11T15:41:23.573Z
Learnt from: elibosley
Repo: unraid/api PR: 1563
File: web/pages/login.vue:31-48
Timestamp: 2025-08-11T15:41:23.573Z
Learning: In the Unraid API codebase, debug/test views (such as the debug modal in `web/pages/login.vue`) may intentionally display sensitive information like passwords in plain text for debugging and testing purposes. This is an accepted practice for development tools in this project.

Applied to files:

  • api/README.md
📚 Learning: 2025-09-14T14:07:28.265Z
Learnt from: elibosley
Repo: unraid/api PR: 1702
File: web/eslint.config.mjs:106-106
Timestamp: 2025-09-14T14:07:28.265Z
Learning: The unraid/api codebase specifically requires Node 20+ as a minimum version, so modern Node.js features like import.meta.dirname (available since Node v20.11.0) can be used without compatibility fallbacks.

Applied to files:

  • api/README.md
📚 Learning: 2025-01-29T16:35:43.699Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:39-41
Timestamp: 2025-01-29T16:35:43.699Z
Learning: In the Unraid API, FileModification implementations (apply/rollback methods) don't need to implement their own error handling as it's handled by the UnraidFileModifierService caller.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
📚 Learning: 2025-01-29T16:35:43.699Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:39-41
Timestamp: 2025-01-29T16:35:43.699Z
Learning: The UnraidFileModifierService in the Unraid API provides comprehensive error handling for all FileModification implementations, including detailed error logging with stack traces and modification IDs. Individual FileModification implementations should focus on their core functionality without duplicating error handling.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-04T13:45:07.161Z
Learnt from: pujitm
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/file-modification.ts:149-154
Timestamp: 2025-02-04T13:45:07.161Z
Learning: The base `FileModification` class's `shouldApply` method should include validation for file existence and permissions using `access` with `constants.R_OK | constants.W_OK`, proper error handling with logger.error, and configuration validation for filePath and id.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
📚 Learning: 2025-01-29T16:36:04.777Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:33-37
Timestamp: 2025-01-29T16:36:04.777Z
Learning: The UnraidFileModifierService in the Unraid API provides comprehensive error handling for all FileModification implementations. It includes try/catch blocks, detailed error logging, and safe rollback mechanisms. Individual FileModification implementations (like LogRotateModification) should allow errors to propagate to this service layer rather than handling them internally.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
📚 Learning: 2025-01-29T16:36:04.777Z
Learnt from: elibosley
Repo: unraid/api PR: 1082
File: api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts:33-37
Timestamp: 2025-01-29T16:36:04.777Z
Learning: In the Unraid API, FileModification implementations (like LogRotateModification) don't need to handle errors internally as error handling is managed at the UnraidFileModifierService level.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
📚 Learning: 2025-02-04T18:45:23.106Z
Learnt from: elibosley
Repo: unraid/api PR: 1111
File: api/src/unraid-api/unraid-file-modifier/file-modification.ts:182-187
Timestamp: 2025-02-04T18:45:23.106Z
Learning: In the FileModification class's patch handling:
- `results === false` indicates patch application failure
- Empty string (`results === ''`) is a valid patch result indicating the file should be deleted
- These are distinct conditions and should be handled differently

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-02-03T18:57:53.577Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:80-80
Timestamp: 2025-02-03T18:57:53.577Z
Learning: In the unraid/api codebase, patch files generated for file modifications always use the .patch extension, regardless of the source file type being modified.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/file-modification.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • api/docs/developer/docker.md
  • web/components.d.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/docs/developer/docker.md
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/components.d.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Always use TypeScript imports with `.js` extensions for ESM compatibility

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Avoid using casting whenever possible, prefer proper typing from the start

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/components.d.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Never use the `any` type. Always prefer proper typing

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-09-09T14:34:07.189Z
Learnt from: pujitm
Repo: unraid/api PR: 1582
File: packages/unraid-shared/src/util/processing.ts:69-71
Timestamp: 2025-09-09T14:34:07.189Z
Learning: In TypeScript code, pujitm considers it acceptable to use `any` type for private/internal fields when the type is not exposed through the public API and is masked by properly typed method declarations. The focus is on ensuring the public interface maintains type safety rather than enforcing strict typing on all internal implementation details.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-03-07T19:29:12.540Z
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.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-02-03T17:21:26.738Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php:203-207
Timestamp: 2025-02-03T17:21:26.738Z
Learning: The project uses patches to override existing Unraid pages rather than modifying them directly.

Applied to files:

  • api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.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/docker/docker.resolver.ts
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.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/docker/docker.resolver.ts
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.{query.ts,mutation.ts} : All GraphQL under `web/` must follow the `.query.ts` and `.mutation.ts` naming convention

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-07-22T13:29:32.175Z
Learnt from: elibosley
Repo: unraid/api PR: 1526
File: api/src/unraid-api/graph/resolvers/ups/ups.resolver.ts:82-85
Timestamp: 2025-07-22T13:29:32.175Z
Learning: In GraphQL subscriptions using the graphql-subscriptions PubSub, the correct method is `asyncIterableIterator()`, not `asyncIterator()`. The `asyncIterableIterator` method returns an `AsyncIterableIterator` that conforms to both `AsyncIterable` and `AsyncIterator` interfaces, which is required for GraphQL subscription resolvers. Using `asyncIterator()` would cause TypeScript type errors as it only returns an `AsyncIterator` that doesn't satisfy the `AsyncIterable` interface requirement.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
Repo: unraid/api PR: 1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
📚 Learning: 2025-03-27T13:34:53.438Z
Learnt from: pujitm
Repo: unraid/api PR: 1252
File: api/src/environment.ts:56-56
Timestamp: 2025-03-27T13:34:53.438Z
Learning: For critical components in the Unraid API, such as retrieving version information from package.json, failing fast (allowing crashes) is preferred over graceful degradation with fallback values.

Applied to files:

  • api/src/common/get-unraid-version-sync.ts
📚 Learning: 2025-04-23T20:22:36.001Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: api/src/unraid-api/app/pubsub.module.ts:24-28
Timestamp: 2025-04-23T20:22:36.001Z
Learning: In the Unraid API project, string literals are intentionally used for dependency injection tokens instead of Symbols to enable "collision" behavior across modules that don't directly import from each other. This is a temporary approach during refactoring until a shared library for constants is implemented.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to api/**/*.{ts,tsx} : cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-08-30T23:23:58.997Z
Learnt from: elibosley
Repo: unraid/api PR: 1618
File: api/src/unraid-api/graph/resolvers/sso/oidc-state.service.ts:0-0
Timestamp: 2025-08-30T23:23:58.997Z
Learning: For cache-manager v7, TTL values must be specified in milliseconds, not seconds. When reviewing cache.set() calls, verify that TTL values are in milliseconds (e.g., 600000 for 10 minutes, not 600).

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2024-11-05T14:49:07.308Z
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`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
Repo: unraid/api PR: 1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/components.d.ts
📚 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/components.d.ts
📚 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/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Find Vue components with `findComponent(ComponentName)`

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-06T17:34:16.133Z
Learnt from: pujitm
Repo: unraid/api PR: 974
File: web/components/Loading/Error.vue:50-50
Timestamp: 2024-12-06T17:34:16.133Z
Learning: In this project, the `Button` component from `~/components/shadcn/Button.vue` is autoloaded and does not need to be imported manually in components like `web/components/Loading/Error.vue`.

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-06T17:38:40.999Z
Learnt from: elibosley
Repo: unraid/api PR: 974
File: web/components/Loading/Error.vue:1-3
Timestamp: 2024-12-06T17:38:40.999Z
Learning: In Nuxt.js projects, components are automatically imported, so explicit import statements for components like `LoadingSpinner` in `web/components/Loading/Error.vue` are not necessary.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `findAll` to check for multiple elements in Vue components

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-02-18T17:36:22.670Z
Learnt from: pujitm
Repo: unraid/api PR: 1143
File: web/components/DummyServerSwitcher.vue:16-19
Timestamp: 2025-02-18T17:36:22.670Z
Learning: The DummyServerSwitcher component in web/components/DummyServerSwitcher.vue is a development tool for testing different server states, and includes intentional debug elements to aid development.

Applied to files:

  • web/components.d.ts
📚 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/components.d.ts
🧬 Code graph analysis (5)
api/src/common/compare-semver-version.spec.ts (1)
api/src/common/compare-semver-version.ts (1)
  • compareVersions (18-44)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
api/src/common/compare-semver-version.ts (1)
  • compareVersions (18-44)
api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (2)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
  • ShouldApplyWithReason (15-24)
api/src/environment.ts (1)
  • ENABLE_NEXT_DOCKER_RELEASE (128-128)
api/src/common/get-unraid-version-sync.ts (3)
packages/unraid-shared/src/util/file.ts (1)
  • fileExistsSync (22-29)
api/src/core/utils/misc/parse-config.ts (1)
  • parseConfig (146-203)
api/src/common/compare-semver-version.ts (1)
  • compareVersions (18-44)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (4)
api/src/unraid-api/graph/resolvers/docker/utils/docker-client.ts (1)
  • getDockerClient (5-12)
api/src/core/utils/network.ts (1)
  • getLanIp (6-19)
api/src/core/pubsub.ts (1)
  • pubsub (12-12)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
🔇 Additional comments (26)
web/components.d.ts (1)

1-158: LGTM! Auto-generated type declarations correctly reflect new components.

This auto-generated file properly declares all the new Docker-related components, common UI components, and Nuxt UI dependencies introduced in this PR. The paths are correctly structured and the declarations align with the PR objective of replacing the Docker overview table with web components.

web/postcss/scopeTailwindToUnapi.ts (2)

16-28: LGTM! Type declarations are appropriate.

The type declarations provide safe, typed access to PostCSS AST nodes with optional properties for methods that may not always be available.


33-33: LGTM! OnceExit hook addition is appropriate.

The hook extension allows cleanup operations after all rules have been processed.

api/src/unraid-api/unraid-file-modifier/file-modification.ts (3)

10-10: LGTM!

Import correctly uses .js extension for ESM compatibility as per coding guidelines.


216-225: LGTM!

The optional checkOsVersion parameter with a default of true maintains backward compatibility while allowing subclasses to opt out of the OS version check. This is a clean way to enable the Docker containers page modification to handle its own version logic.


280-280: LGTM!

Centralizing the comparison logic by delegating to compareVersions removes duplication and ensures consistent prerelease handling across the codebase.

api/src/common/compare-semver-version.ts (1)

1-44: LGTM!

Well-documented utility with clear semantics. The function reference comparison (compareFn === gte) works correctly since these are the same function instances from the semver module. The prerelease handling logic correctly treats prereleases as "greater than" stable for gte/gt operators when base versions match, aligning with the documented behavior.

api/src/common/compare-semver-version.spec.ts (1)

1-234: LGTM!

Comprehensive test suite covering basic comparisons, prerelease handling scenarios, the includePrerelease flag, and edge cases. The tests are well-organized and follow the project's testing conventions using Vitest.

api/src/common/get-unraid-version-sync.ts (2)

16-29: LGTM!

The synchronous version getter appropriately returns 'unknown' when the file doesn't exist or can't be parsed. This is reasonable for version utilities that may run in development/test environments where /etc/unraid-version doesn't exist.


38-59: LGTM!

The comparison function correctly handles edge cases by returning false when the version is unknown or coercion fails. This provides a safe default behavior where version checks will fail-closed rather than incorrectly passing.

api/src/unraid-api/unraid-file-modifier/modifications/docker-containers-page.modification.ts (3)

9-12: LGTM!

Class properly extends FileModification with a descriptive id and correct filePath pointing to the Docker containers page.


14-38: LGTM!

The shouldApply logic correctly chains the base class check with checkOsVersion: false, then applies the feature flag and version-specific conditions. The modification is gated behind both ENABLE_NEXT_DOCKER_RELEASE and requires Unraid >= 7.3.0.


47-60: LGTM!

The generated page content includes proper metadata headers and embeds the <unraid-docker-container-overview> web component as intended.

web/src/components/Common/BaseTreeTable.vue (1)

233-233: Verify that clearing selection after drop is intended.

After a successful drop, rowSelection is reset to an empty object, which deselects all rows. Confirm this is the desired UX—users may expect their selection to persist.

api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (3)

275-283: LGTM!

The normalizeRepository method now correctly handles images with ports in the registry (e.g., registry:5000/repo:tag) by only stripping the tag when the colon appears after the last slash. Digest handling is also properly addressed.


34-57: LGTM!

The bootstrap scan logic with retry backoff is well-structured. The feature flag check prevents unnecessary work when the feature is disabled, and error handling properly logs and retries up to the maximum attempts.


123-196: LGTM!

The getTemplateDetails and extractTemplatePorts methods have robust error handling and proper null checks. The port extraction correctly handles the Config array normalization and validates parsed port numbers.

api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (6)

73-82: LGTM!

The container field resolver correctly fetches from cache and returns null when not found. The feature flag and permission guards are properly applied.


88-118: LGTM!

The containers resolver correctly uses GraphQL field introspection to optimize data fetching, safely handles container names with optional chaining, and properly triggers template synchronization when needed.


120-134: LGTM!

The logs resolver correctly accepts optional parameters with proper GraphQL types and delegates to the service layer.


44-60: LGTM!

The constructor properly registers the Docker stats topic with the subscription tracker, enabling clean start/stop lifecycle management for the stats stream.


358-367: LGTM!

The subscription correctly uses the helper service for tracked subscriptions with proper permission guards.


335-356: LGTM!

The reset mutation properly resets to defaults, validates the config, and clears the container cache to ensure consistency.

api/src/unraid-api/graph/resolvers/docker/docker.service.ts (3)

460-475: LGTM!

The error handling helpers provide consistent error translation and logging. The catchHandlers.docker pattern properly handles socket errors while allowing other errors to propagate.


250-278: LGTM!

The removeContainer method properly handles optional image removal with graceful degradation (logging warning on image removal failure without failing the container removal). Cache invalidation and pub/sub notification are correctly implemented.


49-59: LGTM!

The constructor properly injects all required services and initializes the Docker client using the shared utility function.

- [DockerManifestService](#dockermanifestservice)
- [DockerPhpService](#dockerphpservice)
- [DockerTailscaleService](#dockertailscaleservice)
- [DockerTemplateScannerService](#dockertemplatescannerservice)
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

Missing DockerTemplateIconService in Core Services section.

The Module Structure at line 67 states there are "13 services" exported, but the Core Services section documents only 12. According to the PR context, DockerTemplateIconService (for reading icons from XML templates) should be included. The Table of Contents at line 22 also lacks a reference to this service.

Add DockerTemplateIconService to the documentation:

  1. Add to Table of Contents (after line 21):
  - [DockerTailscaleService](#dockertailscaleservice)
+  - [DockerTemplateIconService](#dockertemplateiconsservice)
  - [DockerTemplateScannerService](#dockertemplatescannerservice)
  1. Add a new section between DockerTailscaleService and DockerTemplateScannerService:
### DockerTemplateIconService

**File:** `docker-template-icon.service.ts`

Retrieves container template icons from XML template files.

**Features:**

- Reads XML template metadata for icon URLs
- Caches icon resolution results
- Integrates with template scanner for container metadata
  1. Update line 67 to accurately reflect service count or add this service.

Also applies to: 67-67

🤖 Prompt for AI Agents
In api/docs/developer/docker.md around line 22 (and also update line 67), the
Table of Contents and Core Services list are missing DockerTemplateIconService;
add a TOC entry right after the existing line 21 pointing to
DockerTemplateIconService, insert a new section between DockerTailscaleService
and DockerTemplateScannerService titled "DockerTemplateIconService" with the
provided description and feature bullets (file name, reads XML template metadata
for icon URLs, caches icon resolution results, integrates with template
scanner), and update the Module Structure line at 67 to reflect the correct
count (include this service so the total matches the text or change the number
to 13).

Comment on lines +420 to +458
onMousedown: (e: MouseEvent) => {
console.log('[DragHandle] mousedown', {
rowId: enhancedRow.original.id,
rowName: enhancedRow.original.name,
isBusy,
isDraggingThis,
draggable: !isBusy && !isDraggingThis,
target: e.target,
currentTarget: e.currentTarget,
});
},
onDragstart: (e: DragEvent) => {
console.log('[DragHandle] dragstart fired', {
rowId: enhancedRow.original.id,
rowName: enhancedRow.original.name,
isBusy,
dataTransfer: e.dataTransfer,
});
if (isBusy) {
console.log('[DragHandle] dragstart prevented - row is busy');
e.preventDefault();
return;
}
handleDragStart(e, enhancedRow.original);
console.log('[DragHandle] handleDragStart called, draggingIds:', draggingIds.value);
},
onDragend: (e: DragEvent) => {
console.log('[DragHandle] dragend fired', {
rowId: enhancedRow.original.id,
dropEffect: e.dataTransfer?.dropEffect,
});
handleDragEnd();
},
onDrag: (e: DragEvent) => {
// Log occasionally during drag (throttled by checking if clientX changed significantly)
if (e.clientX % 50 < 5) {
console.log('[DragHandle] dragging...', { x: e.clientX, y: e.clientY });
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove debug console.log statements before merging.

The drag handle contains extensive logging that should be removed or gated behind a debug flag for production.

Apply this diff to remove debug logging:

           onMousedown: (e: MouseEvent) => {
-            console.log('[DragHandle] mousedown', {
-              rowId: enhancedRow.original.id,
-              rowName: enhancedRow.original.name,
-              isBusy,
-              isDraggingThis,
-              draggable: !isBusy && !isDraggingThis,
-              target: e.target,
-              currentTarget: e.currentTarget,
-            });
+            // mousedown handler (logging removed)
           },
           onDragstart: (e: DragEvent) => {
-            console.log('[DragHandle] dragstart fired', {
-              rowId: enhancedRow.original.id,
-              rowName: enhancedRow.original.name,
-              isBusy,
-              dataTransfer: e.dataTransfer,
-            });
             if (isBusy) {
-              console.log('[DragHandle] dragstart prevented - row is busy');
               e.preventDefault();
               return;
             }
             handleDragStart(e, enhancedRow.original);
-            console.log('[DragHandle] handleDragStart called, draggingIds:', draggingIds.value);
           },
           onDragend: (e: DragEvent) => {
-            console.log('[DragHandle] dragend fired', {
-              rowId: enhancedRow.original.id,
-              dropEffect: e.dataTransfer?.dropEffect,
-            });
             handleDragEnd();
           },
           onDrag: (e: DragEvent) => {
-            // Log occasionally during drag (throttled by checking if clientX changed significantly)
-            if (e.clientX % 50 < 5) {
-              console.log('[DragHandle] dragging...', { x: e.clientX, y: e.clientY });
-            }
+            // drag handler
           },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onMousedown: (e: MouseEvent) => {
console.log('[DragHandle] mousedown', {
rowId: enhancedRow.original.id,
rowName: enhancedRow.original.name,
isBusy,
isDraggingThis,
draggable: !isBusy && !isDraggingThis,
target: e.target,
currentTarget: e.currentTarget,
});
},
onDragstart: (e: DragEvent) => {
console.log('[DragHandle] dragstart fired', {
rowId: enhancedRow.original.id,
rowName: enhancedRow.original.name,
isBusy,
dataTransfer: e.dataTransfer,
});
if (isBusy) {
console.log('[DragHandle] dragstart prevented - row is busy');
e.preventDefault();
return;
}
handleDragStart(e, enhancedRow.original);
console.log('[DragHandle] handleDragStart called, draggingIds:', draggingIds.value);
},
onDragend: (e: DragEvent) => {
console.log('[DragHandle] dragend fired', {
rowId: enhancedRow.original.id,
dropEffect: e.dataTransfer?.dropEffect,
});
handleDragEnd();
},
onDrag: (e: DragEvent) => {
// Log occasionally during drag (throttled by checking if clientX changed significantly)
if (e.clientX % 50 < 5) {
console.log('[DragHandle] dragging...', { x: e.clientX, y: e.clientY });
}
},
onMousedown: (e: MouseEvent) => {
},
onDragstart: (e: DragEvent) => {
if (isBusy) {
e.preventDefault();
return;
}
handleDragStart(e, enhancedRow.original);
},
onDragend: (e: DragEvent) => {
handleDragEnd();
},
onDrag: (e: DragEvent) => {
},
🤖 Prompt for AI Agents
In web/src/components/Common/BaseTreeTable.vue around lines 420 to 458, remove
or gate the debug console.log statements in the drag handlers (onMousedown,
onDragstart, onDragend, onDrag) so production builds don’t leak debug output;
either delete the console.log calls or wrap them with a runtime debug flag
(e.g., a local isDebug or process.env.NODE_ENV !== 'production') or replace them
with a proper logger.debug call so only debug builds emit these messages, and
ensure behavior (preventDefault, handleDragStart/End) is unchanged.

@pujitm pujitm marked this pull request as ready for review December 17, 2025 16:15
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: 2

♻️ Duplicate comments (8)
web/src/composables/gql/gql.ts (1)

34-34: The Apollo cache coherence issue from the previous review is not fully resolved.

Despite being marked as addressed, the mutations CreateDockerFolderWithItems, MoveDockerItemsToPosition, and UpdateDockerViewPreferences still return incomplete meta selections compared to GetDockerContainers. When these mutations execute, Apollo cache drops the missing fields (including templatePath, networkSettings, mounts, lanIpPorts, and other overview metadata), causing Docker overview icons and other UI elements to disappear until a full refetch occurs.

Since this is a generated file, update the source .mutation.ts files to include the complete meta field set that matches GetDockerContainers, then run pnpm codegen to regenerate.

Based on past review comments identifying this exact issue.

Also applies to: 39-39, 53-53

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

893-896: Fix sort comparator: ISO timestamps → NaN with Number(); breaks ordering.

Number() on an ISO string like "2025-01-01T00:00:00.000Z" returns NaN, causing unstable sort behavior.

Apply this diff to fix:

     private sortLatestFirst(a: Notification, b: Notification) {
-        const defaultTimestamp = 0;
-        return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);
+        const toMs = (t?: string | null) => (t ? Date.parse(t) : 0);
+        return toMs(b.timestamp) - toMs(a.timestamp);
     }
api/generated-schema.graphql (1)

873-876: Tri-state Boolean parameters remain in destructive mutations.

As flagged previously, removeContainer(id: PrefixedID!, withImage: Boolean) and updateAutostartConfiguration(..., persistUserPreferences: Boolean) use optional Booleans, creating tri-state ambiguity (true|false|null). Consider changing to Boolean! = false in the source resolver to provide explicit safe defaults.

api/src/unraid-api/graph/resolvers/docker/docker.service.ts (5)

2-2: Remove unused import.

OnModuleInit is imported but never used. The class does not implement this interface.

Apply this diff:

-import { Inject, Injectable, Logger } from '@nestjs/common';
+import { Inject, Injectable, Logger } from '@nestjs/common';

Actually, just remove OnModuleInit from the import if it was included (the current line shown doesn't include it, but static analysis flagged it).


45-45: Rename constant to reflect milliseconds for clarity.

The constant is named CACHE_TTL_SECONDS = 60 but is multiplied by 1000 at every call site (lines 193, etc.). This is error-prone and contradicts cache-manager v7's expectation of millisecond values. Rename to CACHE_TTL_MS = 60_000 and remove the multiplication.

Based on coding guidelines, cache-manager v7 expects TTL in milliseconds.

Apply this diff:

-    public static readonly CACHE_TTL_SECONDS = 60;
+    public static readonly CACHE_TTL_MS = 60_000;

Then update call sites at lines 193:

-            DockerService.CACHE_TTL_SECONDS * 1000
+            DockerService.CACHE_TTL_MS

318-344: Add proper cache invalidation, error handling, and remove useless assignment.

Three issues:

  1. Line 321 only invalidates CONTAINER_CACHE_KEY but should invalidate both caches via clearContainerCache() (like start() and stop() do)
  2. Line 324: Initial containers assignment is unused (overwritten in loop) - static analysis warning
  3. No try/catch around container.pause() for consistent error handling

Apply this diff:

     public async pause(id: string): Promise<DockerContainer> {
         const container = this.client.getContainer(id);
-        await container.pause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after pausing ${id}`);
+        try {
+            await container.pause();
+        } catch (error) {
+            this.logger.error(`Failed to pause container ${id}:`, error);
+            catchHandlers.docker(error as NodeJS.ErrnoException);
+            throw error;
+        }
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after pausing ${id}`);
 
-        let containers = await this.getContainers({ skipCache: true });
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
+            const containers = await this.getContainers({ skipCache: true });
             updatedContainer = containers.find((c) => c.id === id);

346-372: Mirror the fixes from pause: cache invalidation, error handling, and assignment.

Same three issues as pause():

  1. Line 349 should call clearContainerCache() instead of only invalidating one cache key
  2. Line 352: Initial containers assignment is unused - static analysis warning
  3. No try/catch around container.unpause()

Apply this diff:

     public async unpause(id: string): Promise<DockerContainer> {
         const container = this.client.getContainer(id);
-        await container.unpause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after unpausing ${id}`);
+        try {
+            await container.unpause();
+        } catch (error) {
+            this.logger.error(`Failed to unpause container ${id}:`, error);
+            catchHandlers.docker(error as NodeJS.ErrnoException);
+            throw error;
+        }
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after unpausing ${id}`);
 
-        let containers = await this.getContainers({ skipCache: true });
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
+            const containers = await this.getContainers({ skipCache: true });
             updatedContainer = containers.find((c) => c.id === id);

388-393: Remove unnecessary shell and add timeout to prevent hangs.

Using shell: 'bash' is unnecessary for this command and increases security risk. Add a timeout to prevent indefinite hangs during updates.

Apply this diff:

         try {
             await execa(
                 '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
-                [encodeURIComponent(containerName)],
-                { shell: 'bash' }
+                [containerName],
+                { timeout: 300_000 }
             );
         } catch (error) {

Note: Also removed encodeURIComponent since shell is not used.

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

877-891: Potential redundant full-scan in hasUnreadNotificationWithFingerprint.

notifyIfUnique calls hasUnreadNotificationWithFingerprint, which loads all unread notifications via loadUnreadNotifications(). Then if no duplicate is found, createNotification is called. Consider caching the loaded notifications or using a more targeted lookup if this becomes a hot path.

api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (1)

78-78: Mock usage correctly updated with optional cleanup opportunity.

The test now properly uses mockDockerClientInstance.getEvents (line 87) to align with the centralized client pattern.

The getDockerClient property in mockDockerServiceImpl (line 78) is no longer used since the service now calls the centralized utility directly. Consider removing it:

 const mockDockerServiceImpl = {
-    getDockerClient: vi.fn(),
     clearContainerCache: vi.fn(),
     getAppInfo: vi.fn().mockResolvedValue({ info: { apps: { installed: 1, running: 1 } } }),
 };

Also applies to: 87-87

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

180-188: Container name handling is safe but could be clearer.

The code uses c.names[0]?.replace(/^\//, '').toLowerCase() ?? '' which safely handles undefined names. However, when names[0] is undefined, an empty string is used as the template mapping key, which always returns undefined anyway. Consider making this more explicit for clarity.

 const containersWithTemplatePaths = containers.map((c) => {
-    const containerName = c.names[0]?.replace(/^\//, '').toLowerCase() ?? '';
+    const rawName = c.names?.[0]?.replace(/^\//, '');
+    const containerName = rawName?.toLowerCase();
     const templatePath = config.templateMappings?.[containerName] || undefined;
     return {
         ...c,
         templatePath,
         isOrphaned: !templatePath,
     };
 });
api/scripts/build.ts (1)

10-10: The @app alias resolves correctly to the scripts directory. Using @app/../scripts/get-deployment-version.js works because the @app alias maps to ./src, and the baseUrl is the api directory, so @app/../scripts resolves to ./scripts/get-deployment-version.js. However, since build.ts is already in the scripts directory, a simple relative import like ./get-deployment-version.js would be more direct and maintainable than relying on the alias to escape into a parallel directory structure.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8ad0b and 7e8e604.

⛔ Files ignored due to path filters (2)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (7 hunks)
  • api/package.json (1 hunks)
  • api/scripts/build.ts (1 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (7 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (11 hunks)
  • web/__test__/components/Wrapper/mount-engine.test.ts (4 hunks)
  • web/package.json (1 hunks)
  • web/src/assets/main.css (2 hunks)
  • web/src/components/Wrapper/mount-engine.ts (6 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
✅ Files skipped from review due to trivial changes (1)
  • api/dev/configs/api.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/src/environment.ts
  • web/test/components/Wrapper/mount-engine.test.ts
  • web/package.json
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • web/src/components/Wrapper/mount-engine.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/src/composables/gql/gql.ts
api/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • api/generated-schema.graphql
  • api/package.json
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • web/src/components/Wrapper/mount-engine.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/src/composables/gql/gql.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • web/src/assets/main.css
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • web/src/components/Wrapper/mount-engine.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • api/generated-schema.graphql
  • api/package.json
  • web/src/composables/gql/gql.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/assets/main.css
  • web/src/components/Wrapper/mount-engine.ts
  • web/src/composables/gql/gql.ts
web/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/composables/gql/gql.ts
api/package.json

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Use pnpm as the package manager (not npm or yarn)

Files:

  • api/package.json
🧠 Learnings (58)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
Learnt from: mdatelle
Repo: unraid/api PR: 1106
File: unraid-ui/src/components/index.ts:2-2
Timestamp: 2025-02-04T17:21:39.710Z
Learning: The unraid-ui package is undergoing a major refactoring process, and breaking changes are expected during this transition period.
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
Repo: unraid/api PR: 1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/src/assets/main.css
  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-02-04T17:21:39.710Z
Learnt from: mdatelle
Repo: unraid/api PR: 1106
File: unraid-ui/src/components/index.ts:2-2
Timestamp: 2025-02-04T17:21:39.710Z
Learning: The unraid-ui package is undergoing a major refactoring process, and breaking changes are expected during this transition period.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-03-13T16:17:21.897Z
Learnt from: elibosley
Repo: unraid/api PR: 1211
File: unraid-ui/src/components/form/number/NumberFieldInput.vue:1-21
Timestamp: 2025-03-13T16:17:21.897Z
Learning: The unraid-ui is a UI library being published externally, where wrapping third-party components (like those from reka-ui) is an intentional design choice to maintain a consistent interface, standardize styling, and control the exposed API.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-02-03T17:21:26.738Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php:203-207
Timestamp: 2025-02-03T17:21:26.738Z
Learning: The project uses patches to override existing Unraid pages rather than modifying them directly.

Applied to files:

  • web/src/assets/main.css
📚 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/assets/main.css
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-07-18T16:13:55.872Z
Learnt from: elibosley
Repo: unraid/api PR: 1522
File: unraid-ui/src/styles/globals.css:172-174
Timestamp: 2025-07-18T16:13:55.872Z
Learning: In Tailwind CSS v4, `--breakpoint-*` is valid syntax within the `theme` directive for defining custom breakpoints. The wildcard pattern allows you to define breakpoints like `--breakpoint-xs`, `--breakpoint-2xl`, etc. Setting `--breakpoint-*: initial` is the documented way to remove default breakpoints. This syntax is specific to Tailwind's theme system and is not subject to standard CSS custom property naming rules.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-07-17T14:09:57.364Z
Learnt from: elibosley
Repo: unraid/api PR: 1522
File: web/components/Modal.vue:0-0
Timestamp: 2025-07-17T14:09:57.364Z
Learning: In Tailwind CSS v4, `outline-hidden` is a valid utility class that applies `outline: 2px solid transparent; outline-offset: 2px;`. It's the recommended replacement for v3's `outline-none` as it preserves accessibility by keeping outlines visible in high-contrast/forced-colors modes while hiding them visually in normal modes. The v4 `outline-none` now only sets `outline-style: none;` which completely removes outlines including for assistive technologies.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-04-25T13:29:07.868Z
Learnt from: elibosley
Repo: unraid/api PR: 1369
File: web/components/Activation/WelcomeModal.ce.vue:40-40
Timestamp: 2025-04-25T13:29:07.868Z
Learning: The `#confirmPassword` selector in the WelcomeModal.ce.vue component is intentionally looking for an element that exists in the Unraid OS login page (.login.php), not within the component itself. This is used as part of a font-size adjustment workaround to handle different CSS between login and authenticated pages.

Applied to files:

  • web/src/assets/main.css
📚 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/assets/main.css
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2024-12-06T17:38:40.999Z
Learnt from: elibosley
Repo: unraid/api PR: 974
File: web/components/Loading/Error.vue:1-3
Timestamp: 2024-12-06T17:38:40.999Z
Learning: In Nuxt.js projects, components are automatically imported, so explicit import statements for components like `LoadingSpinner` in `web/components/Loading/Error.vue` are not necessary.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-03-14T19:14:10.408Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:0-0
Timestamp: 2025-03-14T19:14:10.408Z
Learning: *.ce.vue components in `web/components` get built as custom elements and prefixed with `unraid-`, making them available as HTML custom elements at runtime.

Applied to files:

  • web/src/assets/main.css
📚 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/assets/main.css
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • api/scripts/build.ts
📚 Learning: 2025-11-24T17:51:46.348Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-11-24T17:51:46.348Z
Learning: Always run scripts from api/package.json unless explicitly requested otherwise

Applied to files:

  • api/scripts/build.ts
📚 Learning: 2025-02-06T17:25:45.397Z
Learnt from: mdatelle
Repo: unraid/api PR: 1122
File: web/package.json:16-16
Timestamp: 2025-02-06T17:25:45.397Z
Learning: The build script in web/package.json should include type-check command (`npm run type-check`) even when NODE_ENV is production, as it provides value for local builds by catching type errors before pushing to CI/CD.

Applied to files:

  • api/scripts/build.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Mock external services and API calls

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:51:46.348Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-11-24T17:51:46.348Z
Learning: Applies to api/**/*.test.{ts,tsx} : Prefer to not mock simple dependencies

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-02-03T18:57:28.442Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:55-56
Timestamp: 2025-02-03T18:57:28.442Z
Learning: Using console.log is acceptable in test files, especially for debugging test execution. The logger instances in test files are often used as mocks/dependencies for the classes being tested, not for actual logging.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
Repo: unraid/api PR: 1575
File: packages/unraid-shared/src/services/socket-config.service.spec.ts:10-13
Timestamp: 2025-08-09T01:03:29.676Z
Learning: Vitest is used for all testing across all repositories in the unraid organization, not Jest. Always use `vi` for mocking utilities, not `jest`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.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/notifications/notifications.resolver.ts
  • api/generated-schema.graphql
📚 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/notifications/notifications.resolver.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.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/notifications/notifications.resolver.ts
  • api/generated-schema.graphql
📚 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.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.resolver.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
Repo: unraid/api PR: 1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 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/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-04-23T20:22:36.001Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: api/src/unraid-api/app/pubsub.module.ts:24-28
Timestamp: 2025-04-23T20:22:36.001Z
Learning: In the Unraid API project, string literals are intentionally used for dependency injection tokens instead of Symbols to enable "collision" behavior across modules that don't directly import from each other. This is a temporary approach during refactoring until a shared library for constants is implemented.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to api/**/*.{ts,tsx} : cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-08-30T23:23:58.997Z
Learnt from: elibosley
Repo: unraid/api PR: 1618
File: api/src/unraid-api/graph/resolvers/sso/oidc-state.service.ts:0-0
Timestamp: 2025-08-30T23:23:58.997Z
Learning: For cache-manager v7, TTL values must be specified in milliseconds, not seconds. When reviewing cache.set() calls, verify that TTL values are in milliseconds (e.g., 600000 for 10 minutes, not 600).

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2024-11-05T14:49:07.308Z
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`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Avoid using casting whenever possible, prefer proper typing from the start

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-03-07T19:29:12.540Z
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.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2024-11-04T22:00:41.946Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/auth/auth.service.ts:42-105
Timestamp: 2024-11-04T22:00:41.946Z
Learning: In the 'AuthService' within `api/src/unraid-api/auth/auth.service.ts`, rate limiting is not required because the API is not public facing.

Applied to files:

  • api/generated-schema.graphql
📚 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:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.{query.ts,mutation.ts} : All GraphQL under `web/` must follow the `.query.ts` and `.mutation.ts` naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
🧬 Code graph analysis (4)
api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (1)
api/src/unraid-api/graph/resolvers/docker/utils/docker-client.ts (1)
  • getDockerClient (5-12)
api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (4)
api/src/unraid-api/cli/generated/graphql.ts (3)
  • Notification (1612-1625)
  • Mutation (1408-1458)
  • Subscription (2260-2274)
web/composables/gql/graphql.ts (3)
  • Notification (1361-1374)
  • Mutation (1199-1239)
  • Subscription (2032-2044)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/core/pubsub.ts (1)
  • createSubscription (19-23)
web/src/components/Wrapper/mount-engine.ts (2)
web/src/utils/unapiScope.ts (3)
  • ensureUnapiScope (82-88)
  • ensureUnapiScopeForSelectors (97-117)
  • observeUnapiScope (132-188)
web/src/components/Wrapper/component-registry.ts (1)
  • componentMappings (24-168)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (4)
api/src/unraid-api/graph/resolvers/docker/utils/docker-client.ts (1)
  • getDockerClient (5-12)
api/src/core/utils/network.ts (1)
  • getLanIp (6-19)
api/src/core/pubsub.ts (1)
  • pubsub (12-12)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[warning] 324-324: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.


[warning] 352-352: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.

🔇 Additional comments (22)
api/package.json (1)

107-107: No changes required. Version 5.3.0 is valid; the latest version is 5.3.2 and the latest non-vulnerable version is also 5.3.2, and the caret range allows automatic patch updates. The library demonstrates positive version release cadence with at least one new version released in the past 3 months. The dependency specification follows npm/pnpm conventions and is appropriate for the Docker template scanning feature.

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

52-57: LGTM!

The warningsAndAlerts ResolveField is appropriately placed under the Notifications resolver, which already requires READ_ANY permission on Resource.NOTIFICATIONS at the parent query level (lines 28-32). The delegation to NotificationsService.getWarningsAndAlerts() is clean.


106-116: Verify that the missing @UsePermissions decorator is intentional.

The past review flagged this mutation as lacking authorization, and it was marked as "addressed." However, the current code still shows no @UsePermissions decorator on notifyIfUnique. Other mutations in this resolver (e.g., createNotification, archiveNotification) also lack the decorator, suggesting this may be intentional for internal/programmatic use. Please confirm this is the intended authorization model.


186-193: LGTM!

The subscription is correctly protected with @UsePermissions for READ_ANY on NOTIFICATIONS resource, consistent with other subscriptions in this resolver. The return type [Notification] and pubsub channel usage are appropriate.

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

146-158: LGTM!

The publishWarningsAndAlerts method properly wraps the async operation in a try-catch, logs errors without propagating them, and uses void prefix at call sites to signal intentional fire-and-forget behavior. This is a sensible pattern for non-critical broadcast operations.


599-655: LGTM with minor performance consideration.

The notifyIfUnique and getWarningsAndAlerts implementations are correct:

  • Fingerprint-based deduplication is sound
  • Early termination when limit is reached is efficient
  • Error handling delegates to underlying methods

Note that getWarningsAndAlerts loads all unread notifications into memory before filtering. For typical notification volumes this should be fine, but if notification counts grow large, consider filtering at the file-listing level.


912-929: LGTM!

The fingerprint generation is deterministic and uses a sensible combination of fields (importance, title, subject, description, link) with proper null handling via makePart. The pipe-delimited format is simple and effective for deduplication purposes.

api/generated-schema.graphql (2)

2659-2663: LGTM!

The notifyIfUnique mutation is correctly exposed in the schema with nullable return type (Notification without !), matching the resolver's behavior of returning null when a duplicate is detected.


2895-2900: LGTM!

The new subscriptions notificationsWarningsAndAlerts and dockerContainerStats are properly typed and exposed in the schema, consistent with their resolver implementations.

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

10-10: LGTM! Clean refactoring to centralized Docker client.

The change correctly adopts the centralized getDockerClient() utility pattern, creating a singleton Docker client instance. The .js extension is properly used for ESM compatibility.

Also applies to: 70-70

api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (2)

2-2: LGTM! Proper import cleanup.

Correctly removed unused Docker and Logger type imports while retaining the necessary PassThrough stream import.


52-58: LGTM! Correct centralized mock structure.

The mock correctly simulates the centralized getDockerClient() utility pattern used by the service. The relative path and .js extension are properly specified.

web/src/components/Wrapper/mount-engine.ts (4)

22-38: LGTM!

The iframe navigation hiding logic is correct and includes proper SSR safety checks.


58-80: Verify the z-index value is appropriate.

The portal root uses a z-index of 999999, which is extremely high. While this ensures modals appear above other content, verify that this value doesn't conflict with other high-z-index elements in the application or cause stacking context issues.


242-244: LGTM!

The portal target is correctly wired into the UApp component to support modal rendering.


264-264: LGTM!

Applying the unapi scope after mounting ensures proper CSS isolation for the component.

web/src/assets/main.css (3)

12-12: Nuxt UI import correctly uses public API.

The internal build artifact import has been properly removed, and only the documented public import remains. This resolves the previous pipeline failure.


38-95: Compatibility styles look reasonable.

The z-index, iframe, and banner gradient styles appear intentional for integrating with the existing Unraid WebGUI. The extremely high z-index (999999) for modals is appropriate for ensuring they stay on top of legacy UI elements.


27-29: Verify preflight scoping approach after previous concerns.

The previous review flagged importing preflight inside .unapi as problematic because preflight targets document root elements that cannot be scoped to a container. While marked as addressed, the current code still uses the same pattern.

Please confirm that:

  • The custom PostCSS plugin scopeTailwindToUnapi correctly handles this transformation
  • Layout and styling regressions have been tested after this change
  • This approach doesn't conflict with Nuxt UI v3's styling assumptions
#!/bin/bash
# Verify the PostCSS plugin configuration is still in place
cat web/vite.config.ts | grep -A 5 "scopeTailwindToUnapi"

# Check if the plugin file exists and review its implementation
cat web/postcss/scopeTailwindToUnapi.ts 2>/dev/null || cat web/postcss/scopeTailwindToUnapi.js 2>/dev/null || echo "Plugin file not found"
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (3)

103-107: Port type handling looks good.

The fallback to 'tcp' when port.Type is undefined properly addresses the previous concern about potential runtime errors.


198-224: Clean delegation to specialized services.

The new methods properly delegate to specialized services (DockerPortService, DockerLogService, DockerNetworkService), maintaining good separation of concerns.


459-474: Well-structured error handling helpers.

The private error handling methods provide consistent error processing with proper logging and type-safe message extraction.

Comment on lines +211 to +213
const targetElement = element as HTMLElement;
// ensureContainerScope(targetElement, mapping);
componentsToMount.push({ mapping, element: targetElement });
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove commented-out code.

The commented-out call to ensureContainerScope should either be removed or uncommented with a clear explanation if it's needed for future reference.

Based on coding guidelines that state: "Never add comments unless they are needed for clarity of function" and to avoid dead code.

🤖 Prompt for AI Agents
In web/src/components/Wrapper/mount-engine.ts around lines 211 to 213 there is a
commented-out call to ensureContainerScope; remove that dead/commented-out line
(or if the call is actually required, uncomment it and add a one-line comment
explaining why it must remain), leaving only the active code that pushes
componentsToMount with a clean, comment-free block.

Comment on lines +257 to 259
// ensureUnapiScope(element);
// ensureContainerScope(element, mapping);
element.replaceChildren();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove commented-out code.

Lines 257-258 contain commented-out scope-related calls that should either be removed entirely or uncommented with clear documentation explaining why they're temporarily disabled.

Based on coding guidelines that state to avoid commented-out code unless needed for clarity.

🤖 Prompt for AI Agents
In web/src/components/Wrapper/mount-engine.ts around lines 257 to 259, remove
the two commented-out calls ensureUnapiScope(element); and
ensureContainerScope(element, mapping); because commented-out code should not
remain in the codebase; either delete these lines or, if they are temporarily
disabled for a specific reason, uncomment them and add a concise inline comment
explaining why they must remain disabled and for how long. Ensure the final
change leaves only active, documented code (or a short explanatory comment) and
run tests/build to verify no regressions.

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: 1

♻️ Duplicate comments (8)
web/src/assets/main.css (1)

20-23: Comment describes preflight scoping approach flagged as critical issue in past reviews.

This comment documents the approach of importing tailwindcss/preflight.css inside the .unapi selector (lines 27-29), which was previously identified as critically flawed because preflight targets document root elements (html, body) that cannot be scoped to a container selector.

Based on learnings, past reviews recommended either removing preflight and providing custom scoped resets, or using a PostCSS nesting plugin to properly wrap the import.

The comment update suggests this approach is intentional, but the underlying scoping issue remains unresolved.

api/generated-schema.graphql (2)

1248-1255: Consider a stable opaque cursor type for Docker logs

Using DateTime as the cursor risks duplicates or gaps when multiple log lines share the same timestamp. A more robust approach is an opaque String! cursor (encoding timestamp + sequence) or a composite cursor object, so pagination/streaming can resume precisely without relying on timestamp uniqueness.


872-877: Avoid tri-state Boolean args on destructive Docker mutations

withImage: Boolean and persistUserPreferences: Boolean are nullable, which introduces a true|false|null tri-state on destructive/persisting behavior. Prefer explicit, non-nullable args with safe defaults, e.g.:

removeContainer(id: PrefixedID!, withImage: Boolean! = false): Boolean!
updateAutostartConfiguration(
  entries: [DockerAutostartEntryInput!]!,
  persistUserPreferences: Boolean! = false
): Boolean!

to make client intent unambiguous and avoid accidental null behavior.

web/src/composables/gql/gql.ts (1)

34-39: Align organizer mutations’ meta selection with GetDockerContainers

The organizer mutations (CreateDockerFolderWithItems, MoveDockerItemsToPosition, UpdateDockerViewPreferences, etc.) still select a reduced meta (no templatePath, lanIpPorts, icon/support URLs, shell, templatePorts, tailscaleEnabled, etc.), while GetDockerContainers fetches the full overview shape. When these mutations write into the cache, affected entries will lose those omitted fields until a refetch.

Please update the underlying .mutation.ts documents so their views.flatEntries.meta selection matches GetDockerContainers, then re-run pnpm codegen so this gql.ts stays in sync.

Also applies to: 53-53, 104-109, 123-123

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

146-158: Fix sortLatestFirst to correctly order ISO timestamps for dedup logic

The new fingerprint-based dedup (notifyIfUnique, getWarningsAndAlerts, loadUnreadNotifications) depends on sortLatestFirst, but the comparator still does:

const defaultTimestamp = 0;
return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);

Since timestamp is ISO text, Number('2025-01-01T...') is NaN, so the sort order becomes effectively undefined. This undermines the “latest-first” semantics you rely on when picking which duplicate to keep.

Consider parsing to epoch millis instead:

-    private sortLatestFirst(a: Notification, b: Notification) {
-        const defaultTimestamp = 0;
-        return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);
-    }
+    private sortLatestFirst(a: Notification, b: Notification) {
+        const toMs = (t?: string | null) => (t ? Date.parse(t) : 0);
+        return toMs(b.timestamp) - toMs(a.timestamp);
+    }

so dedup and warning/alert snapshots consistently favor the most recent notification.

Also applies to: 599-655, 877-896

api/src/unraid-api/graph/resolvers/docker/docker.service.ts (3)

179-188: Guard against missing container names when mapping templatePath

This line can still throw if c.names[0] is undefined:

const containerName = c.names[0]?.replace(/^\//, '').toLowerCase() ?? '';

because ?. only protects the replace call; toLowerCase() still runs on undefined. To avoid crashes for containers without names, derive the raw name first and only lowercase when present:

-        const containersWithTemplatePaths = containers.map((c) => {
-            const containerName = c.names[0]?.replace(/^\//, '').toLowerCase() ?? '';
-            const templatePath = config.templateMappings?.[containerName] || undefined;
-            return {
-                ...c,
-                templatePath,
-                isOrphaned: !templatePath,
-            };
-        });
+        const containersWithTemplatePaths = containers.map((c) => {
+            const rawName = c.names?.[0]?.replace(/^\//, '');
+            const containerName = rawName ? rawName.toLowerCase() : undefined;
+            const templatePath = containerName
+                ? config.templateMappings?.[containerName] || undefined
+                : undefined;
+            return {
+                ...c,
+                templatePath,
+                isOrphaned: !templatePath,
+            };
+        });

318-372: Use clearContainerCache() and drop redundant containers assignment in pause/unpause

Both pause and unpause currently:

  • Invalidate only CONTAINER_CACHE_KEY instead of both container caches (missing size cache).
  • Assign let containers = await this.getContainers(...) before the loop, but immediately overwrite it in the loop (CodeQL warning).

You can simplify and make cache behavior consistent with start/stop:

-        await container.pause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after pausing ${id}`);
-
-        let containers = await this.getContainers({ skipCache: true });
-        let updatedContainer: DockerContainer | undefined;
+        await container.pause();
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after pausing ${id}`);
+
+        let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
-            updatedContainer = containers.find((c) => c.id === id);
+            const containers = await this.getContainers({ skipCache: true });
+            updatedContainer = containers.find((c) => c.id === id);
@@
-        await container.unpause();
-        await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY);
-        this.logger.debug(`Invalidated container cache after unpausing ${id}`);
-
-        let containers = await this.getContainers({ skipCache: true });
-        let updatedContainer: DockerContainer | undefined;
+        await container.unpause();
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after unpausing ${id}`);
+
+        let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
-            containers = await this.getContainers({ skipCache: true });
-            updatedContainer = containers.find((c) => c.id === id);
+            const containers = await this.getContainers({ skipCache: true });
+            updatedContainer = containers.find((c) => c.id === id);

You may also want a try/catch that routes errors through catchHandlers.docker for consistency with getContainers.


374-397: Avoid shell: 'bash' in execa and add a timeout for container updates

Running the update script via execa with { shell: 'bash' } is unnecessary and increases risk; execa can execute the script directly. It’s also easy for this call to hang indefinitely without a timeout.

Consider:

-        try {
-            await execa(
-                '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
-                [encodeURIComponent(containerName)],
-                { shell: 'bash' }
-            );
+        try {
+            await execa(
+                '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
+                [containerName],
+                { timeout: 300_000 } // e.g. 5 minutes
+            );

This keeps arguments positional, avoids spawning an extra shell, and bounds the update duration.

🧹 Nitpick comments (3)
web/src/components/Wrapper/mount-engine.ts (1)

22-38: Consider simplifying query parameter check.

The iframe query parameter check could be simplified using a more concise conditional.

Apply this diff:

-  const params = new URLSearchParams(window.location.search);
-  if (params.get('iframe')?.toLowerCase() !== 'true') {
-    return;
-  }
+  const isIframe = new URLSearchParams(window.location.search).get('iframe')?.toLowerCase() === 'true';
+  if (!isIframe) {
+    return;
+  }
web/__test__/components/Wrapper/mount-engine.test.ts (1)

104-104: Remove duplicate reset.

lastUAppPortal is reset twice in beforeEach (lines 104 and 137). The second reset is redundant.

Apply this diff:

     consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
     consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

     vi.clearAllMocks();

     // Clean up DOM
     document.body.innerHTML = '';
-    lastUAppPortal = undefined;
   });

Also applies to: 137-137

api/scripts/build.ts (1)

10-10: Simplify the import path to use a relative import instead of the alias.

The import path @app/../scripts/get-deployment-version.js resolves correctly, but uses an unnecessarily complex construction with the alias followed by parent directory navigation. Since both build.ts and get-deployment-version.ts are in the same api/scripts/ directory, use a simpler relative import: ./get-deployment-version.js.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8ad0b and 7e8e604.

⛔ Files ignored due to path filters (2)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (7 hunks)
  • api/package.json (1 hunks)
  • api/scripts/build.ts (1 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (7 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts (11 hunks)
  • web/__test__/components/Wrapper/mount-engine.test.ts (4 hunks)
  • web/package.json (1 hunks)
  • web/src/assets/main.css (2 hunks)
  • web/src/components/Wrapper/mount-engine.ts (6 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
✅ Files skipped from review due to trivial changes (1)
  • api/dev/configs/api.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/src/environment.ts
  • web/package.json
  • api/package.json
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/src/composables/gql/gql.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/src/assets/main.css
  • web/src/composables/gql/gql.ts
web/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/composables/gql/gql.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/src/composables/gql/gql.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • web/src/assets/main.css
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • web/src/composables/gql/gql.ts
api/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer adding new files to the NestJS repo located at api/src/unraid-api/ instead of the legacy code

Files:

  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Files:

  • api/scripts/build.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts
  • api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use .rejects.toThrow() without arguments to test that functions throw errors, not exact error message strings

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
web/__test__/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries like find('button') rather than data-test IDs
Use createPinia() and setActivePinia when testing Store files, only use createTestingPinia if you specifically need its testing features

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
**/*.test.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/*.test.ts: Use .rejects.toThrow() without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Use vi.mock() for module-level mocks
Specify return values for component methods with vi.spyOn()
Reset mocks between tests with vi.clearAllMocks()
Always await async operations before making assertions

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Mock external dependencies and services in Vue component tests
Test component behavior and output, not implementation details
Verify that the expected elements are rendered in Vue components
Test component interactions such as clicks and inputs
Check for expected prop handling and event emissions in Vue components
Use createTestingPinia() for mocking stores in Vue components
Use semantic queries like find('button') or find('[data-test="id"]') but prefer not to use data test ID's when finding elements
Find Vue components with findComponent(ComponentName)
Use findAll to check for multiple elements in Vue components
Assert on rendered text content with wrapper.text()
Assert on element attributes with element.attributes()
Verify element existence with expect(element.exists()).toBe(true)
Check component state through rendered output
Trigger events with await element.trigger('click')
Set input values with await input.setValue('value')
Test emitted events with wrapper.emitted()
Use await nextTick() for DOM updates in Vue component tests
Use flushPromises() for complex promise chains in Vue component tests

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
🧠 Learnings (66)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
Repo: unraid/api PR: 1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/composables/gql/gql.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 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/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
Repo: unraid/api PR: 1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/assets/main.css
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:51:46.348Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-11-24T17:51:46.348Z
Learning: Always run scripts from api/package.json unless explicitly requested otherwise

Applied to files:

  • api/scripts/build.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Mock external services and API calls

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:51:46.348Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-11-24T17:51:46.348Z
Learning: Applies to api/**/*.test.{ts,tsx} : Prefer to not mock simple dependencies

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-02-03T18:57:28.442Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts:55-56
Timestamp: 2025-02-03T18:57:28.442Z
Learning: Using console.log is acceptable in test files, especially for debugging test execution. The logger instances in test files are often used as mocks/dependencies for the classes being tested, not for actual logging.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
Repo: unraid/api PR: 1575
File: packages/unraid-shared/src/services/socket-config.service.spec.ts:10-13
Timestamp: 2025-08-09T01:03:29.676Z
Learning: Vitest is used for all testing across all repositories in the unraid organization, not Jest. Always use `vi` for mocking utilities, not `jest`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Stub complex child components that aren't the focus of the test

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test emitted events with `wrapper.emitted()`

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Assert on rendered text content with `wrapper.text()`

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Stub complex child components that aren't the focus of the test

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check component state through rendered output

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: This is a Nuxt.js app but tests are run with vitest outside of the Nuxt environment

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions such as clicks and inputs

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.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/notifications/notifications.resolver.ts
  • api/generated-schema.graphql
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • api/src/unraid-api/graph/resolvers/notifications/notifications.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/notifications/notifications.resolver.ts
  • api/generated-schema.graphql
📚 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.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.resolver.ts
📚 Learning: 2025-02-04T17:21:39.710Z
Learnt from: mdatelle
Repo: unraid/api PR: 1106
File: unraid-ui/src/components/index.ts:2-2
Timestamp: 2025-02-04T17:21:39.710Z
Learning: The unraid-ui package is undergoing a major refactoring process, and breaking changes are expected during this transition period.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-03-13T16:17:21.897Z
Learnt from: elibosley
Repo: unraid/api PR: 1211
File: unraid-ui/src/components/form/number/NumberFieldInput.vue:1-21
Timestamp: 2025-03-13T16:17:21.897Z
Learning: The unraid-ui is a UI library being published externally, where wrapping third-party components (like those from reka-ui) is an intentional design choice to maintain a consistent interface, standardize styling, and control the exposed API.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-02-03T17:21:26.738Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php:203-207
Timestamp: 2025-02-03T17:21:26.738Z
Learning: The project uses patches to override existing Unraid pages rather than modifying them directly.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-01-31T22:01:41.842Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch is used to remove the old jGrowl notification system from Unraid pages, as notifications are handled by a new system implemented on a different page.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-01-31T22:01:41.842Z
Learnt from: elibosley
Repo: unraid/api PR: 1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch removes the old jGrowl notification system and is complemented by the unraid-toaster component implementation. The new system is added through the DefaultPageLayout modification which inserts the toaster component with proper position configuration based on user preferences.

Applied to files:

  • web/src/assets/main.css
📚 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/assets/main.css
📚 Learning: 2025-07-18T16:13:55.872Z
Learnt from: elibosley
Repo: unraid/api PR: 1522
File: unraid-ui/src/styles/globals.css:172-174
Timestamp: 2025-07-18T16:13:55.872Z
Learning: In Tailwind CSS v4, `--breakpoint-*` is valid syntax within the `theme` directive for defining custom breakpoints. The wildcard pattern allows you to define breakpoints like `--breakpoint-xs`, `--breakpoint-2xl`, etc. Setting `--breakpoint-*: initial` is the documented way to remove default breakpoints. This syntax is specific to Tailwind's theme system and is not subject to standard CSS custom property naming rules.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-07-17T14:09:57.364Z
Learnt from: elibosley
Repo: unraid/api PR: 1522
File: web/components/Modal.vue:0-0
Timestamp: 2025-07-17T14:09:57.364Z
Learning: In Tailwind CSS v4, `outline-hidden` is a valid utility class that applies `outline: 2px solid transparent; outline-offset: 2px;`. It's the recommended replacement for v3's `outline-none` as it preserves accessibility by keeping outlines visible in high-contrast/forced-colors modes while hiding them visually in normal modes. The v4 `outline-none` now only sets `outline-style: none;` which completely removes outlines including for assistive technologies.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-04-25T13:29:07.868Z
Learnt from: elibosley
Repo: unraid/api PR: 1369
File: web/components/Activation/WelcomeModal.ce.vue:40-40
Timestamp: 2025-04-25T13:29:07.868Z
Learning: The `#confirmPassword` selector in the WelcomeModal.ce.vue component is intentionally looking for an element that exists in the Unraid OS login page (.login.php), not within the component itself. This is used as part of a font-size adjustment workaround to handle different CSS between login and authenticated pages.

Applied to files:

  • web/src/assets/main.css
📚 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/assets/main.css
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2024-12-06T17:38:40.999Z
Learnt from: elibosley
Repo: unraid/api PR: 974
File: web/components/Loading/Error.vue:1-3
Timestamp: 2024-12-06T17:38:40.999Z
Learning: In Nuxt.js projects, components are automatically imported, so explicit import statements for components like `LoadingSpinner` in `web/components/Loading/Error.vue` are not necessary.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2025-03-14T19:14:10.408Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:0-0
Timestamp: 2025-03-14T19:14:10.408Z
Learning: *.ce.vue components in `web/components` get built as custom elements and prefixed with `unraid-`, making them available as HTML custom elements at runtime.

Applied to files:

  • web/src/assets/main.css
📚 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/assets/main.css
📚 Learning: 2024-11-04T22:00:41.946Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/auth/auth.service.ts:42-105
Timestamp: 2024-11-04T22:00:41.946Z
Learning: In the 'AuthService' within `api/src/unraid-api/auth/auth.service.ts`, rate limiting is not required because the API is not public facing.

Applied to files:

  • api/generated-schema.graphql
📚 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/generated-schema.graphql
📚 Learning: 2025-04-23T20:22:36.001Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: api/src/unraid-api/app/pubsub.module.ts:24-28
Timestamp: 2025-04-23T20:22:36.001Z
Learning: In the Unraid API project, string literals are intentionally used for dependency injection tokens instead of Symbols to enable "collision" behavior across modules that don't directly import from each other. This is a temporary approach during refactoring until a shared library for constants is implemented.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to api/**/*.{ts,tsx} : cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-08-30T23:23:58.997Z
Learnt from: elibosley
Repo: unraid/api PR: 1618
File: api/src/unraid-api/graph/resolvers/sso/oidc-state.service.ts:0-0
Timestamp: 2025-08-30T23:23:58.997Z
Learning: For cache-manager v7, TTL values must be specified in milliseconds, not seconds. When reviewing cache.set() calls, verify that TTL values are in milliseconds (e.g., 600000 for 10 minutes, not 600).

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2024-11-05T14:49:07.308Z
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`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.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:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.{query.ts,mutation.ts} : All GraphQL under `web/` must follow the `.query.ts` and `.mutation.ts` naming convention

Applied to files:

  • web/src/composables/gql/gql.ts
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/src/composables/gql/gql.ts
🧬 Code graph analysis (4)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/utils/unapiScope.ts (3)
  • ensureUnapiScope (82-88)
  • ensureUnapiScopeForSelectors (97-117)
  • observeUnapiScope (132-188)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
  • mountUnifiedApp (126-280)
api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (1)
api/src/unraid-api/graph/resolvers/docker/utils/docker-client.ts (1)
  • getDockerClient (5-12)
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (5)
api/src/unraid-api/graph/resolvers/docker/utils/docker-client.ts (1)
  • getDockerClient (5-12)
web/src/composables/gql/graphql.ts (2)
  • Docker (704-715)
  • DockerAutostartEntryInput (749-756)
api/src/core/utils/network.ts (1)
  • getLanIp (6-19)
api/src/core/pubsub.ts (1)
  • pubsub (12-12)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[warning] 324-324: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.


[warning] 352-352: Useless assignment to local variable
The initial value of containers is unused, since it is always overwritten.

🔇 Additional comments (12)
web/src/components/Wrapper/mount-engine.ts (4)

14-14: LGTM!

The import of unapiScope utilities aligns with the portal and scope management infrastructure introduced in the PR.


58-80: LGTM!

The portal root setup is well-structured with proper SSR guards, appropriate styling, and correct integration with the unapi scope system.


166-192: LGTM!

The container scope logic properly integrates with the decorateContainer flag from the component registry and correctly applies unapi scope to parent containers. The observer setup ensures dynamic elements are handled appropriately.


156-157: LGTM!

The portal target initialization and integration with the UApp wrapper is correctly implemented. The scope application on line 264 after mounting ensures the element receives the unapi class appropriately.

Also applies to: 242-244, 264-264

web/__test__/components/Wrapper/mount-engine.test.ts (3)

8-8: LGTM!

The UApp mock enhancement properly tracks the portal prop and exposes it via the data-portal attribute, enabling test verification of portal configuration.

Also applies to: 14-22


232-249: LGTM!

The test properly verifies portal root creation, unapi scope application, and portal target configuration. Clear assertions and good coverage of the portal functionality.


251-270: LGTM!

The test correctly verifies that both the container and child element receive the unapi class when decorateContainer is true. This provides good coverage for the container decoration feature.

web/src/assets/main.css (1)

12-12: Nuxt UI import at line 12 does not require explicit layer specification.

The custom @layer declarations are organizational during the build phase only—they're removed entirely by the scopeTailwindToUnapi PostCSS plugin, which extracts and scopes all CSS rules to .unapi before outputting final CSS. Nuxt UI styles integrate correctly through the Vite + Tailwind v4 + PostCSS pipeline without explicit layer() syntax.

Likely an incorrect or invalid review comment.

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

10-11: Using shared getDockerClient() in DockerEventService looks good

Swapping to the shared Docker client utility decouples this service from DockerService for client access while still using DockerService for cache/appInfo updates. This matches the new docker client abstraction and keeps the event stream setup unchanged.

Also applies to: 69-71

api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (1)

52-58: DockerEventService tests correctly mock the shared Docker client

Mocking getDockerClient to return a shared mockDockerClientInstance and wiring getEvents to a PassThrough stream matches the service’s new dependency on the docker-client util and keeps the existing event-processing tests valid and isolated.

Also applies to: 75-88

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

52-57: Resolver wiring for warnings/alerts and notifyIfUnique looks consistent

warningsAndAlerts and notificationsWarningsAndAlerts correctly expose the deduplicated WARNING/ALERT snapshot, and notifyIfUnique is typed nullable to match the “no-op on duplicate” behavior in the service. Permission guards on the new subscription align with existing notifications subscriptions.

Also applies to: 106-116, 186-193

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

82-142: Container transform + LAN port enrichment looks solid

Using DockerAutostartService for primary name/auto-start metadata and DockerPortService + getLanIp() to derive deduplicated ports and lanIpPorts keeps all container-network and autostart logic centralized. The mapping to ContainerPortType with a guarded default also handles missing/unknown port.Type safely.

if (element.matches(selector) && !processedMappings.has(mapping)) {
componentsToMount.push({ mapping, element: element as HTMLElement });
const targetElement = element as HTMLElement;
// ensureContainerScope(targetElement, mapping);
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

Remove commented-out code or add explanatory comments.

The commented-out calls to ensureContainerScope and ensureUnapiScope reduce code clarity. If these are intentionally disabled because scope is applied elsewhere (lines 176-192, 264), consider adding brief inline comments explaining why. Otherwise, remove them.

Example for line 212:

           const targetElement = element as HTMLElement;
-          // ensureContainerScope(targetElement, mapping);
+          // Container scope already applied in lines 176-192
           componentsToMount.push({ mapping, element: targetElement });

Also applies to: 257-258

🤖 Prompt for AI Agents
In web/src/components/Wrapper/mount-engine.ts around line 212 (and also lines
257-258), there are commented-out calls to ensureContainerScope/ensureUnapiScope
which reduce clarity; either delete these dead lines or replace them with a
brief inline comment explaining why the calls are intentionally disabled (for
example, "scope is applied earlier at lines 176-192 and 264, so calling here
would be redundant") and reference the exact lines where scope is applied, so
future readers understand the rationale.

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: 3

♻️ Duplicate comments (4)
web/src/components/Docker/DockerContainersTable.vue (1)

503-515: Replace window.prompt with a modal dialog.

Using window.prompt() blocks the UI thread, has poor accessibility, and inconsistent styling across browsers. Consider using a dedicated "Create Folder" modal similar to the other modals already in use (MoveToFolderModal, RemoveContainerModal).

web/src/composables/useDockerTableColumns.ts (3)

100-118: Normalize container state before color mapping.

row.original.state may be lowercase (e.g., "running", "paused", "exited"), but the colorMap is keyed by ContainerState enum values which are uppercase. This causes silent fallback to 'neutral' for valid states.


176-230: Add rel="noopener noreferrer" to external link anchors.

The links column opens URLs in new tabs using target: '_blank' without the rel attribute. This can expose window.opener and is a security best practice gap.


237-251: Remove fallback to organizer entry ID for Tailscale containerId.

Line 247 uses row.original.containerId || row.original.id as fallback. Since row.id is the organizer entry ID (not the Docker container ID), passing it to DockerTailscaleIndicator would result in incorrect queries when containerId is missing.

🧹 Nitpick comments (3)
web/src/components/Common/MoveToFolderModal.vue (2)

98-107: Add accessible labels for expand/collapse state.

The chevron button lacks an accessible label indicating whether the folder is expanded or collapsed, which impacts screen reader users.

Add an aria-label that reflects the current state:

             <UButton
               v-if="row.hasChildren"
               color="neutral"
               size="xs"
               variant="ghost"
               icon="i-lucide-chevron-right"
               :class="expandedFolders.has(row.id) ? 'rotate-90' : ''"
+              :aria-label="expandedFolders.has(row.id) ? 'Collapse folder' : 'Expand folder'"
               square
               @click="$emit('toggle-expand', row.id)"
             />

110-116: Consider adding accessible labels for radio inputs.

The radio inputs are visually associated with folder names but lack explicit programmatic labels, which may impact screen reader users.

Consider wrapping the radio and folder name in a label element, or add aria-label:

             <input
               type="radio"
               :value="row.id"
               :checked="selectedFolderId === row.id"
               class="accent-primary"
+              :aria-label="`Select ${row.name}`"
               @change="$emit('update:selectedFolderId', row.id)"
             />
web/src/composables/useDockerContainerStats.ts (1)

15-20: Avoid type assertion; rely on generated GraphQL types.

The as DockerContainerStats | undefined cast can be avoided if the subscription type is properly inferred from the generated GraphQL types. As per coding guidelines, prefer proper typing from the start.

-  onStatsResult((result) => {
-    const stat = result.data?.dockerContainerStats as DockerContainerStats | undefined;
-    if (stat && stat.id) {
-      containerStats.set(stat.id, stat);
-    }
-  });
+  onStatsResult((result) => {
+    const stat = result.data?.dockerContainerStats;
+    if (stat?.id) {
+      containerStats.set(stat.id, stat);
+    }
+  });

Verify that pnpm codegen has been run and the subscription return type is correctly inferred. As per coding guidelines, always run pnpm codegen for GraphQL code generation in the web directory.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a67576 and 4ed5ba1.

📒 Files selected for processing (9)
  • web/src/components/Common/ConfirmActionsModal.vue (1 hunks)
  • web/src/components/Common/MoveToFolderModal.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/RemoveContainerModal.vue (1 hunks)
  • web/src/components/RClone/RCloneOverview.vue (0 hunks)
  • web/src/composables/useDockerBulkActions.ts (1 hunks)
  • web/src/composables/useDockerContainerStats.ts (1 hunks)
  • web/src/composables/useDockerRowActions.ts (1 hunks)
  • web/src/composables/useDockerTableColumns.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • web/src/components/RClone/RCloneOverview.vue
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/src/composables/useDockerContainerStats.ts
  • web/src/composables/useDockerBulkActions.ts
  • web/src/composables/useDockerTableColumns.ts
  • web/src/composables/useDockerRowActions.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/composables/useDockerContainerStats.ts
  • web/src/composables/useDockerBulkActions.ts
  • web/src/components/Common/ConfirmActionsModal.vue
  • web/src/components/Common/MoveToFolderModal.vue
  • web/src/composables/useDockerTableColumns.ts
  • web/src/composables/useDockerRowActions.ts
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
web/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Files:

  • web/src/composables/useDockerContainerStats.ts
  • web/src/composables/useDockerBulkActions.ts
  • web/src/composables/useDockerTableColumns.ts
  • web/src/composables/useDockerRowActions.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/src/composables/useDockerContainerStats.ts
  • web/src/composables/useDockerBulkActions.ts
  • web/src/composables/useDockerTableColumns.ts
  • web/src/composables/useDockerRowActions.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/src/composables/useDockerContainerStats.ts
  • web/src/composables/useDockerBulkActions.ts
  • web/src/components/Common/ConfirmActionsModal.vue
  • web/src/components/Common/MoveToFolderModal.vue
  • web/src/composables/useDockerTableColumns.ts
  • web/src/composables/useDockerRowActions.ts
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
🧠 Learnings (7)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 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/composables/useDockerContainerStats.ts
📚 Learning: 2025-04-25T13:29:07.868Z
Learnt from: elibosley
Repo: unraid/api PR: 1369
File: web/components/Activation/WelcomeModal.ce.vue:40-40
Timestamp: 2025-04-25T13:29:07.868Z
Learning: The `#confirmPassword` selector in the WelcomeModal.ce.vue component is intentionally looking for an element that exists in the Unraid OS login page (.login.php), not within the component itself. This is used as part of a font-size adjustment workaround to handle different CSS between login and authenticated pages.

Applied to files:

  • web/src/components/Common/ConfirmActionsModal.vue
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/composables/useDockerTableColumns.ts
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
📚 Learning: 2025-01-31T14:42:06.752Z
Learnt from: pujitm
Repo: unraid/api PR: 1075
File: web/components/Notifications/Sidebar.vue:41-58
Timestamp: 2025-01-31T14:42:06.752Z
Learning: When handling errors in code that uses toast notifications, avoid using toast in the error handler itself since toast functionality might be the source of the error. Use console.error or other reliable logging mechanisms instead.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-02-21T18:59:47.977Z
Learnt from: mdatelle
Repo: unraid/api PR: 1183
File: web/components/Modal.vue:120-120
Timestamp: 2025-02-21T18:59:47.977Z
Learning: In the Modal.vue component, the modal's width is controlled through the `maxWidth` prop (defaults to 'sm:max-w-lg'). The parent containers with `w-screen` and `w-full` classes ensure proper width behavior, making additional width classes on the inner modal content div redundant.

Applied to files:

  • web/src/components/Docker/RemoveContainerModal.vue
🧬 Code graph analysis (4)
web/src/composables/useDockerContainerStats.ts (1)
web/src/components/Docker/docker-stats.subscription.ts (1)
  • DOCKER_STATS_SUBSCRIPTION (3-14)
web/src/composables/useDockerBulkActions.ts (2)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/composables/useDockerRowActions.ts (2)
  • DropdownMenuItems (17-17)
  • ActionDropdownItem (8-15)
web/src/composables/useDockerTableColumns.ts (3)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/utils/docker.ts (1)
  • normalizeMultiValue (133-146)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (4)
  • projectUrl (61-67)
  • registryUrl (75-81)
  • supportUrl (89-95)
  • tailscaleEnabled (215-228)
web/src/composables/useDockerRowActions.ts (2)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/utils/docker.ts (3)
  • stripLeadingSlash (6-9)
  • getWebUiUrl (49-52)
  • openLanIpInNewTab (54-62)
🔇 Additional comments (28)
web/src/composables/useDockerBulkActions.ts (4)

1-6: LGTM! Clean and correct imports.

The imports are properly structured: runtime import from Vue and type-only imports from internal composables. Type-only imports don't require .js extensions since they're erased at compile time.


8-21: LGTM! Well-defined interface contract.

The options interface is properly typed with explicit Ref types for reactive values and clear callback signatures. This provides good type safety and developer experience.


23-93: LGTM! Solid composable implementation with proper reactivity.

The implementation correctly uses Vue's reactivity system with computed for derived state. The separation between global actions (affecting all containers) and selection-based actions is clear and logical. The distinction between hasSelectedEntries and hasSelectedContainers properly handles cases where folders might be selected alongside containers.


95-100: LGTM! Clean and complete API surface.

The return object exposes all necessary reactive helpers for consumers to build bulk action UIs with proper state management.

web/src/components/Common/ConfirmActionsModal.vue (2)

1-30: LGTM: Script setup is well-structured.

The TypeScript interfaces, props, emits, and handlers are correctly implemented. The code follows Vue 3 composition API best practices with proper type safety.


32-56: Well-implemented modal template.

The template correctly implements the modal UI with proper event handling, conditional rendering for non-empty groups, and accessible markup using semantic HTML elements (ul/li). The pass-through emit on line 37 is the correct pattern for allowing parent components to manage modal state via v-model.

web/src/components/Docker/RemoveContainerModal.vue (2)

1-37: LGTM!

The script setup is well-structured with proper TypeScript typing for props and emits. The watch pattern for resetting removeWithImage when the modal opens is appropriate.


39-64: LGTM!

The template structure is clean with proper v-model binding and event handling. The modal follows accessibility patterns with clear action buttons.

web/src/composables/useDockerContainerStats.ts (1)

8-34: LGTM!

The composable structure is clean. Per retrieved learnings, Vue Apollo handles subscription cleanup automatically when the component unmounts, so no manual cleanup is needed here.

web/src/components/Docker/DockerContainersTable.vue (8)

1-45: LGTM!

Imports are well-organized with proper TypeScript type imports separated from value imports.


56-98: LGTM!

Props and emits are properly typed with clear interfaces for the component's API surface.


119-125: LGTM!

Composable usage is well-structured, and the Apollo client integration is properly set up.


329-352: LGTM!

The container removal flow is well-implemented with proper busy state management and error handling.


517-541: LGTM!

The drag-and-drop logic with insertion index computation and folder operations is well-structured.


626-705: LGTM!

The template structure is clean with proper component composition and event bindings for the tree table and toolbar.


769-777: LGTM!

The RemoveContainerModal integration properly passes the required props and handles the confirm event with the withImage parameter.


161-167: Verify window.toast global type declaration.

The showToast and showError helpers rely on a global window.toast object. Ensure the global type declaration makes the options parameter optional to match this usage pattern.

#!/bin/bash
# Search for window.toast type declarations to verify options is optional
rg -n "interface Window" -A 10 -g "*.ts" -g "*.d.ts" | head -50
web/src/composables/useDockerTableColumns.ts (5)

1-14: LGTM!

Imports are well-organized with proper separation of type imports.


35-50: LGTM!

The makeMultiValueCell helper function is a clean abstraction for rendering multi-value cells.


284-313: LGTM!

The actions column is well-implemented with proper dropdown menu rendering and accessibility attributes.


320-354: LGTM!

The getDefaultColumnVisibility function provides sensible defaults for both compact and full views.


356-370: LGTM!

The DOCKER_SEARCHABLE_KEYS constant provides a comprehensive list of searchable fields.

web/src/composables/useDockerRowActions.ts (6)

8-18: LGTM!

The type definitions for ActionDropdownItem and DropdownMenuItems are clean and well-typed.


19-39: LGTM!

The DockerRowActionsOptions interface provides a comprehensive and well-typed API for configuring row actions.


64-87: LGTM!

The getContainerNameFromRow and getReorderActions helpers are clean with proper null handling.


89-122: LGTM!

The folder action items are well-organized with logical grouping of actions.


124-229: LGTM!

The container action items logic is comprehensive with proper conditional rendering based on container state, update availability, and Tailscale status. The action grouping follows a logical UX pattern.


231-242: LGTM!

The dispatcher pattern in getRowActionItems cleanly separates folder and container actions.

<div v-if="group.items.length" class="space-y-1">
<div class="text-sm font-medium">{{ group.label }}</div>
<ul class="list-disc pl-5 text-sm text-gray-600 dark:text-gray-300">
<li v-for="item in group.items" :key="item.name" class="truncate">{{ item.name }}</li>
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

Use a unique identifier for the item key.

Using item.name as the :key can cause Vue warnings and rendering issues if duplicate names exist within the same group. Consider adding a unique id property to the items interface or using the index as a fallback.

Apply this diff to add an optional id and use it with a fallback:

 export interface ConfirmActionGroup {
   label: string;
-  items: { name: string }[];
+  items: { name: string; id?: string }[];
 }

Then update the template:

-              <li v-for="item in group.items" :key="item.name" class="truncate">{{ item.name }}</li>
+              <li v-for="(item, index) in group.items" :key="item.id ?? `${group.label}-${index}`" class="truncate">{{ item.name }}</li>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In web/src/components/Common/ConfirmActionsModal.vue around line 45, the
template uses item.name as the v-for key which can collide for duplicate names;
update the items data shape to include an optional unique id (e.g., id?: string)
and when rendering use that id with a stable fallback (such as a composite key
using the group index and item index or the raw index) so keys are unique and
stable across renders; adjust any TypeScript interfaces or props to accept the
optional id and update where items are created so they populate an id when
available.

Comment on lines +124 to +132
<input
:value="renameValue"
class="border-default bg-default flex-1 rounded border px-2 py-1"
@input="$emit('update:renameValue', ($event.target as HTMLInputElement).value)"
@keydown.enter.prevent="$emit('commit-rename', row.id)"
@keydown.esc.prevent="$emit('cancel-rename')"
@blur="$emit('commit-rename', row.id)"
autofocus
/>
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

Add validation before committing rename.

The rename input emits commit-rename on blur without validating that renameValue is non-empty. If a user clears the input and clicks away, this could result in a folder with an empty or whitespace-only name.

Consider adding validation similar to handleCreateFolder:

-                  @blur="$emit('commit-rename', row.id)"
+                  @blur="renameValue.trim() && $emit('commit-rename', row.id)"

Alternatively, emit cancel-rename when the value is empty:

-                  @blur="$emit('commit-rename', row.id)"
+                  @blur="renameValue.trim() ? $emit('commit-rename', row.id) : $emit('cancel-rename')"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
:value="renameValue"
class="border-default bg-default flex-1 rounded border px-2 py-1"
@input="$emit('update:renameValue', ($event.target as HTMLInputElement).value)"
@keydown.enter.prevent="$emit('commit-rename', row.id)"
@keydown.esc.prevent="$emit('cancel-rename')"
@blur="$emit('commit-rename', row.id)"
autofocus
/>
<input
:value="renameValue"
class="border-default bg-default flex-1 rounded border px-2 py-1"
@input="$emit('update:renameValue', ($event.target as HTMLInputElement).value)"
@keydown.enter.prevent="$emit('commit-rename', row.id)"
@keydown.esc.prevent="$emit('cancel-rename')"
@blur="renameValue.trim() ? $emit('commit-rename', row.id) : $emit('cancel-rename')"
autofocus
/>
🤖 Prompt for AI Agents
In web/src/components/Common/MoveToFolderModal.vue around lines 124-132, the
rename <input> currently emits commit-rename on blur without validating the
value; change the blur (and enter) handler to trim and validate renameValue
before emitting commit-rename and instead emit cancel-rename (or no event) when
the trimmed value is empty. Concretely: on @blur and @keydown.enter, compute
const v = (renameValue ?? '').trim() and if v.length > 0 emit commit-rename with
row.id (or emit with the trimmed value if needed); otherwise emit cancel-rename
(or focus back / show validation) so empty or whitespace-only names are not
committed.

Comment on lines +264 to +282
async function handleVisitTailscale(containerId: string) {
try {
const { data } = await apolloClient.query<GetContainerTailscaleStatusQuery>({
query: GET_CONTAINER_TAILSCALE_STATUS,
variables: { id: containerId },
fetchPolicy: 'network-only',
});
const webUiUrl = data?.docker?.container?.tailscaleStatus?.webUiUrl;
if (webUiUrl) {
window.open(webUiUrl, '_blank');
} else {
showError('Tailscale WebUI not available', {
description: 'The container may need to authenticate with Tailscale first.',
});
}
} catch {
showError('Failed to fetch Tailscale status');
}
}
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

Add rel="noopener noreferrer" to the window.open call.

When opening external URLs in new tabs, use window.open(url, '_blank', 'noopener,noreferrer') to prevent window.opener leaks.

     if (webUiUrl) {
-      window.open(webUiUrl, '_blank');
+      window.open(webUiUrl, '_blank', 'noopener,noreferrer');
     } else {
🤖 Prompt for AI Agents
In web/src/components/Docker/DockerContainersTable.vue around lines 264 to 282,
the call to window.open(webUiUrl, '_blank') can leak window.opener; update the
call to include the features string that disables opener and referrer (e.g.,
window.open(webUiUrl, '_blank', 'noopener,noreferrer')), or alternatively open
via a safe anchor element with rel="noopener noreferrer"; ensure the call uses
one of these patterns so new tabs cannot access the opener.

@pujitm
Copy link
Member Author

pujitm commented Dec 17, 2025

good to merge from my pov @elibosley

@pujitm
Copy link
Member Author

pujitm commented Dec 17, 2025

  • wait going to update a few components to use semantic styles instead of dark: variants which should make it easier to read in the future

all done now!

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 (4)
web/src/components/Common/BaseTreeTable.vue (4)

4-14: Add .js extensions to imports for ESM compatibility.

All composable and utility imports are missing the required .js extension per coding guidelines.

Apply this pattern to all imports:

-import { useDragDrop } from '@/composables/useDragDrop';
+import { useDragDrop } from '@/composables/useDragDrop.js';
-import { useDropProjection } from '@/composables/useDropProjection';
+import { useDropProjection } from '@/composables/useDropProjection.js';
-import { getSelectableDescendants, useRowSelection } from '@/composables/useRowSelection';
+import { getSelectableDescendants, useRowSelection } from '@/composables/useRowSelection.js';
-import { useTreeExpansion } from '@/composables/useTreeExpansion';
+import { useTreeExpansion } from '@/composables/useTreeExpansion.js';
-import { useTreeFilter } from '@/composables/useTreeFilter';
+import { useTreeFilter } from '@/composables/useTreeFilter.js';
-import {
-  createDropIndicator,
-  createSelectColumnCell,
-  wrapCellWithRow,
-  wrapHeaderContent,
-} from '@/utils/tableRenderers';
+import {
+  createDropIndicator,
+  createSelectColumnCell,
+  wrapCellWithRow,
+  wrapHeaderContent,
+} from '@/utils/tableRenderers.js';

Based on coding guidelines, TypeScript imports require .js extensions.


241-257: Column visibility not restored when exiting compact mode.

When compact becomes true, the watcher hides all columns except select and name. However, when compact returns to false, the hidden columns are not restored, leaving them permanently hidden.

Apply this diff to restore columns when exiting compact mode:

 watch(
   () => props.compact,
   (isCompact) => {
     if (isCompact) {
       const hideColumns: Record<string, boolean> = {};
       props.columns.forEach((col) => {
         const key = ((col as { id?: string; accessorKey?: string }).id ||
           (col as { id?: string; accessorKey?: string }).accessorKey) as string;
         if (key && key !== 'select' && key !== 'name') {
           hideColumns[key] = false;
         }
       });
       columnVisibility.value = hideColumns;
+    } else {
+      columnVisibility.value = {};
     }
   },
   { immediate: true }
 );

420-458: Remove debug console.log statements before merging.

The drag handle contains extensive logging that should be removed or gated behind a debug flag for production.

Apply this diff to remove debug logging:

           onMousedown: (e: MouseEvent) => {
-            console.log('[DragHandle] mousedown', {
-              rowId: enhancedRow.original.id,
-              rowName: enhancedRow.original.name,
-              isBusy,
-              isDraggingThis,
-              draggable: !isBusy && !isDraggingThis,
-              target: e.target,
-              currentTarget: e.currentTarget,
-            });
+            // mousedown handler
           },
           onDragstart: (e: DragEvent) => {
-            console.log('[DragHandle] dragstart fired', {
-              rowId: enhancedRow.original.id,
-              rowName: enhancedRow.original.name,
-              isBusy,
-              dataTransfer: e.dataTransfer,
-            });
             if (isBusy) {
-              console.log('[DragHandle] dragstart prevented - row is busy');
               e.preventDefault();
               return;
             }
             handleDragStart(e, enhancedRow.original);
-            console.log('[DragHandle] handleDragStart called, draggingIds:', draggingIds.value);
           },
           onDragend: (e: DragEvent) => {
-            console.log('[DragHandle] dragend fired', {
-              rowId: enhancedRow.original.id,
-              dropEffect: e.dataTransfer?.dropEffect,
-            });
             handleDragEnd();
           },
           onDrag: (e: DragEvent) => {
-            // Log occasionally during drag (throttled by checking if clientX changed significantly)
-            if (e.clientX % 50 < 5) {
-              console.log('[DragHandle] dragging...', { x: e.clientX, y: e.clientY });
-            }
+            // drag handler
           },

576-577: Avoid any type per coding guidelines.

The getRowId and getRowCanSelect props use any type which violates coding guidelines. Use proper typing.

-      :get-row-id="(row: any) => row.id"
-      :get-row-can-select="(row: any) => canSelectRow(row.original)"
+      :get-row-id="(row: FlatRow<T>) => row.id"
+      :get-row-can-select="(row: { original: FlatRow<T> }) => canSelectRow(row.original)"

Based on coding guidelines, never use the any type.

🧹 Nitpick comments (1)
web/src/components/Common/BaseTreeTable.vue (1)

485-485: Remove useless nullish coalescing operator.

wrapColumnHeaderRenderer always returns a value (lines 302-317), never undefined, making the ?? originalHeader fallback dead code.

Apply this diff:

-      const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
+      const header = wrapColumnHeaderRenderer(originalHeader);
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ed5ba1 and 9f65101.

📒 Files selected for processing (2)
  • web/components.d.ts (6 hunks)
  • web/src/components/Common/BaseTreeTable.vue (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/components.d.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/components.d.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/components.d.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/components.d.ts
🧠 Learnings (26)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Always use TypeScript imports with `.js` extensions for ESM compatibility

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/components.d.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Avoid using casting whenever possible, prefer proper typing from the start

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Never use the `any` type. Always prefer proper typing

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-09-09T14:34:07.189Z
Learnt from: pujitm
Repo: unraid/api PR: 1582
File: packages/unraid-shared/src/util/processing.ts:69-71
Timestamp: 2025-09-09T14:34:07.189Z
Learning: In TypeScript code, pujitm considers it acceptable to use `any` type for private/internal fields when the type is not exposed through the public API and is masked by properly typed method declarations. The focus is on ensuring the public interface maintains type safety rather than enforcing strict typing on all internal implementation details.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-03-07T19:29:12.540Z
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.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
Repo: unraid/api PR: 1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/components.d.ts
📚 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/components.d.ts
📚 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/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Find Vue components with `findComponent(ComponentName)`

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-06T17:34:16.133Z
Learnt from: pujitm
Repo: unraid/api PR: 974
File: web/components/Loading/Error.vue:50-50
Timestamp: 2024-12-06T17:34:16.133Z
Learning: In this project, the `Button` component from `~/components/shadcn/Button.vue` is autoloaded and does not need to be imported manually in components like `web/components/Loading/Error.vue`.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/components.d.ts
📚 Learning: 2024-12-06T17:38:40.999Z
Learnt from: elibosley
Repo: unraid/api PR: 974
File: web/components/Loading/Error.vue:1-3
Timestamp: 2024-12-06T17:38:40.999Z
Learning: In Nuxt.js projects, components are automatically imported, so explicit import statements for components like `LoadingSpinner` in `web/components/Loading/Error.vue` are not necessary.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-04-25T13:29:07.868Z
Learnt from: elibosley
Repo: unraid/api PR: 1369
File: web/components/Activation/WelcomeModal.ce.vue:40-40
Timestamp: 2025-04-25T13:29:07.868Z
Learning: The `#confirmPassword` selector in the WelcomeModal.ce.vue component is intentionally looking for an element that exists in the Unraid OS login page (.login.php), not within the component itself. This is used as part of a font-size adjustment workaround to handle different CSS between login and authenticated pages.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/components.d.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `findAll` to check for multiple elements in Vue components

Applied to files:

  • web/components.d.ts
📚 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/components.d.ts
🪛 GitHub Check: CodeQL
web/src/components/Common/BaseTreeTable.vue

[warning] 485-485: Useless conditional
This call to wrapColumnHeaderRenderer always evaluates to true.

🔇 Additional comments (1)
web/components.d.ts (1)

1-161: LGTM!

This auto-generated TypeScript declaration file correctly reflects the new Docker-related components and supporting UI components being added in this PR. The declarations provide proper type safety for Nuxt's auto-imported components.

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: 4

♻️ Duplicate comments (12)
web/src/components/Docker/DockerOrphanedAlert.vue (1)

27-39: Code duplication still present despite previous fix.

Line 28 duplicates the container name formatting logic from formatContainerName (line 42). The only difference is the fallback value ('container' vs 'Unknown'). Consider using the helper function for consistency, or if the different fallbacks are intentional, extract both to use a shared utility with a fallback parameter.

Apply this diff to use the existing helper:

 async function handleRemove(container: DockerContainer) {
-  const name = stripLeadingSlash(container.names[0]) || 'container';
+  const name = formatContainerName(container);
   if (!confirm(`Are you sure you want to remove orphaned container "${name}"?`)) return;

Note: This changes the fallback from 'container' to 'Unknown'. If 'container' is specifically required here, consider adding a parameter to formatContainerName to accept a custom fallback value.

web/src/components/Docker/DockerConsoleViewer.vue (2)

68-84: Add error handling to prevent UI state inconsistency on failure.

If the fetch call fails, isPoppedOut remains true, causing the UI to incorrectly show "Console opened in separate window" when it didn't.

This was flagged in a previous review. The suggested fix wraps the function in try/catch:

 async function openFullscreen() {
+  try {
     isPoppedOut.value = true;
     markPoppedOut(props.containerName);
 
     await new Promise((resolve) => setTimeout(resolve, 100));
 
     const params = new URLSearchParams({
       tag: 'docker',
       name: props.containerName,
       more: props.shell,
     });
 
     await fetch(`/webGui/include/OpenTerminal.php?${params.toString()}`);
     await new Promise((resolve) => setTimeout(resolve, 300));
 
     window.open(socketPath.value, '_blank', 'width=1200,height=800');
+  } catch {
+    hasError.value = true;
+    isPoppedOut.value = false;
+  }
 }

136-136: Template references undefined containerName and shell.

In Vue 3 script setup, props returned from defineProps are not automatically available as top-level template bindings. The template uses {{ containerName }} and {{ shell }} directly, but only props is defined in the script. This will cause a runtime error.

Apply this fix:

-      <span class="text-muted-foreground text-sm"> {{ containerName }}: /bin/{{ shell }} </span>
+      <span class="text-muted-foreground text-sm"> {{ props.containerName }}: /bin/{{ props.shell }} </span>

Alternatively, destructure props at the script level:

const { containerName, shell } = toRefs(props);

Note: A past review comment flagged this issue as addressed, but it appears the fix may not have been applied.

web/src/components/Docker/ContainerOverviewCard.vue (1)

40-44: stateLabel produces inconsistent casing with uppercase input.

The state values are uppercase ('RUNNING', 'PAUSED', 'EXITED'), but this function only capitalizes the first character without lowercasing the rest. "RUNNING" becomes "RUNNING" instead of "Running".

Apply this fix:

 const stateLabel = computed(() => {
   const state = props.container?.state;
   if (!state) return 'Unknown';
-  return state.charAt(0).toUpperCase() + state.slice(1);
+  const lower = state.toLowerCase();
+  return lower.charAt(0).toUpperCase() + lower.slice(1);
 });
web/src/components/Docker/DockerContainerManagement.vue (1)

431-440: Align button disabled state with handler guard.

The "Customize Start Order" button only disables on loading, but openAutostartSettings guards on props.disabled. When disabled && !loading, the button appears clickable but does nothing.

             <UButton
               size="xs"
               color="neutral"
               variant="outline"
               icon="i-lucide-list-checks"
-              :disabled="loading"
+              :disabled="loading || props.disabled"
               @click="openAutostartSettings"
             >
web/src/components/Docker/DockerAutostartSettings.vue (2)

234-236: Remove redundant 'inside' area check.

Since :can-drop-inside="() => false" on line 404, area === 'inside' will never be true. Simplify the condition.

   let insertionIndex = targetIndex;
-  if (area === 'after' || area === 'inside') {
+  if (area === 'after') {
     insertionIndex += 1;
   }

402-402: Replace any with proper type for better type safety.

The can-select callback parameter should be typed as TreeRow<AutostartEntry> instead of any.

-      :can-select="(row: any) => row.type === 'container'"
+      :can-select="(row: TreeRow<AutostartEntry>) => row.type === 'container'"
web/src/components/Docker/DockerContainersTable.vue (2)

272-273: Add rel="noopener noreferrer" to the window.open call.

When opening external URLs in new tabs, use window.open(url, '_blank', 'noopener,noreferrer') to prevent window.opener leaks.

     if (webUiUrl) {
-      window.open(webUiUrl, '_blank');
+      window.open(webUiUrl, '_blank', 'noopener,noreferrer');
     }

503-514: Replace window.prompt with a modal dialog.

Using window.prompt() blocks the UI thread, has poor accessibility, and inconsistent styling. Replace with a proper modal for better UX.

web/src/components/Common/BaseTreeTable.vue (3)

241-257: Column visibility not restored when exiting compact mode.

When compact becomes true, the watcher hides columns. When compact returns to false, columns remain hidden.

 watch(
   () => props.compact,
   (isCompact) => {
     if (isCompact) {
       const hideColumns: Record<string, boolean> = {};
       props.columns.forEach((col) => {
         const key = ((col as { id?: string; accessorKey?: string }).id ||
           (col as { id?: string; accessorKey?: string }).accessorKey) as string;
         if (key && key !== 'select' && key !== 'name') {
           hideColumns[key] = false;
         }
       });
       columnVisibility.value = hideColumns;
+    } else {
+      columnVisibility.value = {};
     }
   },
   { immediate: true }
 );

420-458: Remove debug console.log statements before merging.

The drag handle contains extensive logging that should be removed or gated behind a debug flag for production.


575-576: Avoid any type per coding guidelines.

The getRowId and getRowCanSelect props use any type which violates coding guidelines.

-      :get-row-id="(row: any) => row.id"
-      :get-row-can-select="(row: any) => canSelectRow(row.original)"
+      :get-row-id="(row: FlatRow<T>) => row.id"
+      :get-row-can-select="(row: { original: FlatRow<T> }) => canSelectRow(row.original)"
🧹 Nitpick comments (11)
web/src/components/Docker/Preview.vue (1)

37-52: Semantic utility migration looks good; consider updating line 39 for consistency.

The migration from dark mode variants to semantic utilities (border-border, bg-muted, text-muted-foreground) improves theme consistency and aligns with your stated goal of improving readability.

Minor inconsistency: Line 39 still uses the hardcoded text-gray-500 class instead of a semantic utility. For full consistency with the semantic styling approach, consider updating it:

-        <UIcon name="i-lucide-lock" class="h-4 w-4 text-gray-500" />
+        <UIcon name="i-lucide-lock" class="h-4 w-4 text-muted-foreground" />
web/src/components/Docker/DockerOrphanedAlert.vue (1)

46-87: LGTM! Clean, accessible alert UI.

The template follows semantic styling patterns, includes proper accessibility attributes (button types, aria-hidden on icons, title tooltips), and provides clear visual hierarchy for the orphaned container warning.

Optional enhancement: Consider tracking which specific container is being removed to provide per-button loading indicators instead of disabling all buttons globally. This would allow better visual feedback and prevent confusion if a removal takes longer than expected. However, the current approach (global disabled state) is acceptable and prevents potential race conditions.

web/src/components/Docker/DockerLogViewerModal.vue (1)

24-28: Consider using direct imports instead of resolveComponent.

The resolveComponent pattern here is unconventional for Nuxt/Vue applications where components are typically auto-imported or directly imported. This adds runtime overhead and loses type safety. If these are Nuxt UI components, they should be auto-imported.

If you need explicit imports for clarity, consider:

-const UModal = resolveComponent('UModal');
-const UFormField = resolveComponent('UFormField');
-const USelectMenu = resolveComponent('USelectMenu');
-const USwitch = resolveComponent('USwitch');
-const UButton = resolveComponent('UButton');
+// These should be auto-imported in Nuxt, no explicit declaration needed

Otherwise, if there's a specific reason for dynamic resolution (e.g., avoiding circular dependencies), a comment explaining this choice would be helpful.

web/src/components/Docker/ContainerOverviewCard.vue (1)

347-365: Minor: Repeated string splitting in template.

mount.split(' → ') is called twice per iteration—once for source and once for destination. Consider computing this in JavaScript for cleaner templates.

If you want to optimize:

const parsedVolumeMounts = computed(() =>
  formatVolumes(props.container).map((mount) => {
    const [source, destination] = mount.split(' → ');
    return { source, destination };
  })
);

Then in template:

<div v-for="(mount, index) in parsedVolumeMounts" :key="index">
  <p>{{ mount.source }}</p>
  <p>{{ mount.destination }}</p>
</div>
web/src/components/Docker/DockerTailscaleIndicator.vue (1)

36-42: Consider deferring hasFetched until load completes.

Similar to the issue in ContainerOverviewCard.vue, hasFetched is set to true synchronously before load() completes. While the impact here is minor (the refresh button appears slightly early but loading skeletons show correctly), aligning with a consistent pattern would improve maintainability.

 function handlePopoverOpen(open: boolean) {
   popoverOpen.value = open;
   if (open && !hasFetched.value && isRunning.value) {
-    hasFetched.value = true;
-    load();
+    load()?.then(() => {
+      hasFetched.value = true;
+    });
   }
 }
web/src/components/Docker/ContainerSizesModal.vue (1)

38-62: Two-pass mapping is intentional but could be streamlined.

The current approach maps containers twice: once to create sortable objects with raw bytes, then again to format for display. While this separation of concerns is clear, it could be combined if performance becomes a concern with large container lists.

If you prefer a single pass:

const tableRows = computed(() => {
  return containers.value
    .map((container) => {
      const primaryName = stripLeadingSlash(container.names?.[0]) || 'Unknown';
      const totalBytes = container.sizeRootFs ?? 0;
      const writableBytes = container.sizeRw ?? 0;
      const logBytes = container.sizeLog ?? 0;

      return {
        id: container.id,
        name: primaryName,
        totalBytes, // Keep for sorting
        total: formatBytes(totalBytes),
        writable: formatBytes(writableBytes),
        log: formatBytes(logBytes),
      };
    })
    .sort((a, b) => b.totalBytes - a.totalBytes);
});

The current implementation is fine for typical container counts.

web/src/components/Docker/DockerContainerManagement.vue (2)

580-586: Consider adding sandbox attribute to iframe for defense in depth.

The iframe loads external legacy edit URLs. While the content is from the same origin, adding a sandbox attribute with appropriate permissions provides an extra layer of protection.

             <iframe
               v-if="legacyEditUrl"
               :key="legacyEditUrl"
               :src="legacyEditUrl"
               class="h-[70vh] w-full border-0"
               loading="lazy"
+              sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
             />

42-58: Consider logging or tracking router availability failures.

The tryUseRoute and tryUseRouter functions silently swallow exceptions. While the fallback behavior is correct, logging these failures in development could help diagnose integration issues.

web/src/components/Docker/DockerPortConflictsAlert.vue (1)

68-70: Key generation may produce duplicates for edge cases.

If conflict.lanIpPort or conflict.type are undefined/empty, the key could become lan-- or container-undefined-. Consider adding a fallback or including an index.

             <div
               v-for="(conflict, index) in lanConflicts"
-              :key="`lan-${conflict.lanIpPort}-${conflict.type}`"
+              :key="`lan-${index}-${conflict.lanIpPort || ''}-${conflict.type || 'TCP'}`"

Also applies to: 91-94

web/src/components/Docker/DockerContainersTable.vue (1)

159-167: Declare toast interface with optional parameters.

The global window.toast interface should have optional options parameter to match actual usage in showToast(message).

Add a type declaration at the top of the file or in a separate .d.ts:

declare global {
  interface Window {
    toast?: {
      success: (title: string, options?: { description?: string }) => void;
      error?: (title: string, options?: { description?: string }) => void;
    };
  }
}
web/src/components/Docker/RemoveContainerModal.vue (1)

53-56: Consider explicit checkbox labeling for accessibility.

While implicit labeling (wrapping the input in the label) is valid, explicit id and for attributes provide more robust accessibility support across all assistive technologies.

Apply this diff for explicit labeling:

-        <label class="flex items-center gap-2 text-sm">
-          <input v-model="removeWithImage" type="checkbox" class="accent-primary h-4 w-4 rounded" />
+        <label for="remove-image-checkbox" class="flex items-center gap-2 text-sm">
+          <input id="remove-image-checkbox" v-model="removeWithImage" type="checkbox" class="accent-primary h-4 w-4 rounded" />
           <span>Also remove image</span>
         </label>
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f65101 and 9f54e53.

📒 Files selected for processing (15)
  • web/src/components/Common/BaseTreeTable.vue (1 hunks)
  • web/src/components/Docker/ContainerOverviewCard.vue (1 hunks)
  • web/src/components/Docker/ContainerSizesModal.vue (1 hunks)
  • web/src/components/Docker/DockerAutostartSettings.vue (1 hunks)
  • web/src/components/Docker/DockerConsoleViewer.vue (1 hunks)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
  • web/src/components/Docker/DockerContainerOverview.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/DockerLogViewerModal.vue (1 hunks)
  • web/src/components/Docker/DockerOrphanedAlert.vue (1 hunks)
  • web/src/components/Docker/DockerPortConflictsAlert.vue (1 hunks)
  • web/src/components/Docker/DockerTailscaleIndicator.vue (1 hunks)
  • web/src/components/Docker/Overview.vue (1 hunks)
  • web/src/components/Docker/Preview.vue (1 hunks)
  • web/src/components/Docker/RemoveContainerModal.vue (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • web/src/components/Docker/Overview.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Docker/DockerContainerOverview.vue
🧰 Additional context used
📓 Path-based instructions (2)
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/Docker/DockerTailscaleIndicator.vue
  • web/src/components/Docker/DockerOrphanedAlert.vue
  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/ContainerOverviewCard.vue
  • web/src/components/Docker/Preview.vue
  • web/src/components/Docker/DockerPortConflictsAlert.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
  • web/src/components/Docker/ContainerSizesModal.vue
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/src/components/Docker/DockerTailscaleIndicator.vue
  • web/src/components/Docker/DockerOrphanedAlert.vue
  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/ContainerOverviewCard.vue
  • web/src/components/Docker/Preview.vue
  • web/src/components/Docker/DockerPortConflictsAlert.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
  • web/src/components/Docker/ContainerSizesModal.vue
🧠 Learnings (18)
📓 Common learnings
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.
📚 Learning: 2025-07-24T18:48:43.590Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Console.vue:22-29
Timestamp: 2025-07-24T18:48:43.590Z
Learning: The Docker Console component in web/components/Docker/Console.vue is temporary/dummy code with placeholder terminal responses, used for building out the UI layout while developing the real components in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerTailscaleIndicator.vue
  • web/src/components/Docker/DockerOrphanedAlert.vue
  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Docker/ContainerOverviewCard.vue
  • web/src/components/Docker/Preview.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
  • web/src/components/Docker/ContainerSizesModal.vue
📚 Learning: 2025-07-24T18:48:44.035Z
Learnt from: mdatelle
Repo: unraid/api PR: 1533
File: web/components/Docker/Edit.vue:16-32
Timestamp: 2025-07-24T18:48:44.035Z
Learning: In web/components/Docker/Edit.vue, the hardcoded configuration values in the config ref are intentional temporary/dummy data used during initial UI development phase while building out the real Docker components, as clarified by mdatelle in PR #1533.

Applied to files:

  • web/src/components/Docker/DockerTailscaleIndicator.vue
  • web/src/components/Docker/DockerOrphanedAlert.vue
  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Docker/ContainerOverviewCard.vue
  • web/src/components/Docker/Preview.vue
  • web/src/components/Docker/DockerPortConflictsAlert.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/RemoveContainerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
  • web/src/components/Docker/ContainerSizesModal.vue
📚 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/Docker/DockerOrphanedAlert.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Always use TypeScript imports with `.js` extensions for ESM compatibility

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Avoid using casting whenever possible, prefer proper typing from the start

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerConsoleViewer.vue
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components

Applied to files:

  • web/src/components/Docker/DockerContainerManagement.vue
  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerPortConflictsAlert.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to **/*.{ts,tsx} : Never use the `any` type. Always prefer proper typing

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerLogViewerModal.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
📚 Learning: 2025-09-09T14:34:07.189Z
Learnt from: pujitm
Repo: unraid/api PR: 1582
File: packages/unraid-shared/src/util/processing.ts:69-71
Timestamp: 2025-09-09T14:34:07.189Z
Learning: In TypeScript code, pujitm considers it acceptable to use `any` type for private/internal fields when the type is not exposed through the public API and is masked by properly typed method declarations. The focus is on ensuring the public interface maintains type safety rather than enforcing strict typing on all internal implementation details.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
📚 Learning: 2025-03-07T19:29:12.540Z
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.

Applied to files:

  • web/src/components/Common/BaseTreeTable.vue
  • web/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/DockerAutostartSettings.vue
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • web/src/components/Docker/Preview.vue
📚 Learning: 2025-01-31T14:42:06.752Z
Learnt from: pujitm
Repo: unraid/api PR: 1075
File: web/components/Notifications/Sidebar.vue:41-58
Timestamp: 2025-01-31T14:42:06.752Z
Learning: When handling errors in code that uses toast notifications, avoid using toast in the error handler itself since toast functionality might be the source of the error. Use console.error or other reliable logging mechanisms instead.

Applied to files:

  • web/src/components/Docker/DockerContainersTable.vue
📚 Learning: 2025-02-21T18:59:47.977Z
Learnt from: mdatelle
Repo: unraid/api PR: 1183
File: web/components/Modal.vue:120-120
Timestamp: 2025-02-21T18:59:47.977Z
Learning: In the Modal.vue component, the modal's width is controlled through the `maxWidth` prop (defaults to 'sm:max-w-lg'). The parent containers with `w-screen` and `w-full` classes ensure proper width behavior, making additional width classes on the inner modal content div redundant.

Applied to files:

  • web/src/components/Docker/RemoveContainerModal.vue
  • web/src/components/Docker/ContainerSizesModal.vue
📚 Learning: 2025-01-23T21:26:17.766Z
Learnt from: elibosley
Repo: unraid/api PR: 1063
File: api/src/unraid-api/cli/sso/validate-token.command.ts:76-81
Timestamp: 2025-01-23T21:26:17.766Z
Learning: In the unraid/api codebase, store dispatches using Redux Toolkit's `unwrap()` method already handle errors appropriately and don't require additional try-catch blocks.

Applied to files:

  • web/src/components/Docker/DockerConsoleViewer.vue
🪛 GitHub Check: CodeQL
web/src/components/Common/BaseTreeTable.vue

[warning] 484-484: Useless conditional
This call to wrapColumnHeaderRenderer always evaluates to true.

🔇 Additional comments (19)
web/src/components/Docker/DockerOrphanedAlert.vue (5)

1-7: LGTM!

The imports are well-organized, properly typed, and follow Vue 3 composition API best practices with Apollo integration.


9-15: LGTM!

Props are properly defined with TypeScript types and sensible defaults. The previous unused variable issue has been resolved.


17-17: LGTM!

Type-safe emit definition follows Vue 3 best practices.


19-25: LGTM!

The GraphQL mutation is correctly defined and the Apollo composable is properly configured to expose both the mutation function and loading state.


41-43: LGTM!

The utility function provides clean name formatting with an appropriate fallback.

web/src/components/Docker/DockerLogViewerModal.vue (1)

1-46: LGTM overall structure and logic.

The component properly implements v-model patterns for open and activeSessionId, uses defensive checks for optional values, and has clean conditional rendering logic.

web/src/components/Docker/DockerConsoleViewer.vue (1)

104-130: LGTM: Proper lifecycle management.

Event listeners and ResizeObserver are correctly set up on mount and cleaned up on unmount. The scroll listener's capture phase (true) is appropriate for catching scroll events from parent containers.

web/src/components/Docker/DockerTailscaleIndicator.vue (1)

69-93: LGTM: Popover trigger and accessibility.

The button includes appropriate title text for accessibility, data-stop-row-click to prevent event bubbling in table contexts, and clear visual indicators for the Tailscale state (Enabled/Offline badges).

web/src/components/Docker/ContainerSizesModal.vue (2)

27-34: LGTM: Query configuration.

Good use of fetchPolicy: 'network-only' to ensure fresh data on each modal open, and enabled: computed(() => isOpen.value) to defer fetching until the modal is visible.


108-131: LGTM: Robust byte formatting.

The formatBytes function handles edge cases well (null, undefined, NaN, zero) and uses Intl.NumberFormat for locale-aware number formatting with appropriate precision based on magnitude.

web/src/components/Docker/DockerContainerManagement.vue (1)

175-193: LGTM on the GraphQL query setup.

The query configuration with cache-and-network policy and skipCache variable is appropriate for ensuring fresh container data while still benefiting from cache on initial render.

web/src/components/Docker/DockerAutostartSettings.vue (2)

190-198: Direct entry mutation relies on ref reactivity.

The code mutates entry.autoStart and entry.wait directly. This works because entries come from entries.value array which is reactive. However, this pattern can be fragile if the entry reference changes during the async operation.

The snapshot/rollback pattern in persistConfiguration properly handles failures, and the mutation order (mutate then persist) provides optimistic UI updates.


46-61: LGTM on the sorting logic.

The sortContainers function correctly prioritizes enabled containers by their autoStartOrder, then falls back to alphabetical sorting for disabled containers.

web/src/components/Docker/DockerPortConflictsAlert.vue (1)

1-116: LGTM! Clean, well-structured component.

The component handles optional props gracefully with sensible defaults, provides clear formatting for conflict labels, and uses appropriate semantic HTML with accessibility considerations (title attributes, aria-hidden on icons).

web/src/components/Docker/DockerContainersTable.vue (2)

127-139: LGTM on the useTreeData integration.

The tree data composable is properly configured with flat entries, container data, and the row builder function. The derived maps (entryParentById, folderChildrenIds, etc.) enable efficient lookups for drag-drop operations.


479-494: Well-implemented insertion index calculation.

The computeInsertionIndex function correctly handles the edge cases: empty siblings, filtering out moving items, and adjusting for 'after' drop area. The bounds clamping ensures a valid index.

web/src/components/Common/BaseTreeTable.vue (2)

163-176: LGTM on drag-drop composable integration.

The separation of useDragDrop for drag state management and useDropProjection for visual feedback is clean. The handleDragEnd wrapper correctly clears both states.


515-533: Recursive parent folder discovery is correct.

The findParentFolderIds function correctly traverses the tree to build the path of parent folders, only including folder-type nodes in the path. This enables proper auto-expansion when activeId changes.

web/src/components/Docker/RemoveContainerModal.vue (1)

1-17: Well-structured component with proper TypeScript typing.

The component demonstrates good practices:

  • Clean TypeScript interface definitions for props and emits
  • Proper use of withDefaults for optional props
  • Separate event handlers for different close actions
  • Appropriate loading state management
  • Semantic button colors (error for destructive action)

Also applies to: 30-45, 59-62

Comment on lines +482 to +484
...props.columns.map((col, colIndex) => {
const originalHeader = col.header as ColumnHeaderRenderer | undefined;
const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
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

Simplify redundant conditional check.

The static analysis tool correctly identifies that wrapColumnHeaderRenderer(originalHeader) always returns a truthy value (either a function or a new renderer), so the ?? originalHeader fallback is never reached.

       const originalHeader = col.header as ColumnHeaderRenderer | undefined;
-      const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
+      const header = wrapColumnHeaderRenderer(originalHeader);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
...props.columns.map((col, colIndex) => {
const originalHeader = col.header as ColumnHeaderRenderer | undefined;
const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
...props.columns.map((col, colIndex) => {
const originalHeader = col.header as ColumnHeaderRenderer | undefined;
const header = wrapColumnHeaderRenderer(originalHeader);
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 484-484: Useless conditional
This call to wrapColumnHeaderRenderer always evaluates to true.

🤖 Prompt for AI Agents
In web/src/components/Common/BaseTreeTable.vue around lines 482 to 484, the
assignment uses a redundant nullish coalescing fallback (`?? originalHeader`)
because wrapColumnHeaderRenderer(originalHeader) always returns a truthy
renderer; remove the unnecessary `?? originalHeader` and assign header directly
from wrapColumnHeaderRenderer(originalHeader). Ensure the code compiles with the
expected ColumnHeaderRenderer type (add an explicit cast if your
linter/typechecker requires one).

Comment on lines +101 to +111
watch(
() => props.container?.id,
(newId, oldId) => {
if (newId && newId !== oldId && isTailscaleEnabled.value && isContainerRunning.value) {
tailscaleFetched.value = false;
loadTailscaleStatus();
tailscaleFetched.value = true;
}
},
{ immediate: true }
);
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

tailscaleFetched is set to true before the query completes.

The loadTailscaleStatus() call is async but not awaited, so tailscaleFetched.value = true executes immediately. This causes the refresh button to appear before data is loaded, which is inconsistent with the intended behavior.

Consider moving the flag to after the load completes:

 watch(
   () => props.container?.id,
   (newId, oldId) => {
     if (newId && newId !== oldId && isTailscaleEnabled.value && isContainerRunning.value) {
       tailscaleFetched.value = false;
-      loadTailscaleStatus();
-      tailscaleFetched.value = true;
+      loadTailscaleStatus()?.then(() => {
+        tailscaleFetched.value = true;
+      });
     }
   },
   { immediate: true }
 );

Alternatively, derive tailscaleFetched from the query result/loading state rather than tracking it manually.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watch(
() => props.container?.id,
(newId, oldId) => {
if (newId && newId !== oldId && isTailscaleEnabled.value && isContainerRunning.value) {
tailscaleFetched.value = false;
loadTailscaleStatus();
tailscaleFetched.value = true;
}
},
{ immediate: true }
);
watch(
() => props.container?.id,
(newId, oldId) => {
if (newId && newId !== oldId && isTailscaleEnabled.value && isContainerRunning.value) {
tailscaleFetched.value = false;
loadTailscaleStatus()?.then(() => {
tailscaleFetched.value = true;
});
}
},
{ immediate: true }
);
🤖 Prompt for AI Agents
In web/src/components/Docker/ContainerOverviewCard.vue around lines 101 to 111,
tailscaleFetched is set to true immediately after calling the async
loadTailscaleStatus(), causing the UI to show the refresh button before the
query completes; either await the async call and set tailscaleFetched.value =
true only after it finishes (use try/finally to ensure the flag is updated on
both success and failure), or remove the manual flag entirely and derive
tailscaleFetched from the query's loading/result state (e.g., use the query's
isLoading/isFetched or equivalent) so the UI reflects actual load status.

Comment on lines +25 to +27
const { load, result, loading, error, refetch } = useLazyQuery(GET_CONTAINER_TAILSCALE_STATUS, {
id: props.containerId,
});
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

Query variables are not reactive to prop changes.

The useLazyQuery is initialized with a static object { id: props.containerId }, which won't update when containerId prop changes. This means if the same component instance is reused with a different container, the query will use the stale ID.

Use a reactive function for variables:

-const { load, result, loading, error, refetch } = useLazyQuery(GET_CONTAINER_TAILSCALE_STATUS, {
-  id: props.containerId,
-});
+const { load, result, loading, error, refetch } = useLazyQuery(
+  GET_CONTAINER_TAILSCALE_STATUS,
+  () => ({ id: props.containerId })
+);

This matches the pattern used in ContainerOverviewCard.vue at lines 90-92.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { load, result, loading, error, refetch } = useLazyQuery(GET_CONTAINER_TAILSCALE_STATUS, {
id: props.containerId,
});
const { load, result, loading, error, refetch } = useLazyQuery(
GET_CONTAINER_TAILSCALE_STATUS,
() => ({ id: props.containerId })
);
🤖 Prompt for AI Agents
In web/src/components/Docker/DockerTailscaleIndicator.vue around lines 25-27,
the useLazyQuery is given a static variables object so it won’t update when the
containerId prop changes; change the variables argument to be reactive (e.g.
pass a function or computed that returns { id: props.containerId }) so the query
uses the latest prop value, mirroring the pattern used in
ContainerOverviewCard.vue (lines ~90-92), and ensure any refetch/load calls
still work with the reactive variables.

Comment on lines +19 to +28
const removeWithImage = ref(true);

watch(
() => props.open,
(isOpen) => {
if (isOpen) {
removeWithImage.value = true;
}
}
);
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

🏁 Script executed:

#!/bin/bash
# First, locate and read the RemoveContainerModal.vue file
find . -name "RemoveContainerModal.vue" -type f 2>/dev/null | head -5

Repository: unraid/api

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Search for similar modal/checkbox patterns in Vue files
rg -n -C3 "ref\(true\)" --type=vue --glob="*Modal*" --glob="*Dialog*" | head -50

Repository: unraid/api

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Look for other remove/delete modals to check consistency
fd -e vue -path "*Modal*" -o -path "*Dialog*" | grep -i "remove\|delete" | head -20

Repository: unraid/api

Length of output: 222


🏁 Script executed:

#!/bin/bash
cd /tmp && git clone --depth 1 https://github.com/unraid/api.git 2>&1 | grep -E "Cloning|fatal" || true
find . -name "RemoveContainerModal.vue" -type f 2>/dev/null

Repository: unraid/api

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Read the RemoveContainerModal.vue file if it exists
if [ -f "./web/src/components/Docker/RemoveContainerModal.vue" ]; then
  echo "=== File found at ./web/src/components/Docker/RemoveContainerModal.vue ==="
  cat -n "./web/src/components/Docker/RemoveContainerModal.vue"
elif [ -f "./web/components/Docker/RemoveContainerModal.vue" ]; then
  echo "=== File found at ./web/components/Docker/RemoveContainerModal.vue ==="
  cat -n "./web/components/Docker/RemoveContainerModal.vue"
else
  echo "File not found in expected locations. Searching..."
  find . -name "RemoveContainerModal.vue" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== Found at: {} ===" && cat -n {}'
fi

Repository: unraid/api

Length of output: 2083


🏁 Script executed:

#!/bin/bash
# Search for similar remove/delete modals and their default checkbox values
echo "=== Searching for similar Modal patterns with ref(true) or ref(false) ==="
rg -A5 "ref\((true|false)\)" --type=vue | grep -B3 -A5 -E "remove|delete|Modal|Dialog" | head -80

echo ""
echo "=== Searching for checkbox defaults in Docker-related Vue files ==="
fd -e vue | grep -iE "(docker|container|remove|delete)" | head -20 | xargs rg -l "ref\(" 2>/dev/null | head -5 | xargs -I {} sh -c 'echo "\n=== File: {} ===" && head -30 {}'

Repository: unraid/api

Length of output: 7312


Change checkbox default to unchecked for safety.

The "Also remove image" checkbox defaults to true (checked), making image removal the default action. Since removing images is destructive and typically requires explicit user confirmation, defaulting to false (unchecked) aligns with standard Docker UI patterns and reduces the risk of unintended data loss.

Consider updating line 19 from ref(true) to ref(false) and the watcher reset on line 25 from true to false.

🤖 Prompt for AI Agents
In web/src/components/Docker/RemoveContainerModal.vue around lines 19 to 28 the
checkbox "Also remove image" defaults to checked (removeWithImage initialized to
ref(true)) and the watcher resets it to true when the modal opens; change the
default to unchecked by initializing removeWithImage to ref(false) and update
the watcher reset to set removeWithImage.value = false so the image removal is
opt-in rather than the default.

@elibosley
Copy link
Member

Merge after disabling cron job for container update checking.

@pujitm pujitm changed the title feat: replace docker overview table with web component feat: replace docker overview table with web component (7.3+) Dec 18, 2025
@github-actions
Copy link
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/PR1764/dynamix.unraid.net.plg

@pujitm pujitm merged commit 277ac42 into main Dec 18, 2025
10 of 13 checks passed
@pujitm pujitm deleted the feat/docker-overview-table branch December 18, 2025 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants