Skip to content

Conversation

@pujitm
Copy link
Member

@pujitm pujitm commented Oct 27, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Docker container pause/unpause and update operations
    • Container port conflict detection and alerts
    • Container auto-start configuration management with reordering
    • Container log viewer with follow mode and tail options
    • Container size information display
    • Folder-based container organization with drag-and-drop support
    • Template synchronization and orphaned container detection
    • Notification deduplication and warnings/alerts subscriptions
  • Improvements

    • Enhanced table with persistent column visibility and drag-and-drop reordering
    • Improved sidebar tree navigation for containers and folders
    • Better error handling and notifications

✏️ 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

Walkthrough

A comprehensive Docker container management system is introduced with template scanning, auto-start configuration, port conflict detection, live stats streaming, and container logging. New services manage template metadata, container lifecycle operations, and organizer views. The frontend provides a tree-based UI for container browsing, draggable folder organization, and bulk container operations.

Changes

Cohort / File(s) Summary
Docker Template Management
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.model.ts, docker-template-scanner.service.ts, docker-template-scanner.service.spec.ts, docker-template-icon.service.ts
Adds DockerTemplateSyncResult model and DockerTemplateScannerService to scan XML templates, parse container metadata, and match containers to templates. DockerTemplateIconService reads Icon fields from template XML files and maps icons to containers.
Docker Auto-Start Configuration
api/src/unraid-api/graph/resolvers/docker/docker-autostart.service.ts, docker-autostart.service.spec.ts
Introduces DockerAutostartService to manage auto-start entries, parse configuration files (docker-autostart, docker-userprefs), and persist autostart settings with optional wait times and ordering.
Docker Ports & Networks
api/src/unraid-api/graph/resolvers/docker/docker-port.service.ts, docker-port.service.spec.ts, docker-network.service.ts, docker-network.service.spec.ts
New services to detect container port conflicts (containerPorts and LAN ports), deduplicate port entries, manage Docker networks with caching, and fetch network metadata.
Docker Logs & Stats
api/src/unraid-api/graph/resolvers/docker/docker-log.service.ts, docker-log.service.spec.ts, docker-stats.service.ts
DockerLogService retrieves container logs via docker CLI with timestamp, tail, and since support. DockerStatsService streams real-time container stats (CPU, memory, I/O) via pub/sub.
Docker Form & Container Overviews
api/src/unraid-api/graph/resolvers/docker/docker-form.service.ts
New service generates table-friendly container overviews with dataSchema and uiSchema for form rendering.
Docker Service Extensions
api/src/unraid-api/graph/resolvers/docker/docker.service.ts, docker.service.spec.ts, docker.service.integration.spec.ts
Expands DockerService with pause, unpause, container updates, autostart management, port conflict detection, log retrieval, and enriched container transformations (LAN IPs, template paths, orphaned status, autostart properties).
Docker Config & Client
api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts, docker-config.service.ts, utils/docker-client.ts
Adds templateMappings and skipTemplatePaths to DockerConfig. Introduces shared getDockerClient() utility for Docker instance management.
Docker GraphQL Layer
api/src/unraid-api/graph/resolvers/docker/docker.model.ts, docker.resolver.ts, docker.resolver.spec.ts, docker.mutations.resolver.ts, docker-container.resolver.ts
Expands GraphQL types with DockerPortConflicts, DockerContainerLogs, DockerContainerStats, DockerContainerOverviewForm, DockerAutostartEntryInput. Adds queries/mutations for port conflicts, logs, organizer operations, autostart config, container updates, and template syncing. New resolver fields for projectUrl, registryUrl, supportUrl, iconUrl.
Docker Module Setup
api/src/unraid-api/graph/resolvers/docker/docker.module.ts, docker.module.spec.ts
Registers new Docker services (FormService, LogService, PortService, NetworkService, StatsService, TemplateScannerService, TemplateIconService, AutostartService) and imports NotificationsModule and ServicesModule.
Organizer Model & API
api/src/unraid-api/organizer/organizer.model.ts, organizer.ts
Replaces nested root structure with rootId and introduces flatEntries: FlatOrganizerEntry[]. Adds helper functions for flat-entry enrichment, folder operations (move, rename, create), and flat-view resolution.
Organizer Service
api/src/unraid-api/organizer/docker-organizer.service.ts, docker-organizer.service.spec.ts
Extends organizer service with skipCache support, new folder operations (move items, rename, create with items), and view preference updates.
Organizer Tests
api/src/unraid-api/organizer/organizer.resolution.test.ts, organizer.test.ts
Updated tests validate flatEntries-based representation and graceful handling of missing resources/entries. Added test for duplicate prevention in root.
Notifications Enhancements
api/src/unraid-api/graph/resolvers/notifications/notifications.model.ts, notifications.resolver.ts, notifications.service.ts, notifications.module.ts
Adds warningsAndAlerts field, notifyIfUnique mutation, and getWarningsAndAlerts API. Implements fingerprint-based deduplication and publishWarningsAndAlerts broadcasts. Introduces NotificationsModule.
Pub/Sub & Channels
packages/unraid-shared/src/pubsub/graphql.pubsub.ts
Adds NOTIFICATION_WARNINGS_AND_ALERTS and DOCKER_STATS enum members to GRAPHQL_PUBSUB_CHANNEL.
Environment & Config
api/.env.development, api/.env.staging, api/.env.production, api/src/environment.ts, api/src/store/modules/paths.ts
Adds PATHS_DOCKER_TEMPLATES environment variable and path configuration. Adds docker-userprefs path entry. Enables ENABLE_NEXT_DOCKER_RELEASE in staging/production.
File Modifications
api/src/unraid-api/unraid-file-modifier/file-modification.ts, docker-containers-page.modification.ts
Updates FileModification to accept checkOsVersion option. Adds DockerContainersPageModification to inject Docker overview into DockerContainers.page for Unraid < 7.3.
Frontend: Docker Components
web/src/components/Docker/DockerContainerManagement.vue, DockerContainerOverview.vue, DockerContainerOverview.standalone.vue, DockerContainersTable.vue, DockerSidebarTree.vue, DockerAutostartSettings.vue, DockerNameCell.vue, DockerPortConflictsAlert.vue, DockerOrphanedAlert.vue, ContainerSizesModal.vue, DockerLogViewerModal.vue, DockerContainerStatCell.vue
Comprehensive Docker UI: main management view with organizer tree and container table, autostart configuration, port conflict alerts, orphaned container removal, container sizes modal, log viewer, and per-cell renderers for names and stats.
Frontend: Docker GraphQL
web/src/components/Docker/docker-*.query.ts, docker-*.mutation.ts, docker-*.subscription.ts
Extensive GraphQL documents for fetching containers, port conflicts, logs, overview forms; mutations for folder operations, container lifecycle, autostart config, view preferences; subscriptions for stats and warnings/alerts.
Frontend: Docker Types & Utils
web/src/components/Docker/docker-port-conflicts.types.ts, web/src/utils/docker.ts
Port conflict type definitions and comprehensive Docker utility helpers (port formatting, IP extraction, uptime calculation, tree-row conversion).
Frontend: Tree/Table Composables
web/src/composables/useTreeData.ts, useTreeFilter.ts, useTreeExpansion.ts, useRowSelection.ts, useFolderTree.ts, useFolderOperations.ts, useDropProjection.ts, useDragDrop.ts, useColumnDragDrop.ts
Foundation composables for hierarchical tree rendering, filtering, expansion, selection, folder operations, and drag-and-drop support.
Frontend: Table Components
web/src/components/Common/BaseTreeTable.vue, TableColumnMenu.vue, MultiValueCopyBadges.vue
Reusable tree-table component with column visibility menu, copy-to-clipboard badges, resizing, and drag support.
Frontend: Container Actions
web/src/composables/useContainerActions.ts, useDockerUpdateActions.ts, useDockerLogSessions.ts, useDockerEditNavigation.ts, useDockerColumnVisibility.ts
Composables managing container lifecycle (start/stop/pause), bulk updates, log session management, legacy edit page navigation, and per-view column visibility.
Frontend: Notifications
web/src/components/Notifications/CriticalNotifications.standalone.vue, graphql/notification.query.ts, graphql/notification.subscription.ts
Standalone critical notifications component with live warning/alert subscriptions and dismissal support.
Frontend: UI & Styling
web/src/assets/main.css, web/src/components/Wrapper/mount-engine.ts, web/src/utils/unapiScope.ts, web/src/main.ts, web/src/store/theme.ts, web/postcss/scopeTailwindToUnapi.ts
Nuxt UI integration via CSS import, unapi scope management for DOM elements, portal root setup, theme dynamic CSS variables, and Tailwind scoping for the unapi namespace.
Frontend: Component Registry & Navigation
web/src/components/Wrapper/component-registry.ts, web/public/test-pages/all-components.html
Registers new Docker and critical notifications components with optional decorateContainer flag. Adds test page for component showcase.
Frontend: Utilities & Rendering
web/src/composables/gql/gql.ts, web/src/composables/gql/graphql.ts, web/src/utils/tableRenderers.ts, web/src/composables/useContextMenu.ts, web/src/composables/useDropProjection.ts, web/src/composables/usePersistentColumnVisibility.ts, web/src/helpers/env.ts
GraphQL type overloads for new operations, table cell and header rendering helpers, context menu management, drop projection calculations, persistent column visibility, and feature flag environment helpers.
Frontend: Dependencies
web/package.json
Adds @formkit/auto-animate and @tanstack/vue-table dependencies.
Frontend: Build Configuration
web/.prettierignore, web/eslint.config.mjs, web/src/composables/gql/index.ts
Excludes build directories from Prettier, updates Vue feature version threshold, reformats export paths.
API: Dependencies & Config
api/package.json, api/.gitignore, api/.eslintrc.ts, api/.prettierignore, api/justfile, api/generated-schema.graphql, api/dev/configs/api.json
Adds fast-xml-parser dependency, excludes docker-templates and notifications directories, ignores dist/, adds watch justfile target, updates schema version.
Core Utilities
api/src/core/utils/misc/catch-handlers.ts, api/src/core/utils/network.ts, api/src/unraid-api/auth/api-key.service.ts
Makes DockerError.address optional, adds getLanIp() utility, gracefully handles missing API key directory.
Tests & Integration
web/__test__/components/Wrapper/mount-engine.test.ts, api/src/__test__/store/modules/paths.test.ts, api/src/unraid-api/app/__test__/app.module.integration.spec.ts, api/src/unraid-api/graph/resolvers/vms/vms.service.spec.ts, web/components.d.ts
Updates mount engine tests for portal and scope management, path snapshot, app integration mocks, VM service QEMU availability checks, and component type declarations.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as DockerContainerManagement
    participant Table as DockerContainersTable
    participant Service as DockerService
    participant Scanner as DockerTemplateScannerService
    participant Port as DockerPortService
    participant Logger as Logger

    User->>UI: Load Docker overview
    activate UI
    UI->>Table: Fetch containers
    Table->>Service: GET_DOCKER_CONTAINERS
    activate Service
    Service->>Scanner: syncMissingContainers()
    activate Scanner
    Scanner->>Scanner: loadAllTemplates()
    Scanner->>Scanner: matchContainers()
    Scanner->>Service: updateMappings()
    Service->>Service: enrichContainers()
    deactivate Scanner
    Service->>Port: getPortConflicts()
    activate Port
    Port->>Port: deduplicateContainerPorts()
    Port->>Port: calculateConflicts()
    deactivate Port
    Service-->>Table: containers + ports + organizer
    deactivate Service
    UI->>UI: Render tree + table
    deactivate UI
    
    User->>UI: Drag container to folder
    activate UI
    UI->>Table: emit row:drop
    Table->>Service: moveDockerItemsToPosition()
    activate Service
    Service->>Service: Update organizer view
    deactivate Service
    Service-->>Table: Updated view
    Table->>UI: Refresh display
    deactivate UI
    
    User->>UI: Update container
    activate UI
    UI->>Service: updateContainer(id)
    activate Service
    Service->>Service: pause/unpause/start/stop
    Service->>Service: invalidateCache()
    Service->>Logger: publishContainerUpdate(pubsub)
    deactivate Service
    deactivate UI
Loading
sequenceDiagram
    participant Resolver as DockerResolver
    participant Stats as DockerStatsService
    participant PubSub as PubSub
    participant Subscriber as Subscription Client

    Resolver->>Stats: onModuleInit()
    activate Stats
    Stats->>Stats: startStatsStream()
    Stats->>Stats: spawn docker stats
    Note over Stats: docker stats --no-stream --format json
    loop Process stdout lines
        Stats->>Stats: parseStatsLine()
        Stats->>Stats: extract cpu%, mem, etc.
        Stats->>PubSub: publish(DOCKER_STATS, stats)
    end
    deactivate Stats
    
    Subscriber->>Resolver: subscribe dockerContainerStats
    activate Resolver
    Resolver->>PubSub: subscribe(DOCKER_STATS)
    PubSub-->>Resolver: Stream of stats
    Resolver-->>Subscriber: Push stats updates
    deactivate Resolver
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Areas requiring extra attention:

  • Template scanner logic (docker-template-scanner.service.ts): XML parsing, container-to-template matching, normalization, and mapping deduplication with error aggregation.
  • Port conflict detection (docker-port.service.ts): Deduplication logic, LAN IP port mapping, and conflict aggregation across containers.
  • Auto-start configuration (docker-autostart.service.ts): File parsing/formatting, persistence to multiple files (docker-autostart and docker-userprefs), and handling of edge cases (empty configs, invalid wait values).
  • Stats streaming (docker-stats.service.ts): Process management, line parsing with ANSI code cleaning, and pub/sub integration for real-time updates.
  • Organizer refactoring (organizer.ts, organizer.model.ts): Transition from nested root structure to flatEntries representation, folder operations (move, rename, create), and impact on existing organizer tests.
  • Tree-table composition (BaseTreeTable.vue, related composables): Complex state management for selection, expansion, drag-and-drop, filtering, and column visibility with reactive synchronization.
  • Dragon-drop integration (useDropProjection.ts, useDragDrop.ts): Projection calculations, row metrics caching, and area detection (before/inside/after).
  • GraphQL schema updates (generated-schema.graphql, resolver changes): New mutations, subscriptions, and argument types; verify backward compatibility and feature flag guards.
  • Feature flag gates (ENABLE_NEXT_DOCKER_RELEASE): Mutations and resolvers guarded by feature flags; ensure correct scoping and permission checks.

Possibly related PRs

  • feat(api): determine if docker container has update #1582: Introduces FeatureFlags infrastructure (UseFeatureFlag decorator, OmitIf, checkFeatureFlag), AsyncMutex utility, and foundational Docker services (DockerManifest, DockerPhp, ContainerStatusJob) that this PR builds upon and integrates.

Suggested reviewers

  • zackspear
  • elibosley

Poem

🐰 Templates scanned, containers tamed,
Trees unfold where folders named,
Stats stream bright through pubsub lanes,
Drag and drop through flat domains,
A Docker dream, now organized! 🐳✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% 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 specific and directly references the main change in the changeset—migrating the Docker container overview table to a web component architecture.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/docker-overview-table

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 3 days ago

To fix the problem, remove the initial assignment to containers on line 312 in the pause method of the DockerService class, since it is immediately overwritten in the loop. This will reduce confusion, improve code clarity, and remove an unnecessary operation. The rest of the logic does not depend on the initializer; the loop will properly assign the first value. No new imports or definitions are required for this fix.


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
@@ -309,7 +309,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
@@ -309,7 +309,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 3 days ago

