Skip to content

Conversation

@pujitm
Copy link
Member

@pujitm pujitm commented Aug 12, 2025

  • Add a new utility class, AsyncMutex in unraid-shared -> processing.ts, for ergonomically de-duplicating async operations.
  • Add an @OmitIf decorator for omitting graphql queries, mutations, or field resolvers from the runtime graphql schema.
  • Add feature-flagging system
    • FeatureFlags export from consts.ts
    • @UseFeatureFlag decorator built upon OmitIf
    • checkFeatureFlag for constructing & throwing a ForbiddenError if the given feature flag evaluates to false.

Summary by CodeRabbit

  • New Features

    • Expose disk spinning state, per-container "update available" and "rebuild ready" indicators, a structured per-container update-status list (enum: UP_TO_DATE, UPDATE_AVAILABLE, REBUILD_READY, UNKNOWN), and a mutation to refresh Docker digests. Periodic and post-startup digest refreshes added (feature-flag gated).
  • Chores

    • Centralized scheduler setup and cron wiring; build now bundles a PHP wrapper asset; added feature-flag env var and local keys .gitignore entry.
  • Documentation

    • Added developer guide for feature flags.
  • Tests

    • New test suites for concurrency mutex, parser, decorators, Docker config, and Docker status parsing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 12, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds GraphQL fields and types for Docker update status, new services to read/refresh PHP-backed digests, a scheduled job to refresh digests, feature-flag decorators and schema transformer, AsyncMutex utility and tests, Vite plugin to emit PHP assets, config split for docker runtime vs organizer, and related tests and env/gitignore updates.

Changes

Cohort / File(s) Summary of changes
GraphQL schema & types
api/generated-schema.graphql
Adds ArrayDisk.isSpinning: Boolean, Disk.isSpinning: Boolean!, DockerContainer.isUpdateAvailable: Boolean, DockerContainer.isRebuildReady: Boolean, Docker.containerUpdateStatuses: [ExplicitStatusItem!]!, Mutation.refreshDockerDigests: Boolean!, new ExplicitStatusItem type and UpdateStatus enum.
Docker resolvers & models
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts, api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts, api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts, api/src/unraid-api/graph/resolvers/docker/docker-update-status.model.ts
Adds DockerContainerResolver (resolve fields: isUpdateAvailable, isRebuildReady; mutation refreshDockerDigests). Adds containerUpdateStatuses ResolveField on DockerResolver. Introduces DockerConfig model and ExplicitStatusItem/UpdateStatus models. Applies feature-flag gating to several Docker resolver paths.
PHP integration & manifest services
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts, api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts, api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts, api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
New DockerManifestService (refreshDigests, isUpdateAvailableCached, isRebuildReady) and DockerPhpService (read cached status file, refreshDigestsViaPhp, getContainerUpdateStatuses). Adds parser parseDockerPushCalls and tests; exports CachedStatusEntry type.
Cron/job wiring & scheduled job
api/src/unraid-api/app/app.module.ts, api/src/unraid-api/cron/cron.module.ts, api/src/unraid-api/cron/job.module.ts, api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
Introduces JobModule to centralize ScheduleModule.forRoot(), updates modules to import JobModule, and adds ContainerStatusJob that registers a cron job to call dockerManifestService.refreshDigests() and triggers an initial @Timeout refresh at startup.
Config refactor: runtime vs organizer
api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts, api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts, api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts, api/src/unraid-api/graph/resolvers/docker/docker-config.service.spec.ts
DockerConfigService now persists a minimal DockerConfig (updateCheckCronSchedule) with cron validation and new key/filename; adds DockerOrganizerConfigService to manage organizer-shaped config. DockerOrganizerService now depends on DockerOrganizerConfigService. Tests added/updated.
Module wiring & tests
api/src/unraid-api/graph/resolvers/docker/docker.module.ts, api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts, api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts, api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
DockerModule imports JobModule and registers ContainerStatusJob; new providers added (DockerContainerResolver, DockerManifestService, DockerPhpService, DockerOrganizerConfigService). Tests updated to mock/new providers and adjust import paths.
Feature-flag tooling & schema transformer
api/src/unraid-api/decorators/omit-if.decorator.ts, api/src/unraid-api/decorators/use-feature-flag.decorator.ts, api/src/unraid-api/decorators/*.spec.ts, api/src/unraid-api/graph/graph.module.ts, api/src/unraid-api/utils/feature-flag.helper.ts, api/src/consts.ts, api/src/environment.ts, api/.env.development
Adds OmitIf decorator and omitIfSchemaTransformer to remove schema fields via extensions; UseFeatureFlag decorator (type-safe wrapper). Adds runtime checkFeatureFlag helper, FeatureFlags export, ENABLE_NEXT_DOCKER_RELEASE env flag (and sets it in .env.development). GraphModule composes omit-if transformer with permissions transformer. Tests added.
AsyncMutex utility and tests
packages/unraid-shared/src/util/processing.ts, packages/unraid-shared/src/util/__tests__/processing.test.ts
Adds exported AsyncMutex<T> class to deduplicate concurrent async operations and a comprehensive Vitest test suite covering concurrency, errors, per-call overrides, and edge cases.
Build tooling
api/vite.config.ts
Adds a Vite plugin copy-php-files to watch and emit specified PHP files (e.g., wrapper.php) into build assets during bundling.
Dependency & repo config
api/package.json, api/.gitignore
Downgrades cron dependency from 4.3.34.3.0. Adds dev/keys/* ignore entry to api/.gitignore.
Docs
api/docs/developer/feature-flags.md
New documentation describing feature-flag definitions, schema omission and runtime checks, testing guidance, and usage patterns.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as AppModule
  participant JM as JobModule
  participant CSJ as ContainerStatusJob
  participant DConf as DockerConfigService
  participant DMan as DockerManifestService
  participant DPHP as DockerPhpService

  App->>JM: import JobModule (initializes scheduler)
  App->>CSJ: provide ContainerStatusJob
  CSJ->>DConf: getConfig()
  DConf-->>CSJ: { updateCheckCronSchedule }
  CSJ->>CSJ: register CronJob(schedule) & start
  Note over CSJ: initial @Timeout refresh at startup
  CSJ-->>DMan: refreshDigests()
  DMan->>DPHP: refreshDigestsViaPhp()
  DPHP-->>DMan: success/failure
  DMan-->>CSJ: Boolean
  Note over CSJ: cron repeats -> onTick -> refresh flow
Loading
sequenceDiagram
  autonumber
  participant Client
  participant DRes as DockerResolver
  participant DCont as DockerContainerResolver
  participant DMan as DockerManifestService
  participant DPHP as DockerPhpService

  Client->>DRes: query containerUpdateStatuses
  DRes->>DPHP: getContainerUpdateStatuses()
  DPHP-->>DRes: [ExplicitStatusItem]
  DRes-->>Client: statuses

  Client->>DCont: resolve isUpdateAvailable(container)
  DCont->>DMan: isUpdateAvailableCached(imageRef)
  DMan-->>DCont: true/false/null
  DCont-->>Client: value

  Client->>DCont: mutation refreshDockerDigests
  DCont->>DMan: refreshDigests()
  DMan-->>DCont: Boolean
  DCont-->>Client: Boolean
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

"I hopped through schema fields and cron,
I parsed the pushes till the lawn was gone.
A Mutex saved the day from double runs,
Flags tucked fields away till morning suns.
Digests refreshed — carrots for everyone!" 🥕


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d0135e and 3c4b007.

📒 Files selected for processing (2)
  • packages/unraid-shared/src/util/__tests__/processing.test.ts (1 hunks)
  • packages/unraid-shared/src/util/processing.ts (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/docker-template

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

@claude
Copy link

claude bot commented Aug 12, 2025

Claude finished @pujitm's task —— View job


I'll analyze this and get back to you.

@pujitm pujitm force-pushed the feat/docker-template branch from 242d6a2 to a6df0bd Compare August 12, 2025 19: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: 11

🧹 Nitpick comments (7)
api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts (1)

185-327: Consider adding unit tests for getLocalDigest, isRebuildReady, and isUpdateAvailable.

You already mock docker utils at Line 13. Adding small, behavior-focused tests for:

  • getLocalDigest: returns digest from RepoDigests, handles empty/missing arrays.
  • isRebuildReady: returns true when the referenced container in networkMode cannot be inspected.
  • isUpdateAvailable: returns boolean when both digests exist; null otherwise.

This closes coverage gaps for local state paths that don’t involve network I/O.

api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (2)

16-27: Relax strict 401 assertion to tolerate proxy/CDN variance.

Some paths may intermittently return 405, 403, or 429. To avoid flakes, assert that it’s a challenge/unauthorized class rather than exactly 401.

- expect(result.statusCode).toBe(401);
+ expect([401, 403, 405, 429]).toContain(result.statusCode);

125-134: Tighten concurrency to avoid rate limiting or parameterize it via env.

Ten concurrent HEAD requests to Docker Hub can spike rate limits. Either reduce default concurrency or make it configurable.

-const promises = Array.from({ length: 10 }, () => service.getRemoteDigest('alpine:latest'));
+const concurrent = Number(process.env.DOCKER_TEST_CONCURRENCY ?? 5);
+const promises = Array.from({ length: concurrent }, () => service.getRemoteDigest('alpine:latest'));
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (2)

44-53: Harden WWW-Authenticate parsing (avoid naive split on commas).

Headers can vary in spacing/order; robust parsing avoids future edge cases.

-    parseWWWAuth(wwwAuth: string): Partial<DockerWwwAuthParts> {
-        // www-authenticate: Bearer realm="...",service="...",scope="repository:repo/name:pull"
-        const parts: Partial<DockerWwwAuthParts> = {};
-        const rawParts = wwwAuth.replace(/^Bearer\s+/i, '').split(',') || [];
-        rawParts.forEach((pair) => {
-            const [k, v] = pair.split('=');
-            parts[k.trim()] = v?.replace(/^"|"$/g, '');
-        });
-        return parts;
-    }
+    parseWWWAuth(wwwAuth: string): Partial<DockerWwwAuthParts> {
+        // Expected: Bearer realm="...", service="...", scope="repository:repo/name:pull"
+        const parts: Partial<DockerWwwAuthParts> = {};
+        const m = wwwAuth.match(/^Bearer\s+(.*)$/i);
+        if (!m) return parts;
+        const kv = m[1];
+        const regex = /(\w+)\s*=\s*"([^"]*)"/g;
+        let match: RegExpExecArray | null;
+        while ((match = regex.exec(kv)) !== null) {
+            const [, key, value] = match;
+            (parts as any)[key] = value;
+        }
+        return parts;
+    }

62-71: Avoid mixing responseType: 'json' with .json() convenience; use one.

Using both is redundant. Since you’re calling .json(), drop responseType.

-        const { token } = await got
-            .get(parts.realm, {
+        const { token } = await got
+            .get(parts.realm, {
                 searchParams: { service: parts.service, scope: parts.scope },
                 username: basicCreds.username,
                 password: basicCreds.password,
                 timeout: { request: 15_000 },
-                responseType: 'json',
             })
             .json<{ token?: string }>();
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (2)

121-130: Consider returning explicit error states instead of false

The method returns false when the container exists but could benefit from distinguishing between "container exists" and "error during inspection" for better debugging and error handling.

Consider refactoring to return a more descriptive result type that distinguishes between different states, or at least add logging for debugging purposes:

     async isRebuildReady(networkMode: string) {
         if (!networkMode || !networkMode.startsWith('container:')) return false;
         const target = networkMode.slice('container:'.length);
         try {
             await docker.getContainer(target).inspect();
             return false;
-        } catch {
+        } catch (error) {
+            // Container doesn't exist or inspection failed
             return true; // unresolved target -> ':???' equivalent
         }
     }

132-139: Consider adding timeout for network operations

The isUpdateAvailable method performs concurrent network operations but doesn't implement a timeout mechanism. If the remote registry is slow or unresponsive, this could lead to long wait times.

Consider implementing a timeout wrapper or using Promise.race with a timeout:

async isUpdateAvailable(imageRef: string, timeoutMs = 30000): Promise<boolean | null> {
    const timeoutPromise = new Promise<null>((resolve) => 
        setTimeout(() => resolve(null), timeoutMs)
    );
    
    const resultPromise = Promise.all([
        this.getLocalDigest(imageRef),
        this.getRemoteDigest(imageRef),
    ]).then(([local, remote]) => {
        if (local && remote) return local !== remote;
        return null;
    });
    
    return Promise.race([resultPromise, timeoutPromise]);
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27dbfde and 14021b6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • api/package.json (1 hunks)
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (1 hunks)
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.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/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
🧠 Learnings (1)
📚 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/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
🧬 Code Graph Analysis (2)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1)
  • Injectable (19-73)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
  • Injectable (12-140)
🔇 Additional comments (7)
api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2)

3-9: ESM-friendly imports and provider wiring look correct.

The .js import specifiers comply with our ESM rule, and both services are properly added to providers. No issues spotted here.


25-26: Confirm whether DockerAuthService/DockerManifestService need to be exported.

If other modules resolve these services via Nest DI, you’ll need to export them. If they’re strictly internal to this module’s resolvers/services, current exports are fine.

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

6-12: Network client mocking approach is solid.

Mocking got at the module level and resetting per test isolates HTTP effects well. This keeps unit tests fast while preserving behavior-focused assertions.


165-183: HEAD→GET fallback is correctly exercised.

This ensures registries that disallow HEAD are still supported. Good coverage of this behavior.

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

16-43: Well-implemented image reference parsing logic

The parseImageRef method correctly handles various Docker image reference formats including:

  • Default tag assignment when missing
  • Docker Hub special cases and library namespace
  • Registry detection logic
  • Proper separation of registry, repository path, and tag

The implementation aligns well with Docker's conventions.


45-62: Robust manifest probing with fallback mechanism

The headManifest method implements a good fallback strategy from HEAD to GET requests, which handles registries that don't support HEAD requests properly. The timeout configuration is appropriate.


5-6: Use .js extension in import specifiers for ESM compatibility

TypeScript source files must use import specifiers with .js extensions for ESM compatibility according to the coding guidelines.

Apply this diff to fix the import extensions:

-import { docker } from '@app/core/utils/index.js';
-import { DockerAuthService } from '@app/unraid-api/graph/resolvers/docker/docker-auth.service.js';
+import { docker } from '@app/core/utils/index.js';
+import { DockerAuthService } from '@app/unraid-api/graph/resolvers/docker/docker-auth.service.js';

Wait, I see the imports already have .js extensions. Let me reconsider.

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

🧹 Nitpick comments (12)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (2)

29-33: Clarify logger usage and include error context in the message.

Currently logging the error object as the message and the string as the context inverts typical Logger usage, and loses stack/message context in the log line.

-            this.logger.debug(error, `Failed to read Docker auth from '${configPath}'`);
+            const msg = `Failed to read Docker auth from '${configPath}': ${
+                error instanceof Error ? error.message : String(error)
+            }`;
+            this.logger.debug(msg);

24-33: Prefer os.homedir() over HOME fallback and validate config shape.

Using os.homedir() is more portable, and validating cfg.auths is safer than assuming it exists as an object.

-import { join } from 'path';
+import { join } from 'path';
+import { homedir } from 'os';
@@
-            configPath ??= join(process.env.HOME || '/root', '.docker/config.json');
-            const cfg = JSON.parse(await readFile(configPath, 'utf8'));
-            return cfg.auths || {};
+            configPath ??= join(homedir(), '.docker/config.json');
+            const cfg = JSON.parse(await readFile(configPath, 'utf8'));
+            return cfg && typeof cfg === 'object' && cfg.auths && typeof cfg.auths === 'object'
+                ? cfg.auths
+                : {};
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (7)

45-62: headManifest: propagate status consistently and consider HEAD-only preference.

Current fallback to GET is fine. Consider explicitly checking for method-not-allowed (405) before falling back to GET to avoid hiding transient network issues on HEAD.

-        try {
-            return await got.head(url, opts);
-        } catch {
-            // Some registries don’t allow HEAD; try GET to read headers
-            return await got.get(url, opts);
-        }
+        try {
+            return await got.head(url, opts);
+        } catch {
+            // Some registries don’t allow HEAD; try GET to read headers
+            const res = await got.get(url, opts);
+            return res;
+        }

Optional: check for 405 specifically before GET.


107-119: Type the imageRef parameter in getLocalDigest.

Missing type annotation reduces clarity; add string type. Also minor nit: prefer strict null return on empty digests.

-    async getLocalDigest(imageRef) {
+    async getLocalDigest(imageRef: string): Promise<string | null> {

121-130: Type the networkMode parameter in isRebuildReady.

Add types for better inference and call sites.

-    async isRebuildReady(networkMode) {
+    async isRebuildReady(networkMode: string | null | undefined): Promise<boolean> {

8-11: Consider exporting ACCEPT_MANIFEST for reuse across tests/utilities.

If other parts (tests/utilities) need consistent Accept headers, export this constant.

-const ACCEPT_MANIFEST =
+export const ACCEPT_MANIFEST =
     'application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json';

132-139: Optional: return false when remote digest retrieval definitively fails.

Right now null conflates “unknown” with “couldn’t fetch”. If a 404/401 occurs, you might treat as false to avoid prompting updates. If you intend tri-state, keep as is.

Would you like a tri-state enum (Unknown/NoUpdate/HasUpdate) to make downstream logic explicit?


80-100: Handle non-Bearer registries and missing tokens more defensively.

Some registries return Basic without scope, or Bearer but without scope (then token endpoint expects repository query). Current logic requires scope; consider relaxing or constructing a default scope from repoPath.

If interested, I can provide a patch to derive scope "repository:${repoPath}:pull" when missing.


64-105: Token caching can significantly reduce registry load and speed up batch requests.

Given you run 10–100 parallel requests in tests, caching tokens by key (realm+service+scope) for their TTL would reduce latency and rate-limit risk.

I can add a simple in-memory LRU or leverage cache-manager already in deps to store tokens until expiry. Want me to draft that?

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

11-27: Export new services if they need to be injected outside this module.

If other modules/resolvers depend on DockerManifestService or DockerAuthService, add them to exports. If intentionally private to this module, ignore.

-@Module({
-    providers: [
+@Module({
+    providers: [
         // Services
         DockerService,
         DockerConfigService,
         DockerOrganizerService,
         DockerAuthService,
         DockerManifestService,
         // DockerEventService,
 
         // Resolvers
         DockerResolver,
         DockerMutationsResolver,
     ],
-    exports: [DockerService],
+    exports: [DockerService, DockerAuthService, DockerManifestService],
 })
api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (1)

125-161: High-volume real HTTP tests may be flaky or hit rate limits.

100 live registry requests in CI can be brittle. Consider gating these under an env flag or reducing counts to keep CI stable.

I can add a small helper to skip these when CI=true or to use it.concurrent with a lower batch size.

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

13-22: Consider avoiding mocking simple dependencies.

According to the coding guidelines for API tests, prefer not to mock simple dependencies. The docker utilities from @app/core/utils/index.js appear to be simple helper functions that could potentially be used without mocking.

Consider whether these utilities are complex enough to warrant mocking, or if they could be tested with real implementations to improve test authenticity.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27dbfde and 14021b6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • api/package.json (1 hunks)
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (1 hunks)
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.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/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
🧠 Learnings (1)
📚 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/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1)
  • Injectable (19-73)
🔇 Additional comments (8)
api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (2)

85-95: Mocking readDockerAuth here masks a bug in getRemoteDigest.

Because getRemoteDigest didn’t await readDockerAuth, mocking it to resolve to {} didn’t affect behavior. After fixing the await, this test remains valid but now actually exercises the mock correctly.

Please re-run tests after the await fix to ensure no hidden assumptions remain.


1-13: Good use of Vitest and real integration boundaries.

Services are instantiated directly without over-mocking, aligning with our testing guidelines.

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

1-11: Mock setup follows TypeScript/ESM best practices.

The import structure and module mocking patterns are well-implemented, following the coding guidelines for TypeScript files with proper .js extensions for ESM compatibility.


29-44: Excellent mock management and service setup.

The beforeEach setup properly clears mocks between tests and follows the guideline to use vi.clearAllMocks() for test isolation. The service instantiation with a mocked dependency is clean and well-structured.


46-145: Comprehensive test coverage for parseImageRef method.

The test suite thoroughly covers various image reference parsing scenarios including:

  • Default tags
  • Explicit tags
  • Docker Hub organization images
  • Custom registries with/without ports
  • Complex tag formats
  • Localhost registries
  • Deep nested repository paths

The test assertions are precise and cover edge cases well.


147-183: Well-structured headManifest tests with proper fallback logic.

The tests correctly verify both the successful HEAD request scenario and the fallback-to-GET behavior when HEAD fails. The mock setup and assertions are appropriate for testing this resilience pattern.


185-327: Excellent coverage of getRemoteDigest authentication flows.

The test suite comprehensively covers:

  • Anonymous access for public images
  • Array digest header handling
  • Bearer token authentication flow
  • Private registry authentication
  • Authentication failure scenarios
  • Missing digest handling
  • Custom registry support
  • Default tag behavior
  • Error handling

The tests follow the coding guideline to focus on behavior rather than exact error messages, and properly use mocks as objects rather than overusing behavioral mocking.


256-273: Authentication failure test follows proper error testing guidelines.

The test correctly focuses on the behavioral outcome (returning null) rather than specific error message details, which aligns with the coding guidelines for testing behavior over implementation details.

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

🧹 Nitpick comments (8)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (4)

29-31: Fix Logger.debug misuse to include the intended message

Logger.debug(message, context?) expects the message first. Currently the error object is logged as the message and your text is treated as the context tag, losing the intended message.

Apply this diff:

-            this.logger.debug(error, `Failed to read Docker auth from '${configPath}'`);
+            this.logger.debug(
+                `Failed to read Docker auth from '${configPath}': ${
+                    error instanceof Error ? error.message : String(error)
+                }`
+            );

44-51: Make WWW-Authenticate parsing more robust

Key/value parsing by simple split('=') can misbehave for unexpected values. A regex-based parse improves resilience while keeping behavior unchanged for standard headers.

Apply this diff:

-    parseWWWAuth(wwwAuth: string): Partial<DockerWwwAuthParts> {
-        // www-authenticate: Bearer realm="...",service="...",scope="repository:repo/name:pull"
-        const parts: Partial<DockerWwwAuthParts> = {};
-        const rawParts = wwwAuth.replace(/^Bearer\s+/i, '').split(',') || [];
-        rawParts.forEach((pair) => {
-            const [k, v] = pair.split('=');
-            parts[k.trim()] = v?.replace(/^"|"$/g, '');
-        });
-        return parts;
-    }
+    parseWWWAuth(wwwAuth: string): Partial<DockerWwwAuthParts> {
+        // www-authenticate: Bearer realm="...",service="...",scope="repository:repo/name:pull"
+        const parts: Partial<DockerWwwAuthParts> = {};
+        const header = wwwAuth.replace(/^Bearer\s+/i, '');
+        const kv = header.matchAll(/(\w+)=(".*?"|[^,]+)/g);
+        for (const [, k, raw] of kv) {
+            parts[k.trim() as keyof DockerWwwAuthParts] = raw.replace(/^"|"$/g, '');
+        }
+        return parts;
+    }

22-23: Remove the empty constructor

The empty constructor adds noise without value.

Apply this diff:

-    constructor() {}
-

24-33: Consider adding an explicit return type to readDockerAuth

Explicit typing improves clarity for consumers and test doubles.

Apply this diff:

-    async readDockerAuth(configPath?: string) {
+    async readDockerAuth(
+        configPath?: string
+    ): Promise<Record<string, { auth?: string }>> {
api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts (1)

26-44: Tighten typing of mockDockerAuthService for better IntelliSense and safety

Typing as Partial improves refactor safety without increasing complexity.

Apply this diff:

-    let mockDockerAuthService: any;
+    let mockDockerAuthService: Partial<DockerAuthService>;
@@
-        mockDockerAuthService = {
+        mockDockerAuthService = {
             readDockerAuth: vi.fn(),
             decodeAuth: vi.fn(),
             getBearerToken: vi.fn(),
-        };
+        };
@@
-        service = new DockerManifestService(mockDockerAuthService);
+        service = new DockerManifestService(mockDockerAuthService as DockerAuthService);
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (3)

107-119: Add explicit types to getLocalDigest for clarity and consistency

Public method should declare parameter and return types.

Apply this diff:

-    async getLocalDigest(imageRef) {
+    async getLocalDigest(imageRef: string): Promise<string | null> {

121-130: Type the public API of isRebuildReady

Add parameter and return types to avoid implicit any and to document the contract.

Apply this diff:

-    async isRebuildReady(networkMode) {
+    async isRebuildReady(networkMode?: string): Promise<boolean> {

45-62: Optional: surface response type in headManifest return for easier consumption

Currently inferred, but being explicit helps maintainers. If you keep using got’s default instance, consider annotating the return type to Promise<Response<string>> (or the correct got Response type in your version).

If you want to keep code minimal, you can skip; tests already validate behavior.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27dbfde and 14021b6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • api/package.json (1 hunks)
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (1 hunks)
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.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/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts
  • api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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-auth.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
🧠 Learnings (1)
📚 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/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1)
  • Injectable (19-73)
🔇 Additional comments (6)
api/package.json (1)

104-104: fast-xml-parser@5.2.5 Unaffected by Known GHSA Advisories

All reported vulnerabilities on GitHub (GHSA-mpg4-rc92-vx8v, GHSA-6w63-h3fj-q4vw, GHSA-x3cc-x39p-42qx, GHSA-gpv5-7x3g-ghjv) target v4.x ranges (patched by 4.4.1 and earlier). Since you’re using 5.2.5, there are no known security issues for this release. Safe to merge.

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

17-18: Providers registration looks good and ESM import specifiers are compliant

The new services are correctly added to providers and use .js extensions per ESM guideline.

api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.integration.test.ts (1)

29-39: Broaden acceptance range only if required

The 200–499 range hides potential auth issues for this specific flow. If the objective is to assert “no crash,” fine; otherwise consider tightening expectations once the flow stabilizes.

Would you like to narrow the expected codes (e.g., 200/401/403) to increase signal?

api/src/__test__/unraid-api/graph/resolvers/docker/docker-manifest.service.test.ts (3)

6-12: Mock shape for got is appropriate and isolated

Top-level vi.mock with a factory keeps mocks predictable and avoids hoisting pitfalls. LGTM.


102-111: Verify expected repoPath for docker.io explicit registry

This asserts repoPath 'docker.io/nginx'. Many implementations normalize docker.io to library/ when no org is present. Ensure this aligns with your intended behavior across the app.

If normalization to library/nginx is desired, adjust either the service or the test to keep consistency with other docker.io cases in the suite.


212-238: Good coverage of Bearer auth retry path

Unauthorized -> token -> authorized flow is well represented, focusing on behavior rather than messages. Nice.

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 (5)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (5)

5-9: Export the public return type for reuse.

ExplicitStatusItem appears in a public method signature; exporting it improves reusability and clarity for consumers.

-type ExplicitStatusItem = {
+export type ExplicitStatusItem = {
     name: string;
     updateStatus: 'up to date' | 'update available' | 'rebuild ready' | 'unknown';
 };

1-1: Don't silently swallow phpLoader errors; log at debug for observability.

Keeping the method non-throwing is fine, but record failures so we can diagnose offline/misconfig states.

-import { Injectable } from '@nestjs/common';
+import { Injectable, Logger } from '@nestjs/common';
@@
 export class DockerPhpService {
-    constructor() {}
+    private readonly logger = new Logger(DockerPhpService.name);
+    constructor() {}
@@
-        } catch {
-            // ignore; offline may keep remote as 'undef'
-        }
+        } catch (err) {
+            // allow offline; keep remote as 'undef', but record for observability
+            this.logger.debug(
+                `refreshDigests failed for ${dockerUpdatePath}: ${
+                    err instanceof Error ? err.message : String(err)
+                }`
+            );
+        }

Also applies to: 12-13, 19-26


40-59: Simplify status mapping with a typed const record.

Removes overload boilerplate, keeps literal types, and centralizes the mapping.

-    private updateStatusToString(updateStatus: 0): 'up to date';
-    private updateStatusToString(updateStatus: 1): 'update available';
-    private updateStatusToString(updateStatus: 2): 'rebuild ready';
-    private updateStatusToString(updateStatus: 3): 'unknown';
-    // prettier-ignore
-    private updateStatusToString(updateStatus: StatusItem['updateStatus']): ExplicitStatusItem['updateStatus'];
-    private updateStatusToString(
-        updateStatus: StatusItem['updateStatus']
-    ): ExplicitStatusItem['updateStatus'] {
-        switch (updateStatus) {
-            case 0:
-                return 'up to date';
-            case 1:
-                return 'update available';
-            case 2:
-                return 'rebuild ready';
-            default:
-                return 'unknown';
-        }
-    }
+    private static readonly STATUS_MAP = {
+        0: 'up to date',
+        1: 'update available',
+        2: 'rebuild ready',
+        3: 'unknown',
+    } as const;
+
+    private updateStatusToString(
+        updateStatus: StatusItem['updateStatus']
+    ): ExplicitStatusItem['updateStatus'] {
+        return DockerPhpService.STATUS_MAP[updateStatus] ?? 'unknown';
+    }

66-67: Remove redundant method: 'GET' (phpLoader defaults to GET).

This is a no-op parameter and can be dropped.

-            file: dockerContainersPath,
-            method: 'GET',
+            file: dockerContainersPath,

61-71: Clarify desired behavior when PHP endpoints are unavailable (offline).

Currently this method throws (propagates PhpError). If the UI should degrade gracefully (like refreshDigests), catch and return an empty list with a debug log. Otherwise, keep throwing. Confirm expectation.

Proposed graceful fallback:

     async getContainerUpdateStatuses(
         dockerContainersPath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerContainers.php'
     ): Promise<ExplicitStatusItem[]> {
-        const stdout = await phpLoader({
-            file: dockerContainersPath,
-            method: 'GET',
-        });
-        const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
-        const js = parts[1] || '';
-        return this.parseStatusesFromDockerPush(js);
+        try {
+            const stdout = await phpLoader({
+                file: dockerContainersPath,
+            });
+            const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
+            const js = parts[1] || '';
+            return this.parseStatusesFromDockerPush(js);
+        } catch (err) {
+            // Align behavior with refreshDigests: fail open but record for observability
+            this.logger?.debug?.(
+                `getContainerUpdateStatuses failed for ${dockerContainersPath}: ${
+                    err instanceof Error ? err.message : String(err)
+                }`
+            );
+            return [];
+        }
     }

If you'd prefer to propagate errors, I can adjust the message and rethrow with added context instead.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14021b6 and 883a14b.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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-php.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
🔇 Additional comments (1)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)

3-3: ESM import specifier suffix is correct (.js).

Good compliance with our ESM import rule.

@pujitm pujitm force-pushed the feat/docker-template branch from 48252f9 to 1e44af4 Compare August 13, 2025 14:25
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 (2)
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)

59-66: Resolver DI wiring updated to include DockerPhpService — add a module-level override for stability

Good to see DockerPhpService mocked for the Resolver test. To keep the "should compile the module" test resilient as DockerModule gains more providers, also override DockerPhpService there to avoid instantiating real dependencies.

Apply this diff within the first test’s setup chain:

         })
             .overrideProvider(DockerService)
             .useValue({ getDockerClient: vi.fn() })
+            .overrideProvider(DockerPhpService)
+            .useValue({
+                getContainerUpdateStatuses: vi.fn(),
+                refreshDigests: vi.fn(),
+            })
             .overrideProvider(DockerConfigService)
             .useValue({ getConfig: vi.fn() })
             .compile();
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)

142-150: Add optional skipCache arg for parity with other fields

containers() and networks() both accept skipCache; consider exposing the same here for consistency and control over freshness, if DockerPhpService supports it.

If the service already supports options, update the resolver like this:

-    @ResolveField(() => GraphQLJSON)
-    public async containerUpdateStatuses() {
-        return this.dockerPhpService.getContainerUpdateStatuses();
-    }
+    @ResolveField(() => GraphQLJSON)
+    public async containerUpdateStatuses(
+        @Args('skipCache', { defaultValue: false, type: () => Boolean }) skipCache: boolean
+    ) {
+        return this.dockerPhpService.getContainerUpdateStatuses({ skipCache });
+    }

Please confirm DockerPhpService.getContainerUpdateStatuses accepts an options bag; if not, I can generate the corresponding service change as well.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 883a14b and 1e44af4.

📒 Files selected for processing (4)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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.module.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
api/**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.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/docker.module.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
🧠 Learnings (4)
📚 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: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
PR: unraid/api#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
📚 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/graph/resolvers/docker/docker.resolver.spec.ts
📚 Learning: 2025-08-11T15:07:39.222Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/api-rules.mdc:0-0
Timestamp: 2025-08-11T15:07:39.222Z
Learning: Applies to api/**/*.{test,spec}.{js,jsx,ts,tsx} : Use Vitest for tests in the api; do not use Jest

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
🔇 Additional comments (4)
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)