The fix is straightforward: remove the initial assignment to containers on line 340 in the unpause method. The variable containers can be declared without an initial value and will receive its first value inside the loop. Change let containers = await this.getContainers({ skipCache: true }); to just let containers;. No change is needed to the remainder of the function since the value assigned to containers within the loop suffices for all following logic. This edit ensures that unnecessary code is removed while preserving all existing functionality.

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
@@ -337,7 +337,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;
         let updatedContainer: DockerContainer | undefined;
         for (let i = 0; i < 5; i++) {
             await sleep(500);
EOF
@@ -337,7 +337,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;
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 days ago

To fix the problem, remove the unused destructured variable resources from both the parameter destructuring { view, sourceEntryIds, destinationFolderId, position, resources } and from the interface MoveItemsToPositionParams where it is defined as optional. Specifically:

  • On line 642, remove resources from the destructuring assignment.
  • In the MoveItemsToPositionParams interface, remove the resources?: OrganizerV1['resources']; line.
  • No other changes are required, as resources is not used elsewhere in this function.

This will improve code clarity and eliminate unnecessary computation associated with unused variables.

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
@@ -631,7 +631,6 @@
     sourceEntryIds: Set<string>;
     destinationFolderId: string;
     position: number;
-    resources?: OrganizerV1['resources'];
 }
 
 /**
@@ -639,7 +638,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
@@ -631,7 +631,6 @@
sourceEntryIds: Set<string>;
destinationFolderId: string;
position: number;
resources?: OrganizerV1['resources'];
}

/**
@@ -639,7 +638,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 days ago

To fix the problem, the destructuring of resources from the argument params within the createFolderWithItems function should be modified to exclude resources. This means editing the destructuring assignment so resources is not extracted, while leaving all other extracted variables intact. No other changes are needed in the function as resources is not used. The edit is limited to line 702 of file api/src/unraid-api/organizer/organizer.ts.


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
@@ -699,7 +699,7 @@
  * Combines createFolder + moveItems + positioning in a single atomic operation.
  */
 export function createFolderWithItems(params: CreateFolderWithItemsParams): OrganizerView {
-    const { view, folderId, folderName, parentId, sourceEntryIds = [], position, resources } = params;
+    const { view, folderId, folderName, parentId, sourceEntryIds = [], position } = params;
 
     let newView = createFolderInView({
         view,
EOF
@@ -699,7 +699,7 @@
* Combines createFolder + moveItems + positioning in a single atomic operation.
*/
export function createFolderWithItems(params: CreateFolderWithItemsParams): OrganizerView {
const { view, folderId, folderName, parentId, sourceEntryIds = [], position, resources } = params;
const { view, folderId, folderName, parentId, sourceEntryIds = [], position } = params;

let newView = createFolderInView({
view,
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 +202
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 +54
@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 896
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.

Comment on lines +44 to +54
function getObserverRoot(root: ScopeRoot): Element | null {
if (root instanceof Document) {
return root.body ?? root.documentElement;
}

if (root instanceof DocumentFragment) {
return root.firstElementChild;
}

return root;
}
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

Bug: Observing DocumentFragment via firstElementChild misses later additions.

You can observe a DocumentFragment directly. Current approach returns null if empty, so mutations are never observed.

Apply:

@@
-function getObserverRoot(root: ScopeRoot): Element | null {
-  if (root instanceof Document) {
-    return root.body ?? root.documentElement;
-  }
-  if (root instanceof DocumentFragment) {
-    return root.firstElementChild;
-  }
-  return root;
-}
+function getObserverRoot(root: ScopeRoot): Node | null {
+  if (root instanceof Document) return root;
+  if (root instanceof DocumentFragment) return root;
+  return root; // Element
+}
@@
-  const observerRoot = getObserverRoot(getQueryRoot(root));
+  const observerRoot = getObserverRoot(getQueryRoot(root));
   if (!observerRoot) {
     return;
   }

This ensures future nodes appended to the fragment are captured.

Also applies to: 121-124

🤖 Prompt for AI Agents
In web/src/utils/unapiScope.ts around lines 44 to 54, the function returns
root.firstElementChild for DocumentFragment which misses mutations added later;
change the logic to return the DocumentFragment itself instead of its
firstElementChild and update the function return type from Element | null to
Node | null (and adjust any call sites to accept Node) so MutationObserver can
be attached directly to the fragment; apply the same fix to the similar code at
lines ~121-124.

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.57253% with 6309 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.26%. Comparing base (375dcd0) to head (6dd201b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...eb/src/components/Docker/DockerContainersTable.vue 0.09% 1051 Missing ⚠️
...rc/components/Docker/DockerContainerManagement.vue 0.22% 446 Missing ⚠️
web/src/components/Common/BaseTreeTable.vue 0.27% 363 Missing ⚠️
.../src/components/Docker/DockerAutostartSettings.vue 0.34% 286 Missing ⚠️
web/src/composables/useContainerActions.ts 0.00% 243 Missing and 1 partial ⚠️
web/src/utils/tableRenderers.ts 0.00% 240 Missing and 1 partial ⚠️
web/src/composables/useDockerLogSessions.ts 0.00% 224 Missing and 1 partial ⚠️
...Notifications/CriticalNotifications.standalone.vue 0.45% 218 Missing ⚠️
web/src/components/Common/MultiValueCopyBadges.vue 0.55% 178 Missing ⚠️
...nraid-api/graph/resolvers/docker/docker.service.ts 40.37% 161 Missing ⚠️
... and 78 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1764      +/-   ##
==========================================
- Coverage   52.04%   47.26%   -4.78%     
==========================================
  Files         874      947      +73     
  Lines       50372    58011    +7639     
  Branches     5017     5424     +407     
==========================================
+ Hits        26214    27420    +1206     
- Misses      24083    30476    +6393     
- Partials       75      115      +40     

☔ 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 187
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 +2817
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 +706
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 +50
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.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 041a3e0 to 69e28ac Compare November 21, 2025 04:42
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 (5)
web/src/components/Docker/DockerContainersTable.vue (2)

304-320: Normalize container state before mapping to badge colors (repeat from prior review)

You’re still indexing colorMap with row.original.state directly. If that value ever comes through lowercase or otherwise normalized differently than the ContainerState enum, you’ll silently fall back to 'neutral'.

You can make this more robust by normalizing the key first:

-        const state = row.original.state ?? '';
+        const state = row.original.state ?? '';
+        const stateKey = typeof state === 'string' ? state.toUpperCase() : state;
         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';

This keeps the UI correct even if upstream state casing varies.


593-613: Replace window.prompt with a non-blocking folder-name modal (repeat from prior review)

createFolderFromDrop still uses window.prompt to collect the new folder name. This blocks the main thread, isn’t styled like the rest of the UI, and has poor accessibility compared to your existing modal patterns.

You already have a folder creation flow in the “Move to folder” modal (folderOps.newTreeFolderName etc.). Consider reusing that pattern (or adding a small dedicated “Create folder from selection” modal) instead of window.prompt, and moving the createFolderWithItemsMutation logic into the modal’s confirm handler.

This will keep UX consistent and avoid blocking browser prompts.

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

6-6: Extract duplicated FlatRow type to shared location.

Same duplication issue as noted in useDropProjection.ts.

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

23-23: Extract duplicated FlatRow type to shared location.

Third occurrence of the same FlatRow<T> type. Consolidate with the other definitions.


219-235: Column visibility not restored when exiting compact mode.

As noted in a previous review, when compact becomes 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 }
 );
🧹 Nitpick comments (5)
web/src/utils/tableRenderers.ts (3)

103-124: Avoid shadowing row variable in drop indicator ref

Inside the ref callback you redeclare const row = cell?.closest('tr');, shadowing the outer row parameter. This is harmless but makes the code harder to scan.

A simple rename improves clarity:

-      ref: (el) => {
-        if (el && el instanceof HTMLElement) {
-          const cell = el.closest('td');
-          const row = cell?.closest('tr');
-          if (row && cell) {
-            const rowWidth = row.offsetWidth;
+      ref: (el) => {
+        if (el && el instanceof HTMLElement) {
+          const cell = el.closest('td');
+          const tr = cell?.closest('tr');
+          if (tr && cell) {
+            const rowWidth = tr.offsetWidth;
             const cellLeft = cell.offsetLeft;
             el.style.cssText = `width: ${rowWidth}px; left: -${cellLeft}px;`;
           }
         }
       },

152-212: Gate draggable attribute on enableDragDrop

Right now the div is always draggable={draggable}, even when enableDragDrop is false. That prevents your drag handlers from firing but still allows the browser’s native drag behaviour.

Consider tying draggable to enableDragDrop:

-      draggable,
+      draggable: enableDragDrop && draggable,

This keeps cells non-draggable when DnD is disabled while preserving your existing guards in onDragstart.


274-323: Clarify onRowSelect semantics for bulk child selection

For expandable rows you call onSelectionChange with the full updated map, but onRowSelect is only invoked once for the first descendant. If any consumer treats onRowSelect as “fires per row toggled”, this will under-report bulk changes.

If the intended contract is “notify about the last-touched row only”, this is fine. Otherwise, consider either:

  • Emitting one onRowSelect per descendant, or
  • Introducing a dedicated callback for “bulk selection changed” and using that here instead of reusing onRowSelect.

This will keep selection events aligned with consumer expectations.

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

118-131: Consider a more descriptive label for the LAN IP “Open” badge

Right now the badge text is just “Open” with role="button". For accessibility, consider adding an aria-label that includes the container name or destination (e.g., “Open web UI for {{ treeRow.name }}”) so screen readers convey what will be opened when activated.

-    <UBadge
+    <UBadge
       v-if="canOpenLanIp && firstLanIp"
       color="primary"
       variant="subtle"
       size="sm"
       class="ml-2 cursor-pointer select-none"
       role="button"
       tabindex="0"
       data-stop-row-click="true"
+      :aria-label="`Open LAN IP for ${treeRow.name}`"
       @click="handleOpenLanIp"
       @keydown="handleOpenLanIpKeydown"
     >
web/src/composables/useDropProjection.ts (1)

7-7: Extract duplicated FlatRow type to shared location.

The FlatRow<T> type is defined identically in multiple files (useDropProjection.ts, useTreeExpansion.ts, and BaseTreeTable.vue). This duplication creates a maintenance burden and risks inconsistency if definitions diverge.

Consider extracting this to a shared types file, such as web/src/composables/useTreeData.ts or a new web/src/types/tree.ts:

// In shared types file
export type FlatRow<T> = TreeRow<T> & { depth: number; parentId?: string };

Then import it in all three files:

-type FlatRow<T> = TreeRow<T> & { depth: number; parentId?: string };
+import type { FlatRow } from '@/composables/useTreeData';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef5065c and 69e28ac.

📒 Files selected for processing (13)
  • web/auto-imports.d.ts (1 hunks)
  • web/components.d.ts (5 hunks)
  • web/eslint.config.mjs (1 hunks)
  • web/src/components/Common/BaseTreeTable.vue (1 hunks)
  • web/src/components/Docker/DockerAutostartSettings.vue (1 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/DockerNameCell.vue (1 hunks)
  • web/src/composables/useDragDrop.ts (1 hunks)
  • web/src/composables/useDropProjection.ts (1 hunks)
  • web/src/composables/useRowSelection.ts (1 hunks)
  • web/src/composables/useTreeExpansion.ts (1 hunks)
  • web/src/composables/useTreeFilter.ts (1 hunks)
  • web/src/utils/tableRenderers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/components/Docker/DockerAutostartSettings.vue
  • web/src/composables/useDragDrop.ts
🧰 Additional context used
🧠 Learnings (13)
📓 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: 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.
📚 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
  • web/auto-imports.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
  • web/auto-imports.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
  • web/src/components/Docker/DockerNameCell.vue
  • 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/components.d.ts
  • web/src/components/Docker/DockerNameCell.vue
  • web/src/components/Docker/DockerContainersTable.vue
📚 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: 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-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: 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/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
📚 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/auto-imports.d.ts
🧬 Code graph analysis (5)
web/src/composables/useDropProjection.ts (2)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/composables/useDragDrop.ts (1)
  • DropArea (6-6)
web/src/composables/useTreeFilter.ts (1)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/utils/tableRenderers.ts (3)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/composables/useDragDrop.ts (1)
  • DropArea (6-6)
web/src/composables/useRowSelection.ts (1)
  • getSelectableDescendants (131-155)
web/src/composables/useTreeExpansion.ts (1)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
web/src/composables/useRowSelection.ts (1)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
🔇 Additional comments (14)
web/auto-imports.d.ts (1)

1-62: LGTM! Auto-generated file updated consistently.

This auto-generated file reflects a pnpm dependency path update where all Nuxt UI runtime references have been consistently updated to the new hash a4468a8ffdf957a224d04f3c8b1a0956. The changes align with the broader Nuxt UI integration updates mentioned in the PR summary.

web/components.d.ts (3)

1-10: LGTM! Auto-generated type declarations.

This file is correctly marked as auto-generated by unplugin-vue-components. Any issues should be addressed in source component files rather than editing this file directly.


23-23: No issues found—all component files are present and properly declared.

Verification confirms that all 15 new component declarations in web/components.d.ts have corresponding files in the codebase. The auto-generation has correctly captured existing components without orphaned references.


125-149: Update @nuxt/ui from v4.0.0-alpha.0 to v4.1.0 (current stable).

The v4.1.0 stable release includes 100+ components and unifies Nuxt UI + Pro into one open-source package, compared to the alpha version which lacks production-ready features and documentation. Breaking/migration changes between the alpha and stable versions include component modifier renames and Nuxt 4 compatibility updates.

The alpha version is explicitly pinned in web/package.json and is outdated. Upgrade to v4.1.0 for production stability, official support, and access to bug fixes and improvements released since alpha.

⛔ Skipped due to learnings
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.
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.
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.
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.
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.
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`.
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.
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.
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.
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.
web/src/utils/tableRenderers.ts (1)

10-66: Header rendering and resize handle look correct

The normalization of header content, width binding via context.column.getSize(), and resize affordance wired to header.getResizeHandler() are all consistent with TanStack’s header context and Vue’s render APIs. No issues here.

web/eslint.config.mjs (1)

71-76: Vue unsupported-features rule now targets ^3.5.0

Bumping vue/no-unsupported-features to ^3.5.0 matches the Vue 3.5.x usage in this repo and should reduce false positives for newer APIs. Just ensure your minimum deployed Vue runtime is ≥ 3.5 so lint and runtime expectations stay aligned.

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

25-77: Update and LAN-IP interaction flow looks solid

The computed guards (hasUpdate, canOpenLanIp), propagation control in the click/keydown handlers, and the isUpdating check in handleUpdateConfirm all look correct and should avoid accidental double-actions. Expand handling via toggle-expand emit is also cleanly separated.

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

15-151: Tree filtering logic is robust and API-friendly

The combination of searchableKeys + searchAccessor, cycle-safe collectSearchableStrings, and filterRowsByTerm that preserves tree structure gives a flexible and safe filtering mechanism. DEV-only logging around searchAccessor failures is a nice touch to avoid noisy production logs. The returned API (globalFilter, filterTerm, filteredData, setGlobalFilter) is clear and easy for table/tree consumers to integrate.

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

516-542: Container row resolution helpers are correct and nicely encapsulated

getContainerRows, getRowDisplayLabel, allContainerRows, and updateCandidateRows correctly lean on getRowById + flattenRows and row.meta to resolve only container rows and user-friendly labels. This makes the bulk update and logging flows easier to reason about and avoids mixing folders into container-only actions.


741-769: Row action routing is well-structured

handleRowAction cleanly branches per action string, uses containerActions for lifecycle ops, and routes logs/settings to the right helpers/emits while guarding on row.type !== 'container'. This keeps the per-row actions centralized and reduces duplication in the menu definitions.

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

24-134: LGTM! Well-structured drop projection logic.

The composable correctly implements drop projection with:

  • Stable metrics collection during drag operations
  • Proper filtering of dragging rows from drop targets
  • Accurate area calculation using 25%-75% thresholds for 'inside' drops
  • Edge case handling for pointer above/below all rows
web/src/composables/useTreeExpansion.ts (1)

12-59: LGTM! Solid tree expansion implementation.

The composable correctly:

  • Creates new Set instances for reactive updates in toggleExpanded
  • Recursively flattens tree nodes with proper depth tracking
  • Respects expansion state (only includes children of expanded nodes)
  • Provides efficient lookup via flatRowMap
web/src/composables/useRowSelection.ts (1)

13-155: LGTM! Comprehensive row selection implementation.

The composable provides robust selection management with:

  • Flexible selection criteria via isSelectable callback or selectableType fallback
  • Parent-to-child selection propagation in getSelectedRowIds
  • Efficient synchronization with external selectedIds using set-based comparison
  • Automatic pruning of invalid selections when tree data changes
  • Clean utility for collecting selectable descendants

The logic correctly handles tree selection semantics and maintains consistency between internal and external state.

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

1-440: Well-architected component integration.

The component successfully integrates multiple composables (tree expansion, selection, drag-drop, filtering) with:

  • Clean separation of concerns via composables
  • Comprehensive prop and emit definitions
  • Proper state synchronization between internal and external state
  • Auto-expansion of parent folders when activeId changes (good UX)
  • Extensive drag-and-drop support with projection and area detection

The architecture demonstrates good Vue 3 composition API patterns.

createSelectColumn(),
...props.columns.map((col, colIndex) => {
const originalHeader = col.header as ColumnHeaderRenderer | undefined;
const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;

Check warning

Code scanning / CodeQL

Useless conditional Warning

This call to wrapColumnHeaderRenderer always evaluates to true.

Copilot Autofix

AI 3 days ago

To fix the useless conditional, we should remove the redundant use of the nullish coalescing operator (?? originalHeader) from line 381. Instead, assign the result of wrapColumnHeaderRenderer(originalHeader) to header directly, as its result is always truthy. This edit should be made in the computed property that constructs processedColumns, specifically in the callback mapping over props.columns. Only line 381 needs to be edited. No changes to imports, function definitions, or other code are necessary.

Suggested changeset 1
web/src/components/Common/BaseTreeTable.vue

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/web/src/components/Common/BaseTreeTable.vue b/web/src/components/Common/BaseTreeTable.vue
--- a/web/src/components/Common/BaseTreeTable.vue
+++ b/web/src/components/Common/BaseTreeTable.vue
@@ -378,7 +378,7 @@
     createSelectColumn(),
     ...props.columns.map((col, colIndex) => {
       const originalHeader = col.header as ColumnHeaderRenderer | undefined;
-      const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
+      const header = wrapColumnHeaderRenderer(originalHeader);
       const cell = (col as { cell?: unknown }).cell
         ? ({ row }: { row: TableInstanceRow<T> }) => {
             const cellFn = (col as { cell: (args: unknown) => VNode | string | number }).cell;
EOF
@@ -378,7 +378,7 @@
createSelectColumn(),
...props.columns.map((col, colIndex) => {
const originalHeader = col.header as ColumnHeaderRenderer | undefined;
const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader;
const header = wrapColumnHeaderRenderer(originalHeader);
const cell = (col as { cell?: unknown }).cell
? ({ row }: { row: TableInstanceRow<T> }) => {
const cellFn = (col as { cell: (args: unknown) => VNode | string | number }).cell;
Copilot is powered by AI and may make mistakes. Always verify output.
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

🧹 Nitpick comments (4)
web/postcss/scopeTailwindToUnapi.ts (1)

181-208: OnceExit logic for unwrapping @layer looks correct; consider a small safety refactor

The flow—bail on missing parent, move all child nodes before the @layer when insertBefore exists, then remove the at‑rule (via .remove() when available or parent.removeChild)—is sound and preserves node order.

If you ever want to decouple this from the assumption that parent.insertBefore also re‑parents node (as in PostCSS), you could make the loop more explicit and avoid relying on mutation of removableAtRule.nodes:

-        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);
-          }
-        }
+        const parentContainer = parent as ParentContainer;
+        if (
+          Array.isArray(removableAtRule.nodes) &&
+          removableAtRule.nodes.length > 0 &&
+          typeof parentContainer.insertBefore === 'function'
+        ) {
+          const nodesToMove = [...removableAtRule.nodes];
+          for (const node of nodesToMove) {
+            parentContainer.insertBefore?.(atRule as unknown as Container, node);
+          }
+        }

This still preserves order, avoids the while + mutation pattern, and makes the behavior less dependent on specific PostCSS internals.

Please double‑check this against the exact PostCSS version you’re targeting to ensure insertBefore’s re‑parenting semantics match your expectations.

web/src/components/Wrapper/mount-engine.ts (2)

13-18: Portal root helper and portalTarget wiring look solid

The ensurePortalRoot helper and portalTarget threading into the wrapper keep the portal configuration centralized and idempotent, with a safe early-exit when document is unavailable. Applying ensureUnapiScope to the portal root itself is a nice touch to keep teleported content inside the scoped tree.

One minor, non-blocking thought: zIndex = '999999' is a hard-coded magic number; if this pattern spreads, consider centralizing it (CSS variable or shared constant) or adding a brief comment about why this specific value is chosen. Not required for this PR.

Also applies to: 38-60, 136-137, 221-223


145-154: Container decoration and unapi-scope observation are consistent; consider cleaning up commented code

The combination of:

  • ensureContainerScope keyed off mapping.decorateContainer,
  • a single selectorToMapping map over all component selectors,
  • initial ensureUnapiScopeForSelectors(selectors) plus a pass to decorate parent containers, and
  • observeUnapiScope with an onMatch callback that decorates containers for dynamically-added matches

gives you both initial and incremental coverage for scoped elements and their containers without duplicating logic per mapping. The final ensureUnapiScope(element) after render ensures the host element itself is always scoped, independent of the earlier selector-based pass.

The only nit is the now-redundant, commented-out calls around mounting:

// ensureUnapiScope(element);
// ensureContainerScope(element, mapping);

Since container decoration is already handled in the earlier block and element scope is applied on Line 243, these comments can likely be removed to avoid confusion unless you plan to re-enable them under different conditions later.

Also applies to: 155-172, 190-192, 236-237, 243-243

web/src/store/theme.ts (1)

263-264: Consider extracting gradient computation to a helper function.

The logic for computing gradient start/end colors using hexToRgba is duplicated between the header gradient (lines 263-264) and the banner gradient (lines 270-277). Additionally, similar logic exists in the bannerGradient computed property (lines 197-204).

Consider extracting this into a reusable helper function:

const computeGradientColors = (
  bgColor: string | undefined,
  hasCustomBg: boolean
): { start: string; end: string } => {
  if (bgColor && hasCustomBg) {
    return {
      start: hexToRgba(bgColor, 0),
      end: hexToRgba(bgColor, 0.7),
    };
  }
  return {
    start: 'var(--header-gradient-start)',
    end: 'var(--header-gradient-end)',
  };
};

Then use it at lines 263-264 and 270-277:

if (theme.value.bgColor) {
  dynamicVars['--custom-header-background-color'] = theme.value.bgColor;
  const { start, end } = computeGradientColors(theme.value.bgColor, true);
  dynamicVars['--custom-header-gradient-start'] = start;
  dynamicVars['--custom-header-gradient-end'] = end;
  customClasses.push('has-custom-header-bg');
}

// ...

if (theme.value.banner && theme.value.bannerGradient) {
  const { start, end } = computeGradientColors(theme.value.bgColor, !!theme.value.bgColor);
  dynamicVars['--banner-gradient'] = `linear-gradient(90deg, ${start} 0, ${end} 90%)`;
  customClasses.push('has-banner-gradient');
}

Also applies to: 270-277

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69e28ac and 4d5c3b8.

📒 Files selected for processing (8)
  • api/dev/configs/api.json (1 hunks)
  • api/package.json (1 hunks)
  • web/components.d.ts (5 hunks)
  • web/package.json (1 hunks)
  • web/postcss/scopeTailwindToUnapi.ts (2 hunks)
  • web/src/components/Wrapper/component-registry.ts (3 hunks)
  • web/src/components/Wrapper/mount-engine.ts (6 hunks)
  • web/src/store/theme.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/components.d.ts
  • api/package.json
🧰 Additional context used
🧠 Learnings (17)
📓 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: 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.
📚 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/package.json
  • web/src/components/Wrapper/component-registry.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
📚 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
  • web/src/components/Wrapper/component-registry.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
  • web/src/components/Wrapper/component-registry.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
  • web/src/components/Wrapper/component-registry.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-01-30T19:38:02.478Z
Learnt from: pujitm
Repo: unraid/api PR: 1075
File: unraid-ui/src/register.ts:15-34
Timestamp: 2025-01-30T19:38:02.478Z
Learning: In the web components registration process for unraid-ui, use a soft-fail approach (logging warnings/errors) instead of throwing errors, to ensure other components can still register successfully even if one component fails.

Applied to files:

  • web/src/components/Wrapper/component-registry.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/src/components/Wrapper/component-registry.ts
📚 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/components/Wrapper/component-registry.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/src/components/Wrapper/component-registry.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/Wrapper/component-registry.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.

Applied to files:

  • web/src/store/theme.ts
📚 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/store/theme.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • web/src/store/theme.ts
🧬 Code graph analysis (2)
web/src/components/Wrapper/mount-engine.ts (2)
web/src/utils/unapiScope.ts (3)
  • ensureUnapiScope (65-71)
  • ensureUnapiScopeForSelectors (73-93)
  • observeUnapiScope (97-154)
web/src/components/Wrapper/component-registry.ts (1)
  • componentMappings (24-168)
web/src/store/theme.ts (2)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • theme (58-60)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (1)
  • hexToRgba (176-186)
🔇 Additional comments (10)
api/dev/configs/api.json (1)

2-2: Verify the version increment aligns with project versioning strategy.

The version is bumped from 4.25.3 to 4.27.2, which has an unusual pattern: the minor version jumps by 2 while the patch version decreases. This deviates from standard semantic versioning progression.

Please confirm this version change is intentional and follows your project's versioning strategy (e.g., whether it tracks an upstream version, follows a custom scheme, etc.).

web/package.json (1)

112-112: All verification checks passed; code change is safe.

@tanstack/vue-table 8.21.3 is the latest version and was published 7 months ago, and the @tanstack/vue-table package works with Vue 3. 101 projects in the npm registry are using @tanstack/vue-table, confirming active ecosystem adoption. No known security vulnerabilities exist for this version, and the dependency addition aligns well with the PR's Docker UI component updates.

web/postcss/scopeTailwindToUnapi.ts (2)

16-28: Helper types cleanly model the minimal PostCSS surface you need

WalkAtRulesRoot, ParentContainer, and RemovableAtRule line up well with how the plugin uses walkAtRules, insertBefore, removeChild, nodes, and remove, without pulling in full PostCSS types. The optional members plus runtime guards keep this safe against slightly different container implementations.


30-34: Good to declare OnceExit on PostcssPlugin

Adding OnceExit?(root: WalkAtRulesRoot) keeps the plugin’s declared shape in sync with the implementation and gives you contextual typing for root in the handler, which is exactly what you need here.

web/src/components/Wrapper/component-registry.ts (2)

95-101: LGTM!

The critical-notifications component mapping follows the established pattern with proper async loading and naming conventions.


19-19: Implementation verified — no issues found.

The decorateContainer flag is properly implemented. When set to true on a component mapping, it correctly applies the .unapi class to the parent element. The implementation chain is sound:

  1. mount-engine.ts checks the decorateContainer flag (line 146)
  2. Calls ensureUnapiScope(container) to apply the class (line 151)
  3. ensureUnapiScope delegates to addScopeClass, which adds the unapi CSS class to the element
  4. addScopeClass includes proper safeguards to avoid duplicate class additions

The docker-container-overview component correctly uses this feature with decorateContainer: true.

web/src/store/theme.ts (4)

42-60: LGTM!

The iframe navigation hiding logic is well-implemented with appropriate SSR guards and a safe URL parameter check.


128-137: LGTM!

The dynamic variable keys and type definition follow best practices with exact types and concise naming conventions.


342-344: LGTM!

The setDevOverride function is correctly implemented with a clean, straightforward approach.


6-6: Add hex-to-rgba to package.json dependencies.

The package is legitimate and secure (no vulnerabilities), but it's not listed in the project's dependencies. This import at line 6 will fail at runtime. Add "hex-to-rgba": "^2.0.1" to the dependencies section of package.json (or web/package.json if that's where web dependencies are managed).

⛔ Skipped due to learnings
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.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.

Comment on lines +162 to +167
{
component: defineAsyncComponent(() => import('../Docker/DockerContainerOverview.standalone.vue')),
selector: 'unraid-docker-container-overview',
appId: 'docker-container-overview',
decorateContainer: 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

🧩 Analysis chain

Verify test coverage for the decorateContainer behavior.

The docker-container-overview component is the only mapping using decorateContainer: true. Ensure that this unique behavior is properly tested.

Run the following script to check for test coverage:


🏁 Script executed:

#!/bin/bash
# Description: Find tests that verify decorateContainer behavior

# Search for test files related to component registry or mount engine
fd -e spec.ts -e test.ts . web/src/components/Wrapper/

# Search for decorateContainer in test files
rg -nP 'decorateContainer' --type-add 'test:*.{spec,test}.{ts,js}' -t test web/

Length of output: 89


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Get context around line 248 in mount-engine.test.ts
sed -n '230,270p' web/__test__/components/Wrapper/mount-engine.test.ts

# Search for docker-container-overview or docker references in the test file
echo "=== Docker-related references ==="
rg -nP 'docker|Docker' web/__test__/components/Wrapper/mount-engine.test.ts

# Search for decorateContainer usage to see all instances
echo "=== All decorateContainer references ==="
rg -nP 'decorateContainer' -C5 web/__test__/components/Wrapper/mount-engine.test.ts

Length of output: 1859


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Search for docker-container-overview test files
echo "=== Searching for docker-container-overview tests ==="
fd -i 'docker.*container.*overview' web/__test__/

# Search for DockerContainerOverview test files
echo "=== Searching for DockerContainerOverview tests ==="
fd -i 'dockercontainer' web/__test__/

# Broader search for Docker-related tests
echo "=== Docker-related test files ==="
fd -i 'docker' web/__test__/

# Search for any references to docker-container-overview in test files
echo "=== References to docker-container-overview in tests ==="
rg -i 'docker-container-overview' web/__test__/

Length of output: 196


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find the DockerContainerOverview.standalone.vue component
echo "=== Finding Docker component ==="
fd -i 'dockercontainer.*standalone' web/src/

# Check if the component exists at the path from the import
ls -la web/src/components/Docker/DockerContainerOverview.standalone.vue 2>/dev/null || echo "File not found at expected path"

# Look for any Docker component tests
echo "=== Docker component tests ==="
fd -i 'docker' web/src/components/Docker/

# Search for component registry test patterns
echo "=== Component registry mapping tests ==="
rg -l 'component.*registry\|componentMappings' web/__test__/ -g '*.ts' -g '*.js'

Length of output: 2272


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Search for component-registry tests
echo "=== Component registry test files ==="
fd -i 'component-registry' web/__test__/

# Check what mount-engine.test.ts imports and tests
echo "=== Imports in mount-engine.test.ts ==="
head -50 web/__test__/components/Wrapper/mount-engine.test.ts | grep -E 'import|from'

# Check if mount-engine.test.ts uses real component mappings or mocks
echo "=== Mock setup in mount-engine.test.ts ==="
grep -n 'mockComponentMappings\|componentRegistry\|import.*component-registry' web/__test__/components/Wrapper/mount-engine.test.ts | head -20

Length of output: 1283


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check both component-registry test files for docker or decorateContainer
echo "=== web/__test__/components/Wrapper/component-registry.test.ts ==="
rg -i 'docker|decorateContainer' web/__test__/components/Wrapper/component-registry.test.ts -C3

echo ""
echo "=== web/__test__/components/component-registry.test.ts ==="
rg -i 'docker|decorateContainer' web/__test__/components/component-registry.test.ts -C3

# Get file sizes to understand scope
echo ""
echo "=== File sizes ==="
wc -l web/__test__/components/Wrapper/component-registry.test.ts web/__test__/components/component-registry.test.ts

Length of output: 280


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# View both component-registry test files to understand scope
echo "=== web/__test__/components/Wrapper/component-registry.test.ts ==="
cat web/__test__/components/Wrapper/component-registry.test.ts

echo ""
echo "=== web/__test__/components/component-registry.test.ts ==="
cat web/__test__/components/component-registry.test.ts

Length of output: 15429


Add docker-container-overview to the component registry test expectations.

The decorateContainer feature is tested generically in mount-engine.test.ts, validating that the flag correctly decorates parent containers. However, the new docker-container-overview component mapping is not included in the expectedAppIds list in web/test/components/Wrapper/component-registry.test.ts (line ~140-163), which means it won't be verified as part of the component registry tests. Add 'docker-container-overview' to the expectedAppIds array to ensure the new component mapping is covered.

🤖 Prompt for AI Agents
In web/__test__/components/Wrapper/component-registry.test.ts around lines 140
to 163, the test's expectedAppIds array is missing the new
'docker-container-overview' appId; update that array by adding
'docker-container-overview' so the component registry test verifies the new
mapping (decorateContainer behavior is already covered generically).

Comment on lines +307 to +311
// if (darkMode.value) {
// target.classList.add('dark');
// } else {
// target.classList.remove('dark');
// }
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.

The commented-out dark mode logic should be removed to keep the codebase clean, as the functionality is already handled by applyDarkClass on line 315.

-      // if (darkMode.value) {
-      //   target.classList.add('dark');
-      // } else {
-      //   target.classList.remove('dark');
-      // }
📝 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
// if (darkMode.value) {
// target.classList.add('dark');
// } else {
// target.classList.remove('dark');
// }
🤖 Prompt for AI Agents
In web/src/store/theme.ts around lines 307 to 311, remove the commented-out dark
mode block (the three-line if/else adding/removing 'dark' on target) because the
functionality is already handled by applyDarkClass on line 315; delete these
commented lines to keep the code clean and commit the change.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 9bcc00f to 734d7c9 Compare November 21, 2025 14:54
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 (6)
web/src/components/Docker/DockerContainerManagement.vue (1)

7-26: Ensure GET_DOCKER_CONTAINERS returns containers/portConflicts and consider switching to generated types

This component assumes that GET_DOCKER_CONTAINERS exposes:

  • docker.organizer.views[].flatEntries
  • docker.containers
  • docker.portConflicts

and wires those into containers, orphanedContainers, and the various *PortConflicts computeds. If the GraphQL document does not actually select containers and portConflicts, these areas of the UI will silently render as empty/disabled.

Given the earlier review note about a mismatch between the query and docker.containers, please double‑check that the query document now includes the required fields and that its shape matches what this component expects. Also, to avoid this drifting again, it would be safer to rely on the codegen types instead of a manual inline generic.

For example:

-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<{
-  docker: {
-    id: string;
-    organizer: {
-      views: Array<{
-        id: string;
-        name: string;
-        rootId: string;
-        prefs?: Record<string, unknown> | null;
-        flatEntries: FlatOrganizerEntry[];
-      }>;
-    };
-    containers: DockerContainer[];
-    portConflicts: DockerPortConflictsResult;
-  };
-}>(GET_DOCKER_CONTAINERS, {
+const { result, loading, refetch } = useQuery<
+  GetDockerContainersQuery,
+  GetDockerContainersQueryVariables
+>(GetDockerContainersDocument, {
   fetchPolicy: 'cache-and-network',
   variables: { skipCache: true },
 });

To verify the document matches these expectations, you can run:

#!/bin/bash
# Inspect the GetDockerContainers query to ensure it selects containers and portConflicts.

# Locate the query file
fd 'docker-containers.query' -t f

# Show the GetDockerContainers definition and check for fields
rg -n --no-heading 'GetDockerContainers' .
fd 'docker-containers.query' -t f | xargs -I{} rg -n --no-heading 'containers|portConflicts' {}

Also applies to: 169-213

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

2-3: Constructor wiring and imports largely good; clean up unused AppError and TTL naming

The DI setup for cache, config, notifications, manifests, autostart, logs, networks, and ports looks solid, and using getDockerClient() centralizes Dockerode configuration. Two small nits:

  • AppError is imported but never used—safe to remove, especially since Docker errors are already funneled through catchHandlers.docker.
  • CACHE_TTL_SECONDS combined with * 1000 at call sites is misleading given cache-manager v7 expects TTL in milliseconds; prefer a millisecond-named constant (e.g., CACHE_TTL_MS = 60_000) and pass it directly to cache.set for clarity and to avoid unit mistakes.

Also applies to: 6-7, 8-18, 19-27, 28-31, 40-48, 50-61


63-74: Container transformation is richer but port type and autostart-derived fields need small hardening

The transformContainer enrichment (autoStart*, lanIpPorts, normalized ports, etc.) is a nice improvement and aligns with the extended DockerContainer type. A few robustness tweaks:

  • Guard port.Type before calling toUpperCase(). As previously noted, Dockerode’s port entries may omit Type; calling port.Type.toUpperCase() will throw. Defaulting to 'tcp' (or ContainerPortType.TCP) before normalization avoids runtime errors.
  • lanIpPorts is populated only when port.PublicPort is set, which is fine, but you can drop the if (lanPort) check since the string is always truthy—this is harmless, just redundant.
  • The autostart-derived primaryName and autoStartEntry handling is good; just ensure getContainerPrimaryName always returns a safe string (or undefined) so ?? '' can’t introduce unexpected values into downstream mappings.

Also applies to: 82-84, 86-112, 120-138, 140-145


147-199: getContainers: error handling is solid, but templatePath mapping can throw and TTL naming is confusing

The new getContainers flow—with skipCache handling, Docker list try/catch routed through handleDockerListError, and enrichment via template mappings—is a clear improvement. Two concrete issues:

  1. Possible crash when names[0] is missing
const containerName = c.names[0]?.replace(/^\//, '').toLowerCase();

Due to operator precedence, this effectively does (c.names[0]?.replace(...)).toLowerCase(). If names[0] is undefined, .replace short-circuits to undefined, and .toLowerCase() throws. Safer pattern:

const rawName = c.names?.[0]?.replace(/^\//, '');
const containerName = rawName ? rawName.toLowerCase() : undefined;
const templatePath = containerName ? config.templateMappings?.[containerName] : undefined;

This also avoids indexing names when it might be undefined.

  1. TTL naming/units

CACHE_TTL_SECONDS = 60 then CACHE_TTL_SECONDS * 1000 in cache.set is easy to misinterpret with cache-manager v7’s millisecond TTLs. Renaming to CACHE_TTL_MS = 60_000 (and passing it directly) would match the project’s cache TTL guideline.

You can address both with a small diff local to this method and the constant definition.


276-304: Pause/unpause still lack idempotency, consistent cache invalidation, and have minor inefficiencies

These methods mostly mirror stop, but a few issues remain:

  • Not idempotent: Calling pause on an already-paused container or unpause on a running one will go straight to container.pause() / .unpause() without checking current state. That can yield unnecessary Docker errors; adding an initial inspect()-based state check and early return would make them idempotent.
  • Partial cache invalidation: Both methods only delete CONTAINER_CACHE_KEY, not CONTAINER_WITH_SIZE_CACHE_KEY, whereas clearContainerCache() invalidates both. To keep behavior consistent with start/stop, you should call clearContainerCache() here.
  • No Docker error funneling: Unlike getContainers, these methods don’t route Docker failures through catchHandlers.docker, so ENOENT/“socket unavailable” won’t be normalized the same way. Consider wrapping container.pause/unpause() in try/catch and delegating to catchHandlers.docker for consistency.
  • Redundant initial assignment: The first let containers = await this.getContainers({ skipCache: true }); is overwritten on the first loop iteration; you can drop the initial assignment and declare let updatedContainer only, as flagged by static analysis.

These concerns were raised previously and are still present in this version.

Also applies to: 306-332, 334-360


362-401: updateContainer/updateContainers/updateAllContainers are functionally correct; tighten execa usage and parallelism

The update flow (single, batch, and “update all”) is well structured and mirrors the legacy “Update All” semantics, but a couple of refinements would help:

  • Avoid shell: 'bash' and add a timeout in execa
    Spawning via { shell: 'bash' } is unnecessary here and increases risk. You’re already passing a single argument, and it’s encodeURIComponent(containerName), so you can safely invoke:

    await execa(
      '/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/update_container',
      [containerName],
      { timeout: 300_000 } // or your preferred upper bound
    );

    This removes the shell and prevents indefinite hangs.

  • Sequential updates may be slow for many containers
    updateContainers calls updateContainer in a for ... of loop with await inside, so “update all” becomes fully serial. If you expect many containers, consider a bounded parallelism approach (e.g., Promise.allSettled on small batches) to keep runtime manageable while controlling load.

  • Error reporting
    Re-throwing generic Error messages (e.g., “Failed to update container X”) is fine for GraphQL, but if you later adopt AppError or a GraphQL-specific error type, these would be good candidates to standardize.

🧹 Nitpick comments (5)
web/src/components/Docker/DockerOrphanedAlert.vue (2)

26-38: Replace native dialogs with Unraid's notification system.

The component uses native browser confirm() and alert() dialogs. Based on learnings, Unraid has a modern notification system with a custom unraid-toaster component. Consider replacing these native dialogs with the proper notification system for better UX consistency and to align with the rest of the application.

For the confirmation dialog, consider using a modal component, and for error notifications, use the toast notification service instead of alert().

Based on learnings.


18-22: Consider extracting the mutation to a separate file.

For consistency with the codebase pattern where GraphQL queries are extracted to separate .query.ts files (e.g., docker-containers.query.ts), consider extracting this mutation to a separate docker-remove-container.mutation.ts file. This improves reusability if the mutation needs to be used in other components.

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

251-262: Consider router-based navigation for Add Container when available

handleAddContainerClick always uses window.location.assign, which forces a full page reload even when the Vue router is present. If you don’t rely on a legacy full‑reload behavior for /AddContainer, you could prefer router navigation when available and fall back to window.location otherwise:

 function handleAddContainerClick() {
   if (props.disabled) return;
   if (typeof window === 'undefined') return;

   const basePathFromRoute = hasRouter && route ? route.path : null;
   const rawPath =
     basePathFromRoute && basePathFromRoute !== '/' ? basePathFromRoute : window.location.pathname;
   const sanitizedPath = rawPath.replace(/\?.*$/, '').replace(/\/+$/, '');
   const withoutAdd = sanitizedPath.replace(/\/AddContainer$/i, '');
   const targetPath = withoutAdd ? `${withoutAdd}/AddContainer` : '/AddContainer';
-  window.location.assign(targetPath);
+  if (hasRouter && router) {
+    router.push({ path: targetPath });
+  } else {
+    window.location.assign(targetPath);
+  }
 }

If the hard reload is intentional to integrate with existing non‑SPA Docker pages, then the current implementation is fine as‑is.


315-330: Avoid showing epoch timestamp when created is missing

When c.created is falsy, creationDate becomes new Date(0).toISOString(), which surfaces as 1970‑01‑01T00:00:00.000Z—a plausible but misleading value. Since other fields fall back to '—', you may want to do the same here:

 const details = computed(() => {
   const c = activeContainer.value;
   if (!c) return undefined;
   const network = c.hostConfig?.networkMode || 'bridge';
   const lanIpPort = Array.isArray(c.lanIpPorts) && c.lanIpPorts.length ? c.lanIpPorts.join(', ') : '—';
+  const creationDate =
+    typeof c.created === 'number' && c.created > 0
+      ? new Date(c.created * 1000).toISOString()
+      : '—';
   return {
     network,
     lanIpPort,
     containerIp: c.networkSettings?.IPAddress || '—',
     uptime: '—',
     containerPort: c.ports?.[0]?.privatePort?.toString?.() || '—',
-    creationDate: new Date((c.created || 0) * 1000).toISOString(),
+    creationDate,
     containerId: c.id,
     maintainer: c.labels?.maintainer || '—',
   };
 });
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)

229-235: Lifecycle helpers are mostly consistent; consider aligning cache invalidation and return types

The new lifecycle helpers integrate well with the existing start/stop flow:

  • removeContainer correctly forces removal, clears caches, logs, recomputes appInfo, and publishes INFO, mirroring start/stop. Wrapping in try/catch is good, though it may be worth surfacing a typed error (or leveraging catchHandlers.docker) instead of a generic Error to be consistent with other Docker paths.
  • updateAutostartConfiguration refreshes config and then clears caches; that’s appropriate, though you may want to publish an INFO event if any UI elements depend on autostart counts.

Minor nit: clearContainerCache() logs “due to external event” even when triggered internally; if you care about log accuracy, consider differentiating the messages or adding an optional reason parameter.

Also applies to: 237-250, 252-265, 267-274

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d5c3b8 and 734d7c9.

📒 Files selected for processing (9)
  • api/generated-schema.graphql (8 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (8 hunks)
  • web/components.d.ts (5 hunks)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
  • web/src/components/Docker/DockerOrphanedAlert.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts
🧰 Additional context used
🧠 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.
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.
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.
📚 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/DockerOrphanedAlert.vue
  • web/components.d.ts
  • web/src/components/Docker/docker-containers.query.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/src/components/Docker/DockerContainerManagement.vue
  • web/components.d.ts
  • web/src/components/Docker/docker-containers.query.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.mutations.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/docker/docker.mutations.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/docker/docker.mutations.resolver.ts
📚 Learning: 2024-11-01T17:52:55.311Z
Learnt from: pujitm
Repo: unraid/api PR: 941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.

Applied to files:

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

Applied to files:

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

Applied to files:

  • web/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-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: 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-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-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: 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/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
📚 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-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-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: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

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/components/Docker/docker-containers.query.ts
  • web/src/composables/gql/gql.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-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-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/composables/gql/gql.ts
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.ts (3)
web/src/composables/gql/graphql.ts (2)
  • DockerContainer (739-772)
  • DockerAutostartEntryInput (730-737)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/decorators/use-feature-flag.decorator.ts (1)
  • UseFeatureFlag (20-22)
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 (691-701)
  • DockerContainer (739-772)
api/src/core/utils/network.ts (1)
  • getLanIp (6-19)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (79-108)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
🪛 GitHub Actions: CI - Main (API)
web/src/components/Docker/DockerOrphanedAlert.vue

[error] 12-12: TS6133: 'props' is declared but its value is never read.

🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[notice] 8-8: Unused variable, import, function or class
Unused import AppError.


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


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

🔇 Additional comments (14)
web/src/components/Docker/docker-containers.query.ts (1)

3-96: Query structure looks comprehensive and addresses previous concerns.

The query now includes both the containers field (lines 26-48) and the organizer field (lines 49-93), which resolves the previous review feedback about missing container data. The field selections are consistent and the skipCache parameter is properly threaded through all relevant fields.

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

40-42: Good defensive coding in name formatting.

The formatContainerName function properly handles edge cases with optional chaining and a fallback value, and correctly strips Docker's leading slash convention from container names.

web/components.d.ts (6)

23-23: BaseTreeTable global component declaration looks correct

The new BaseTreeTable entry follows the same import/typing pattern as other Common components and points at the expected Common/BaseTreeTable.vue path. No issues spotted.


40-41: ContainerSizesModal and CriticalNotifications.standalone are wired consistently

Both ContainerSizesModal and 'CriticalNotifications.standalone' use the standard typeof import(...).default pattern and align with existing Docker/Notifications component naming. Nothing concerning here.


51-61: Docker overview and management components are correctly exposed as global components

The new Docker-related entries (autostart, management, overview, table, stat cell, log viewer modal, name cell, orphaned/port-conflict alerts, sidebar tree) are all typed consistently and mapped to plausible Docker/*.vue files, which matches the PR’s Docker overview refactor.


94-94: MultiValueCopyBadges declaration is consistent with Common component conventions

MultiValueCopyBadges is added under Common/ with the same pattern as other shared UI helpers, so TS global typing will pick it up correctly.


119-119: TableColumnMenu global typing aligns with new table UX

TableColumnMenu is declared under Common/TableColumnMenu.vue and matches the existing style of common-table utilities in this file, so templates using it should now type-check cleanly.


125-149: Nuxt UI U* component declarations are consistent with existing generated entries

The UBadge, UButton, UCard, UCheckbox, UDrawer, UDropdownMenu, UFormField, UIcon, UInput, UModal, UNavigationMenu, UPopover, USelectMenu, USkeleton, USwitch, UTable, and UTabs entries all follow the same generated pattern, pointing into the pnpm-resolved @nuxt/ui runtime component paths. Given this file is generated by unplugin-vue-components, these additions look in sync with the current Nuxt UI setup; no manual adjustments needed.

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

70-167: Route/query syncing and router-less fallback look solid

The two-way sync between activeId and the container query param using normalizeContainerQuery, the syncingFromRoute flag, and the router vs window.history branches is clean and avoids feedback loops while staying SSR-safe. No changes requested here.

api/generated-schema.graphql (1)

859-887: Schema additions for Docker, notifications, and organizer look consistent with resolvers and models

The new fields and types (DockerMutations, DockerAutostartEntryInput, DockerPortConflicts*, DockerContainer size/log/LAN fields, PAUSED state, logs/stats, DockerContainerOverviewForm, notification types/filters, organizer flatEntries/rootId/prefs, organizer mutations, notifyIfUnique, and related subscriptions) align with the resolver and model changes in this PR. Since this file is generated, just ensure:

  • All schema changes originate from the source SDL (e.g., *.graphql files / decorators) rather than manual edits here.
  • Codegen for TS client types and gql documents has been re-run after updating the schema so frontend and backend stay in sync.

Also applies to: 888-897, 1115-1136, 1142-1181, 1183-1187, 1207-1239, 1241-1253, 1255-1260, 1262-1319, 1321-1326, 1328-1353, 2470-2552, 2554-2615, 2617-2623, 2792-2806

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

31-86: Typed Document map and graphql overloads look coherent

The added Docker/notifications operations are wired consistently across Documents, the documents map, and the graphql(...) overloads, matching the generated document types in ./graphql.ts. Given this file is codegen output and duplication is intentional, no changes needed—just ensure it’s regenerated whenever you add/rename GraphQL operations so the string keys, Documents map, and overload signatures remain in lockstep.

Also applies to: 102-157, 171-448

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

7-11: Docker mutation resolvers are thin and correctly permissioned

The new pause/unpause, removeContainer, updateAutostartConfiguration, and update* mutations cleanly delegate to DockerService, use @ResolveField per nested-mutation pattern, and apply UsePermissions on the DOCKER resource (with UseFeatureFlag gating the autostart mutation). This matches the generated schema signatures and keeps auth concerns consistent; no changes needed—just ensure the ENABLE_NEXT_DOCKER_RELEASE flag key matches your feature-flag config.

Also applies to: 39-54, 56-83, 85-117

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

201-219: New helpers for port conflicts, logs, and networks look clean

getPortConflicts, getContainerLogSizes, getContainerLogs, and getNetworks are thin delegations to specialized services, which keeps this class focused and testable. The signatures line up with the GraphQL schema types (DockerPortConflicts, DockerContainerLogs, DockerNetwork). No changes needed here.

Also applies to: 225-227


447-451: Docker list error handling and notifications are a nice improvement

handleDockerListError and its helpers (notifyDockerListError, getDockerErrorMessage) provide a solid pattern:

  • User-facing notifications via notificationsService.notifyIfUnique with truncated subject/description.
  • Technical classification through catchHandlers.docker for ENOENT/socket issues.
  • Defensive handling of unknown error shapes.

This should make Docker socket failures much more discoverable without spamming logs. No changes needed here.

Also applies to: 453-479

Comment on lines +242 to +245
function openAutostartSettings() {
if (props.disabled) return;
viewMode.value = 'autostart';
}
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

Align “Customize Start Order” button disabled state with handler guard

openAutostartSettings early‑returns when props.disabled is true, but the button only disables on loading. That means in the disabled && !loading case the button looks clickable but does nothing.

Consider disabling the button when either loading or disabled:

-        <UButton
-          size="xs"
-          variant="outline"
-          icon="i-lucide-list-checks"
-          :disabled="loading"
-          @click="openAutostartSettings"
-        >
+        <UButton
+          size="xs"
+          variant="outline"
+          icon="i-lucide-list-checks"
+          :disabled="loading || props.disabled"
+          @click="openAutostartSettings"
+        >
           Customize Start Order
         </UButton>

Also applies to: 361-368

🤖 Prompt for AI Agents
In web/src/components/Docker/DockerContainerManagement.vue around lines 242-245
(and similarly at 361-368), the openAutostartSettings handler guards on
props.disabled but the corresponding "Customize Start Order" button only
disables on loading, so it can appear clickable yet do nothing; update the
button disabled binding to disable when either loading or props.disabled (e.g.,
use loading || props.disabled) and ensure any tooltip/aria-disabled state
matches this combined condition so the UI accurately reflects the handler's
guard.

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

34-34: Incomplete meta fields in organizer mutations will cause Apollo cache corruption.

Several organizer mutations (CreateDockerFolderWithItems, MoveDockerItemsToPosition, RenameDockerFolder, UpdateDockerViewPreferences) return meta objects but omit critical fields present in the GetDockerContainers query: templatePath, networkSettings, mounts, lanIpPorts, isOrphaned, projectUrl, registryUrl, supportUrl, and iconUrl. When Apollo writes these mutation responses to the cache, it overwrites the complete cached entries with incomplete data, causing fields like templatePath to vanish and Docker overview icons to disappear until a full refetch.

Update the source GraphQL mutation documents (likely in .graphql files) to include these fields in their selection sets, then regenerate this file with GraphQL CodeGen.

Also applies to: 39-39, 43-43, 53-53

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

59-77: syncMissingContainers will rescan templates on every call for containers that have no matching template.

Right now:

  • scanTemplates writes newMappings[containerName] = null when no template matches.
  • syncMissingContainers tests !mappings[containerName], so both undefined and null are treated as "missing".
  • For containers that genuinely have no template, you'll trigger a full scanTemplates() on every resolver call, which is unnecessary work.

You can treat "present but null" as "already scanned, no match" by checking key existence instead of truthiness:

     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);
     });

Based on learnings


231-233: 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();
+    // 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();
 }

Based on learnings

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

48-48: Rename constant and consolidate TTL to milliseconds for clarity.

cache-manager v7 requires TTL values in milliseconds. The current code has a naming/clarity issue: CACHE_TTL_SECONDS = 60 is then multiplied by 1000 at call sites (lines 193-197, and elsewhere), making it error-prone.

Rename the constant to CACHE_TTL_MS = 60_000 and remove the multiplication:

-    public static readonly CACHE_TTL_SECONDS = 60;
+    public static readonly CACHE_TTL_MS = 60_000;

Then update call sites (lines 193-197, etc.):

-            DockerService.CACHE_TTL_SECONDS * 1000
+            DockerService.CACHE_TTL_MS

Based on learnings


104-110: Fix: guard port.Type before toUpperCase().

port.Type can be undefined; calling toUpperCase() throws. Default to 'tcp' first.

Apply:

         type:
-                ContainerPortType[port.Type.toUpperCase() as keyof typeof ContainerPortType] ||
-                ContainerPortType.TCP,
+            ContainerPortType[
+                ((port.Type ?? 'tcp') as string).toUpperCase() as keyof typeof ContainerPortType
+            ] || ContainerPortType.TCP,

Based on learnings


183-191: Fix: avoid toLowerCase() on possibly undefined container name.

c.names[0] may be undefined; current code can throw. Also avoid indexing with undefined.

     const containersWithTemplatePaths = containers.map((c) => {
-        const containerName = c.names[0]?.replace(/^\//, '').toLowerCase();
+        const rawName = c.names?.[0]?.replace(/^\//, '');
+        const containerName = rawName ? rawName.toLowerCase() : undefined;
         const templatePath = config.templateMappings?.[containerName] || undefined;
         return {
             ...c,
             templatePath,
             isOrphaned: !templatePath,
         };
     });

Based on learnings


306-332: Add idempotency and error handling for pause, and invalidate both caches.

  • If already paused, return early (idempotent).
  • Wrap Docker calls in try/catch and route through catchHandlers.docker for consistent errors.
  • Call clearContainerCache() instead of only deleting CONTAINER_CACHE_KEY to invalidate both caches.
  • Drop the initial containers fetch (useless assignment flagged by static analysis) and poll immediately.
 public async pause(id: string): Promise<DockerContainer> {
     const container = this.client.getContainer(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 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);

Based on learnings


334-360: Mirror the same fixes for unpause.

  • Invalidate both caches via clearContainerCache().
  • Add idempotency (if already running) + try/catch.
  • Remove initial unnecessary containers 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}`);
+    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);

Based on learnings


8-8: Remove unused import.

AppError is imported but never used in this file.

-import { AppError } from '@app/core/errors/app-error.js';
api/src/unraid-api/graph/resolvers/docker/docker.model.ts (1)

347-360: Add validation decorators to DockerAutostartEntryInput.

The InputType is missing class-validator decorators that are standard in this codebase. Without these, invalid input could reach the resolver unchecked.

Apply this diff to add validation:

+import { IsBoolean, IsInt, IsOptional, IsString, Min } from 'class-validator';
+
 @InputType()
 export class DockerAutostartEntryInput {
     @Field(() => PrefixedID, { description: 'Docker container identifier' })
+    @IsString()
     id!: string;

     @Field(() => Boolean, { description: 'Whether the container should auto-start' })
+    @IsBoolean()
     autoStart!: boolean;

     @Field(() => Int, {
         nullable: true,
         description: 'Number of seconds to wait after starting the container',
     })
+    @IsOptional()
+    @IsInt()
+    @Min(0)
     wait?: number | null;
 }

Based on learnings

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

650-670: Replace window.prompt with a non‑blocking “Create Folder” modal

createFolderFromDrop still uses window.prompt:

const name = window.prompt('New folder name?')?.trim();
if (!name) return;

This blocks the UI thread, doesn’t match app styling, and is hard to make accessible. There’s already modal infrastructure in this component (e.g., the move/rename folder modal) that you can reuse for this “create folder from drop” flow.

Refactor to:

  • Track createFolderModalOpen and newFolderNameForDrop in reactive state.
  • Open the modal instead of calling prompt() inside createFolderFromDrop.
  • Move the folder creation logic into the modal’s confirm handler, including validation and the existing mutation/refetch/toast.

This will give a consistent, accessible UX for folder creation via drag‑and‑drop.


220-222: Align window.toast typings with showToast / showError usage

Window.toast currently requires an options argument, but both helpers call it in ways that violate the declared type:

  • Line 220: showError(message: string, options?: { description?: string }) can pass options as undefined.
  • Line 569: showToast(message: string) calls window.toast?.success(message) with only a single argument.

This will either surface as TypeScript errors or force callers to lie about the API.

Consider updating the global declaration to make the options parameter optional so it matches actual usage:

 declare global {
   interface Window {
     toast?: {
-      success: (title: string, options: { description?: string }) => void;
-      error?: (title: string, options: { description?: string }) => void;
+      success: (title: string, options?: { description?: string }) => void;
+      error?: (title: string, options?: { description?: string }) => void;
     };
   }
 }

You may also want showError to log to console.error in addition to using toast, per prior guidance about not relying solely on toast inside error handlers.

Also applies to: 560-571

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (1)

224-229: Consider adding test coverage for icon resolution.

The mock provider is correctly configured and uses Vitest's vi.fn() as expected. However, while the tests verify folder operations and resource synchronization, none currently exercise the icon resolution functionality introduced with DockerTemplateIconService. Based on learnings

Consider adding at least one test case that:

  • Verifies getIconsForContainers is called during organizer operations
  • Validates that icon data flows correctly through the organizer resolution
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 734d7c9 and 6c61cd8.

📒 Files selected for processing (12)
  • api/generated-schema.graphql (8 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (10 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (7 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (6 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/DockerOrphanedAlert.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Docker/DockerOrphanedAlert.vue
🧰 Additional context used
🧠 Learnings (15)
📓 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-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/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.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
  • web/src/components/Docker/docker-containers.query.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/src/components/Docker/DockerContainersTable.vue
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts
  • web/src/components/Docker/docker-containers.query.ts
📚 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-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
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts
  • web/src/components/Docker/docker-containers.query.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/composables/gql/gql.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-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-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: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-04-21T18:44:15.414Z
Learnt from: elibosley
Repo: unraid/api PR: 1352
File: packages/unraid-api-plugin-connect/src/config.entity.ts:16-26
Timestamp: 2025-04-21T18:44:15.414Z
Learning: For the Unraid API project, class-validator should be used for validation to avoid mismatches between different validation schemas (like Zod).

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.model.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-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
🧬 Code graph analysis (5)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (2)
api/src/unraid-api/utils/graphql-field-helper.ts (1)
  • GraphQLFieldHelper (13-63)
web/src/composables/gql/graphql.ts (2)
  • DockerContainer (739-780)
  • DockerContainerLogs (788-794)
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 (40-480)
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 (79-108)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (3)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • Resolver (44-341)
web/src/composables/gql/graphql.ts (1)
  • DockerContainer (739-780)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
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/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (79-108)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
api/src/unraid-api/graph/resolvers/docker/docker.model.ts (2)
web/src/composables/gql/graphql.ts (9)
  • DockerPortConflictContainer (916-920)
  • DockerContainerPortConflict (804-809)
  • DockerLanPortConflict (826-832)
  • DockerPortConflicts (922-926)
  • DockerContainerLogLine (782-786)
  • DockerContainerLogs (788-794)
  • DockerContainerStats (811-824)
  • DockerContainerOverviewForm (796-802)
  • DockerAutostartEntryInput (730-737)
api/src/unraid-api/types/json-forms.ts (1)
  • DataSlice (21-21)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[notice] 8-8: Unused variable, import, function or class
Unused import AppError.


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


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

🔇 Additional comments (15)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (2)

5-5: LGTM!

The import is correctly added to support mocking DockerTemplateIconService in the test setup.


42-42: LGTM!

The isOrphaned: false field is consistently added across all test fixtures, correctly aligning with the extended DockerContainer model.

Also applies to: 67-67, 89-89, 131-131

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

48-48: LGTM!

Test fixtures correctly updated with the new isOrphaned field to match the extended DockerContainer interface.

Also applies to: 69-69

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

6-106: LGTM!

Test setup correctly extended with new service mocks (DockerFormService, DockerTemplateScannerService, DockerStatsService, subscription services) and appropriate mock implementations. The use of mockImplementation instead of mockReturnValue for isFieldRequested properly supports the field-conditional test logic.


189-236: LGTM!

New test coverage for sizeRw and sizeLog fields correctly validates:

  • Field detection via GraphQLFieldHelper.isFieldRequested
  • Proper flag passing to getContainers (size flag)
  • Log size fetching and assignment logic

275-291: LGTM!

Container logs test correctly validates the resolver passes through arguments (since, tail) to the service method and returns the expected DockerContainerLogs result.

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

16-19: LGTM!

Constructor correctly wired with DockerTemplateScannerService dependency alongside the existing DockerManifestService.


46-86: LGTM!

The new resolver fields (projectUrl, registryUrl, supportUrl) follow a consistent, safe pattern:

  • Guard against missing templatePath
  • Fetch details from DockerTemplateScannerService
  • Return the appropriate field or null

All properly decorated with feature flag and permissions.


88-103: LGTM!

The iconUrl resolver correctly prioritizes the container label (net.unraid.docker.icon) over the template-derived icon, falling back to template details when the label is absent. This provides flexibility for users to override icons via labels.

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

43-86: LGTM!

Port conflict types (DockerPortConflictContainer, DockerContainerPortConflict, DockerLanPortConflict, DockerPortConflicts) are well-defined with proper GraphQL decorators and clear field types.


151-218: LGTM!

DockerContainer extensions properly defined:

  • LAN port information (lanIpPorts)
  • Size metrics (sizeRw, sizeLog)
  • Autostart configuration (autoStartOrder, autoStartWait)
  • Template metadata (templatePath, projectUrl, registryUrl, supportUrl, iconUrl)
  • Orphaned state (isOrphaned)

All fields include appropriate descriptions and nullable/non-nullable settings.


266-310: LGTM!

Container logs and stats types properly defined:

  • DockerContainerLogLine with timestamp and message
  • DockerContainerLogs with containerId, lines array, and cursor for pagination
  • DockerContainerStats with comprehensive metrics (CPU, memory, network I/O, block I/O)

All use appropriate GraphQL scalars (DateTime, PrefixedID, Float, String).


322-345: LGTM!

New Docker fields and DockerContainerOverviewForm properly defined:

  • portConflicts exposed on Docker type
  • logs field with clear description
  • DockerContainerOverviewForm follows JSON Forms schema pattern with dataSchema, uiSchema, and data fields
api/generated-schema.graphql (1)

1-3: Auto-generated schema reflects model updates.

This file is auto-generated from the GraphQL type definitions and correctly reflects the Docker surface expansions (pause/unpause mutations, new container fields, port conflicts, logs, stats, notifications, organizer, etc.). No manual review needed.

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

3-104: GET_DOCKER_CONTAINERS shape matches frontend needs

The query now fetches portConflicts, containers(skipCache: $skipCache), and organizer(skipCache: $skipCache).views.flatEntries.meta with the fields used in the Docker table and related UIs. Wiring of skipCache is consistent across subfields, and the selection looks coherent for the described consumers.

No issues from a schema/consumer perspective.

Comment on lines +64 to +67
const needsSync = containers.filter((c) => {
const containerName = this.normalizeContainerName(c.names[0]);
return !mappings[containerName] && !skipSet.has(containerName);
});
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

Guard against empty names array before normalizing.

Line 65 accesses c.names[0] without verifying the array is non-empty. If a container has an empty names array, this will pass undefined to normalizeContainerName, which will then call .replace() on undefined and throw.

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);
     });
📝 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 needsSync = containers.filter((c) => {
const containerName = this.normalizeContainerName(c.names[0]);
return !mappings[containerName] && !skipSet.has(containerName);
});
const needsSync = containers.filter((c) => {
if (!c.names?.[0]) return false;
const containerName = this.normalizeContainerName(c.names[0]);
return !mappings[containerName] && !skipSet.has(containerName);
});
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
around lines 64 to 67, the code accesses c.names[0] without confirming c.names
exists or has elements; update the filter to first guard that c.names is an
array with at least one entry (or that c.names?.length > 0) before calling
normalizeContainerName, and skip containers with empty or missing names (or pass
a safe default) so normalizeContainerName never receives undefined; ensure the
check short-circuits before any call to .replace() inside
normalizeContainerName.

Comment on lines +97 to +111
for (const container of containers) {
const containerName = this.normalizeContainerName(container.names[0]);
if (skipSet.has(containerName)) {
result.skipped++;
continue;
}

const match = this.matchContainerToTemplate(container, templates);
if (match) {
newMappings[containerName] = match.filePath;
result.matched++;
} else {
newMappings[containerName] = null;
}
}
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

Guard against empty names array in scanTemplates loop.

Line 98 accesses container.names[0] without verifying it exists. This can 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]);
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts
around lines 97 to 111, the loop assumes container.names[0] exists and will
throw if names is empty; before accessing names[0] check that container.names is
an array with length > 0 and handle the empty case (e.g., increment
result.skipped and continue, or assign a safe default name and proceed) so you
never access undefined; apply the same guard consistently wherever
container.names[0] is read and keep result counters and newMappings behavior
aligned with the chosen handling.

Comment on lines 353 to 406
accessorKey: 'links',
header: 'Links',
cell: ({ row }) => {
if (row.original.type === 'folder') return '';
const meta = row.original.meta;
const projectUrl = meta?.projectUrl;
const registryUrl = meta?.registryUrl;
const supportUrl = meta?.supportUrl;
if (!projectUrl && !registryUrl && !supportUrl) return '';
return h('div', { class: 'flex gap-2 items-center' }, [
projectUrl
? h(
'a',
{
href: projectUrl,
target: '_blank',
title: 'Project Page',
class:
'text-gray-500 hover:text-primary-500 dark:text-gray-400 dark:hover:text-primary-400',
},
h('span', { class: 'i-lucide-globe w-4 h-4 block' })
)
: null,
registryUrl
? h(
'a',
{
href: registryUrl,
target: '_blank',
title: 'Registry',
class:
'text-gray-500 hover:text-primary-500 dark:text-gray-400 dark:hover:text-primary-400',
},
h('span', { class: 'i-lucide-external-link w-4 h-4 block' })
)
: null,
supportUrl
? h(
'a',
{
href: supportUrl,
target: '_blank',
title: 'Support',
class:
'text-gray-500 hover:text-primary-500 dark:text-gray-400 dark:hover:text-primary-400',
},
h('span', { class: 'i-lucide-life-buoy w-4 h-4 block' })
)
: null,
]);
},
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 external links opened in new tabs

The “Links” column anchors use target="_blank" without rel:

target: '_blank',

To avoid window.opener leaks and align with common security best practices, add rel="noopener noreferrer" (or at least noopener) to each external link:

-  target: '_blank',
+  target: '_blank',
+  rel: 'noopener noreferrer',

This should be applied to the project, registry, and support links.

🤖 Prompt for AI Agents
In web/src/components/Docker/DockerContainersTable.vue around lines 353 to 405,
the anchor elements for project, registry, and support links open in new tabs
using target="_blank" but lack rel attributes; update each anchor props to
include rel: 'noopener noreferrer' (or at minimum 'noopener') so external links
opened in new tabs do not expose window.opener and follow security best
practices.

Comment on lines +797 to +827
function handleRowAction(row: TreeRow<DockerContainer>, action: string) {
if (row.type !== 'container') return;
const containerId = (row as { containerId?: string }).containerId || row.id;
if (action === 'Start / Stop') {
containerActions.handleRowStartStop(row);
return;
}
if (action === 'Pause / Resume') {
containerActions.handleRowPauseResume(row);
return;
}
if (action === 'View logs') {
if (!containerId) return;
logs.openLogsForContainers([{ id: containerId, label: getRowDisplayLabel(row, row.name) }], {
reset: false,
});
return;
}
if (action === 'Manage Settings') {
const container = row.meta as DockerContainer | undefined;
emit('row:click', {
id: row.id,
type: 'container',
name: row.name,
containerId: container?.id,
});
return;
}
showToast(`${action}: ${row.name}`);
}
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

Don’t fall back to organizer entry ID when opening logs

handleRowAction derives containerId as:

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

After the earlier change to use organizer entry IDs for row.id, this fallback means:

  • If row.containerId is missing, containerId becomes the organizer entry ID, not the Docker container ID.
  • logs.openLogsForContainers will then be called with an incorrect ID, likely breaking log retrieval.

Given that log helpers and stats use the real container ID (e.g., stat.id, meta.id), this should rely solely on row.containerId and bail if it’s absent:

-function handleRowAction(row: TreeRow<DockerContainer>, action: string) {
-  if (row.type !== 'container') return;
-  const containerId = (row as { containerId?: string }).containerId || row.id;
+function handleRowAction(row: TreeRow<DockerContainer>, action: string) {
+  if (row.type !== 'container') return;
+  const containerId = (row as { containerId?: string }).containerId;
@@
   }
   if (action === 'View logs') {
-    if (!containerId) return;
-    logs.openLogsForContainers([{ id: containerId, label: getRowDisplayLabel(row, row.name) }], {
+    if (!containerId) return;
+    logs.openLogsForContainers([{ id: containerId, label: getRowDisplayLabel(row, row.name) }], {
       reset: false,
     });
     return;
   }

This keeps organizer entry IDs and Docker container IDs clearly separated.

🤖 Prompt for AI Agents
In web/src/components/Docker/DockerContainersTable.vue around lines 797 to 826,
the code currently falls back to row.id (organizer entry ID) when
row.containerId is missing, causing logs.openLogsForContainers to be called with
an organizer ID instead of the real Docker container ID; change the logic so
containerId is read only from row.containerId (no fallback to row.id) and add an
early return (or toast/error) if containerId is falsy before calling
logs.openLogsForContainers, ensuring logs.openLogsForContainers is only ever
invoked with the actual Docker container ID.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 6c61cd8 to 802901c Compare November 21, 2025 15:55
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 (16)
web/src/utils/docker.ts (1)

52-57: Verify type field availability.

Similar to formatExternalPorts, this function accesses port.type at line 56 without explicit verification in the type guard.

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

33-36: Organizer mutations return only a subset of overview meta fields—confirm this still matches UI needs

The organizer-related Docker mutations (CreateDockerFolderWithItems, MoveDockerItemsToPosition, RenameDockerFolder, UpdateDockerViewPreferences) are selecting a reduced meta shape compared to GetDockerContainers (they omit fields like lanIpPorts, networkSettings, mounts, templatePath, isOrphaned, projectUrl, registryUrl, supportUrl, iconUrl, etc.), while GetDockerContainers returns the full overview meta. If any part of the new Docker overview component still reads those omitted fields from views.flatEntries.meta, newly written/reshuffled entries might not have all the data without an extra refetch or additional query.

Given this is similar to a previously raised concern about templatePath and overview meta, it’s worth double-checking that the current UI only relies on the subset you’re returning here, or else aligning these selection sets with GetDockerContainers for cache consistency.

Also applies to: 43-44, 53-53, 104-107, 114-115, 124-124, 240-252, 280-284, 320-320

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

6-24: Keep TreeRow<T> generic by removing Docker‑specific fields

TreeRow<T> is declared as a generic tree row but includes Docker‑specific properties (state, version, network, containerIp, ports, volumes, autoStart, updates, uptime, containerId). This tightly couples the composable to Docker and makes reuse for other trees awkward.

Either:

  • Strip TreeRow<T> down to generic fields (id, type, name, children, meta, icon) and define a separate DockerTreeRow extending it for Docker use cases, or
  • Make the composable explicitly Docker‑focused (e.g. useDockerTreeData) if that’s the only intended consumer.

3-4: Add .js extension to ESM import

Per this repo’s ESM convention, the type import should include the .js extension so the compiled module resolves correctly:

-import type { FlatOrganizerEntry } from '@/composables/gql/graphql';
+import type { FlatOrganizerEntry } from '@/composables/gql/graphql.js';
api/src/unraid-api/graph/resolvers/docker/docker.service.ts (4)

306-360: Use clearContainerCache() and remove redundant initial fetch in pause/unpause

In pause and unpause:

  • You invalidate only CONTAINER_CACHE_KEY via cacheManager.del, leaving CONTAINER_WITH_SIZE_CACHE_KEY potentially stale, whereas other methods use clearContainerCache() to invalidate both.
  • The initial let containers = await this.getContainers({ skipCache: true }); before the polling loop is redundant—the variable is immediately overwritten in the first iteration—triggering static-analysis warnings.

Consider:

     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 container.pause();
+        await this.clearContainerCache();
+        this.logger.debug(`Invalidated container caches after pausing ${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);
@@
     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 container.unpause();
+        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);

This keeps both caches consistent and removes the useless initial assignment.

Also applies to: 334-360


362-385: Avoid shell: 'bash' for execa and add a timeout

updateContainer invokes execa with { shell: 'bash' }, which is unnecessary and increases surface area for shell‑injection issues. Since you’re calling a fixed script path with a single argument, you can invoke it directly without a shell and add a timeout to prevent hangs:

-        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 } // 5 minutes
+            );

This keeps behavior but avoids an extra shell layer and ensures long‑running updates don’t hang indefinitely.

Confirm recommended security practices for using execa with and without the `shell` option when executing fixed scripts with arguments.

86-113: Guard container port/name usage to avoid runtime exceptions and bad mappings

There are two unsafe accesses here that can throw or misbehave at runtime:

  1. Port type mapping (Line 108)
    port.Type may be undefined, so port.Type.toUpperCase() can throw.

  2. Container name normalization (Lines 184–185)
    c.names[0]?.replace(...) is wrapped in optional chaining only for .replace, but .toLowerCase() is called unconditionally; when names[0] is missing this will throw. It also means containers without names can’t be safely processed for template mappings.

Consider something along these lines:

-        const transformedPorts = uniquePorts.map((port) => {
+        const transformedPorts = uniquePorts.map((port) => {
+            const rawType = (port.Type ?? 'tcp') as string;
+            const typeKey = rawType.toUpperCase() as keyof typeof ContainerPortType;
             if (port.PublicPort) {
@@
-            return {
+            return {
                 ip: port.IP || '',
                 privatePort: port.PrivatePort,
                 publicPort: port.PublicPort,
-                type:
-                    ContainerPortType[port.Type.toUpperCase() as keyof typeof ContainerPortType] ||
-                    ContainerPortType.TCP,
+                type: ContainerPortType[typeKey] ?? ContainerPortType.TCP,
             };
         });
@@
-        const containersWithTemplatePaths = containers.map((c) => {
-            const containerName = c.names[0]?.replace(/^\//, '').toLowerCase();
-            const templatePath = config.templateMappings?.[containerName] || undefined;
+        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,
+                isOrphaned: !templatePath,
             };
         });

This prevents crashes when Docker returns containers without a name or port type and keeps template mapping logic robust.

Also applies to: 182-199


8-8: Remove unused AppError import

AppError is imported but never used in this service. It should be removed to keep the file clean and avoid confusing readers about intended error handling.

-import { AppError } from '@app/core/errors/app-error.js';
web/src/components/Docker/DockerContainersTable.vue (4)

651-671: Avoid window.prompt for createFolderFromDrop

createFolderFromDrop uses window.prompt, which is blocking, hard to style, and poor for accessibility:

const name = window.prompt('New folder name?')?.trim();

Prefer a non‑blocking modal (similar to the move/rename dialogs you already have) that:

  • Opens when creating a folder from a drop,
  • Lets the user input and confirm the folder name,
  • Validates non‑empty/trimmed names,
  • Cancels cleanly.

This keeps UX consistent and avoids blocking the main thread.


354-406: Add rel="noopener noreferrer" to external links

The “Links” column uses target="_blank" without a rel attribute, which can expose window.opener to the target page.

Update these anchors to include rel="noopener noreferrer":

-                {
-                  href: projectUrl,
-                  target: '_blank',
+                {
+                  href: projectUrl,
+                  target: '_blank',
+                  rel: 'noopener noreferrer',
@@
-                {
-                  href: registryUrl,
-                  target: '_blank',
+                {
+                  href: registryUrl,
+                  target: '_blank',
+                  rel: 'noopener noreferrer',
@@
-                {
-                  href: supportUrl,
-                  target: '_blank',
+                {
+                  href: supportUrl,
+                  target: '_blank',
+                  rel: 'noopener noreferrer',

561-568: Fix window.toast type to match actual usage

The global window.toast type requires an options argument, but showToast calls window.toast?.success(message) with a single parameter. This is a type mismatch and will either fail type‑checking or force any casts.

Make the options parameter optional:

 declare global {
   interface Window {
     toast?: {
-      success: (title: string, options: { description?: string }) => void;
-      error?: (title: string, options: { description?: string }) => void;
+      success: (title: string, options?: { description?: string }) => void;
+      error?: (title: string, options?: { description?: string }) => void;
     };
   }
 }

Also applies to: 570-572


798-827: Don’t fall back to organizer entry ID for container actions/logs

handleRowAction derives containerId as:

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

After moving to organizer entry IDs for row.id, this fallback can send the organizer ID (not the Docker container ID) into log and action handlers when containerId is missing, breaking log retrieval and any ID‑based Docker actions.

Use only row.containerId and bail if it’s absent:

 function handleRowAction(row: TreeRow<DockerContainer>, action: string) {
   if (row.type !== 'container') return;
-  const containerId = (row as { containerId?: string }).containerId || row.id;
+  const containerId = (row as { containerId?: string }).containerId;
@@
   if (action === 'View logs') {
-    if (!containerId) return;
-    logs.openLogsForContainers([{ id: containerId, label: getRowDisplayLabel(row, row.name) }], {
+    if (!containerId) return;
+    logs.openLogsForContainers([{ id: containerId, label: getRowDisplayLabel(row, row.name) }], {
       reset: false,
     });
     return;
   }
api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1)

59-67: Harden template sync against empty names and incorrect repository normalization

There are a few issues here that can cause runtime errors or incorrect behavior:

  1. Unsafe names[0] access
  • syncMissingContainers, scanTemplates, and matchContainerToTemplate all call this.normalizeContainerName(c.names[0]) without checking that names[0] exists. Containers can have an empty names array, which will make normalizeContainerName call .replace() on undefined and throw.
  1. Using truthiness on mappings to detect “missing”
  • syncMissingContainers uses !mappings[containerName] to decide if a container needs sync, but scanTemplates stores null for containers that have been scanned and had no matching template. Both undefined and null are falsy, so you will re‑scan on every resolver call for containers that are already known to be “no template”.
  1. Repository normalization breaks images with ports/digests
  • normalizeRepository(repository: string) { return repository.split(':')[0].toLowerCase(); } truncates registry:5000/repo:tag to registry, which is wrong, and doesn’t strip digests like @sha256:....

A concrete fix could look like this:

-    async syncMissingContainers(containers: DockerContainer[]): Promise<boolean> {
+    async syncMissingContainers(containers: DockerContainer[]): Promise<boolean> {
@@
-        const needsSync = containers.filter((c) => {
-            const containerName = this.normalizeContainerName(c.names[0]);
-            return !mappings[containerName] && !skipSet.has(containerName);
-        });
+        const needsSync = containers.filter((c) => {
+            const firstName = c.names?.[0];
+            if (!firstName) return false;
+            const containerName = this.normalizeContainerName(firstName);
+            const hasMapping = Object.prototype.hasOwnProperty.call(mappings, containerName);
+            return !hasMapping && !skipSet.has(containerName);
+        });
@@
-            for (const container of containers) {
-                const containerName = this.normalizeContainerName(container.names[0]);
+            for (const container of containers) {
+                const firstName = container.names?.[0];
+                if (!firstName) {
+                    result.skipped++;
+                    continue;
+                }
+                const containerName = this.normalizeContainerName(firstName);
@@
     private matchContainerToTemplate(
         container: DockerContainer,
         templates: ParsedTemplate[]
     ): ParsedTemplate | null {
-        const containerName = this.normalizeContainerName(container.names[0]);
+        const firstName = container.names?.[0];
+        if (!firstName) return null;
+        const containerName = this.normalizeContainerName(firstName);
         const containerImage = this.normalizeRepository(container.image);
@@
-    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 (so we don't strip port numbers)
+        const lastColon = noDigest.lastIndexOf(':');
+        const lastSlash = noDigest.lastIndexOf('/');
+        const withoutTag = lastColon > lastSlash ? noDigest.slice(0, lastColon) : noDigest;
+        return withoutTag.toLowerCase();
+    }

This:

  • Avoids crashes on containers without names,
  • Ensures containers already scanned (mapped to null) don’t trigger repeated full scans, and
  • Correctly normalizes repositories with ports and digests.

Also applies to: 79-111, 202-233

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

57-64: Avoid (listOptions as any) when calling getContainers

Casting listOptions to any defeats the benefit of typing opts as Partial<ContainerListOptions> & { skipCache?: boolean }. If DockerService.getContainers already accepts a compatible options type, you can keep this fully typed:

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

If its signature is narrower than Partial<ContainerListOptions>, consider introducing a shared GetContainersOptions type (or a localized // @ts-expect-error with explanation) instead of using as any.

api/src/unraid-api/organizer/organizer.ts (1)

89-164: Tighten meta typing in enrichFlatEntries

The traversal logic and FlatOrganizerEntry enrichment look solid, but let meta: any weakens type safety now that OrganizerContainerResource is available. You can keep this precise without losing flexibility:

-        // Resolve resource if ref
-        let meta: any = undefined;
+        // Resolve resource if ref
+        let meta: OrganizerContainerResource['meta'] | undefined;

The rest of the function already narrows correctly when resource.type === 'container', so this should type-check cleanly and preserve the intended GraphQL shape.

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

347-359: Add class-validator decorators to DockerAutostartEntryInput

This input type is still missing validation, which is standard elsewhere in this codebase. Without it, invalid data can reach the resolver unchecked. You can align it with other inputs like so:

-import {
-    Field,
-    Float,
-    GraphQLISODateTime,
-    ID,
-    InputType,
-    Int,
-    ObjectType,
-    registerEnumType,
-} from '@nestjs/graphql';
+import {
+    Field,
+    Float,
+    GraphQLISODateTime,
+    ID,
+    InputType,
+    Int,
+    ObjectType,
+    registerEnumType,
+} from '@nestjs/graphql';
+import { IsBoolean, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
@@
 @InputType()
 export class DockerAutostartEntryInput {
     @Field(() => PrefixedID, { description: 'Docker container identifier' })
+    @IsString()
+    @IsNotEmpty()
     id!: string;

     @Field(() => Boolean, { description: 'Whether the container should auto-start' })
+    @IsBoolean()
     autoStart!: boolean;

     @Field(() => Int, {
         nullable: true,
         description: 'Number of seconds to wait after starting the container',
     })
+    @IsOptional()
+    @IsInt()
+    @Min(0)
     wait?: number | null;
 }

This keeps ID non-empty, enforces a boolean for autoStart, and constrains wait to non-negative integers when provided.

🧹 Nitpick comments (11)
web/src/components/Docker/DockerOrphanedAlert.vue (2)

24-24: Consider per-button loading states for better UX.

The shared removing state disables all buttons when any removal is in progress. If users need to remove multiple orphaned containers, they must wait for each operation to complete. Consider tracking loading state per container ID to allow independent button states.

Also applies to: 77-78


33-36: Temporary alert acknowledged; offer to assist with toast implementation.

The inline comment indicates the alert is temporary and should use a toast notification service.

Would you like me to help generate a toast notification implementation or open an issue to track this improvement?

web/src/utils/docker.ts (3)

70-90: Consider logging errors for better observability.

The function safely handles various network settings structures with good defensive coding. However, the empty catch block at lines 86-87 silently swallows errors, which could make debugging difficult if unexpected data structures are encountered.

Consider adding error logging:

   } catch (e) {
+    console.debug('Failed to parse container IP:', e);
     return [];
   }

92-112: Consider logging errors for better observability.

The function handles mount formatting well with proper type checking. However, the empty catch block at lines 109-110 silently swallows errors.

Consider adding error logging:

   } catch (e) {
+    console.debug('Failed to parse container volumes:', e);
     return '';
   }

135-161: Consider simplifying the meta assignment.

The function correctly constructs a TreeRow with all necessary fields. However, line 159 has a minor redundancy.

The expression meta || undefined can be simplified:

-    meta: meta || undefined,
+    meta,

Since meta is already typed as DockerContainer | null | undefined, the || undefined is redundant and just converts null to undefined, which is unnecessary.

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

5-6: Assert behavior around DockerTemplateIconService in organizer tests

You now inject a DockerTemplateIconService mock but none of the specs verify it’s called or that icon data flows into organizer resources. That makes it easy to break icon wiring without test failures.

Consider adding at least one test that:

  • Asserts getIconsForContainers is invoked with the expected container IDs, and/or
  • Verifies that any icon data returned by the service is correctly reflected in the DockerOrganizerService result.

Also applies to: 224-229

api/src/unraid-api/organizer/organizer.model.ts (1)

225-234: Optionally tighten validation on FlatOrganizerEntry.hasChildren

FlatOrganizerEntry.hasChildren is exposed as a GraphQL field but lacks explicit boolean validation, whereas other fields use IsString, IsNumber, etc. To align with the rest of the model you could add:

-import { /* ... */, IsString, ValidateNested } from 'class-validator';
+import { /* ... */, IsString, IsBoolean, ValidateNested } from 'class-validator';
@@
-    @Field()
-    hasChildren!: boolean;
+    @Field()
+    @IsBoolean()
+    hasChildren!: boolean;

Not required for functionality, but it keeps validation consistent.

Also applies to: 260-305

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

48-48: Rename CACHE_TTL_SECONDS to CACHE_TTL_MS for clarity

cache-manager v7.2.0 uses milliseconds for TTL, and the current code multiplies CACHE_TTL_SECONDS by 1000 at the single usage site (line 193-196). Renaming to CACHE_TTL_MS = 60_000 and removing the per-call multiplication improves clarity and reduces the chance of misuse:

-    public static readonly CACHE_TTL_SECONDS = 60;
+    public static readonly CACHE_TTL_MS = 60_000;
@@
-        await this.cacheManager.set(
-            cacheKey,
-            containersWithTemplatePaths,
-            DockerService.CACHE_TTL_SECONDS * 1000
-        );
+        await this.cacheManager.set(cacheKey, containersWithTemplatePaths, DockerService.CACHE_TTL_MS);
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1)

10-103: Template metadata resolvers look good; minor DRY opportunity

The new DockerTemplateScannerService injection and the projectUrl/registryUrl/supportUrl/iconUrl resolvers are consistent with existing patterns and correctly guard on templatePath and labels. To reduce duplication, you could extract a small helper (e.g. private getTemplateDetailsFor(container)) that does the templatePath null-check and getTemplateDetails call once, and reuse it across these four fields if you find this resolver growing further.

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

268-354: New rename/create/update helpers for the default view are well-structured

renameFolderById, createFolderWithItems, and updateViewPreferences all:

  • Validate inputs and default-view existence.
  • Delegate the core mutation to pure organizer functions.
  • Run the result through validate and replaceConfig.

This keeps side effects centralized in the service and view logic pure. There is some boilerplate repetition (fetch organizer → clone → check default view → validate/replace); if this pattern grows further, consider a small internal helper to wrap that sequence, but it’s fine as-is.

api/src/unraid-api/organizer/organizer.ts (1)

687-731: Remove unused resources destructuring in createFolderWithItems

CreateFolderWithItemsParams exposes an optional resources field, but the implementation never uses it and the local resources variable is unused. To keep things clean and satisfy static analysis:

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

You can keep the resources? property on the params type for future use (as you’re already passing it from the service), but not binding it here avoids the “unused variable” warning.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c61cd8 and 802901c.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (17)
  • api/generated-schema.graphql (8 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-template-scanner.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (10 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts (7 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (6 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (5 hunks)
  • api/src/unraid-api/organizer/organizer.model.ts (2 hunks)
  • api/src/unraid-api/organizer/organizer.ts (5 hunks)
  • web/src/components/Docker/DockerContainersTable.vue (1 hunks)
  • web/src/components/Docker/DockerOrphanedAlert.vue (1 hunks)
  • web/src/components/Docker/docker-containers.query.ts (1 hunks)
  • web/src/composables/gql/gql.ts (6 hunks)
  • web/src/composables/useTreeData.ts (1 hunks)
  • web/src/utils/docker.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/src/unraid-api/graph/resolvers/docker/docker.mutations.resolver.spec.ts
  • web/src/components/Docker/docker-containers.query.ts
🧰 Additional context used
🧠 Learnings (19)
📓 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: 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.
📚 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/DockerOrphanedAlert.vue
  • web/src/utils/docker.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/src/components/Docker/DockerContainersTable.vue
  • web/src/components/Docker/DockerOrphanedAlert.vue
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts
  • web/src/utils/docker.ts
📚 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: 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-08-29T19:48:45.517Z
Learnt from: elibosley
Repo: unraid/api PR: 1618
File: web/components/Logs/SingleLogViewer.vue:248-299
Timestamp: 2025-08-29T19:48:45.517Z
Learning: In Vue Apollo's composition API, subscribeToMore automatically handles subscription cleanup when the related query is stopped or the component unmounts, unlike React Apollo where manual cleanup is required. Manual unsubscribe is only needed when changing subscription variables, not for lifecycle management.

Applied to files:

  • web/src/components/Docker/DockerOrphanedAlert.vue
📚 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.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.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/useTreeData.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
  • api/src/unraid-api/graph/resolvers/docker/docker.model.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/composables/gql/gql.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/organizer/docker-organizer.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
  • api/src/unraid-api/organizer/organizer.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-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-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-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.service.ts
📚 Learning: 2025-04-21T18:44:15.414Z
Learnt from: elibosley
Repo: unraid/api PR: 1352
File: packages/unraid-api-plugin-connect/src/config.entity.ts:16-26
Timestamp: 2025-04-21T18:44:15.414Z
Learning: For the Unraid API project, class-validator should be used for validation to avoid mismatches between different validation schemas (like Zod).

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.model.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-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
🧬 Code graph analysis (9)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (2)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • Resolver (44-341)
web/src/composables/gql/graphql.ts (1)
  • DockerContainer (739-780)
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 (40-480)
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/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 (79-108)
api/src/unraid-api/organizer/organizer.model.ts (2)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • FlatOrganizerEntry (981-993)
  • DockerContainer (739-780)
web/src/composables/gql/graphql.ts (2)
  • FlatOrganizerEntry (981-993)
  • DockerContainer (739-780)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (2)
api/src/unraid-api/utils/graphql-field-helper.ts (1)
  • GraphQLFieldHelper (13-63)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • DockerContainer (739-780)
  • DockerContainerLogs (788-794)
web/src/composables/useTreeData.ts (1)
web/src/composables/gql/graphql.ts (1)
  • FlatOrganizerEntry (981-993)
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 (79-108)
  • organizer (165-169)
api/src/unraid-api/organizer/organizer.ts (6)
  • addMissingResourcesToView (50-87)
  • moveItemsToPosition (641-659)
  • renameFolder (671-685)
  • DEFAULT_ORGANIZER_ROOT_ID (17-17)
  • createFolderWithItems (701-732)
  • DEFAULT_ORGANIZER_VIEW_ID (16-16)
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)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • Docker (691-701)
  • DockerAutostartEntryInput (730-737)
api/src/core/utils/network.ts (1)
  • getLanIp (6-19)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • containers (79-108)
api/src/core/utils/misc/catch-handlers.ts (1)
  • catchHandlers (11-29)
api/src/unraid-api/organizer/organizer.ts (3)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • FlatOrganizerEntry (981-993)
web/composables/gql/graphql.ts (1)
  • OrganizerContainerResource (1487-1493)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (3)
  • moveItemsToPosition (241-266)
  • moveEntriesToFolder (217-239)
  • createFolderWithItems (289-334)
web/src/utils/docker.ts (2)
web/src/composables/gql/graphql.ts (1)
  • DockerContainer (739-780)
web/src/composables/useTreeData.ts (1)
  • TreeRow (6-24)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/docker/docker.service.ts

[notice] 8-8: Unused variable, import, function or class
Unused import AppError.


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


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

api/src/unraid-api/organizer/organizer.ts

[notice] 642-642: Unused variable, import, function or class
Unused variable resources.


[notice] 702-702: Unused variable, import, function or class
Unused variable resources.

🔇 Additional comments (19)
web/src/components/Docker/DockerOrphanedAlert.vue (1)

1-90: Well-structured component with proper TypeScript and Vue 3 patterns.

The component correctly implements:

  • Vue 3 Composition API with <script setup>
  • TypeScript prop and emit typing
  • GraphQL mutation with proper error handling
  • Accessibility attributes on decorative icons
  • Responsive styling with appropriate warning colors
  • Edge case handling with optional chaining
web/src/utils/docker.ts (9)

1-6: LGTM!

Clean imports and well-defined regex constants for list parsing and URL validation.


7-12: LGTM!

Correctly normalizes comma-separated strings with proper trimming and empty-value filtering.


32-40: LGTM!

Safely extracts the first LAN IP with proper null checks and type validation.


42-50: LGTM!

Properly handles URL opening with SSR safety, protocol normalization (defaulting to HTTP for local containers), and secure 'noopener' flag.


59-63: LGTM!

Correctly extracts image tag from various Docker image name formats, with sensible 'latest' default.


65-68: LGTM!

Clean extraction of network mode with proper null safety.


114-118: LGTM!

The regex pattern appropriately extracts uptime from standard Docker status strings, handling both simple and health-annotated formats.


120-133: LGTM!

Comprehensive normalization function that correctly handles multiple input types (arrays, strings, numbers, booleans) with appropriate fallbacks.


14-30: The code is correctly typed—no fix needed.

The type field is defined as required (non-optional) in the ContainerPort type definition at line 518 of the GraphQL schema, so accessing port.type at line 28 is safe without explicit null checking. TypeScript's type system guarantees this field exists.

Likely an incorrect or invalid review comment.

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

31-53: Docker GraphQL operations are wired consistently across Documents, map, and overloads

The new Docker queries/mutations/subscriptions all have matching entries in the Documents type, the documents map, and the graphql() overloads. This keeps typed usage of these operations coherent with the generated documents; no issues from a typing/wiring standpoint. Please keep treating this file as codegen output and rely on regeneration rather than manual edits.

Based on learnings

Also applies to: 102-124, 229-320


60-70: Notification GraphQL additions are also correctly mirrored in all registries

The newly added notification operations (WarningAndAlertNotifications, NotifyIfUnique, NotificationsWarningsAndAlertsSub) are present and consistent in the Documents type, documents map, and corresponding graphql() overloads, so consumers get the expected typed documents. No further changes needed here.

Based on learnings

Also applies to: 131-141, 345-348, 373-376, 385-388

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

6-15: New resolver tests correctly cover sizes and logs behavior

The extended test setup and added cases for sizeRw, sizeLog, and logs() align well with the resolver implementation:

  • GraphQLFieldHelper.isFieldRequested is mocked per test to drive size flag behavior.
  • getContainerLogSizes and getContainerLogs are exercised with realistic inputs and assertions.
  • The use of DockerContainer/DockerContainerLogs types keeps fixtures in sync with the schema.

No changes requested here.

Also applies to: 32-106, 189-236, 275-291

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

86-101: Skip-cache options are threaded correctly through sync and resolve

The new opts/skipCache parameters on syncAndGetOrganizer and resolveOrganizer mirror the docker.resolver.organizer(skipCache) call site and ensure organizer state is built from a consistent container set when cache-busting is requested. Flow and typings look good.


201-210: Re-syncing default view after delete keeps resources and view aligned

Re-applying addMissingResourcesToView after deleteOrganizerEntries ensures that containers whose only references were under deleted entries are reintroduced (as refs) in the default view root instead of disappearing from the organizer entirely. This aligns view state with resources and avoids orphaning data.


241-266: moveItemsToPosition service wrapper is consistent with other organizer mutations

This wrapper follows the established pattern: syncAndGetOrganizer → clone → pure moveItemsToPosition call → validate → replace config. The default-view existence check and error messaging match the other organizer methods. No issues from the service layer side.

api/src/unraid-api/organizer/organizer.ts (2)

63-82: addMissingResourcesToView correctly avoids duplicating already-referenced resources

The enhanced implementation that tracks referencedIds across all folders ensures you only add resources to the root when they aren’t referenced anywhere else, while still creating missing ref entries where needed. This keeps the view consistent without spamming duplicates under the root.


176-187: resolveOrganizerView now aligns with flatEntries-based consumer APIs

Switching resolveOrganizerView to use enrichFlatEntries and returning { rootId, flatEntries } (instead of a nested tree) matches the new public ResolvedOrganizerView/FlatOrganizerEntry shape and simplifies client consumption. Implementation is straightforward and consistent.

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

151-218: Expanded DockerContainer surface and runtime DTOs look consistent; watch non-null fields

The added fields on DockerContainer (lanIpPorts, sizeRw, sizeLog, auto-start order/wait, templatePath, URLs, iconUrl, isOrphaned) and the new DTOs (DockerPortConflicts, DockerContainerLogs, DockerContainerStats, DockerContainerOverviewForm) are all wired with appropriate GraphQL scalar types and descriptions. One thing to double-check is that all code paths populating DockerContainer set isOrphaned (and any other non-nullable fields) so the GraphQL layer never emits null for a non-null field and accidentally tears down the whole query.

Also applies to: 266-310, 322-345

Comment on lines +629 to +659
export interface MoveItemsToPositionParams {
view: OrganizerView;
sourceEntryIds: Set<string>;
destinationFolderId: string;
position: number;
resources?: OrganizerV1['resources'];
}

/**
* Moves entries to a specific position within a destination folder.
* Combines moveEntriesToFolder with position-based insertion.
*/
export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
const { view, sourceEntryIds, destinationFolderId, position, resources } = params;

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

const folder = movedView.entries[destinationFolderId] as OrganizerFolder;
const movedIds = Array.from(sourceEntryIds);
const otherChildren = folder.children.filter((id) => !sourceEntryIds.has(id));

const insertPos = Math.max(0, Math.min(position, otherChildren.length));
const reordered = [
...otherChildren.slice(0, insertPos),
...movedIds,
...otherChildren.slice(insertPos),
];

folder.children = reordered;
return movedView;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix moveItemsToPosition to avoid inserting non-existent entry IDs

Because movedIds is derived from sourceEntryIds, any ID that isn’t present in the view will still be inserted into folder.children, creating broken references (no corresponding entries[id]). This can happen if the caller passes stale IDs.

You can avoid this by deriving movedIds from the destination folder’s children after the moveEntriesToFolder step, and by dropping the unused resources destructuring:

-export interface MoveItemsToPositionParams {
+export interface MoveItemsToPositionParams {
     view: OrganizerView;
     sourceEntryIds: Set<string>;
     destinationFolderId: string;
     position: number;
-    resources?: OrganizerV1['resources'];
 }

 /**
  * Moves entries to a specific position within a destination folder.
  * Combines moveEntriesToFolder with position-based insertion.
  */
 export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
-    const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
-
-    const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });
-
-    const folder = movedView.entries[destinationFolderId] as OrganizerFolder;
-    const movedIds = Array.from(sourceEntryIds);
-    const otherChildren = folder.children.filter((id) => !sourceEntryIds.has(id));
+    const { view, sourceEntryIds, destinationFolderId, position } = params;
+
+    const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });
+
+    const folder = movedView.entries[destinationFolderId] as OrganizerFolder;
+    const movedIds = folder.children.filter((id) => sourceEntryIds.has(id));
+    const otherChildren = folder.children.filter((id) => !sourceEntryIds.has(id));

     const insertPos = Math.max(0, Math.min(position, otherChildren.length));
     const reordered = [
         ...otherChildren.slice(0, insertPos),
         ...movedIds,
         ...otherChildren.slice(insertPos),
     ];

     folder.children = reordered;
     return movedView;
 }

This ensures only actually-existing, moved entries are repositioned, and also cleans up the unused resources variable that CodeQL flagged.

📝 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
export interface MoveItemsToPositionParams {
view: OrganizerView;
sourceEntryIds: Set<string>;
destinationFolderId: string;
position: number;
resources?: OrganizerV1['resources'];
}
/**
* Moves entries to a specific position within a destination folder.
* Combines moveEntriesToFolder with position-based insertion.
*/
export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
const { view, sourceEntryIds, destinationFolderId, position, resources } = params;
const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });
const folder = movedView.entries[destinationFolderId] as OrganizerFolder;
const movedIds = Array.from(sourceEntryIds);
const otherChildren = folder.children.filter((id) => !sourceEntryIds.has(id));
const insertPos = Math.max(0, Math.min(position, otherChildren.length));
const reordered = [
...otherChildren.slice(0, insertPos),
...movedIds,
...otherChildren.slice(insertPos),
];
folder.children = reordered;
return movedView;
}
export interface MoveItemsToPositionParams {
view: OrganizerView;
sourceEntryIds: Set<string>;
destinationFolderId: string;
position: number;
}
/**
* Moves entries to a specific position within a destination folder.
* Combines moveEntriesToFolder with position-based insertion.
*/
export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView {
const { view, sourceEntryIds, destinationFolderId, position } = params;
const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId });
const folder = movedView.entries[destinationFolderId] as OrganizerFolder;
const movedIds = folder.children.filter((id) => sourceEntryIds.has(id));
const otherChildren = folder.children.filter((id) => !sourceEntryIds.has(id));
const insertPos = Math.max(0, Math.min(position, otherChildren.length));
const reordered = [
...otherChildren.slice(0, insertPos),
...movedIds,
...otherChildren.slice(insertPos),
];
folder.children = reordered;
return movedView;
}
🧰 Tools
🪛 GitHub Check: CodeQL

[notice] 642-642: Unused variable, import, function or class
Unused variable resources.

🤖 Prompt for AI Agents
In api/src/unraid-api/organizer/organizer.ts around lines 629 to 659, avoid
inserting stale/non-existent IDs and remove the unused resources variable: after
calling moveEntriesToFolder, stop using movedIds = Array.from(sourceEntryIds)
and instead derive movedIds from the destination folder’s children (e.g., const
movedIds = folder.children.filter(id => sourceEntryIds.has(id))) so only entries
that actually exist in movedView.entries are repositioned; also remove resources
from the destructured params since it’s unused.

Comment on lines +26 to +28
async function handleRemove(container: DockerContainer) {
const name = container.names[0]?.replace(/^\//, '') || 'container';
if (!confirm(`Are you sure you want to remove orphaned container "${name}"?`)) return;
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

Eliminate code duplication by using the existing helper.

Line 27 duplicates the formatting logic from formatContainerName. Use the helper consistently to improve maintainability.

Apply this diff:

 async function handleRemove(container: DockerContainer) {
-  const name = container.names[0]?.replace(/^\//, '') || 'container';
+  const name = formatContainerName(container);
   if (!confirm(`Are you sure you want to remove orphaned container "${name}"?`)) return;
📝 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
async function handleRemove(container: DockerContainer) {
const name = container.names[0]?.replace(/^\//, '') || 'container';
if (!confirm(`Are you sure you want to remove orphaned container "${name}"?`)) return;
async function handleRemove(container: DockerContainer) {
const name = formatContainerName(container);
if (!confirm(`Are you sure you want to remove orphaned container "${name}"?`)) return;
🤖 Prompt for AI Agents
In web/src/components/Docker/DockerOrphanedAlert.vue around lines 26 to 28, the
inline container name formatting duplicates logic from formatContainerName;
replace the inline extraction/replace logic with a call to the existing helper
(e.g., const name = formatContainerName(container)) so the component uses the
shared formatting helper instead of duplicating the regex/fallback logic; ensure
the helper is imported if not already.

@pujitm pujitm force-pushed the feat/docker-overview-table branch from 7992b2c to 6dd201b Compare November 21, 2025 17:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

170-189: Query/data mismatch remains: verify docker.containers is fetched.

This issue was previously flagged. The type annotation expects docker.containers: DockerContainer[], but if GET_DOCKER_CONTAINERS doesn't fetch this field, the computed containers at line 196 will always be empty.


367-374: Align button disabled state with handler guard.

This issue was previously flagged. The openAutostartSettings handler guards on props.disabled (line 244), but the button only disables on loading. In the disabled && !loading case, the button appears clickable but does nothing.

🧹 Nitpick comments (2)
web/src/components/Docker/DockerContainerManagement.vue (2)

349-383: Consider consistent disabled state across action buttons.

The action buttons have inconsistent disabled logic:

  • Refresh: disables only on loading
  • Add Container: disables only on props.disabled
  • Customize Start Order: disables only on loading (but handler checks props.disabled)
  • Container Size: disables only on props.disabled

For better UX consistency, consider whether each button should disable on both loading || props.disabled, or document the intentional behavior differences.


252-263: URL construction via string manipulation is fragile.

The handleAddContainerClick function constructs the target URL by removing /AddContainer suffix and re-appending it. This string-based approach could break if the path structure changes. Consider whether a more robust routing mechanism is available, or document that this is intentional for legacy integration.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81dc760 and 7992b2c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • web/components.d.ts (6 hunks)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📓 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/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/src/components/Docker/DockerContainerManagement.vue
  • 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-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: 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-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-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: 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/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
🔇 Additional comments (2)
web/src/components/Docker/DockerContainerManagement.vue (1)

37-157: Well-designed router abstraction and dual-mode synchronization.

The tryUseRoute/tryUseRouter utilities enable the component to work both with and without Vue Router. The dual synchronization approach (router-based at lines 98-122, popstate-based at lines 123-157) is comprehensive, and the syncingFromRoute guard correctly prevents infinite update loops. The cleanup logic at lines 159-168 is thorough.

web/components.d.ts (1)

1-152: Auto-generated file; no review required.

This file is auto-generated by unplugin-vue-components as indicated in the header. The new component declarations align with the components added in this PR.

@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

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)
web/src/components/Docker/DockerContainerManagement.vue (3)

342-418: Overview mode template is well-structured, but button disabled state mismatch persists.

The overview layout with alerts, table, and autostart settings is clean. However, the "Customize Start Order" button at lines 366-374 still needs the disabled binding fix mentioned in previous reviews.

As flagged previously, apply this fix:

         <UButton
           size="xs"
           variant="outline"
           icon="i-lucide-list-checks"
-          :disabled="loading"
+          :disabled="loading || props.disabled"
           @click="openAutostartSettings"
         >
           Customize Start Order
         </UButton>

170-214: Query/data mismatch still needs resolution.

This is the same issue flagged in previous reviews: the type annotation includes containers and portConflicts fields (lines 182-183), but the actual GET_DOCKER_CONTAINERS query may not fetch these fields. If the query hasn't been updated, all container-dependent computed properties (lines 196-214) will return empty arrays or null values.

Verify the query includes the required fields:

#!/bin/bash
# Verify GET_DOCKER_CONTAINERS query includes containers and portConflicts fields
ast-grep --pattern $'export const GET_DOCKER_CONTAINERS = gql`
  $$$
`'

243-246: Disabled state mismatch remains unresolved.

This is the same issue flagged in previous reviews: openAutostartSettings guards on props.disabled, but the corresponding button (lines 366-374) only disables on loading, not on props.disabled.

🧹 Nitpick comments (2)
web/src/components/Docker/DockerContainerManagement.vue (1)

252-263: Path construction is functional but fragile.

The manual path manipulation with multiple regex replacements works but is brittle. Since hasRouter indicates router availability, consider using router.push when available instead of window.location.assign.

Consider this approach:

 function handleAddContainerClick() {
   if (props.disabled) return;
-  if (typeof window === 'undefined') return;
 
-  const basePathFromRoute = hasRouter && route ? route.path : null;
-  const rawPath =
-    basePathFromRoute && basePathFromRoute !== '/' ? basePathFromRoute : window.location.pathname;
-  const sanitizedPath = rawPath.replace(/\?.*$/, '').replace(/\/+$/, '');
-  const withoutAdd = sanitizedPath.replace(/\/AddContainer$/i, '');
-  const targetPath = withoutAdd ? `${withoutAdd}/AddContainer` : '/AddContainer';
-  window.location.assign(targetPath);
+  if (hasRouter && router) {
+    const currentPath = route!.path.replace(/\/AddContainer$/i, '');
+    const targetPath = currentPath ? `${currentPath}/AddContainer` : '/AddContainer';
+    router.push(targetPath);
+  } else if (typeof window !== 'undefined') {
+    const sanitizedPath = window.location.pathname.replace(/\?.*$/, '').replace(/\/+$/, '');
+    const withoutAdd = sanitizedPath.replace(/\/AddContainer$/i, '');
+    const targetPath = withoutAdd ? `${withoutAdd}/AddContainer` : '/AddContainer';
+    window.location.assign(targetPath);
+  }
 }
web/package.json (1)

105-105: Version pinning: consistency recommended, but lower priority given Renovate automation.

Both dependencies are compatible with Vue 3.5.20 and have no known security vulnerabilities. However, they use caret ranges (^0.9.0, ^8.21.3) while the project uses exact version pinning elsewhere (e.g., "vue": "3.5.20").

While Renovate's automated updates mitigate some risks of looser pinning, consider aligning with project conventions by using exact versions for consistency. If the project pattern favors exact pinning for reproducibility, update both to exact versions.

Also applies to: 113-113

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81dc760 and 6dd201b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • web/components.d.ts (6 hunks)
  • web/package.json (1 hunks)
  • web/src/components/Docker/DockerContainerManagement.vue (1 hunks)
🧰 Additional context used
🧠 Learnings (13)
📓 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-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/package.json
  • 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/src/components/Docker/DockerContainerManagement.vue
  • 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/src/components/Docker/DockerContainerManagement.vue
  • 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: 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/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-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: 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-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: 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/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
🔇 Additional comments (15)
web/components.d.ts (6)

23-23: New BaseTreeTable global component declaration looks consistent

BaseTreeTable follows the existing pattern for globally-typed common components and points to the expected Common/BaseTreeTable.vue path. No issues from a typing perspective.


40-42: New ContainerSizesModal and CriticalNotifications.standalone entries are correctly wired

Both declarations match existing naming conventions (*.standalone keys and Notifications/* paths) and use the standard typeof import(... )['default'] pattern, so they should integrate cleanly with auto-registered components.


51-61: Docker management/overview components correctly exposed to GlobalComponents

The added Docker components (autostart settings, container management/overview, table, stat/name cells, log modal, orphaned/port-conflict alerts, sidebar tree) are all typed consistently and point into ./src/components/Docker/*. This matches the PR’s Docker UI direction and should make them available in templates without local imports.


94-94: MultiValueCopyBadges declaration matches existing Common component patterns

MultiValueCopyBadges is declared alongside other Common/* components using the same import/typing style, so it’s correctly surfaced for global usage.


119-119: TableColumnMenu global registration appears correct

TableColumnMenu is routed to Common/TableColumnMenu.vue and follows the same pattern as other shared UI pieces, which should support the new table/column management UI without type gaps.


125-148: Nuxt UI U component imports look consistent; verify dependency alignment*

UModal, UPopover, USkeleton, and UTable all import from the @nuxt/ui runtime under the same pnpm path as the other U* components above, so the typing surface is internally consistent. Please just confirm that this path matches the installed @nuxt/ui version and that no mixed paths remain elsewhere in the project.

web/src/components/Docker/DockerContainerManagement.vue (9)

1-35: LGTM: Clean imports and props definition.

The imports are well-organized, and the disabled prop with a default value is correctly defined.


37-57: Excellent defensive programming for router-optional usage.

The try-catch wrappers allow this component to work both inside and outside a Vue Router context, which aligns well with the standalone usage mentioned in the AI summary.


59-157: Robust navigation sync with router and history API fallbacks.

The dual implementation supporting both Vue Router and browser history API is well-architected. The syncingFromRoute flag correctly prevents circular updates, and the debounced switching state provides smooth transitions.


159-168: LGTM: Proper resource cleanup.

The unmount hook correctly cleans up the timeout and event listener.


218-235: Navigation helpers look good.

The logic for finding organizer entries by container ID and focusing containers from conflicts is sound. The type assertion at line 222 is acceptable given the organizer entry types.


265-296: LGTM: Event handlers are straightforward.

The refresh, table, and sidebar event handlers correctly manage state updates and container selection.


298-339: LGTM: Active container state and details derivation.

The computed properties safely extract container details with proper optional chaining. The type assertions are consistent with the organizer entry patterns used elsewhere in the component.


420-553: Detail view template is comprehensive and well-structured.

The split between legacy iframe and modern component-based views is clean. The sidebar, cards, and conditional rendering all correctly handle loading and disabled states. The use of isDetailsDisabled for consistent pointer-events and opacity styling is good practice.


554-554: LGTM: Modal integration.

The ContainerSizesModal is correctly bound to the reactive showSizesModal ref.

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