9-9: ESM import extension is correct

Using .js in TS import specifiers aligns with our ESM guideline.

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

7-7: ESM import extension is correct

Using .js in TS import specifiers aligns with our ESM guideline.

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

12-12: New dependency injection is appropriate

Injecting DockerPhpService directly into the resolver keeps concerns separated (resolver delegates computation). Good.


9-9: JSON scalar source consistency confirmed
All GraphQLJSON imports across the repository reference graphql-scalars; there are no imports from graphql-type-json. No changes required.

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 (2)
api/vite.config.ts (2)

73-73: Remove redundant comment

The plugin name already communicates the intent. Per repo guideline, avoid obvious comments.

-            // Copy PHP files to assets directory

80-88: Optional: generalize to support multiple PHP assets via glob

If you anticipate adding more PHP helpers, consider discovering files dynamically (e.g., src/**/*.php) with fast-glob to avoid hardcoding paths.

Example (not a diff, since it adds a new dependency):

const fg = await import('fast-glob');
const phpFiles: string[] = await fg.default('src/**/*.php', { dot: false });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e44af4 and 30bcb7a.

📒 Files selected for processing (1)
  • api/vite.config.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/vite.config.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/vite.config.ts

Comment on lines 77 to 98
const { readFileSync } = await import('fs');
const { join, basename } = await import('path');

const phpFiles = ['src/core/utils/plugins/wrapper.php'];
phpFiles.forEach((file) => {
const content = readFileSync(file);
this.emitFile({
type: 'asset',
fileName: join('assets', basename(file)),
source: content,
});
});
},
},
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

⚠️ Potential issue

Fix cross-platform asset paths and prefer node: protocol; add missing-file guard

Rollup expects POSIX-style paths in emitted asset fileNames. Using path.join on Windows will produce backslashes and can create malformed asset paths. Also, prefer node:fs/node:path for clarity and to avoid unintended bundling. While here, add a guard to warn if the source file is missing instead of throwing.

Apply this diff:

-                    const { readFileSync } = await import('fs');
-                    const { join, basename } = await import('path');
+                    const { readFileSync, existsSync } = await import('node:fs');
+                    const { posix: pathPosix, basename } = await import('node:path');
@@
-                    const phpFiles = ['src/core/utils/plugins/wrapper.php'];
-                    phpFiles.forEach((file) => {
-                        const content = readFileSync(file);
-                        this.emitFile({
-                            type: 'asset',
-                            fileName: join('assets', basename(file)),
-                            source: content,
-                        });
-                    });
+                    const phpFiles = ['src/core/utils/plugins/wrapper.php'];
+                    for (const file of phpFiles) {
+                        if (!existsSync(file)) {
+                            this.warn(`[copy-php-files] missing file: ${file}`);
+                            continue;
+                        }
+                        const content = readFileSync(file);
+                        this.emitFile({
+                            type: 'asset',
+                            fileName: pathPosix.join('assets', basename(file)),
+                            source: content,
+                        });
+                    }
📝 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 { readFileSync } = await import('fs');
const { join, basename } = await import('path');
const phpFiles = ['src/core/utils/plugins/wrapper.php'];
phpFiles.forEach((file) => {
const content = readFileSync(file);
this.emitFile({
type: 'asset',
fileName: join('assets', basename(file)),
source: content,
});
});
},
},
const { readFileSync, existsSync } = await import('node:fs');
const { posix: pathPosix, basename } = await import('node:path');
const phpFiles = ['src/core/utils/plugins/wrapper.php'];
for (const file of phpFiles) {
if (!existsSync(file)) {
this.warn(`[copy-php-files] missing file: ${file}`);
continue;
}
const content = readFileSync(file);
this.emitFile({
type: 'asset',
fileName: pathPosix.join('assets', basename(file)),
source: content,
});
}
🤖 Prompt for AI Agents
In api/vite.config.ts around lines 77 to 90, replace the dynamic imports with
node: protocol imports (node:fs, node:path), add an existence guard using
fs.existsSync or try/catch to warn via this.warn and skip missing files instead
of throwing, and ensure emitted asset fileName uses POSIX-style paths by using
path.posix.join('assets', basename(file)) (keep basename from path but use
path.posix.join for the join operation); read the file only when it exists and
pass its content to this.emitFile.

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

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1)

33-35: Avoid N+1 remote lookups: memoize per-request or use DataLoader

Resolving isUpdateAvailable for a list of containers can result in many registry calls. Consider a request-scoped cache/DataLoader in DockerManifestService keyed by imageRef to dedupe.

I can draft a DataLoader-based pattern for NestJS if desired.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30bcb7a and 1d7ab6a.

📒 Files selected for processing (4)
  • api/generated-schema.graphql (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1)

23-26: Resolver wiring looks correct

Constructor DI for DockerManifestService and class-level @resolver(() => DockerContainer) are appropriate for adding computed fields.

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

3-9: ESM imports and service additions look good

Local imports use .js extensions per repo ESM guideline, and new services are correctly imported for DI.


19-28: Providers updated; resolvers retained and containerUpdateStatuses verified

  • The summary mistakenly claimed DockerResolver and DockerMutationsResolver were removed; they remain correctly registered alongside DockerContainerResolver in docker.module.ts.
  • In docker.resolver.ts, containerUpdateStatuses is protected by
    @UsePermissions({
        action: AuthActionVerb.READ,
        resource: Resource.DOCKER,
        possession: AuthPossession.ANY,
    })
    @ResolveField(() => GraphQLJSON)
    public async containerUpdateStatuses() {  }
    and DockerPhpService.getContainerUpdateStatuses() always returns an array (empty on failure), satisfying the non-null JSON! schema.

No further changes required.

@coderabbitai coderabbitai bot requested review from elibosley and zackspear August 13, 2025 18:40
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 (5)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (2)

38-46: Honor nullable field contract: return null on error instead of throwing AppError

Per project pattern, handle errors at the service level and keep resolvers fail-safe. This field is nullable; don’t surface errors to clients.

-    @ResolveField(() => Boolean, { nullable: true })
-    public async isUpdateAvailable(@Parent() container: DockerContainer) {
-        try {
-            return await this.dockerManifestService.isUpdateAvailableCached(container.image);
-        } catch (error) {
-            this.logger.error(error);
-            throw new AppError('Failed to read cached update status. See graphql-api.log for details.');
-        }
-    }
+    @ResolveField(() => Boolean, {
+        nullable: true,
+        description: 'True if a newer remote image digest exists; null when unknown or on error.',
+    })
+    public async isUpdateAvailable(@Parent() container: DockerContainer): Promise<boolean | null> {
+        try {
+            return await this.dockerManifestService.isUpdateAvailableCached(container.image);
+        } catch (error) {
+            this.logger.error(error);
+            return null;
+        }
+    }

53-56: Mirror fail-safe pattern and add explicit return type

Keep resolver behavior consistent; return null on errors and declare the explicit return type.

-    @ResolveField(() => Boolean, { nullable: true })
-    public async isRebuildReady(@Parent() container: DockerContainer) {
-        return this.dockerManifestService.isRebuildReady(container.hostConfig?.networkMode);
-    }
+    @ResolveField(() => Boolean, {
+        nullable: true,
+        description: 'True if rebuild is needed (e.g., target container not found); null on error.',
+    })
+    public async isRebuildReady(@Parent() container: DockerContainer): Promise<boolean | null> {
+        try {
+            return await this.dockerManifestService.isRebuildReady(container.hostConfig?.networkMode);
+        } catch (error) {
+            this.logger.error(error);
+            return null;
+        }
+    }
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (3)

75-116: Bug: readDockerAuth is async and not awaited; add types and robust auth key matching

Without await, dockerAuths is a Promise, breaking credentials lookup. Also add explicit types and resilient key matching for private registries.

-    async getRemoteDigest(imageRef) {
+    async getRemoteDigest(imageRef: string): Promise<string | null> {
         const { registryBaseURL, repoPath, tag, authConfigKey } = this.parseImageRef(imageRef);
         const manifestURL = `${registryBaseURL}/v2/${repoPath}/manifests/${tag}`;
 
-        const dockerAuths = this.dockerAuthService.readDockerAuth();
-        const authEntry = dockerAuths[authConfigKey];
-        const basicCreds = authEntry?.auth
+        const dockerAuths = await this.dockerAuthService.readDockerAuth();
+        const registryHost = new URL(registryBaseURL).host;
+        const authEntry =
+            dockerAuths[authConfigKey] ??
+            dockerAuths[registryHost] ??
+            dockerAuths[`https://${registryHost}`] ??
+            dockerAuths[`http://${registryHost}`] ??
+            Object.entries(dockerAuths).find(([k]) => k.includes(registryHost))?.[1];
+        const basicCreds = (authEntry as any)?.auth
             ? this.dockerAuthService.decodeAuth(authEntry.auth)
             : { username: '', password: '' };
 
         // 1) Probe without auth to learn challenge
         let resp = await this.headManifest(manifestURL, { Accept: ACCEPT_MANIFEST });
         const digestHeaderRaw = resp.headers?.['docker-content-digest'];
         const digestHeader = Array.isArray(digestHeaderRaw) ? digestHeaderRaw[0] : digestHeaderRaw;
         if (resp.statusCode >= 200 && resp.statusCode < 300 && digestHeader) return digestHeader.trim();
 
         const wwwAuth = (resp.headers?.['www-authenticate'] || '').toString();
         if (/Bearer/i.test(wwwAuth)) {
             const token = await this.dockerAuthService.getBearerToken(wwwAuth, basicCreds);
             if (!token) return null;
             // 2) Repeat with Bearer
             resp = await this.headManifest(
                 manifestURL,
                 { Accept: ACCEPT_MANIFEST },
                 { Authorization: `Bearer ${token}` }
             );
         } else if (/Basic/i.test(wwwAuth) && basicCreds.username && basicCreds.password) {
             // 2) Repeat with Basic
             const basic =
                 'Basic ' +
                 Buffer.from(`${basicCreds.username}:${basicCreds.password}`).toString('base64');
             resp = await this.headManifest(
                 manifestURL,
                 { Accept: ACCEPT_MANIFEST },
                 { Authorization: basic }
             );
         }
 
         const digestRaw = resp.headers?.['docker-content-digest'];
         const digest = Array.isArray(digestRaw) ? digestRaw[0] : digestRaw;
         return digest ? digest.trim() : null;
     }

10-12: Accept header missing OCI manifest type; digest resolution may fail against OCI-only registries

Some registries require application/vnd.oci.image.manifest.v1+json to return digest headers.

-const ACCEPT_MANIFEST =
-    'application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json';
+const ACCEPT_MANIFEST =
+    'application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json,application/vnd.oci.image.manifest.v1+json';

27-54: Fix image reference parsing for explicit docker.io, localhost, and digest refs (@)

Current logic misclassifies localhost/docker.io and breaks on digest refs by force-appending :latest and splitting on colon.

-    parseImageRef(imageRef: string) {
-        // Normalize to repo:tag and extract registry/repo/name/tag
-        let ref = imageRef;
-        if (!ref.includes(':')) ref += ':latest';
-
-        // Registry present?
-        const firstSlash = ref.indexOf('/');
-        const maybeRegistry = firstSlash > -1 ? ref.slice(0, firstSlash) : '';
-        const hasDotOrColon = maybeRegistry.includes('.') || maybeRegistry.includes(':');
-        const isDockerHub = !hasDotOrColon || maybeRegistry === 'docker.io';
-
-        const registry = isDockerHub ? 'registry-1.docker.io' : maybeRegistry;
-        const rest = isDockerHub ? ref : ref.slice(maybeRegistry.length + 1);
-
-        const lastColon = rest.lastIndexOf(':');
-        const namePart = rest.slice(0, lastColon);
-        const tag = rest.slice(lastColon + 1);
-
-        // Ensure docker hub library namespace
-        const repoPath = isDockerHub && !namePart.includes('/') ? `library/${namePart}` : namePart;
-
-        return {
-            registryBaseURL: `https://${registry}`,
-            authConfigKey: isDockerHub ? 'https://index.docker.io/v1/' : maybeRegistry,
-            repoPath, // e.g. library/nginx or org/image
-            tag,
-        };
-    }
+    parseImageRef(imageRef: string) {
+        // Normalize and extract registry/repo/name and tag or digest
+        const ref = imageRef.trim();
+
+        // Registry present?
+        const firstSlash = ref.indexOf('/');
+        const maybeRegistry = firstSlash > -1 ? ref.slice(0, firstSlash) : '';
+        const hasRegistryPrefix =
+            firstSlash > -1 &&
+            (maybeRegistry.includes('.') ||
+                maybeRegistry.includes(':') ||
+                maybeRegistry === 'localhost' ||
+                maybeRegistry === 'docker.io');
+        const registryHost = hasRegistryPrefix
+            ? (maybeRegistry === 'docker.io' ? 'registry-1.docker.io' : maybeRegistry)
+            : 'registry-1.docker.io';
+        const rest = hasRegistryPrefix ? ref.slice(maybeRegistry.length + 1) : ref;
+
+        // Split name vs tag/digest
+        const atPos = rest.indexOf('@');
+        let namePart = rest;
+        let tag = 'latest';
+        if (atPos > -1) {
+            namePart = rest.slice(0, atPos);
+            tag = rest.slice(atPos + 1); // digest (e.g., sha256:...)
+        } else {
+            const lastColon = rest.lastIndexOf(':');
+            if (lastColon > -1) {
+                namePart = rest.slice(0, lastColon);
+                tag = rest.slice(lastColon + 1);
+            }
+        }
+
+        const isDockerHub = registryHost === 'registry-1.docker.io';
+        const repoPath = isDockerHub && !namePart.includes('/') ? `library/${namePart}` : namePart;
+
+        return {
+            registryBaseURL: `https://${registryHost}`,
+            authConfigKey: isDockerHub ? 'https://index.docker.io/v1/' : registryHost,
+            repoPath, // e.g. library/nginx or org/image
+            tag,
+        };
+    }
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1)

63-66: Add explicit return type to mutation for clarity

Small readability/contract improvement.

-    @Mutation(() => Boolean)
-    public async refreshDigests() {
-        return this.dockerPhpService.refreshDigests();
-    }
+    @Mutation(() => Boolean)
+    public async refreshDigests(): Promise<boolean> {
+        return this.dockerPhpService.refreshDigests();
+    }
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)

118-130: Add explicit return type for getLocalDigest

Clarifies contract and improves IDE support.

-    async getLocalDigest(imageRef: string) {
+    async getLocalDigest(imageRef: string): Promise<string | null> {
         try {
             const data = await docker.getImage(imageRef).inspect();
             const digests = data.RepoDigests || [];
             if (digests.length === 0) return null;
             // Prefer a digest matching this repo if present; else first
             const pick = digests.find((d) => d.startsWith(imageRef.split(':')[0] + '@')) || digests[0];
             const [, shaDigestString] = pick.split('@');
             return shaDigestString ?? null;
         } catch {
             return 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 1d7ab6a and 1d915c4.

📒 Files selected for processing (3)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
🧠 Learnings (1)
📚 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-container.resolver.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1)
api/src/core/errors/app-error.ts (1)
  • AppError (4-37)

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-manifest.service.ts (2)

33-41: Handle digest-only refs and unknown status; type the return.

  • Don’t append :latest when the ref uses a digest (@).
  • Return null when status is unknown (null).
  • Add an explicit return type.
-    async isUpdateAvailableCached(imageRef: string, cacheData?: Record<string, CachedStatusEntry>) {
-        let taggedRef = imageRef;
-        if (!taggedRef.includes(':')) taggedRef += ':latest';
-
-        cacheData ??= await this.readCachedUpdateStatus();
-        const containerData = cacheData[taggedRef];
-        if (!containerData) return null;
-        return containerData.status?.toLowerCase() === 'true';
-    }
+    async isUpdateAvailableCached(
+        imageRef: string,
+        cacheData?: Record<string, CachedStatusEntry>
+    ): Promise<boolean | null> {
+        let taggedRef = imageRef.trim();
+        // If neither a tag (:) nor a digest (@) is present, default to :latest
+        if (!/[:@]/.test(taggedRef)) taggedRef += ':latest';
+
+        cacheData ??= await this.readCachedUpdateStatus();
+        const containerData = cacheData[taggedRef];
+        if (!containerData) return null;
+        if (containerData.status == null) return null;
+        return String(containerData.status).toLowerCase() === 'true';
+    }

27-31: Guard cache reads; return an empty object on failure.

The method throws if the file is missing or JSON is invalid. Prefer a resilient read with logging.

-    async readCachedUpdateStatus(cacheFile = '/var/lib/docker/unraid-update-status.json') {
-        const cache = await readFile(cacheFile, 'utf8');
-        const cacheData = JSON.parse(cache);
-        return cacheData as Record<string, CachedStatusEntry>;
-    }
+    async readCachedUpdateStatus(
+        cacheFile = '/var/lib/docker/unraid-update-status.json'
+    ): Promise<Record<string, CachedStatusEntry>> {
+        try {
+            const cache = await readFile(cacheFile, 'utf8');
+            const parsed = JSON.parse(cache);
+            return (parsed && typeof parsed === 'object' ? parsed : {}) as Record<string, CachedStatusEntry>;
+        } catch (error) {
+            this.logger.warn(`Failed to read update status cache '${cacheFile}': ${(error as Error).message}`);
+            return {};
+        }
+    }
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)

61-69: Make docker.push parser resilient to property order and quote styles.

Support both single/double quotes and property reordering.

-        const re = /docker\.push\(\{[^}]*name:'([^']+)'[^}]*update:(\d)[^}]*\}\);/g;
-        for (const m of js.matchAll(re)) {
-            const name = m[1];
-            const updateStatus = Number(m[2]) as StatusItem['updateStatus'];
+        const re =
+            /docker\.push\(\{(?=[^}]*\bname:\s*(['"])([^'"]+)\1)(?=[^}]*\bupdate:\s*(\d))[^}]*\}\);/g;
+        for (const m of js.matchAll(re)) {
+            const name = m[2];
+            const updateStatus = Number(m[3]) as StatusItem['updateStatus'];
             items.push({ name, updateStatus: this.updateStatusToString(updateStatus) });
         }
🧹 Nitpick comments (8)
packages/unraid-shared/src/util/processing.ts (1)

45-66: Single-flight logic is solid; consider reentrancy-safe assignment.

Current dedupe works as intended. A small refinement avoids a narrow reentrancy window if an operation synchronously calls back into do() before currentOperation is set.

Apply this diff to set currentOperation before invoking the operation and make the cleanup robust:

-    const op = (operation || this.defaultOperation) as AsyncOperation<U | T>;
-    
-    const promise = this.executeOperation(op).finally(() => {
-      if (this.currentOperation === promise) {
-        this.currentOperation = null;
-      }
-    });
-    
-    this.currentOperation = promise;
-    return promise;
+    const op = (operation || this.defaultOperation) as AsyncOperation<U | T>;
+    let promise!: Promise<U | T>;
+    this.currentOperation = promise = (async () => {
+      try {
+        return await this.executeOperation(op);
+      } finally {
+        if (this.currentOperation === promise) {
+          this.currentOperation = null;
+        }
+      }
+    })();
+    return promise;
packages/unraid-shared/src/util/__tests__/processing.test.ts (4)

61-63: Avoid asserting the exact error instance in tests.

Per guidelines, prefer .rejects.toThrow() without arguments to reduce brittleness.

Apply this diff:

-      await expect(mutex.do()).rejects.toThrow(error);
+      await expect(mutex.do()).rejects.toThrow();

126-128: Use generic throw assertion for async failures.

Avoid checking the specific Error object.

-      await expect(mutex.do(failingOp)).rejects.toThrow(error);
+      await expect(mutex.do(failingOp)).rejects.toThrow();

135-139: Don’t assert exact error messages.

Asserting on exact strings is brittle. Use a generic throw assertion.

-      await expect(mutex.do()).rejects.toThrow('No operation provided and no default operation set');
+      await expect(mutex.do()).rejects.toThrow();

283-293: Remove unused innerOp variable.

innerOp isn’t used and can be dropped.

-      const innerOp = vi.fn().mockResolvedValue('inner');
       const outerOp = vi.fn().mockImplementation(async () => {
         return 'outer';
       });
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)

10-11: Include OCI single-manifest media type and consider removing unused constant.

Add application/vnd.oci.image.manifest.v1+json for better registry compatibility. Also, ACCEPT_MANIFEST isn’t used—either wire it into requests or drop it.

-const ACCEPT_MANIFEST =
-    'application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json';
+const ACCEPT_MANIFEST =
+    'application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json,application/vnd.oci.image.manifest.v1+json';
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (2)

21-24: Tighten typing on the mutex for stronger safety.

The default operation returns boolean; parameterize AsyncMutex accordingly.

-    private readonly refreshDigestsMutex = new AsyncMutex(() => {
+    private readonly refreshDigestsMutex = new AsyncMutex<boolean>(() => {
         return this.refreshDigestsViaPhp();
     });

31-35: Add explicit return type for refreshDigests.

Clarifies API and helps callers.

-    async refreshDigests(mutex = this.refreshDigestsMutex, dockerUpdatePath?: string) {
+    async refreshDigests(mutex = this.refreshDigestsMutex, dockerUpdatePath?: string): Promise<boolean> {
         return mutex.do(() => {
             return this.refreshDigestsViaPhp(dockerUpdatePath);
         });
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d915c4 and ef5945c.

📒 Files selected for processing (7)
  • api/generated-schema.graphql (3 hunks)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • packages/unraid-shared/src/util/__tests__/processing.test.ts (1 hunks)
  • packages/unraid-shared/src/util/processing.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • packages/unraid-shared/src/util/__tests__/processing.test.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • packages/unraid-shared/src/util/processing.ts
**/*.{test,spec}.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.{test,spec}.{js,ts,jsx,tsx}: General testing: use .rejects.toThrow() without arguments when asserting thrown errors
General testing: focus on behavior, not exact error message wording
General testing: avoid brittle tests tied to non-essential details (exact messages, log formats)
General testing: use mocks as nouns (objects), not verbs (behavioral overuse)

Files:

  • packages/unraid-shared/src/util/__tests__/processing.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • packages/unraid-shared/src/util/__tests__/processing.test.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • packages/unraid-shared/src/util/processing.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit Inference Engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • packages/unraid-shared/src/util/__tests__/processing.test.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.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/graph/resolvers/docker/docker-manifest.service.ts
📚 Learning: 2024-12-11T20:39:02.511Z
Learnt from: pujitm
PR: unraid/api#978
File: web/helpers/apollo-cache/index.ts:134-139
Timestamp: 2024-12-11T20:39:02.511Z
Learning: In `web/helpers/apollo-cache/index.ts`, the `cache.readQuery` method returns `null` when it fails to read from the cache, rather than throwing an exception.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
🧬 Code Graph Analysis (4)
packages/unraid-shared/src/util/__tests__/processing.test.ts (1)
packages/unraid-shared/src/util/processing.ts (1)
  • AsyncMutex (37-71)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)
  • Injectable (13-104)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (3)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
  • Injectable (6-22)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)
  • Injectable (13-104)
api/src/unraid-api/graph/resolvers/docker/docker-auth.service.ts (1)
  • Injectable (19-73)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (4)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
  • Injectable (6-22)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
  • Injectable (22-53)
packages/unraid-shared/src/util/processing.ts (1)
  • AsyncMutex (37-71)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
🔇 Additional comments (3)
packages/unraid-shared/src/util/processing.ts (1)

35-43: Type alias and class API look good.

The AsyncOperation alias and the overloaded do() signatures are clear and ergonomic. No issues.

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

13-20: Verify cache entry shape matches the producer.

CachedStatusEntry uses keys local and remote; ensure producers and consumers use the same names. If the producer writes localDigest/remoteDigest, this will misalign.

Would you like me to scan the repo for the writer of /var/lib/docker/unraid-update-status.json to confirm the exact keys and align types accordingly?

api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)

10-21: Job wiring looks good; mutex upstream prevents overlap.

Using DockerPhpService with its AsyncMutex ensures single-flight refreshes even if the cron and startup timeout fire close together.

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 (5)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)

60-67: Harden docker.push parser: support any property order and single/double quotes.

Current regex assumes property order and single quotes; it will miss valid variants and silently drop entries.

Apply:

-        const re = /docker\.push\(\{[^}]*name:'([^']+)'[^}]*update:(\d)[^}]*\}\);/g;
-        for (const m of js.matchAll(re)) {
-            const name = m[1];
-            const updateStatus = Number(m[2]) as StatusItem['updateStatus'];
+        const re =
+            /docker\.push\(\{(?=[^}]*\bname:\s*(['"])([^'"]+)\1)(?=[^}]*\bupdate:\s*(\d))[^}]*\}\);/g;
+        for (const m of js.matchAll(re)) {
+            const name = m[2];
+            const updateStatus = Number(m[3]) as StatusItem['updateStatus'];
             items.push({ name, updateStatus: this.updateStatusToString(updateStatus) });
         }

Would you like me to add unit tests for parseStatusesFromDockerPush covering single/double quotes and swapped property order?

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

2-24: Remove unused imports to satisfy code scanning and keep the file lean.

Matches prior code-scanning warnings (Args, Query, GraphQLJSON, and several Docker-related imports are unused).

Apply:

-import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
+import { Mutation, Parent, ResolveField, Resolver } from '@nestjs/graphql';
@@
-import { GraphQLJSON } from 'graphql-scalars';
-
-import { AppError } from '@app/core/errors/app-error.js';
 import { DockerManifestService } from '@app/unraid-api/graph/resolvers/docker/docker-manifest.service.js';
-import { DockerOrganizerService } from '@app/unraid-api/graph/resolvers/docker/docker-organizer.service.js';
-import { DockerPhpService } from '@app/unraid-api/graph/resolvers/docker/docker-php.service.js';
-import {
-    Docker,
-    DockerContainer,
-    DockerNetwork,
-} from '@app/unraid-api/graph/resolvers/docker/docker.model.js';
-import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
-import { DEFAULT_ORGANIZER_ROOT_ID } from '@app/unraid-api/organizer/organizer.js';
-import { OrganizerV1, ResolvedOrganizerV1 } from '@app/unraid-api/organizer/organizer.model.js';
+import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js';

35-43: Honor nullable field contract: return null on error and add explicit return type.

The field is declared nullable, but currently throws. Prefer returning null to avoid surfacing resolver errors to clients. This also aligns with past guidance for this repo to handle errors in services rather than resolvers.

Apply:

-    @ResolveField(() => Boolean, { nullable: true })
-    public async isUpdateAvailable(@Parent() container: DockerContainer) {
-        try {
-            return await this.dockerManifestService.isUpdateAvailableCached(container.image);
-        } catch (error) {
-            this.logger.error(error);
-            throw new AppError('Failed to read cached update status. See graphql-api.log for details.');
-        }
-    }
+    @ResolveField(() => Boolean, {
+        nullable: true,
+        description: 'True if a newer remote image digest exists; null when unable to determine.',
+    })
+    public async isUpdateAvailable(@Parent() container: DockerContainer): Promise<boolean | null> {
+        try {
+            return await this.dockerManifestService.isUpdateAvailableCached(container.image);
+        } catch {
+            return null;
+        }
+    }

To confirm there are no remaining unused imports after this change, run:

#!/bin/bash
rg -nP --type=ts '^\s*import\s+.*\sfrom\s' api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts -n -C0

50-53: Mirror fail-safe pattern and add explicit return type.

Optional: add description and return null on unexpected errors to stay consistent with isUpdateAvailable.

-    @ResolveField(() => Boolean, { nullable: true })
-    public async isRebuildReady(@Parent() container: DockerContainer) {
-        return this.dockerManifestService.isRebuildReady(container.hostConfig?.networkMode);
-    }
+    @ResolveField(() => Boolean, {
+        nullable: true,
+        description: 'True if rebuild is needed (e.g., unresolved container:* network target); null when unknown.',
+    })
+    public async isRebuildReady(@Parent() container: DockerContainer): Promise<boolean | null> {
+        try {
+            return await this.dockerManifestService.isRebuildReady(container.hostConfig?.networkMode);
+        } catch {
+            return null;
+        }
+    }
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)

29-37: Handle digest refs (@), unknown status, and add explicit return type.

Current logic forces :latest and conflates unknown with false. Support digest-only refs and return null when cache has no status.

-    async isUpdateAvailableCached(imageRef: string, cacheData?: Record<string, CachedStatusEntry>) {
-        let taggedRef = imageRef;
-        if (!taggedRef.includes(':')) taggedRef += ':latest';
-
-        cacheData ??= await this.dockerPhpService.readCachedUpdateStatus();
-        const containerData = cacheData[taggedRef];
-        if (!containerData) return null;
-        return containerData.status?.toLowerCase() === 'true';
-    }
+    async isUpdateAvailableCached(
+        imageRef: string,
+        cacheData?: Record<string, CachedStatusEntry>
+    ): Promise<boolean | null> {
+        let taggedRef = imageRef;
+        // If neither a tag (:) nor a digest (@) is present, default to :latest
+        if (!/[:@]/.test(taggedRef)) taggedRef += ':latest';
+
+        cacheData ??= await this.dockerPhpService.readCachedUpdateStatus();
+        const containerData = cacheData[taggedRef];
+        if (!containerData) return null;
+        if (containerData.status == null) return null;
+        return String(containerData.status).toLowerCase() === 'true';
+    }
🧹 Nitpick comments (6)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (2)

26-30: Make cache read resilient and type the return.

If the cache file is missing or malformed, this throws and bubbles up. Prefer a safe default and explicit return type.

Apply:

-    async readCachedUpdateStatus(cacheFile = '/var/lib/docker/unraid-update-status.json') {
-        const cache = await readFile(cacheFile, 'utf8');
-        const cacheData = JSON.parse(cache);
-        return cacheData as Record<string, CachedStatusEntry>;
-    }
+    async readCachedUpdateStatus(
+        cacheFile = '/var/lib/docker/unraid-update-status.json'
+    ): Promise<Record<string, CachedStatusEntry>> {
+        try {
+            const cache = await readFile(cacheFile, 'utf8');
+            const cacheData = JSON.parse(cache);
+            return cacheData as Record<string, CachedStatusEntry>;
+        } catch {
+            // File missing or invalid JSON → treat as empty cache
+            return {};
+        }
+    }

41-44: Add explicit return type for refreshDigestsViaPhp.

Minor clarity/readability improvement.

-    async refreshDigestsViaPhp(
+    async refreshDigestsViaPhp(
         dockerUpdatePath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php'
-    ) {
+    ): Promise<boolean> {
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1)

60-63: Add explicit return type to mutation.

Clarifies intent and improves IDE help.

-    @Mutation(() => Boolean)
-    public async refreshDockerDigests() {
-        return this.dockerManifestService.refreshDigests();
-    }
+    @Mutation(() => Boolean)
+    public async refreshDockerDigests(): Promise<boolean> {
+        return this.dockerManifestService.refreshDigests();
+    }
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (3)

23-27: Type refreshDigests and keep the mutex API explicit.

Adds Promise to the signature; no behavior change.

-    async refreshDigests(mutex = this.refreshDigestsMutex, dockerUpdatePath?: string) {
+    async refreshDigests(
+        mutex = this.refreshDigestsMutex,
+        dockerUpdatePath?: string
+    ): Promise<boolean> {
         return mutex.do(() => {
             return this.dockerPhpService.refreshDigestsViaPhp(dockerUpdatePath);
         });
     }

39-48: Add explicit return type to isRebuildReady.

Keeps signatures clear.

-    async isRebuildReady(networkMode?: string) {
+    async isRebuildReady(networkMode?: string): Promise<boolean> {
         if (!networkMode || !networkMode.startsWith('container:')) return false;
         const target = networkMode.slice('container:'.length);
         try {
             await docker.getContainer(target).inspect();
             return false;
         } catch {
             return true; // unresolved target -> ':???' equivalent
         }
     }

13-16: Confirm mutex behavior with custom dockerUpdatePath.

The default mutex’s executor doesn’t accept params; refreshDigests correctly passes dockerUpdatePath via the closure. Just calling out that this design will dedupe all refreshes regardless of path, which is likely desired. If callers may need distinct queues per path, consider keying the mutex map by path.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 28e3ccb and 53cbcb3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
🧠 Learnings (4)
📚 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/graph/resolvers/docker/docker-manifest.service.ts
📚 Learning: 2024-12-11T20:39:02.511Z
Learnt from: pujitm
PR: unraid/api#978
File: web/helpers/apollo-cache/index.ts:134-139
Timestamp: 2024-12-11T20:39:02.511Z
Learning: In `web/helpers/apollo-cache/index.ts`, the `cache.readQuery` method returns `null` when it fails to read from the cache, rather than throwing an exception.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.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:

  • api/src/unraid-api/graph/resolvers/docker/docker-container.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-container.resolver.ts
🔇 Additional comments (1)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)

10-21: LGTM: schedules look sane; mutex in service will dedupe overlaps.

Daily cron and 5s post-start are reasonable. Since DockerManifestService uses a mutex, concurrent triggers won’t race.

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 (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)

31-39: Fix: don’t conflate unknown status with “false” & support digest-only refs.
Right now, a missing status falls through as false, and defaulting to :latest only checks for : (so digest-only refs with @ aren’t explicitly handled). Also, add an explicit Promise<boolean | null> return type.

File: api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
Lines: 31–39

-    async isUpdateAvailableCached(imageRef: string, cacheData?: Record<string, CachedStatusEntry>) {
-        let taggedRef = imageRef;
-        if (!taggedRef.includes(':')) taggedRef += ':latest';
-
-        cacheData ??= await this.dockerPhpService.readCachedUpdateStatus();
-        const containerData = cacheData[taggedRef];
-        if (!containerData) return null;
-        return containerData.status?.toLowerCase() === 'true';
-    }
+    async isUpdateAvailableCached(
+        imageRef: string,
+        cacheData?: Record<string, CachedStatusEntry>
+    ): Promise<boolean | null> {
+        let taggedRef = imageRef;
+        // Only append :latest if there’s neither a tag (:) nor a digest (@)
+        if (!/[:@]/.test(taggedRef)) taggedRef += ':latest';
+
+        cacheData ??= await this.dockerPhpService.readCachedUpdateStatus();
+        const containerData = cacheData[taggedRef];
+        // Missing entry or missing status ⇒ unknown
+        if (!containerData || containerData.status == null) return null;
+        return containerData.status.toLowerCase() === 'true';
+    }

No schema changes required: the field is already declared nullable (@ResolveField(() => Boolean, { nullable: true }) and isUpdateAvailable: Boolean).

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

15-18: Add explicit generics and return/param types for stronger typing.

Tighten types for the mutex and method to improve IDE help and compile-time safety.

-    private readonly refreshDigestsMutex = new AsyncMutex(() => {
+    private readonly refreshDigestsMutex = new AsyncMutex<boolean>(() => {
         return this.dockerPhpService.refreshDigestsViaPhp();
     });
-    async refreshDigests(mutex = this.refreshDigestsMutex, dockerUpdatePath?: string) {
+    async refreshDigests(
+        mutex: AsyncMutex<boolean> = this.refreshDigestsMutex,
+        dockerUpdatePath?: string
+    ): Promise<boolean> {
         return mutex.do(() => {
             return this.dockerPhpService.refreshDigestsViaPhp(dockerUpdatePath);
         });
     }

Also applies to: 25-29


41-50: Only treat “not found” (404) as rebuild-ready; avoid masking engine errors.

Catching all errors and returning true will mark rebuild-ready on transient Docker daemon issues. Limit “true” to 404/not-found; otherwise return false.

-    async isRebuildReady(networkMode?: string) {
+    async isRebuildReady(networkMode?: string): Promise<boolean> {
         if (!networkMode || !networkMode.startsWith('container:')) return false;
         const target = networkMode.slice('container:'.length);
         try {
             await docker.getContainer(target).inspect();
             return false;
-        } catch {
-            return true; // unresolved target -> ':???' equivalent
+        } catch (err: any) {
+            // Only treat "no such container" as rebuild-ready; other errors are indeterminate
+            const code = err?.statusCode ?? err?.status ?? err?.code;
+            if (code === 404 || err?.message?.toLowerCase().includes('not found')) return true;
+            return false;
         }
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 53cbcb3 and 2566efc.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

TypeScript source files must use import specifiers with .js extensions for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
api/src/unraid-api/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place new API source files in api/src/unraid-api/ rather than legacy locations

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-manifest.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Avoid unnecessary or obvious comments; only add comments when needed for clarity

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.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/graph/resolvers/docker/docker-manifest.service.ts
📚 Learning: 2024-12-11T20:39:02.511Z
Learnt from: pujitm
PR: unraid/api#978
File: web/helpers/apollo-cache/index.ts:134-139
Timestamp: 2024-12-11T20:39:02.511Z
Learning: In `web/helpers/apollo-cache/index.ts`, the `cache.readQuery` method returns `null` when it fails to read from the cache, rather than throwing an exception.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (3)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
  • Injectable (6-22)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (2)
  • Injectable (22-103)
  • CachedStatusEntry (13-20)
packages/unraid-shared/src/util/processing.ts (1)
  • AsyncMutex (37-67)
🔇 Additional comments (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)

1-10: ESM imports and AsyncMutex import look correct.

Good use of .js import specifiers and bringing in AsyncMutex from the shared util. This aligns with our ESM guidelines and resolves the prior missing import issue.

@pujitm pujitm marked this pull request as ready for review August 20, 2025 18:25
@pujitm pujitm force-pushed the feat/docker-template branch from 39cf3b6 to 057bd12 Compare September 3, 2025 14:46
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 (5)
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)

62-69: Add a focused test for containerUpdateStatuses passthrough

We now provide DockerPhpService in the resolver test; add an assertion that resolver.containerUpdateStatuses() calls through and returns the service value.

 describe('DockerResolver', () => {
     let resolver: DockerResolver;
     let dockerService: DockerService;
+    let dockerPhpService: DockerPhpService;

@@
         const module: TestingModule = await Test.createTestingModule({
@@
         }).compile();

         const resolver = module.get<DockerResolver>(DockerResolver);
+        dockerPhpService = module.get<DockerPhpService>(DockerPhpService);
         expect(resolver).toBeInstanceOf(DockerResolver);
     });
@@
+    it('should return container update statuses via php service', async () => {
+        const mockStatuses = { 'container-1': { hasUpdate: true, current: 'sha:abc', latest: 'sha:def' } };
+        vi.mocked(dockerPhpService.getContainerUpdateStatuses).mockResolvedValue(mockStatuses as any);
+
+        const result = await resolver.containerUpdateStatuses();
+        expect(result).toEqual(mockStatuses);
+        expect(dockerPhpService.getContainerUpdateStatuses).toHaveBeenCalledTimes(1);
+    });
api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (1)

34-37: Test coverage: assert containerUpdateStatuses resolves via DockerPhpService

Add a unit test to verify the resolver delegates to DockerPhpService.getContainerUpdateStatuses().

 describe('DockerResolver', () => {
     let resolver: DockerResolver;
     let dockerService: DockerService;
+    let dockerPhpService: DockerPhpService;
@@
         resolver = module.get<DockerResolver>(DockerResolver);
         dockerService = module.get<DockerService>(DockerService);
+        dockerPhpService = module.get<DockerPhpService>(DockerPhpService);
@@
+    it('should return container update statuses via php service', async () => {
+        const mockStatuses = { 'container-1': { hasUpdate: true, current: 'sha:a', latest: 'sha:b' } };
+        vi.mocked(dockerPhpService.getContainerUpdateStatuses).mockResolvedValue(mockStatuses as any);
+
+        const result = await resolver.containerUpdateStatuses();
+        expect(result).toEqual(mockStatuses);
+        expect(dockerPhpService.getContainerUpdateStatuses).toHaveBeenCalledTimes(1);
+    });
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (2)

25-33: Honor nullable field contract: return null on failure and add explicit type/description

Avoid throwing from resolvers for expected read-path failures; log and return null.

-    @ResolveField(() => Boolean, { nullable: true })
-    public async isUpdateAvailable(@Parent() container: DockerContainer) {
-        try {
-            return await this.dockerManifestService.isUpdateAvailableCached(container.image);
-        } catch (error) {
-            this.logger.error(error);
-            throw new AppError('Failed to read cached update status. See graphql-api.log for details.');
-        }
-    }
+    @ResolveField(() => Boolean, {
+        nullable: true,
+        description: 'True if a newer remote image digest exists; null when unable to determine.',
+    })
+    public async isUpdateAvailable(@Parent() container: DockerContainer): Promise<boolean | null> {
+        try {
+            return await this.dockerManifestService.isUpdateAvailableCached(container.image);
+        } catch (error) {
+            this.logger.error(error);
+            return null;
+        }
+    }

40-43: Mirror fail-safe pattern for isRebuildReady and add explicit type/description

-    @ResolveField(() => Boolean, { nullable: true })
-    public async isRebuildReady(@Parent() container: DockerContainer) {
-        return this.dockerManifestService.isRebuildReady(container.hostConfig?.networkMode);
-    }
+    @ResolveField(() => Boolean, {
+        nullable: true,
+        description: 'True if rebuild is needed (e.g., container:* network target missing); null when unknown.',
+    })
+    public async isRebuildReady(@Parent() container: DockerContainer): Promise<boolean | null> {
+        try {
+            return await this.dockerManifestService.isRebuildReady(container.hostConfig?.networkMode);
+        } catch (error) {
+            this.logger.error(error);
+            return null;
+        }
+    }
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1)

72-81: Make docker.push parser resilient to property order and quote styles.

Current regex assumes name precedes update and only single quotes. Use lookaheads to support any order and both quote styles.

-        const re = /docker\.push\(\{[^}]*name:'([^']+)'[^}]*update:(\d)[^}]*\}\);/g;
-        for (const m of js.matchAll(re)) {
-            const name = m[1];
-            const updateStatus = Number(m[2]) as StatusItem['updateStatus'];
+        const re =
+            /docker\.push\(\{(?=[^}]*\bname:\s*(['"])([^'"]+)\1)(?=[^}]*\bupdate:\s*(\d))[^}]*\}\);/g;
+        for (const m of js.matchAll(re)) {
+            const name = m[2];
+            const updateStatus = Number(m[3]) as StatusItem['updateStatus'];
             items.push({ name, updateStatus: this.updateStatusToString(updateStatus) });
         }
🧹 Nitpick comments (16)
api/src/unraid-api/cron/job.module.ts (1)

1-12: Make JobModule global to avoid repeated imports across feature modules

Since this module only centralizes ScheduleModule.forRoot(), mark it global so feature modules (CronModule, DockerModule) don’t need to import it, preventing accidental multiple scheduler wiring.

Apply:

-import { Module } from '@nestjs/common';
+import { Global, Module } from '@nestjs/common';

-@Module({
+@Global()
+@Module({
     imports: [ScheduleModule.forRoot()],
 })
 export class JobModule {}

Then import JobModule only once (e.g., in AppModule) and remove it elsewhere.

api/src/unraid-api/cron/cron.module.ts (1)

3-9: Avoid importing JobModule here (centralize in AppModule or make JobModule @global)

To ensure a single ScheduleModule.forRoot() path and reduce duplication, import JobModule only in AppModule (or mark JobModule as @global and drop imports here).

If adopting @global on JobModule:

-import { JobModule } from '@app/unraid-api/cron/job.module.js';
@@
-@Module({
-    imports: [JobModule],
-    providers: [WriteFlashFileService, LogRotateService],
-})
+@Module({
+    providers: [WriteFlashFileService, LogRotateService],
+})
 export class CronModule {}
api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts (1)

3-7: Validate and document expected cron format/timezone for updateCheckCronSchedule

As this is user/config-sourced, consider:

  • Adding a brief description to the field clarifying it must be a cron expression supported by the “cron” package.
  • Validating on write (e.g., in the service/mutation that persists this value) and surfacing errors early; also decide whether to normalize or store a timezone alongside it.

Example tweak (documentation only):

-    @Field(() => String)
+    @Field(() => String, { description: 'Cron expression used to schedule update checks (cron package syntax). Uses server timezone unless otherwise specified.' })
     updateCheckCronSchedule!: string;
api/src/unraid-api/app/app.module.ts (1)

16-28: Keep a single import path for scheduler bootstrap

AppModule importing JobModule is sufficient; drop JobModule from feature modules (e.g., CronModule, DockerModule) to avoid redundant imports. If you mark JobModule as @global, you can still import it only here to execute forRoot once.

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

50-52: Minor naming nit: consider aligning the param name with the new type

Rename dockerConfigService to organizerConfigService for clarity now that it’s organizer-scoped. No behavior change.

-    constructor(
-        private readonly dockerConfigService: DockerOrganizerConfigService,
-        private readonly dockerService: DockerService
-    ) {}
+    constructor(
+        private readonly organizerConfigService: DockerOrganizerConfigService,
+        private readonly dockerService: DockerService
+    ) {}
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)

1-1: Remove unused import

ConfigModule is imported but not used in this spec. Safe to drop.

-import { ConfigModule } from '@nestjs/config';
api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (1)

32-39: Optional: prefer CronTime-based validation to avoid version coupling

Using CronTime will validate expressions without relying on the newer validateCronExpression helper, making this resilient across cron v3/v4+.

-import { validateCronExpression } from 'cron';
+import { CronTime } from 'cron';
@@
-    async validate(config: object): Promise<DockerConfig> {
-        const dockerConfig = await validateObject(DockerConfig, config);
-        const cronExpression = validateCronExpression(dockerConfig.updateCheckCronSchedule);
-        if (!cronExpression.valid) {
-            throw new AppError(`Cron expression not supported: ${dockerConfig.updateCheckCronSchedule}`);
-        }
-        return dockerConfig;
-    }
+    async validate(config: object): Promise<DockerConfig> {
+        const dockerConfig = await validateObject(DockerConfig, config);
+        try {
+            // Throws if invalid
+            new CronTime(dockerConfig.updateCheckCronSchedule);
+        } catch {
+            throw new AppError(`Cron expression not supported: ${dockerConfig.updateCheckCronSchedule}`);
+        }
+        return dockerConfig;
+    }

CronTime is part of the cron API across versions. (npmjs.com)

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

29-49: Avoid hardcoding 'root' as an object key

Use the constant for the key to prevent drift if DEFAULT_ORGANIZER_ROOT_ID changes.

-                    entries: {
-                        root: {
+                    entries: {
+                        [DEFAULT_ORGANIZER_ROOT_ID]: {
                             type: 'folder',
                             id: DEFAULT_ORGANIZER_ROOT_ID,
                             name: 'Root',
                             children: [],
                         },
                     },

51-58: Tighten API surface and return proper status code on validation failure

  • Accept unknown to match validateObject and avoid unnecessary widening.
  • Use 422/400 instead of default 500 for invalid user/config input.
-    async validate(config: object): Promise<OrganizerV1> {
+    async validate(config: unknown): Promise<OrganizerV1> {
         const organizer = await validateObject(OrganizerV1, config);
         const { isValid, errors } = await validateOrganizerIntegrity(organizer);
         if (!isValid) {
-            throw new AppError(`Docker organizer validation failed: ${JSON.stringify(errors, null, 2)}`);
+            // Avoid giant payloads in the error message; details can be logged upstream if needed.
+            throw new AppError('Docker organizer validation failed', 422);
         }
         return organizer;
     }
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (2)

36-39: Declare explicit return type

Clarifies intent and aligns with our TS conventions.

-    async refreshContainerDigestsAfterStartup() {
+    async refreshContainerDigestsAfterStartup(): Promise<void> {
         await this.dockerManifestService.refreshDigests();
     }

18-31: Optional: react to schedule changes at runtime

If updateCheckCronSchedule can change without a restart, consider re-registering the cron job when config updates (or document that a restart is required). Mutex already prevents overlap.

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

1-14: Drop AppError import if adopting null-return approach

Prevents an “unused import” warning.

-import { AppError } from '@app/core/errors/app-error.js';

50-53: Add explicit return type to mutation

-    public async refreshDockerDigests() {
+    public async refreshDockerDigests(): Promise<boolean> {
         return this.dockerManifestService.refreshDigests();
     }
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (3)

8-14: Fix typo and consider exporting the enum.

  • Typo: “propogate” → “propagate”.
  • Exporting the enum consolidates these user-facing strings for reuse and prevents drift elsewhere.
-/** Note that these values propogate down to api consumers, so be aware of breaking changes. */
+/** Note that these values propagate down to API consumers, so be aware of breaking changes. */
-enum UpdateStatus {
+export enum UpdateStatus {
     UP_TO_DATE = 'UP_TO_DATE',
     UPDATE_AVAILABLE = 'UPDATE_AVAILABLE',
     REBUILD_READY = 'REBUILD_READY',
     UNKNOWN = 'UNKNOWN',
 }

83-102: Tiny simplification: table-lookup over switch.

Functionally equivalent and a bit tighter.

-    private updateStatusToString(
-        updateStatus: StatusItem['updateStatus']
-    ): ExplicitStatusItem['updateStatus'] {
-        switch (updateStatus) {
-            case 0:
-                return UpdateStatus.UP_TO_DATE;
-            case 1:
-                return UpdateStatus.UPDATE_AVAILABLE;
-            case 2:
-                return UpdateStatus.REBUILD_READY;
-            default:
-                return UpdateStatus.UNKNOWN;
-        }
-    }
+    private updateStatusToString(
+        updateStatus: StatusItem['updateStatus']
+    ): ExplicitStatusItem['updateStatus'] {
+        const map = [
+            UpdateStatus.UP_TO_DATE,
+            UpdateStatus.UPDATE_AVAILABLE,
+            UpdateStatus.REBUILD_READY,
+            UpdateStatus.UNKNOWN,
+        ] as const;
+        return map[updateStatus] ?? UpdateStatus.UNKNOWN;
+    }

104-114: Minor: limit split to three parts.

Avoid unnecessary allocations if the output contains more nulls unexpectedly.

-        const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
+        const parts = stdout.split('\0', 3); // [html, "docker.push(...)", busyFlag]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2566efc and a438f82.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • api/package.json (1 hunks)
  • api/src/unraid-api/app/app.module.ts (2 hunks)
  • api/src/unraid-api/cron/cron.module.ts (1 hunks)
  • api/src/unraid-api/cron/job.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts (4 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
🧰 Additional context used
📓 Path-based instructions (7)
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

Prefer adding new API files under api/src/unraid-api/ instead of legacy locations

Files:

  • api/src/unraid-api/cron/cron.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts
  • api/src/unraid-api/cron/job.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts
  • api/src/unraid-api/app/app.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • api/src/unraid-api/cron/cron.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts
  • api/src/unraid-api/cron/job.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts
  • api/src/unraid-api/app/app.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

In API TypeScript, use .js extensions in import specifiers for ESM compatibility

Files:

  • api/src/unraid-api/cron/cron.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts
  • api/src/unraid-api/cron/job.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts
  • api/src/unraid-api/app/app.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

For cache-manager v7, always use TTL values in milliseconds when setting cache TTL

Files:

  • api/src/unraid-api/cron/cron.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.model.ts
  • api/src/unraid-api/cron/job.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts
  • api/src/unraid-api/app/app.module.ts
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.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/graph/resolvers/docker/docker.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

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/graph/resolvers/docker/docker.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx): When testing for errors, use .rejects.toThrow() without asserting specific message strings
Focus tests on behavior, not implementation details like exact error messages
Avoid brittle tests that depend on non-essential details (e.g., log formats)
Use mocks as nouns (objects), not verbs (functions)

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
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
🧠 Learnings (6)
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
PR: unraid/api#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-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/graph/resolvers/docker/organizer/docker-organizer.service.spec.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to api/**/vitest.config.@(js|ts|mjs|cjs) : API test runner is Vitest

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.spec.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:

  • api/src/unraid-api/graph/resolvers/docker/docker-container.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:

  • api/src/unraid-api/graph/resolvers/docker/docker-container.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-container.resolver.ts
🧬 Code graph analysis (8)
api/src/unraid-api/cron/cron.module.ts (3)
api/src/unraid-api/app/app.module.ts (1)
  • Module (22-72)
api/src/unraid-api/cron/job.module.ts (1)
  • Module (9-12)
api/src/unraid-api/graph/resolvers/docker/docker.module.ts (1)
  • Module (15-37)
api/src/unraid-api/cron/job.module.ts (3)
api/src/unraid-api/app/app.module.ts (1)
  • Module (22-72)
api/src/unraid-api/cron/cron.module.ts (1)
  • Module (7-11)
api/src/unraid-api/graph/resolvers/docker/docker.module.ts (1)
  • Module (15-37)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts (5)
api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts (1)
  • Injectable (46-225)
api/src/unraid-api/organizer/organizer.ts (2)
  • DEFAULT_ORGANIZER_VIEW_ID (14-14)
  • DEFAULT_ORGANIZER_ROOT_ID (15-15)
api/src/unraid-api/graph/resolvers/validation.utils.ts (1)
  • validateObject (21-40)
api/src/unraid-api/organizer/organizer.validation.ts (1)
  • validateOrganizerIntegrity (90-102)
api/src/core/errors/app-error.ts (1)
  • AppError (4-37)
api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (3)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
  • Injectable (9-40)
api/src/unraid-api/graph/resolvers/validation.utils.ts (1)
  • validateObject (21-40)
api/src/core/errors/app-error.ts (1)
  • AppError (4-37)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (3)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
  • Injectable (9-40)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
  • Injectable (11-51)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (2)
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)
  • Resolver (22-151)
api/src/core/errors/app-error.ts (1)
  • AppError (4-37)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (2)
api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (1)
  • Injectable (12-40)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
  • Injectable (11-51)
api/src/unraid-api/graph/resolvers/docker/docker.module.ts (2)
api/src/unraid-api/cron/job.module.ts (1)
  • Module (9-12)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
🔇 Additional comments (13)
api/package.json (1)

97-97: Verify cron 4.3.0 downgrade and compatibility

– Found direct imports of validateCronExpression in docker-config.service.ts and use of CronJob.from in container-status.job.ts; confirm that moving from 4.3.3 → 4.3.0 is intentional and that no fixes in 4.3.3 are required for your cron-based jobs.
– Ensure existing behavior (validation, scheduling via @nestjs/schedule 6.0.0) remains unchanged under 4.3.0 and update tests or docs if needed.

api/src/unraid-api/cron/job.module.ts (1)

4-11: Nice centralization of scheduler bootstrap

Wrapping ScheduleModule.forRoot() in a single-purpose JobModule simplifies wiring/testing and aligns with DRY.

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

8-8: DI refactor to DockerOrganizerConfigService looks correct

Import path and type swap align with the organizer-specific config service. No logic changes introduced.

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

23-27: Good: organizer config provider override added

Overriding DockerOrganizerConfigService keeps the module compilation isolated from file IO. Looks good.

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

11-15: LGTM: tests updated to organizer-scoped services

Imports switched to DockerOrganizerConfigService and organizer service path; spec semantics maintained.


141-141: LGTM: DI tokens/types aligned

configService typed/retrieved as DockerOrganizerConfigService and provider override updated accordingly.

Also applies to: 181-188, 223-224

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

26-30: Sensible default

Defaulting updateCheckCronSchedule to CronExpression.EVERY_DAY_AT_6AM centralizes a predictable schedule.


6-7: cron version v4.3.0 supports validateCronExpression
api/package.json depends on cron v4.3.0, which provides the validateCronExpression export.

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

21-27: LGTM: config key and filename

Correct ESM import usage and sensible key/filename choices; aligns with our config-persistence pattern.

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

3-4: LGTM: job wiring

ContainerStatusJob + JobModule integration looks correct.


20-25: LGTM: providers and resolver registration

New services/resolver are correctly provided; ESM import specifiers use .js as required.

Also applies to: 27-29, 33-33


16-16: ScheduleModule.forRoot() invoked only in JobModule
Found a single ScheduleModule.forRoot() call in api/src/unraid-api/cron/job.module.ts (line 10); NestJS dynamic modules are initialized once per application, so no duplicate scheduler initialization.

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

4-4: ESM import suffix looks correct.

Local import uses .js extension per API ESM guideline.

@pujitm pujitm force-pushed the feat/docker-template branch from 057bd12 to a661d89 Compare September 3, 2025 14: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

🧹 Nitpick comments (2)
api/package.json (1)

97-97: Keep cron dependency and add commit note for version pin
I found direct imports of validateCronExpression in api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts and CronJob.from in api/src/unraid-api/graph/resolvers/docker/container-status.job.ts, so cron is required. If pinning to 4.3.0 is intentional (e.g. for compatibility with @nestjs/schedule), add a short commit note explaining the downgrade; otherwise, consider updating to the latest compatible version.

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

22-23: Add an override for DockerPhpService in the test setup
Mock DockerPhpService before .compile() to prevent constructor side effects:

             .overrideProvider(DockerOrganizerConfigService)
             .useValue({ getConfig: vi.fn() })
+            .overrideProvider(DockerPhpService)
+            .useValue({ getContainerUpdateStatuses: vi.fn() })
             .overrideProvider(DockerConfigService)
             .useValue({ getConfig: vi.fn() })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a438f82 and 52d312c.

📒 Files selected for processing (10)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (3 hunks)
  • api/package.json (1 hunks)
  • api/src/unraid-api/app/app.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2 hunks)
  • api/vite.config.ts (2 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 (7)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/vite.config.ts
  • api/src/unraid-api/app/app.module.ts
🧰 Additional context used
📓 Path-based instructions (7)
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

Prefer adding new API files under api/src/unraid-api/ instead of legacy locations

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.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/docker.module.spec.ts
api/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx): When testing for errors, use .rejects.toThrow() without asserting specific message strings
Focus tests on behavior, not implementation details like exact error messages
Avoid brittle tests that depend on non-essential details (e.g., log formats)
Use mocks as nouns (objects), not verbs (functions)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

In API TypeScript, use .js extensions in import specifiers for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

For cache-manager v7, always use TTL values in milliseconds when setting cache TTL

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
🧠 Learnings (1)
📚 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/graph/resolvers/docker/docker.module.spec.ts
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (3)

7-7: Import looks correct and ESM-compliant.

Using .js in the specifier aligns with our ESM guideline.


12-13: Organizer imports moved under organizer/ — good modularization.

Path update is consistent and keeps concerns grouped.


67-67: Good: Resolver test stubs DockerPhpService.getContainerUpdateStatuses.

This keeps the resolver unit-scoped without touching external update checks.

@pujitm pujitm requested a review from zackspear September 3, 2025 15:25
@pujitm pujitm force-pushed the feat/docker-template branch from dc8e020 to 63e37a0 Compare September 3, 2025 15:26
@codecov
Copy link

codecov bot commented Sep 3, 2025

Codecov Report

❌ Patch coverage is 55.78704% with 191 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.48%. Comparing base (193be3d) to head (3c4b007).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
...d-api/graph/resolvers/docker/docker-php.service.ts 39.13% 56 Missing ⚠️
...ocker/organizer/docker-organizer-config.service.ts 48.07% 27 Missing ⚠️
api/src/unraid-api/decorators/omit-if.decorator.ts 47.72% 23 Missing ⚠️
.../graph/resolvers/docker/docker-manifest.service.ts 43.58% 22 Missing ⚠️
...raph/resolvers/docker/docker-container.resolver.ts 48.71% 20 Missing ⚠️
...api/graph/resolvers/docker/container-status.job.ts 52.63% 18 Missing ⚠️
api/src/unraid-api/utils/feature-flag.helper.ts 0.00% 10 Missing ⚠️
...raid-api/graph/resolvers/docker/docker.resolver.ts 73.68% 5 Missing ⚠️
api/src/unraid-api/graph/graph.module.ts 42.85% 4 Missing ⚠️
...aph/resolvers/docker/docker-update-status.model.ts 78.57% 3 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1582      +/-   ##
==========================================
- Coverage   58.53%   58.48%   -0.06%     
==========================================
  Files         624      636      +12     
  Lines       30732    31154     +422     
  Branches     3617     3650      +33     
==========================================
+ Hits        17989    18219     +230     
- Misses      12706    12898     +192     
  Partials       37       37              

☔ 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: 0

♻️ Duplicate comments (4)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)

31-45: Fix digest/tag normalization and honor “unknown” cache status (return null).

Current logic appends :latest even for digest refs and returns false instead of null when status is unknown, violating the method’s contract.

-    async isUpdateAvailableCached(imageRef: string, cacheData?: Record<string, CachedStatusEntry>) {
-        let taggedRef = imageRef;
-        if (!taggedRef.includes(':')) taggedRef += ':latest';
-
-        cacheData ??= await this.dockerPhpService.readCachedUpdateStatus();
-        const containerData = cacheData[taggedRef];
-        if (!containerData) return null;
-        return containerData.status?.toLowerCase() === 'true';
-    }
+    async isUpdateAvailableCached(
+        imageRef: string,
+        cacheData?: Record<string, CachedStatusEntry>
+    ): Promise<boolean | null> {
+        let taggedRef = imageRef.trim();
+        // Default only when neither a tag (:) nor a digest (@) is present
+        if (!/[:@]/.test(taggedRef)) taggedRef += ':latest';
+
+        cacheData ??= await this.dockerPhpService.readCachedUpdateStatus();
+        const containerData = cacheData[taggedRef];
+        if (!containerData) return null;
+        if (containerData.status == null) return null;
+        return containerData.status === 'true';
+    }
api/vite.config.ts (3)

79-82: Good: wrapper.php is now watched

addWatchFile ensures edits trigger rebuilds in watch mode. This addresses prior feedback.


1-2: Use POSIX joins to avoid malformed asset paths on Windows

Rollup expects POSIX-style fileName. Replace path.join with path.posix.join and adjust the import.

-import { basename, join } from 'node:path';
+import { basename, posix as pathPosix } from 'node:path';

91-95: Fix cross-platform asset fileName

Ensure forward slashes in emitted asset paths.

-                        this.emitFile({
-                            type: 'asset',
-                            fileName: join('assets', basename(file)),
-                            source: content,
-                        });
+                        this.emitFile({
+                            type: 'asset',
+                            fileName: pathPosix.join('assets', basename(file)),
+                            source: content,
+                        });
🧹 Nitpick comments (7)
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (2)

22-23: Tighten the config mock and add test hygiene

Consider returning a minimal, realistic config shape (even a small object) to better match call sites. Also add afterEach cleanup to prevent cross-test bleed from vi.fn mocks.

Example hygiene snippet to add near the top-level of this spec file:

afterEach(() => {
  vi.restoreAllMocks();
});

If helpful, I can draft a minimal config stub aligned with DockerOrganizerConfigService’s getConfig signature. Do you want me to?


67-67: Mock async return of getContainerUpdateStatuses with an explicit value
getContainerUpdateStatuses is declared as async returning a Promise<ExplicitStatusItem[]>, so update the mock to return an array:

- { provide: DockerPhpService, useValue: { getContainerUpdateStatuses: vi.fn() } },
+ { provide: DockerPhpService, useValue: { getContainerUpdateStatuses: vi.fn().mockResolvedValue([]) } },
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (4)

15-17: Annotate mutex type and simplify lambda.

Improve clarity and IDE help; no behavior change.

-    private readonly refreshDigestsMutex = new AsyncMutex(() => {
-        return this.dockerPhpService.refreshDigestsViaPhp();
-    });
+    private readonly refreshDigestsMutex: AsyncMutex<boolean> = new AsyncMutex(
+        () => this.dockerPhpService.refreshDigestsViaPhp()
+    );

25-29: Add explicit return/param types; optionally leverage default op when no path is provided.

This tightens typing and avoids an unnecessary wrapper when using the default operation.

-    async refreshDigests(mutex = this.refreshDigestsMutex, dockerUpdatePath?: string) {
-        return mutex.do(() => {
-            return this.dockerPhpService.refreshDigestsViaPhp(dockerUpdatePath);
-        });
-    }
+    async refreshDigests(
+        mutex: AsyncMutex<boolean> = this.refreshDigestsMutex,
+        dockerUpdatePath?: string
+    ): Promise<boolean> {
+        return dockerUpdatePath
+            ? mutex.do(() => this.dockerPhpService.refreshDigestsViaPhp(dockerUpdatePath))
+            : mutex.do();
+    }

47-51: Correct JSDoc example for networkMode.

Docker’s “container:” network mode references a container name/ID, not an image ref.

-     * @param networkMode - The network mode of the container, e.g. "container:unraid/baseimage:latest".
+     * @param networkMode - The network mode of the container, e.g. "container:<container_name>".

52-61: Add explicit return type; consider treating only 404 as rebuild-ready.

Typing improves clarity. Optionally, avoid returning true on generic Docker errors (offline/permission), and only return true when the target container is missing.

-    async isRebuildReady(networkMode?: string) {
+    async isRebuildReady(networkMode?: string): Promise<boolean> {
         if (!networkMode || !networkMode.startsWith('container:')) return false;
         const target = networkMode.slice('container:'.length);
         try {
             await docker.getContainer(target).inspect();
             return false;
-        } catch {
-            return true; // unresolved target -> ':???' equivalent
+        } catch (err) {
+            // unresolved target -> ':???' equivalent
+            // If feasible, narrow to 404 only:
+            // return typeof err === 'object' && err && 'statusCode' in err && (err as { statusCode?: number }).statusCode === 404;
+            return true;
         }
     }

If you want the stricter behavior, confirm that treating only 404 as rebuild-ready matches UI expectations; I can update the code accordingly.

api/vite.config.ts (1)

83-97: Hoist phpFiles and drop unnecessary async; make path resolution cwd-safe

  • Define phpFiles once (outside the plugin) and reuse to remove duplication (Lines 80 and 84).
  • generateBundle doesn’t await anything; remove async.
  • Prefer fileURLToPath + resolve so paths aren’t sensitive to process.cwd (aligns with retrieved learnings).

Apply within this block:

-                buildStart() {
-                    const phpFiles = ['src/core/utils/plugins/wrapper.php'];
+                buildStart() {
                     phpFiles.forEach((file) => this.addWatchFile(file));
                 },
-                async generateBundle() {
-                    const phpFiles = ['src/core/utils/plugins/wrapper.php'];
+                generateBundle() {
                     phpFiles.forEach((file) => {

And add near the top of the file (outside the plugin object):

// cwd-safe path resolution (derived from retrieved learnings)
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';

const rootDir = dirname(fileURLToPath(import.meta.url));
const phpFiles = [resolve(rootDir, 'src/core/utils/plugins/wrapper.php')];
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a438f82 and dffa725.

📒 Files selected for processing (11)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (3 hunks)
  • api/package.json (1 hunks)
  • api/src/unraid-api/app/app.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2 hunks)
  • api/vite.config.ts (2 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 (7)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/package.json
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts
  • api/generated-schema.graphql
  • api/src/unraid-api/app/app.module.ts
🧰 Additional context used
📓 Path-based instructions (7)
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

Prefer adding new API files under api/src/unraid-api/ instead of legacy locations

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/vite.config.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

In API TypeScript, use .js extensions in import specifiers for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
api/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

For cache-manager v7, always use TTL values in milliseconds when setting cache TTL

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
  • api/vite.config.ts
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.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/docker.module.spec.ts
api/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx): When testing for errors, use .rejects.toThrow() without asserting specific message strings
Focus tests on behavior, not implementation details like exact error messages
Avoid brittle tests that depend on non-essential details (e.g., log formats)
Use mocks as nouns (objects), not verbs (functions)

Files:

  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
🧠 Learnings (4)
📚 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/graph/resolvers/docker/docker-manifest.service.ts
📚 Learning: 2024-12-11T20:39:02.511Z
Learnt from: pujitm
PR: unraid/api#978
File: web/helpers/apollo-cache/index.ts:134-139
Timestamp: 2024-12-11T20:39:02.511Z
Learning: In `web/helpers/apollo-cache/index.ts`, the `cache.readQuery` method returns `null` when it fails to read from the cache, rather than throwing an exception.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts
📚 Learning: 2025-03-04T14:55:00.903Z
Learnt from: elibosley
PR: unraid/api#1151
File: plugin/builder/utils/consts.ts:6-6
Timestamp: 2025-03-04T14:55:00.903Z
Learning: The startingDir constant in plugin/builder/utils/consts.ts is defined using process.cwd(), which can cause issues if directory changes occur after importing this constant. Using __dirname.split('/builder')[0] would make it more reliable by making it relative to the file location rather than the current working directory.

Applied to files:

  • api/vite.config.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/graph/resolvers/docker/docker.module.spec.ts
🧬 Code graph analysis (1)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (2)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (2)
  • Injectable (34-132)
  • CachedStatusEntry (25-32)
packages/unraid-shared/src/util/processing.ts (1)
  • AsyncMutex (68-150)
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (2)

7-7: Good ESM import for new dependency

Using .js in the specifier aligns with our ESM guideline and keeps the test isolated. Looks good.


12-13: All old docker-organizer imports removed
No occurrences of the previous @app/unraid-api/graph/resolvers/docker/docker-organizer.service path remain.

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

1-9: Imports look correct and ESM-safe (.js specifiers).

No issues spotted with dependency paths or types.

@pujitm
Copy link
Member Author

pujitm commented Sep 8, 2025

@coderabbitai pause

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

✅ Actions performed

Reviews paused.

@pujitm
Copy link
Member Author

pujitm commented Sep 9, 2025

@coderabbitai resume

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 9, 2025

✅ Actions performed

Reviews resumed.

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)
api/generated-schema.graphql (1)

1092-1094: Add field descriptions and confirm guards for DockerContainer update fields

Consider documenting isUpdateAvailable and isRebuildReady for introspection, and verify resolver-level permission guards exist.

Example:

"""True if an update is available for this container"""
isUpdateAvailable: Boolean
"""True if a rebuild is ready for this container"""
isRebuildReady: Boolean
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (2)

1-145: Verify presence of resolvers for DockerContainer.isUpdateAvailable/isRebuildReady with guards

Ensure @ResolveField methods exist (with READ permissions) for these new fields in docker-container.resolver.ts.

#!/bin/bash
fd docker-container.resolver.ts api | xargs -I{} rg -nC2 "@ResolveField\\(.*is(UpdateAvailable|RebuildReady)" {}

136-144: Align type with schema and harden non-null contract; add error handling

Add a description and return [] on failures; also ensure the list is non-null (schema currently shows JSON!). Decide on JSON vs typed array and keep both resolver and generated schema in sync.

-  @UseFeatureFlag('ENABLE_NEXT_DOCKER_RELEASE')
-  @UsePermissions({
-      action: AuthAction.READ_ANY,
-      resource: Resource.DOCKER,
-  })
-  @ResolveField(() => [ExplicitStatusItem])
-  public async containerUpdateStatuses() {
-      return this.dockerPhpService.getContainerUpdateStatuses();
-  }
+  @UseFeatureFlag('ENABLE_NEXT_DOCKER_RELEASE')
+  @UsePermissions({
+    action: AuthAction.READ_ANY,
+    resource: Resource.DOCKER,
+  })
+  @ResolveField(() => [ExplicitStatusItem], {
+    nullable: false,
+    description: 'Per-container update status snapshot; empty when unavailable.',
+  })
+  public async containerUpdateStatuses(): Promise<ExplicitStatusItem[]> {
+    try {
+      const data = await this.dockerPhpService.getContainerUpdateStatuses();
+      return Array.isArray(data) ? data : [];
+    } catch {
+      return [];
+    }
+  }

If you intend to keep JSON!, switch the ResolveField type to GraphQLJSON and still keep the try/catch and fallback.

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

46-53: Fix Logger.warn usage (message first; avoid passing objects as the message).

Current calls pass the payload/error as the first arg and the message as the second. Prefer a clear string message; include details in the message or log separately.

-            if (success) return data;
-            this.logger.warn(cacheData, 'Invalid cached update status');
+            if (success) return data;
+            this.logger.warn('Invalid cached update status');
             return {};
         } catch (error) {
-            this.logger.warn(error, 'Failed to read cached update status');
+            this.logger.warn(`Failed to read cached update status: ${String(error)}`);
             return {};
         }

84-91: Make docker.push parser resilient to property order, quote styles, and multi-digit codes.

Use lookaheads so name/update can appear in any order and support both single/double quotes (+ allow \d+).

-        const re = /docker\.push\(\{[^}]*name:'([^']+)'[^}]*update:(\d)[^}]*\}\);/g;
+        const re =
+            /docker\.push\(\{(?=[^}]*\bname:\s*(['"])([^'"]+)\1)(?=[^}]*\bupdate:\s*(\d+))[^}]*\}\);/g;
         for (const m of js.matchAll(re)) {
-            const name = m[1];
-            const updateStatus = Number(m[2]) as StatusItem['updateStatus'];
+            const name = m[2];
+            const updateStatus = Number(m[3]) as StatusItem['updateStatus'];
             items.push({ name, updateStatus: this.updateStatusToString(updateStatus) });
         }
🧹 Nitpick comments (19)
api/.env.development (1)

34-34: Reorder the new flag to satisfy dotenv-linter.

dotenv-linter flags UnorderedKey; place ENABLE_NEXT_DOCKER_RELEASE before ENVIRONMENT to keep keys sorted.

-ENABLE_NEXT_DOCKER_RELEASE=true
+# moved above ENVIRONMENT to satisfy dotenv-linter
+ENABLE_NEXT_DOCKER_RELEASE=true
api/src/unraid-api/utils/feature-flag.helper.ts (1)

20-28: Narrow the flag value type to boolean to avoid truthy/falsey surprises.

Today any truthy value passes; tightening to boolean makes misuse harder and improves DX.

-export function checkFeatureFlag<T extends Record<string, any>>(flags: T, key: keyof T): void {
-    const isEnabled = Boolean(flags[key]);
+export function checkFeatureFlag<T extends Record<string, boolean>>(flags: T, key: keyof T): void {
+    const isEnabled = flags[key];
api/src/consts.ts (1)

83-93: Add a typed key alias for reuse across decorators/tests.

This centralizes the flag key type and avoids hard-coded string unions elsewhere.

 export const FeatureFlags = Object.freeze({
     ENABLE_NEXT_DOCKER_RELEASE,
 });
+
+export type FeatureFlagKey = keyof typeof FeatureFlags;
api/src/unraid-api/graph/resolvers/docker/docker-update-status.model.ts (1)

18-25: Add field descriptions to improve schema introspection/docs.

Small win for API consumers navigating the enum/object in GraphQL tooling.

-@ObjectType()
+@ObjectType({ description: 'Explicit container update status item.' })
 export class ExplicitStatusItem {
-    @Field(() => String)
+    @Field(() => String, { description: 'Docker container name.' })
     name!: string;
 
-    @Field(() => UpdateStatus)
+    @Field(() => UpdateStatus, { description: 'Computed update status for the container.' })
     updateStatus!: UpdateStatus;
 }
api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)

61-68: Mocks look good; consider global reset to avoid test bleed.

Add an afterEach to restore spies/mocks across tests.

// at bottom/top-level in this spec file
import { afterEach } from 'vitest';

afterEach(() => {
  vi.restoreAllMocks();
});
api/src/unraid-api/decorators/use-feature-flag.decorator.spec.ts (1)

45-48: Prefer reading metadata from the prototype method rather than the instance

Using instance methods can be sensitive to binding. Query the prototype method to be robust:

-const metadata = reflector.get(OMIT_IF_METADATA_KEY, instance.testQuery);
+const metadata = reflector.get(OMIT_IF_METADATA_KEY, (TestResolver as any).prototype.testQuery);

Apply similarly for other assertions in this file.

Also applies to: 60-63, 84-89, 107-116, 134-140

api/generated-schema.graphql (1)

142-145: Confirm nullability semantics for isSpinning across types

ArrayDisk.isSpinning is nullable while Disk.isSpinning is non-null. If intentional, add short descriptions clarifying the difference (e.g., “nullable when array not started”).

Also applies to: 353-355

api/src/unraid-api/decorators/omit-if.decorator.ts (1)

25-44: Narrow decorator kind or handle property-decorator path

OmitIf is typed as MethodDecorator & PropertyDecorator, but Extensions is a method/class decorator. Either narrow to methods or add a property-safe branch to avoid misuse.

-export function OmitIf(condition: boolean | (() => boolean)): MethodDecorator & PropertyDecorator {
+export function OmitIf(condition: boolean | (() => boolean)): MethodDecorator {
@@
-    return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
+    return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
         if (shouldOmit) {
             SetMetadata(OMIT_IF_METADATA_KEY, true)(
                 target,
                 propertyKey as string,
                 descriptor as PropertyDescriptor
             );
             Extensions({ omitIf: true })(
                 target,
                 propertyKey as string,
                 descriptor as PropertyDescriptor
             );
         }
         return descriptor;
     };
 }
api/src/unraid-api/decorators/omit-if.decorator.spec.ts (1)

24-27: Read metadata from prototype to avoid binding quirks

Access the decorated function via the prototype for consistency.

-const metadata = reflector.get(OMIT_IF_METADATA_KEY, instance.testMethod);
+const metadata = reflector.get(OMIT_IF_METADATA_KEY, (TestResolver as any).prototype.testMethod);

Apply similarly to other occurrences.

Also applies to: 37-40, 68-72, 110-116, 135-140, 165-170

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

90-96: Use constructor or message matching for error assertions

Matching on a concrete error instance can be brittle. Prefer constructor and/or message.

-await expect(service.validate(inputConfig)).rejects.toThrow(
-    new AppError('Cron expression not supported: invalid-cron')
-);
+await expect(service.validate(inputConfig)).rejects.toThrow(AppError);
+await expect(service.validate(inputConfig)).rejects.toThrow(
+  /Cron expression not supported: invalid-cron/
+);

Apply similarly to the other error cases.

Also applies to: 110-116, 130-136

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

56-63: Trim or structure validation error output to avoid huge messages

Stringifying full error objects can be noisy. Consider summarizing count and first N items.

-throw new AppError(`Docker organizer validation failed: ${JSON.stringify(errors, null, 2)}`);
+const summary = Array.isArray(errors) ? `${errors.length} issue(s)` : 'See details in logs';
+// Optionally log full details with a logger
+throw new AppError(`Docker organizer validation failed: ${summary}`);
api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (1)

75-87: Consider normalizing/validating organizer IDs and deduplicating inputs

Guard against duplicates and invalid IDs (e.g., Set for childrenIds, verify parent existence) before persisting.

-childrenIds: childrenIds ?? [],
+childrenIds: Array.from(new Set(childrenIds ?? [])),

Also applies to: 95-104, 111-117, 125-134

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

37-44: Prefer unknown over object for external input

object is too permissive/implicit. unknown encourages explicit validation (which you already do).

-async validate(config: object): Promise<DockerConfig> {
+async validate(config: unknown): Promise<DockerConfig> {
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (6)

121-131: Guard phpLoader failures; return [] instead of throwing from resolver path.

Prevents a transient PHP error from bubbling and failing the entire GraphQL field.

-        const stdout = await phpLoader({
-            file: dockerContainersPath,
-            method: 'GET',
-        });
+        let stdout: string;
+        try {
+            stdout = await phpLoader({
+                file: dockerContainersPath,
+                method: 'GET',
+            });
+        } catch (error) {
+            this.logger.warn(`Failed to fetch container update statuses: ${String(error)}`);
+            return [];
+        }
         const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
         const js = parts[1] || '';
         return this.parseStatusesFromDockerPush(js);

If you intend failures to propagate upstream, skip this change. Please confirm the desired behavior.


65-78: Add explicit return type for clarity.

-    async refreshDigestsViaPhp(
+    async refreshDigestsViaPhp(
         dockerUpdatePath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php'
-    ) {
+    ): Promise<boolean> {

104-113: Be explicit about status code 3 mapping.

Keeps intent obvious even if new codes are added later.

         switch (updateStatus) {
           case 0:
             return UpdateStatus.UP_TO_DATE;
           case 1:
             return UpdateStatus.UPDATE_AVAILABLE;
           case 2:
             return UpdateStatus.REBUILD_READY;
+          case 3:
+            return UpdateStatus.UNKNOWN;
           default:
             return UpdateStatus.UNKNOWN;
         }

18-26: Optionally make the cache schema strict.

If the source file may include extra keys, skip. Otherwise, .strict() prevents silently accepting unexpected fields.

-const CachedStatusEntrySchema = z.object({
+const CachedStatusEntrySchema = z.object({
   /** sha256 digest - "sha256:..." */
   local: z.string(),
   /** sha256 digest - "sha256:..." */
   remote: z.string(),
   /** whether update is available (true), not available (false), or unknown (null) */
   status: z.enum(['true', 'false']).nullable(),
-});
+}).strict();

41-42: Factor default file paths into named constants.

Centralizes config, eases reuse and future changes.

Add near the top (after imports):

const DEFAULT_CACHE_FILE = '/var/lib/docker/unraid-update-status.json';
const DOCKER_UPDATE_PATH =
  '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php';
const DOCKER_CONTAINERS_PATH =
  '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerContainers.php';

Then update signatures:

-        cacheFile = '/var/lib/docker/unraid-update-status.json'
+        cacheFile = DEFAULT_CACHE_FILE

-        dockerUpdatePath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php'
+        dockerUpdatePath = DOCKER_UPDATE_PATH

-        dockerContainersPath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerContainers.php'
+        dockerContainersPath = DOCKER_CONTAINERS_PATH

Also applies to: 66-67, 122-123


32-32: Remove empty constructor.

Not needed; the class has no injected deps.

-    constructor() {}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49189d9 and e1bbe0c.

📒 Files selected for processing (23)
  • api/.env.development (1 hunks)
  • api/.gitignore (1 hunks)
  • api/docs/developer/feature-flags.md (1 hunks)
  • api/generated-schema.graphql (5 hunks)
  • api/src/consts.ts (2 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/cron/job.module.ts (1 hunks)
  • api/src/unraid-api/decorators/omit-if.decorator.spec.ts (1 hunks)
  • api/src/unraid-api/decorators/omit-if.decorator.ts (1 hunks)
  • api/src/unraid-api/decorators/use-feature-flag.decorator.spec.ts (1 hunks)
  • api/src/unraid-api/decorators/use-feature-flag.decorator.ts (1 hunks)
  • api/src/unraid-api/graph/graph.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-update-status.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts (7 hunks)
  • api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.ts (1 hunks)
  • api/src/unraid-api/utils/feature-flag.helper.ts (1 hunks)
  • packages/unraid-shared/src/util/processing.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • api/.gitignore
  • api/docs/developer/feature-flags.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • api/src/unraid-api/cron/job.module.ts
  • api/src/unraid-api/graph/resolvers/docker/container-status.job.ts
  • packages/unraid-shared/src/util/processing.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-container.resolver.ts
🧰 Additional context used
🧠 Learnings (5)
📚 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/decorators/use-feature-flag.decorator.spec.ts
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to {api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx) : Avoid brittle tests that depend on non-essential details (e.g., log formats)

Applied to files:

  • api/src/unraid-api/decorators/omit-if.decorator.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.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} : Focus tests on observable behavior and outcomes, not implementation details such as exact error messages

Applied to files:

  • api/src/unraid-api/decorators/omit-if.decorator.spec.ts
📚 Learning: 2025-09-09T14:03:45.207Z
Learnt from: pujitm
PR: unraid/api#1582
File: api/src/unraid-api/graph/resolvers/docker/docker-config.service.ts:6-6
Timestamp: 2025-09-09T14:03:45.207Z
Learning: The cron package (version 4.3.0) exports validateCronExpression function that returns an object with a `valid` boolean property, which is the correct way to validate cron expressions in this codebase. Don't suggest changes to working cron validation code that uses this pattern.

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-config.service.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/graph/resolvers/docker/docker.module.spec.ts
🪛 dotenv-linter (3.3.0)
api/.env.development

[warning] 34-34: [UnorderedKey] The ENABLE_NEXT_DOCKER_RELEASE key should go before the ENVIRONMENT key

(UnorderedKey)

🔇 Additional comments (11)
api/src/environment.ts (1)

114-115: LGTM: boolean flag derivation is clear and side-effect free.

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

68-73: Transformer order looks right; please confirm behavior under flag flip.

Applying permissions then omit-if is sensible. Verify (in an e2e or schema snapshot test) that disabling ENABLE_NEXT_DOCKER_RELEASE actually removes gated fields from the generated schema when the env var is set before module bootstrap.

api/src/unraid-api/decorators/use-feature-flag.decorator.ts (1)

20-22: LGTM: thin wrapper over OmitIf with strong typing.

api/src/unraid-api/decorators/use-feature-flag.decorator.spec.ts (2)

23-317: Solid coverage and clear scenarios

Good breadth across Query/Mutation/ResolveField, multi-decorator, and ordering behavior.


203-210: No action needed: reflect-metadata is loaded in Vitest setupFiles
The API’s Vitest configuration (api/vite.config.ts line 220) lists reflect-metadata in setupFiles, so all tests already have metadata support.

api/src/unraid-api/decorators/omit-if.decorator.ts (1)

51-80: Transformer implementation looks good

Clean use of mapSchema and extensions flag; object and root fields handled correctly.

api/src/unraid-api/decorators/omit-if.decorator.spec.ts (1)

1-172: Well-structured tests for OmitIf behavior

Covers true/false conditions, function conditions, and GraphQL decorator integration.

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

39-77: Good coverage of valid/invalid cron paths and utility interactions

Mocks and assertions clearly verify both validateObject and validateCronExpression.

Also applies to: 154-171, 173-193

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

22-33: Feature-gated config service and sensible defaults

Enabled flag, configKey, fileName, and default structure look consistent.

Also applies to: 34-54

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

31-44: Cron validation approach LGTM

Use of validateCronExpression().valid matches the established pattern and tests.

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

46-47: LGTM: safeParse + empty-on-error fallback.

Good use of zod.safeParse to validate shape and return {} on invalid JSON.

Copy link
Member

@elibosley elibosley left a comment

Choose a reason for hiding this comment

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

Minor changes.

* @param dockerUpdatePath - Optional path to the DockerUpdate.php file. If not provided, the default path will be used.
* @returns True if the digests were refreshed, false if the operation failed
*/
async refreshDigests(mutex = this.refreshDigestsMutex, dockerUpdatePath?: string) {
Copy link
Member

Choose a reason for hiding this comment

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

sick

dockerUpdatePath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php'
) {
try {
await phpLoader({
Copy link
Member

Choose a reason for hiding this comment

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

dope.


private parseStatusesFromDockerPush(js: string): ExplicitStatusItem[] {
const items: ExplicitStatusItem[] = [];
const re = /docker\.push\(\{[^}]*name:'([^']+)'[^}]*update:(\d)[^}]*\}\);/g;
Copy link
Member

Choose a reason for hiding this comment

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

would "luv" to see some unit tests on this regex

"convert": "5.12.0",
"cookie": "1.0.2",
"cron": "4.3.3",
"cron": "4.3.0",
Copy link
Member

Choose a reason for hiding this comment

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

downgraded?

Copy link
Member Author

Choose a reason for hiding this comment

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

ye, there was a type conflict between 4.3.3 and 4.3.0 (which is what nest scheduler currently depends on)

@pujitm pujitm requested a review from elibosley September 9, 2025 18: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: 0

♻️ Duplicate comments (2)
api/generated-schema.graphql (2)

1105-1107: Add descriptions and verify permission guards for new DockerContainer fields.

Fields lack docstrings in the generated schema; ensure @ResolveField includes descriptions and @UsePermissions guards.

#!/bin/bash
set -euo pipefail
resolver="$(fd -t f docker-container.resolver.ts api/src || true)"
if [[ -n "$resolver" ]]; then
  echo "--- Inspecting: $resolver"
  rg -n "@ResolveField" -n -C2 "$resolver"
  rg -n "isUpdateAvailable|isRebuildReady" -n -C2 "$resolver"
  rg -n "UsePermissions" -n -C2 "$resolver"
else
  echo "Resolver not found"
fi

1137-1138: Non-null list contract: ensure resolver returns [] on errors.

containerUpdateStatuses is [ExplicitStatusItem!]!; resolver must not throw.

#!/bin/bash
set -euo pipefail
res="$(fd -t f docker.resolver.ts api/src || true)"
if [[ -n "$res" ]]; then
  echo "--- Inspecting: $res"
  rg -n "containerUpdateStatuses" -C3 "$res"
  # Look for try/catch and [] fallback
  rg -nP "(try|catch|return\s*\[\])" -C2 "$res"
else
  echo "docker.resolver.ts not found"
fi
🧹 Nitpick comments (9)
api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts (3)

7-7: Make the semicolon optional in the matcher.

Upstream snippets might omit the trailing semicolon; harmless to accept both.

-    const dockerPushRegex = /docker\.push\(\{[^}]*(?:(?:[^{}]|{[^}]*})*)\}\);/g;
+    const dockerPushRegex = /docker\.push\(\{[^}]*(?:(?:[^{}]|{[^}]*})*)\}\);?/g;

13-14: Support both quote styles and constrain update to expected range [0–3].

Future-proofs parsing and filters noise.

-        const nameMatch = objectContent.match(/name\s*:\s*'([^']+)'/);
-        const updateMatch = objectContent.match(/update\s*:\s*(\d)/);
+        const nameMatch = objectContent.match(/name\s*:\s*(["'])([^"']+)\1/);
+        const updateMatch = objectContent.match(/update\s*:\s*([0-3])/);

1-4: Narrow the return type for updateStatus.

Reflects actual domain values and avoids downstream casts.

 export interface DockerPushMatch {
     name: string;
-    updateStatus: number;
+    updateStatus: 0 | 1 | 2 | 3;
 }
api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts (1)

100-124: Add coverage for double quotes and missing semicolon.

These cases are cheap to support and likely to appear in the wild.

     it('should handle all valid update status values', () => {
@@
     });
 
+    it('should support double-quoted names', () => {
+        const jsCode = 'docker.push({name:"nginx",update:1});';
+        const result = parseDockerPushCalls(jsCode);
+        expect(result).toEqual([{ name: 'nginx', updateStatus: 1 }]);
+    });
+
+    it('should support missing trailing semicolon', () => {
+        const jsCode = "docker.push({name:'nginx',update:1})";
+        const result = parseDockerPushCalls(jsCode);
+        expect(result).toEqual([{ name: 'nginx', updateStatus: 1 }]);
+    });
+
     it('should handle nested braces in other properties', () => {
api/generated-schema.graphql (1)

2438-2439: Add description and permissions to refreshDockerDigests mutation.

Document behavior and guard with @usePermissions; regenerate schema after adding decorators.

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

49-54: Fix Logger.warn usage; log strings, not objects, and include error context.

Current calls pass non-string as message; prefer stringified message for consistent logs.

-            if (success) return data;
-            this.logger.warn(cacheData, 'Invalid cached update status');
+            if (success) return data;
+            this.logger.warn(`Invalid cached update status: ${JSON.stringify(cacheData)}`);
             return {};
         } catch (error) {
-            this.logger.warn(error, 'Failed to read cached update status');
+            this.logger.warn(`Failed to read cached update status: ${String(error)}`);
             return {};
         }

66-69: Annotate return type for clarity.

Add Promise for refreshDigestsViaPhp.

-    async refreshDigestsViaPhp(
+    async refreshDigestsViaPhp(
         dockerUpdatePath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php'
-    ) {
+    ): Promise<boolean> {

33-34: Remove empty constructor.

Unneeded noise.

-    constructor() {}
+    // no-op

119-129: Consider local error handling to align with non-null resolver.

Optional: catch PhpError here and return [] so callers don’t need to special-case failures.

-        const stdout = await phpLoader({
-            file: dockerContainersPath,
-            method: 'GET',
-        });
-        const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
-        const js = parts[1] || '';
-        return this.parseStatusesFromDockerPush(js);
+        try {
+            const stdout = await phpLoader({ file: dockerContainersPath, method: 'GET' });
+            const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
+            const js = parts[1] || '';
+            return this.parseStatusesFromDockerPush(js);
+        } catch {
+            return [];
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1bbe0c and 783e818.

📒 Files selected for processing (5)
  • api/generated-schema.graphql (6 hunks)
  • api/src/unraid-api/decorators/omit-if.decorator.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/decorators/omit-if.decorator.ts
🧰 Additional context used
📓 Path-based instructions (8)
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

Prefer adding new API files under api/src/unraid-api/ instead of legacy locations

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts
  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts
  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
api/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

In API TypeScript, use .js extensions in import specifiers for ESM compatibility

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts
  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
api/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

For cache-manager v7, always use TTL values in milliseconds when setting cache TTL

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts
  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.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/utils/docker-push-parser.test.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

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

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
api/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx)

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx): When testing for errors, use .rejects.toThrow() without asserting specific message strings
Focus tests on behavior, not implementation details like exact error messages
Avoid brittle tests that depend on non-essential details (e.g., log formats)
Use mocks as nouns (objects), not verbs (functions)

Files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
🧠 Learnings (1)
📚 Learning: 2025-09-02T14:41:56.235Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T14:41:56.235Z
Learning: Applies to {api,web}/**/*.{spec,test}.@(ts|tsx|js|jsx) : Avoid brittle tests that depend on non-essential details (e.g., log formats)

Applied to files:

  • api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts (1)
api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts (1)
  • parseDockerPushCalls (6-24)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts (4)
api/src/unraid-api/graph/resolvers/docker/container-status.job.ts (1)
  • Injectable (9-47)
api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts (1)
  • Injectable (11-62)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.ts (1)
  • parseDockerPushCalls (6-24)
🔇 Additional comments (4)
api/src/unraid-api/graph/resolvers/docker/utils/docker-push-parser.test.ts (1)

1-5: ESM pathing looks good.

Imports use .js extensions per repo ESM policy.

api/generated-schema.graphql (2)

143-145: Confirm nullability semantics for isSpinning.

ArrayDisk.isSpinning is nullable; Disk.isSpinning is non-null. If intentional, add doc comments clarifying when ArrayDisk value can be null.

Also applies to: 353-355


1053-1064: Typed status model is a solid improvement.

ExplicitStatusItem and UpdateStatus enum clarify API shape and avoid ad-hoc strings.

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

93-112: Status mapping overloads look correct.

Covers known values and defaults to UNKNOWN safely.

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 (1)
packages/unraid-shared/src/util/processing.ts (1)

69-71: Private any usage acknowledged per prior discussion.

Keeping any internally was previously agreed acceptable, but the above refactor removes the need for it entirely.

🧹 Nitpick comments (1)
packages/unraid-shared/src/util/processing.ts (1)

81-89: Docs: avoid implying mixed return types across concurrent operations.

With the invariant “one T per instance,” examples should show operations returning the same T (or use separate mutexes). Otherwise consumers may assume heterogenous U is safe.

Also applies to: 117-128

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 783e818 and 2d0135e.

📒 Files selected for processing (1)
  • packages/unraid-shared/src/util/processing.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type; prefer proper typing
Avoid using type assertions/casting; prefer proper typing from the start

Files:

  • packages/unraid-shared/src/util/processing.ts
🧠 Learnings (1)
📚 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:

  • packages/unraid-shared/src/util/processing.ts
🔇 Additional comments (1)
packages/unraid-shared/src/util/processing.ts (1)

100-129: Re-entrancy gotcha: calling do() from within the in-flight operation can self-await.

If an operation invokes mutex.do(...) on the same instance, it will get the same promise and risk a circular await. Add a brief JSDoc warning and a test to lock this in.

@pujitm pujitm merged commit e57d81e into main Sep 9, 2025
13 of 15 checks passed
@pujitm pujitm deleted the feat/docker-template branch September 9, 2025 20:25
@github-actions
Copy link
Contributor

github-actions bot commented Sep 9, 2025

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/PR1582/dynamix.unraid.net.plg

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.

4 participants