Skip to content

Conversation

elibosley
Copy link
Member

@elibosley elibosley commented Oct 9, 2025

…m time configuration

  • Introduced a new GraphQL type SystemTime to manage system time settings, including current time, timezone, NTP status, and NTP servers.
  • Added systemTime query to retrieve current system time configuration.
  • Implemented updateSystemTime mutation to modify system time settings.
  • Created corresponding service and resolver for handling system time logic.
  • Added input validation for updating system time, including manual date/time handling.
  • Integrated new module into the main resolver module for accessibility.

This update enhances the API's capability to manage and retrieve system time configurations effectively.

Summary by CodeRabbit

  • New Features

    • Multi-step upgrade/onboarding modal with Welcome, Timezone, Plugins and License steps, step navigation and completion tracking.
    • System Time controls to view/update time zone, toggle NTP/manual mode, set manual date/time and manage NTP servers.
    • In‑modal plugin selection & installation with live progress and real‑time updates.
    • Persistent four‑theme selector with global theme store.
  • Tests

    • Expanded onboarding and system‑time tests; removed obsolete ActivationSteps test.
  • Documentation

    • Added Upgrade Onboarding System guide.

Copy link

claude bot commented Oct 9, 2025

Claude encountered an error —— View job


I'll analyze this and get back to you.

Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

Walkthrough

Adds onboarding and system-time features across backend and frontend: new GraphQL schema/types/ops for activationOnboarding and systemTime, OnboardingTracker persistence and complete-step mutations, SystemTime service/resolver and emcmd POST/CSRF changes, plugin-install subsystem, Activation modal refactor with step registry/components, stores, client ops, tests, and module wiring.

Changes

Cohort / File(s) Summary
GraphQL schema & client types
api/generated-schema.graphql, web/src/composables/gql/graphql.ts, web/src/composables/gql/gql.ts
Adds ActivationOnboarding, ActivationOnboardingStep/ID, UpgradeInfo/UpgradeStep, SystemTime, CompleteUpgradeStepInput, UpdateSystemTimeInput; exposes Query.activationOnboarding/systemTime and Mutation.onboarding/updateSystemTime; generates TS types, documents, and graphql() overloads.
Backend — SystemTime feature
api/src/unraid-api/graph/resolvers/system-time/*, api/src/unraid-api/graph/resolvers/resolvers.module.ts, api/src/core/utils/clients/emcmd.ts
New SystemTime model, UpdateSystemTimeInput, resolver, service, module and unit tests; registers module/resolver; refactors emcmd client to ensure CSRF token handling, switch to POST form-encoded requests, and add explicit HTTP/error handling.
Backend — Onboarding tracker & upgrade info
api/src/unraid-api/config/onboarding-tracker.*, api/src/unraid-api/config/api-config.module.ts, api/src/unraid-api/config/api-config.test.ts, api/src/unraid-api/main.ts, api/src/unraid-api/graph/resolvers/info/versions/*, api/src/unraid-api/graph/resolvers/info/info.module.ts
Adds OnboardingTracker module/service/model, persistence and lifecycle hooks, snapshot/step computation, buildUpgradeInfo util, tests, integration into ApiConfig and Info modules, and enables shutdown hooks for graceful persist-on-exit.
Backend — Onboarding mutations wiring
api/src/unraid-api/graph/resolvers/mutation/*, api/src/unraid-api/graph/resolvers/onboarding/*, api/src/unraid-api/graph/resolvers/resolvers.module.ts
Adds OnboardingMutations container, CompleteUpgradeStepInput, OnboardingMutationsResolver delegating to OnboardingTracker, and RootMutations onboarding field.
Backend — Customization → Onboarding refactor
api/src/unraid-api/graph/resolvers/customization/*
Replaces CustomizationService with OnboardingService, adds activation step resolution helpers, exposes activationOnboarding via OnboardingTracker, updates resolver wiring and tests.
Backend — Plugin install subsystem
api/src/unraid-api/graph/resolvers/unraid-plugins/*
New UnraidPluginsService, models (PluginInstallOperation/Event, PluginInstallStatus), resolvers (queries, subscription, mutation), module, and unit tests implementing tracked plugin installation with pubsub.
Web — Activation modal & step components
web/src/components/Activation/ActivationModal.vue, web/src/components/Activation/ActivationSteps.vue, web/src/components/Activation/*.vue, web/src/components/Activation/stepRegistry.ts, web/src/components/Activation/steps/*
Introduces step-driven activation/upgrade modal, dynamic step registry, new step components (Welcome, Timezone, Plugins, License), ActivationSteps API changed to (steps, activeStepIndex, onStepClick), and navigation/complete/skip/back flows.
Web — Stores & GraphQL ops
web/src/components/Activation/store/upgradeOnboarding.ts, web/src/components/Activation/store/activationCodeModal.ts, web/src/components/Activation/activationOnboarding.query.ts, web/src/components/Activation/completeUpgradeStep.mutation.ts, web/src/components/Activation/updateSystemTime.mutation.ts
New Pinia upgradeOnboarding store (visibility, steps, refetch); updated activation modal visibility logic; adds ACTIVATION_ONBOARDING query and COMPLETE_UPGRADE_STEP / UPDATE_SYSTEM_TIME mutations.
Web — Plugin installer & UI integration
web/src/components/Activation/usePluginInstaller.ts, web/src/components/Activation/graphql/*, web/src/components/Activation/ActivationPluginsStep.vue, web/__test__/components/Activation/ActivationPluginsStep.test.ts
Adds usePluginInstaller composable (mutation + subscription flow), client queries/mutations/subscriptions, plugins step UI, and tests covering success/failure flows.
Web — Tests & test pages
web/__test__/**, web/public/test-pages/*
Tests updated/added for activation modal, welcome modal, activationCode store; mocks extended for new components/stores; ActivationSteps.test.ts removed; test page theme selector and activation test buttons added.
Web — Theming, types & tooling
web/src/assets/main.css, @tailwind-shared/css-variables.css, web/components.d.ts, web/src/components/Wrapper/mount-engine.ts, web/package.json
Enables Nuxt UI styles, tweaks dark-mode token, adds global component declarations, exposes themeStore on window, updates theme labels, and adds @vvo/tzdb dependency.
API wiring & module imports
api/src/unraid-api/graph/resolvers/resolvers.module.ts, api/src/unraid-api/graph/resolvers/customization/customization.module.ts, api/src/unraid-api/config/api-config.module.ts, api/src/unraid-api/graph/resolvers/info/info.module.ts
Registers SystemTimeModule, OnboardingTrackerModule, UnraidPluginsModule; swaps provider (CustomizationService → OnboardingService) and updates imports/exports accordingly.
Misc & helpers
api/src/store/watch/state-watch.ts, api/src/core/utils/misc/parse-config.ts, api/src/__test__/core/utils/misc/parse-config.test.ts, packages/unraid-shared/package.json, api/src/core/log.ts
Refactors state-watch file handler into handleStateFileUpdate; normalizes config extension handling and adds test; adds devDependency @types/semver; adjusts pretty log time format.
Removed or simplified pages
web/src/pages/*.vue (index, login, welcome, changelog, redirect, apikeys, flashbackup, tools/*, etc.)
Multiple demo/test pages and some routes removed or stripped of templates/scripts (login, index, welcome, changelog, redirect, apikeys, flashbackup, several tools/test pages).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Web UI (Timezone Step)
  participant GQL as GraphQL API
  participant R as SystemTimeResolver
  participant S as SystemTimeService
  participant C as emcmd Client

  User->>UI: selects timezone, clicks Continue
  UI->>GQL: mutation UpdateSystemTime(input)
  GQL->>R: updateSystemTime(input)
  R->>S: updateSystemTime(input)
  S->>C: POST /update (form body with csrf_token + params)
  C-->>S: 2xx response
  S-->>R: SystemTime result
  R-->>GQL: return SystemTime
  GQL-->>UI: updated systemTime (currentTime, timeZone, useNtp, ntpServers)
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as Web UI (ActivationModal)
  participant GQL as GraphQL API
  participant M as OnboardingMutationsResolver
  participant T as OnboardingTracker
  participant U as buildUpgradeInfoFromSnapshot

  User->>UI: completes step (e.g., PLUGINS)
  UI->>GQL: mutation CompleteUpgradeStep(stepId)
  GQL->>M: completeUpgradeStep(input)
  M->>T: markStepCompleted(stepId)
  T-->>M: UpgradeProgressSnapshot
  M->>U: buildUpgradeInfoFromSnapshot(snapshot)
  U-->>M: UpgradeInfo
  M-->>GQL: return UpgradeInfo
  GQL-->>UI: refreshed onboarding state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • zackspear

Poem

I hopped through branches, bits, and signs,
Marked steps complete and tuned the times.
I fetched CSRF crumbs and posted forms galore,
Wired steps and stores and opened modal doors.
Hop on — onboarding and clocks now roar! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “feat(onboarding): add new onboarding flows for Unraid OS” is misleading because the changeset primarily implements system time management capabilities (SystemTime type, systemTime query, updateSystemTime mutation, and related modules) rather than new onboarding flows. Update the pull request title to reflect the main feature, for example “feat(system-time): add systemTime query and updateSystemTime mutation,” so it accurately summarizes the core changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/timezone-control

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

Copy link

codecov bot commented Oct 9, 2025

Codecov Report

❌ Patch coverage is 71.29882% with 632 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.11%. Comparing base (10f048e) to head (771840c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
web/src/components/Activation/ActivationModal.vue 57.60% 92 Missing ⚠️
...eb/src/components/Activation/usePluginInstaller.ts 9.87% 73 Missing ⚠️
api/src/core/utils/clients/emcmd.ts 7.24% 64 Missing ⚠️
...resolvers/unraid-plugins/unraid-plugins.service.ts 75.72% 59 Missing ⚠️
web/public/test-pages/shared-header.js 0.00% 56 Missing ⚠️
...c/components/Activation/store/upgradeOnboarding.ts 0.00% 38 Missing and 1 partial ⚠️
...src/unraid-api/config/onboarding-tracker.module.ts 90.22% 31 Missing ⚠️
...h/resolvers/unraid-plugins/unraid-plugins.model.ts 59.70% 27 Missing ⚠️
...h/resolvers/customization/activation-code.model.ts 42.10% 22 Missing ⚠️
api/src/store/watch/state-watch.ts 0.00% 19 Missing ⚠️
... and 19 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1746      +/-   ##
==========================================
+ Coverage   52.64%   54.11%   +1.47%     
==========================================
  Files         866      885      +19     
  Lines       49335    50542    +1207     
  Branches     4940     5274     +334     
==========================================
+ Hits        25972    27353    +1381     
+ Misses      23290    23114     -176     
- Partials       73       75       +2     

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

130-142: Align error assertions with Vitest guideline

Please switch these error expectations to use .rejects.toThrow() (no arguments) instead of .rejects.toBeInstanceOf(...) so we follow the mandated testing style.

As per coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 754966d and a2f079e.

📒 Files selected for processing (7)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

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

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

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
🧬 Code graph analysis (4)
api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts (1)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (38-78)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (5)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (14-76)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadStateFiles (145-164)
api/src/core/types/states/var.ts (1)
  • Var (12-242)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (3)
api/src/store/index.ts (2)
  • getters (18-23)
  • store (6-12)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (14-76)
api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts (1)
  • MANUAL_TIME_REGEX (58-58)
api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts (1)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
web/public/test-pages/all-components.html (1)

573-575: Keep dark-mode button label in sync.

When the theme changes via other controls (e.g., <unraid-theme-switcher>), this handler only logs the event, so the fixed-position toggle keeps whatever label it last set itself. Users see “🌙 Dark Mode” even though the theme is already dark. Call updateDarkModeButton() here so the label always reflects the current state.

 $(document).on('unraid:theme-changed', function(e, data) {
   log('Theme changed event received: ' + JSON.stringify(data));
+  updateDarkModeButton();
 });
🧹 Nitpick comments (2)
web/src/components/Wrapper/mount-engine.ts (1)

125-127: Consider adding environment guards for production security.

The implementation correctly exposes the theme store globally, following the existing pattern for globalPinia. However, exposing mutable stores in production environments could allow external scripts to manipulate application state.

Consider adding an environment check to limit this exposure to development builds:

  const themeStore = useThemeStore();

- if (typeof window !== 'undefined') {
+ if (typeof window !== 'undefined' && import.meta.env.DEV) {
    window.themeStore = themeStore;
  }

Note: This same pattern could be applied to globalPinia on line 30 for consistency.

web/src/components/Activation/ActivationTimezoneStep.vue (1)

29-36: Format timezone offsets with minute precision

Dividing minutes by 60 prints offsets like UTC+5.5 or UTC-9.75, which is confusing. Users expect UTC+05:30/UTC-09:45. Format the offset using hours and minutes before composing the label.

Apply this diff:

-  return timeZones.map((tz) => {
-    const offset = tz.currentTimeOffsetInMinutes / 60;
-    const offsetStr = offset >= 0 ? `+${offset}` : `${offset}`;
+  return timeZones.map((tz) => {
+    const totalMinutes = tz.currentTimeOffsetInMinutes;
+    const sign = totalMinutes >= 0 ? '+' : '-';
+    const absMinutes = Math.abs(totalMinutes);
+    const hours = Math.floor(absMinutes / 60)
+      .toString()
+      .padStart(2, '0');
+    const minutes = (absMinutes % 60).toString().padStart(2, '0');
+    const offsetStr = `${sign}${hours}:${minutes}`;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2f079e and 35dd1eb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • api/src/core/utils/clients/emcmd.ts (3 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (3 hunks)
  • web/__test__/components/Activation/ActivationSteps.test.ts (4 hunks)
  • web/__test__/store/activationCodeModal.test.ts (1 hunks)
  • web/components.d.ts (1 hunks)
  • web/package.json (1 hunks)
  • web/public/test-pages/all-components.html (11 hunks)
  • web/src/components/Activation/ActivationModal.vue (5 hunks)
  • web/src/components/Activation/ActivationSteps.vue (6 hunks)
  • web/src/components/Activation/ActivationTimezoneStep.vue (1 hunks)
  • web/src/components/Activation/store/activationCodeModal.ts (2 hunks)
  • web/src/components/Activation/updateSystemTime.mutation.ts (1 hunks)
  • web/src/components/Wrapper/mount-engine.ts (2 hunks)
  • web/src/composables/gql/gql.ts (3 hunks)
  • web/src/composables/gql/graphql.ts (7 hunks)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/components.d.ts
  • web/src/components/Activation/store/activationCodeModal.ts
  • web/src/composables/gql/graphql.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/store/activationCodeModal.test.ts
  • api/src/core/utils/clients/emcmd.ts
  • web/src/components/Activation/updateSystemTime.mutation.ts
  • web/src/composables/gql/gql.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
**/{store,stores}/**/*.ts

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

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • web/src/components/Activation/store/activationCodeModal.ts
  • web/__test__/store/activationCodeModal.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:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/store/activationCodeModal.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/store/activationCodeModal.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/store/activationCodeModal.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/store/activationCodeModal.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/Activation/ActivationTimezoneStep.vue
  • web/src/components/Activation/ActivationSteps.vue
**/__test__/store/**/*.ts

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

**/__test__/store/**/*.ts: For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed
Do not mock the store under test; allow the store to initialize with its natural default state
In store tests, verify action side effects and state changes, and assert that actions are called with the correct parameters
In store tests, mock external dependencies used by the store and verify interactions with those mocks
Test computed properties/getters by accessing them directly and verifying derived state
Thoroughly test async store actions: assert intermediate loading state, await completion, and check final state

Files:

  • web/__test__/store/activationCodeModal.test.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/core/utils/clients/emcmd.ts
web/**/*.mutation.ts

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

GraphQL mutations must be placed in .mutation.ts files

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

Files:

  • web/src/components/Activation/updateSystemTime.mutation.ts
web/**/*.{query,mutation}.ts

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

All GraphQL under web/ must follow this naming convention

Files:

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

Applied to files:

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

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Ensure Vue reactivity imports (computed, ref, watchEffect) are added to store source files

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
🧬 Code graph analysis (4)
web/src/composables/gql/graphql.ts (1)
packages/unraid-api-plugin-connect/src/graphql/generated/client/graphql.ts (3)
  • Scalars (11-31)
  • InputMaybe (4-4)
  • Exact (5-5)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/store/theme.ts (1)
  • useThemeStore (51-242)
api/src/core/utils/clients/emcmd.ts (3)
api/src/core/log.ts (1)
  • appLogger (83-83)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadSingleStateFile (107-141)
web/src/components/Activation/updateSystemTime.mutation.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (300-302)
🪛 GitHub Actions: CI - Main (API)
api/src/core/utils/clients/emcmd.ts

[error] 22-22: TS2769: No overload matches this call. (No overload matches this call.) src/core/utils/clients/emcmd.ts(22,79).

🔇 Additional comments (4)
web/src/components/Wrapper/mount-engine.ts (1)

24-24: Verify this change belongs in this PR.

The type declaration is correct, but this change appears unrelated to the PR's objective of adding system time configuration. Exposing the theme store globally seems like a separate concern that might be better suited for a dedicated PR.

If this exposure is required for the system time UI components, please clarify the dependency. Otherwise, consider moving this to a separate PR focused on debugging/development tooling.

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

20-20: LGTM!

The type map entry for UpdateSystemTime mutation follows the correct pattern and is consistent with other operations in the file.


66-66: LGTM!

The runtime document map entry correctly mirrors the type definition at line 20, ensuring type safety and runtime consistency.


135-138: LGTM!

The graphql function overload for UpdateSystemTime mutation is properly typed and follows the established pattern for GraphQL operation handling.

Comment on lines 21 to 24
} catch (error) {
appLogger.debug(`Unable to read CSRF token from ${VAR_INI_PATH}: %o`, error);
return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle unknown errors before logging

CI is failing with TS2769 on Line 22 because error in a catch clause is unknown (see pipeline failure). Passing it straight into the logger doesn’t satisfy the overloads. The same issue exists in the retry .catch block. Please narrow the error (or wrap it) before logging so the call type-checks.

     } catch (error) {
-        appLogger.debug(`Unable to read CSRF token from ${VAR_INI_PATH}: %o`, error);
+        const logError = error instanceof Error ? error : { message: String(error) };
+        appLogger.debug(
+            { error: logError },
+            `Unable to read CSRF token from ${VAR_INI_PATH}`
+        );
         return undefined;
     }
 ...
-    ).catch((error) => {
-        appLogger.error('Failed to load CSRF token after multiple retries', error);
+    ).catch((error) => {
+        const logError = error instanceof Error ? error : { message: String(error) };
+        appLogger.error(
+            { error: logError },
+            'Failed to load CSRF token after multiple retries'
+        );
         throw new AppError('Failed to load CSRF token after multiple retries');
     });

Also applies to: 63-64

🧰 Tools
🪛 GitHub Actions: CI - Main (API)

[error] 22-22: TS2769: No overload matches this call. (No overload matches this call.) src/core/utils/clients/emcmd.ts(22,79).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
api/src/core/utils/clients/emcmd.ts (1)

116-123: Remove any type annotation.

Line 116 uses error: any, which violates the coding guideline "Never use the any type; prefer precise typing."

Apply this diff to remove the explicit type annotation and let TypeScript infer the error type:

-    } catch (error: any) {
+    } catch (error) {
         if (error.code === 'ENOENT') {
             appLogger.error('emhttpd socket unavailable.', error);
             throw new Error('emhttpd socket unavailable.');
         }
         appLogger.error(`emcmd execution failed: ${error.message}`, error);
         throw error;
     }

Note: You'll also need to add type guards when accessing properties like error.code and error.message to satisfy TypeScript:

     } catch (error) {
-        if (error.code === 'ENOENT') {
-            appLogger.error('emhttpd socket unavailable.', error);
+        if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
+            const logError = error instanceof Error ? error : { message: String(error) };
+            appLogger.error({ error: logError }, 'emhttpd socket unavailable.');
             throw new Error('emhttpd socket unavailable.');
         }
-        appLogger.error(`emcmd execution failed: ${error.message}`, error);
+        const message = error instanceof Error ? error.message : String(error);
+        const logError = error instanceof Error ? error : { message };
+        appLogger.error({ error: logError }, `emcmd execution failed: ${message}`);
         throw error;
     }

As per coding guidelines.

♻️ Duplicate comments (2)
api/src/core/utils/clients/emcmd.ts (2)

21-24: Verify that the TypeScript error is resolved.

The past review flagged a TS2769 error on line 22 because error in the catch clause is unknown. While the logging format has changed to structured logging { error }, the error type hasn't been narrowed as suggested in the previous review. Please verify that this change resolves the TypeScript compilation error.

If the error persists, apply the suggested fix from the previous review to narrow the error type:

     } catch (error) {
-        appLogger.debug({ error }, `Unable to read CSRF token from ${VAR_INI_PATH}`);
+        const logError = error instanceof Error ? error : { message: String(error) };
+        appLogger.debug(
+            { error: logError },
+            `Unable to read CSRF token from ${VAR_INI_PATH}`
+        );
         return undefined;
     }

Run the following script to verify the TypeScript compilation:

#!/bin/bash
# Verify TypeScript compilation for this file
cd api
npm run type-check 2>&1 | grep -A 5 "emcmd.ts"

62-65: Apply the error handling fix from the previous review.

This catch block was also flagged in the previous review for the same TS2769 error. The error must be narrowed before logging to satisfy TypeScript's type checking.

Apply this diff to fix the error handling:

-    ).catch((error) => {
-        appLogger.error('Failed to load CSRF token after multiple retries', error);
+    ).catch((error) => {
+        const logError = error instanceof Error ? error : { message: String(error) };
+        appLogger.error(
+            { error: logError },
+            'Failed to load CSRF token after multiple retries'
+        );
         throw new AppError('Failed to load CSRF token after multiple retries');
     });
🧹 Nitpick comments (7)
web/src/components/Activation/ActivationModal.vue (2)

55-63: Active step mapping looks correct with/without activation; consider simplifying

Logic is fine. Minor: replace if-chain with a map to reduce branches and keep numbers in sync with ActivationSteps.


101-115: Deduplicate plugin step handlers

handlePluginsComplete and handlePluginsSkip are identical. Collapse into one advanceFromPlugins() to reduce maintenance.

-const handlePluginsComplete = () => {
-  if (hasActivationCode.value) {
-    currentStep.value = 'activation';
-  } else {
-    modalStore.setIsHidden(true);
-  }
-};
-
-const handlePluginsSkip = () => {
+const advanceFromPlugins = () => {
   if (hasActivationCode.value) {
     currentStep.value = 'activation';
   } else {
     modalStore.setIsHidden(true);
   }
 };

And pass advanceFromPlugins to both on-complete/on-skip.

web/src/components/Activation/ActivationTimezoneStep.vue (3)

28-37: Format UTC offsets as ±HH:MM for clarity

Current offset prints decimals (e.g., +5.5). Prefer HH:MM.

-  return timeZones.map((tz) => {
-    const offset = tz.currentTimeOffsetInMinutes / 60;
-    const offsetStr = offset >= 0 ? `+${offset}` : `${offset}`;
-    return {
-      value: tz.name,
-      label: `${tz.alternativeName} (${tz.name}) UTC${offsetStr}`,
-    };
-  });
+  return timeZones.map((tz) => {
+    const total = tz.currentTimeOffsetInMinutes;
+    const sign = total >= 0 ? '+' : '-';
+    const abs = Math.abs(total);
+    const hh = String(Math.floor(abs / 60)).padStart(2, '0');
+    const mm = String(abs % 60).padStart(2, '0');
+    const offsetStr = `${sign}${hh}:${mm}`;
+    return {
+      value: tz.name,
+      label: `${tz.alternativeName} (${tz.name}) UTC${offsetStr}`,
+    };
+  });

39-58: Avoid setting an unknown timezone value

If detection doesn’t match tzdb, returning the raw string can set an invalid v-model and send a bad mutation. Return null so the user picks from the list.

-    return detectedTz;
+    return null;

No other changes needed since onMounted already guards if (detected).

Also applies to: 60-65


76-89: Mutation failure is silently ignored but flow advances

If intentional, consider surfacing a non-blocking notice (toast) so users know timezone wasn’t saved.

Is the product decision to always advance even on failure? If not, gate onComplete() on success and show inline error otherwise.

web/public/test-pages/all-components.html (1)

518-569: Dev test harness looks good; avoid duplicating theme toggle logic

You have both a dedicated dark-mode toggle and a “Toggle Theme” button with similar logic. Consider extracting a single function to DRY.

Also applies to: 571-595

web/src/components/Activation/ActivationSteps.vue (1)

132-139: Simplify button variant expression

Both active and completed use 'primary'; reduce to a single check.

-            :variant="state === 'completed' ? 'primary' : state === 'active' ? 'primary' : 'outline'"
+            :variant="state !== 'inactive' ? 'primary' : 'outline'"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35dd1eb and b9d09d2.

📒 Files selected for processing (7)
  • api/src/core/utils/clients/emcmd.ts (3 hunks)
  • web/components.d.ts (1 hunks)
  • web/public/test-pages/all-components.html (11 hunks)
  • web/src/components/Activation/ActivationModal.vue (5 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/components/Activation/ActivationSteps.vue (6 hunks)
  • web/src/components/Activation/ActivationTimezoneStep.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components.d.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationTimezoneStep.vue
  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/Activation/ActivationPluginsStep.vue
  • web/src/components/Activation/ActivationSteps.vue
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/core/utils/clients/emcmd.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

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

Applied to files:

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

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Ensure Vue reactivity imports (computed, ref, watchEffect) are added to store source files

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
🧬 Code graph analysis (1)
api/src/core/utils/clients/emcmd.ts (3)
api/src/core/log.ts (1)
  • appLogger (83-83)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadSingleStateFile (107-141)
🪛 GitHub Actions: CI - Main (API)
web/src/components/Activation/ActivationSteps.vue

[error] 79-79: PuzzlePieceIcon export is missing from the @heroicons/vue/24/outline mock. Ensure the mock provides PuzzlePieceIcon (and related icons) or adjust the component to mock icons differently.

🪛 GitHub Check: Test API
web/src/components/Activation/ActivationSteps.vue

[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > hides activation step when showActivationStep is false
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > uses provided activeStep value
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > uses default activeStep of 1 when not provided
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > renders all four steps with correct titles and descriptions
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > hides activation step when showActivationStep is false
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > uses provided activeStep value
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > uses default activeStep of 1 when not provided
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11


[failure] 79-79: test/components/Activation/ActivationSteps.test.ts > ActivationSteps > renders all four steps with correct titles and descriptions
Error: [vitest] No "PuzzlePieceIcon" export is defined on the "@heroicons/vue/24/outline" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@heroicons/vue/24/outline"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ setup src/components/Activation/ActivationSteps.vue:79:17
❯ callWithErrorHandling ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5310:11

🔇 Additional comments (2)
api/src/core/utils/clients/emcmd.ts (1)

94-94: Confirm CSRF token behavior for /update

  • By default (waitForToken = false), csrf_token falls back to '' if undefined. Does the emhttp /update endpoint accept empty tokens?
  • Are there legitimate cases where we intentionally skip CSRF (i.e. waitForToken: false)?
  • If a token is required, we should guard and throw instead of appending an empty string.
web/src/components/Activation/ActivationPluginsStep.vue (1)

70-78: install() is synchronous—awaiting it has no effect
The install function in useInstallPlugin returns void and catches errors internally, so await install() won’t capture failures or block until completion. To enable proper sequencing and error handling, refactor install to return a Promise that resolves on success (and rejects on failure), then use await install(...).

Likely an incorrect or invalid review comment.

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 (5)
web/public/test-pages/shared-header.js (1)

413-423: Reduce code duplication in theme selector setup.

The theme selector initialization logic (reading from localStorage, setting the value, and attaching the change listener) is duplicated between the DOMContentLoaded path (lines 413-423) and the immediate execution path (lines 434-443).

Consider extracting this into a helper function:

+  function setupThemeSelector() {
+    const themeSelect = document.getElementById('theme-select');
+    if (themeSelect) {
+      const savedTheme = localStorage.getItem('unraid-test-theme') || 'white';
+      themeSelect.value = savedTheme;
+
+      themeSelect.addEventListener('change', (e) => {
+        window.loadUnraidTheme(e.target.value);
+      });
+    }
+  }
+
   if (document.readyState === 'loading') {
     document.addEventListener('DOMContentLoaded', function () {
       window.setBaseFontSize();
       window.loadUnraidTheme();
       window.injectSharedHeader();
       const existingTitle =
         document.querySelector('h1')?.textContent ||
         document.title.replace(' - Unraid Component Test', '');
       window.initializeSharedHeader(existingTitle);
-
-      const themeSelect = document.getElementById('theme-select');
-      if (themeSelect) {
-        const savedTheme = localStorage.getItem('unraid-test-theme') || 'white';
-        themeSelect.value = savedTheme;
-
-        themeSelect.addEventListener('change', (e) => {
-          window.loadUnraidTheme(e.target.value);
-        });
-      }
+      setupThemeSelector();
     });
   } else {
     window.setBaseFontSize();
     window.loadUnraidTheme();
     window.injectSharedHeader();
     const existingTitle =
       document.querySelector('h1')?.textContent ||
       document.title.replace(' - Unraid Component Test', '');
     window.initializeSharedHeader(existingTitle);
-
-    const themeSelect = document.getElementById('theme-select');
-    if (themeSelect) {
-      const savedTheme = localStorage.getItem('unraid-test-theme') || 'white';
-      themeSelect.value = savedTheme;
-
-      themeSelect.addEventListener('change', (e) => {
-        window.loadUnraidTheme(e.target.value);
-      });
-    }
+    setupThemeSelector();
   }

Also applies to: 434-443

web/public/test-pages/all-components.html (1)

427-433: Cleanup the polling interval when themeStore is found.

The setInterval for waiting for window.themeStore doesn't have a timeout, which means it will poll indefinitely if the theme store never loads. While the interval is cleared when the store is found, it should also have a maximum retry count or timeout.

Consider adding a timeout or maximum retry count:

+      let pollCount = 0;
+      const MAX_POLL_ATTEMPTS = 100; // 10 seconds
+      
       const waitForThemeStore = setInterval(function() {
+        pollCount++;
+        
         if (window.themeStore) {
           clearInterval(waitForThemeStore);
           updateThemeButton();
           log('Theme store connected');
+        } else if (pollCount >= MAX_POLL_ATTEMPTS) {
+          clearInterval(waitForThemeStore);
+          log('ERROR: Theme store not available after timeout', 'error');
         }
       }, 100);
web/__test__/components/Activation/ActivationModal.test.ts (3)

50-57: Verify test coverage for new activation step components.

The new mocks for ActivationPluginsStep and ActivationTimezoneStep suggest significant new functionality, but only one test case (lines 218-222) exercises them. Consider adding tests to verify:

  • Plugin step rendering and interactions (onComplete, onSkip, onBack callbacks)
  • Step navigation flow between timezone and plugins steps
  • Conditional rendering based on activation state

218-222: Enhance test to verify activation code presence condition.

The test only verifies that the timezone step renders when an activation code is present. Consider expanding this test or adding sibling tests to verify:

  • Behavior when activationCode is absent or null
  • Behavior when isFreshInstall is false
  • That other steps are not rendered initially

Example expansion:

  it('renders timezone step initially when activation code is present', async () => {
    const wrapper = mountComponent();

    expect(wrapper.html()).toContain('data-testid="timezone-step"');
+   expect(wrapper.html()).not.toContain('data-testid="plugins-step"');
  });
+
+ it('handles missing activation code gracefully', async () => {
+   mockActivationCodeDataStore.activationCode.value = null;
+   const wrapper = mountComponent();
+   // Assert expected behavior when activation code is missing
+ });

86-92: Add tests for upgrade onboarding UI behavior
ActivationModal.test.ts mocks useUpgradeOnboardingStore but never asserts cases when shouldShowUpgradeOnboarding is true (or false); add tests covering both states to verify the conditional UI flows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9d09d2 and c3539b2.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (25)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (6 hunks)
  • api/src/unraid-api/config/api-config.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2 hunks)
  • packages/unraid-shared/src/services/api-config.ts (1 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (6 hunks)
  • web/__test__/components/Activation/ActivationSteps.test.ts (0 hunks)
  • web/components.d.ts (2 hunks)
  • web/public/test-pages/all-components.html (11 hunks)
  • web/public/test-pages/shared-header.js (2 hunks)
  • web/src/assets/main.css (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (4 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/components/Activation/ActivationSteps.vue (1 hunks)
  • web/src/components/Activation/ActivationTimezoneStep.vue (1 hunks)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md (1 hunks)
  • web/src/components/Activation/releaseConfigs.ts (1 hunks)
  • web/src/components/Activation/store/activationCodeModal.ts (2 hunks)
  • web/src/components/Activation/store/upgradeOnboarding.ts (1 hunks)
  • web/src/components/Activation/upgradeInfo.query.ts (1 hunks)
  • web/src/components/ColorSwitcher.standalone.vue (1 hunks)
  • web/src/components/TestThemeSwitcher.standalone.vue (1 hunks)
  • web/src/composables/gql/gql.ts (3 hunks)
  • web/src/composables/gql/graphql.ts (8 hunks)
💤 Files with no reviewable changes (1)
  • web/test/components/Activation/ActivationSteps.test.ts
✅ Files skipped from review due to trivial changes (2)
  • web/src/components/ColorSwitcher.standalone.vue
  • web/src/components/Activation/UPGRADE_ONBOARDING.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components.d.ts
🧰 Additional context used
📓 Path-based instructions (12)
web/**/*.query.ts

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

GraphQL queries must be placed in .query.ts files

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

Files:

  • web/src/components/Activation/upgradeInfo.query.ts
web/**/*.{query,mutation}.ts

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

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Activation/upgradeInfo.query.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/src/components/Activation/upgradeInfo.query.ts
  • packages/unraid-shared/src/services/api-config.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • web/src/components/Activation/store/upgradeOnboarding.ts
  • web/src/components/Activation/store/activationCodeModal.ts
  • web/src/composables/gql/graphql.ts
  • api/src/unraid-api/config/api-config.module.ts
  • web/src/components/Activation/releaseConfigs.ts
  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
**/{store,stores}/**/*.ts

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

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • web/src/components/Activation/store/upgradeOnboarding.ts
  • web/src/components/Activation/store/activationCodeModal.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/TestThemeSwitcher.standalone.vue
  • web/src/components/Activation/ActivationTimezoneStep.vue
  • web/src/components/Activation/ActivationPluginsStep.vue
{**/*.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:

  • web/__test__/components/Activation/ActivationModal.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
🧠 Learnings (12)
📚 Learning: 2024-12-17T13:55:42.068Z
Learnt from: elibosley
PR: unraid/api#972
File: web/components/ColorSwitcher.ce.vue:1-2
Timestamp: 2024-12-17T13:55:42.068Z
Learning: In this Nuxt.js project, components used in templates are automatically imported by Nuxt, so explicit import statements for components are unnecessary.

Applied to files:

  • web/src/assets/main.css
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
PR: unraid/api#972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

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

Applied to files:

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

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Ensure Vue reactivity imports (computed, ref, watchEffect) are added to store source files

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
📚 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:

  • web/__test__/components/Activation/ActivationModal.test.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:

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

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
🧬 Code graph analysis (7)
web/src/components/Activation/upgradeInfo.query.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (306-308)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (2)
packages/unraid-shared/src/services/api-config.ts (1)
  • ObjectType (4-34)
web/src/composables/gql/graphql.ts (5)
  • UpgradeInfo (2320-2328)
  • Node (1380-1382)
  • InfoVersions (1082-1091)
  • CoreVersions (531-539)
  • PackageVersions (1533-1551)
web/src/components/Activation/store/upgradeOnboarding.ts (2)
web/src/components/Activation/upgradeInfo.query.ts (1)
  • UPGRADE_INFO_QUERY (3-17)
web/src/components/Activation/releaseConfigs.ts (2)
  • ReleaseStepConfig (3-7)
  • getUpgradeSteps (58-82)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (7)
  • UpgradeInfo (2320-2328)
  • SystemTime (2077-2087)
  • MutationUpdateSystemTimeArgs (1370-1372)
  • UpdateSystemTimeInput (2309-2318)
  • Scalars (11-75)
  • InputMaybe (4-4)
  • Maybe (3-3)
web/public/test-pages/shared-header.js (1)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • theme (58-60)
api/src/unraid-api/config/api-config.module.ts (2)
web/src/composables/gql/graphql.ts (1)
  • ApiConfig (132-139)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • ApiConfig (132-139)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2)
web/src/composables/gql/graphql.ts (1)
  • UpgradeInfo (2320-2328)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • UpgradeInfo (2320-2328)
🔇 Additional comments (17)
web/src/components/TestThemeSwitcher.standalone.vue (1)

16-17: LGTM! Labels now match values consistently.

The label updates from "Light/Dark" to "White/Black" improve consistency—the displayed labels now match their corresponding values. No behavioral changes, purely cosmetic.

api/src/unraid-api/config/api-config.module.ts (2)

19-19: LGTM!

The addition of lastSeenOsVersion: undefined to the default config is correct and consistent with the optional field definition in the ApiConfig type.


83-97: OS version tracking only occurs at application bootstrap.

The trackOsVersionUpgrade() method correctly detects version changes on application startup. However, it won't detect OS upgrades that occur while the API is running. If the OS is upgraded without restarting the API, the version change won't be captured until the next API restart.

This may be intentional (since OS upgrades typically require a reboot/restart), but please confirm whether runtime OS version change detection is needed. If runtime detection is desired, consider adding a polling mechanism or event listener for OS version changes.

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

50-64: LGTM!

The upgrade() resolver correctly determines upgrade status by comparing currentVersion with lastSeenVersion. The logic properly handles edge cases (first boot, missing versions) and returns appropriate values for the UpgradeInfo type.

web/public/test-pages/shared-header.js (1)

22-30: LGTM!

The theme selector UI addition is straightforward and aligns with the four Unraid themes (white, black, gray, azure).

web/src/components/Activation/store/upgradeOnboarding.ts (1)

1-62: LGTM!

The upgradeOnboarding store is well-implemented with proper reactive patterns and error handling:

  • Vue reactivity utilities are explicitly imported (following coding guidelines)
  • The GraphQL query uses errorPolicy: 'all' for graceful error handling
  • Session storage is used appropriately for UI state persistence
  • The watch effect correctly handles async operations and updates upgradeSteps
  • Computed properties properly derive state from the query result

As per coding guidelines, explicit Vue imports are correctly used instead of relying on auto-imports.

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

44-54: LGTM!

The UpgradeInfo model is properly structured with appropriate GraphQL decorators, field types, and descriptions. The non-nullable isUpgrade field with nullable version fields provides a clear contract for upgrade detection.


64-65: LGTM!

The upgrade field addition to InfoVersions is correct and consistent with the new UpgradeInfo type.

api/generated-schema.graphql (1)

1673-1682: LGTM!

The generated GraphQL schema for UpgradeInfo and the upgrade field on InfoVersions are correct and consistent with the TypeScript models defined in api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts.

Also applies to: 1693-1694

web/public/test-pages/all-components.html (5)

14-19: LGTM!

The dark mode styling additions are consistent and use appropriate CSS transitions for smooth theme changes.

Also applies to: 36-41, 47-51, 60-66, 73-77, 94-98


99-132: LGTM!

The theme selector button styling and positioning are well-implemented with proper dark mode variants and hover effects.

Also applies to: 136-136


527-626: Test implementation accesses internal Pinia APIs.

The activation modal test buttons access Pinia's internal _s map (e.g., window.globalPinia._s.get('activationCodeModal')) to retrieve stores. While this works for testing, it's fragile and could break if Pinia's internal structure changes. The direct Apollo cache modification and use of __activationModalDebug are also non-standard but acceptable for test pages.

Consider whether these tests should use a more stable API for accessing stores, or document that these tests rely on internal APIs and may need updates when dependencies are upgraded. The error handling and logging are good, which will help identify issues quickly.


314-379: LGTM!

The test controls container styling with dark mode support is well-implemented, and the console output area provides good visual feedback for test operations.


293-301: LGTM!

The Activation Modal Tests card provides clear test buttons for different activation scenarios.

packages/unraid-shared/src/services/api-config.ts (1)

30-33: Regenerate GraphQL schema to include lastSeenOsVersion
The lastSeenOsVersion field is added to ApiConfig but is missing in the generated GraphQL types; please run your GraphQL codegen (e.g. npm run generate) and commit the updated schema and types.

web/__test__/components/Activation/ActivationModal.test.ts (1)

131-141: LGTM - proper async factory pattern for partial mock.

The @nuxt/ui mock correctly uses importOriginal with an async factory to preserve original exports while overriding only the UStepper component. This follows the recommended pattern for partial mocks.

Based on learnings

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

20-21: LGTM - consistent auto-generated GraphQL operation additions.

The new UpdateSystemTime mutation and UpgradeInfo query are correctly added in all three required locations:

  1. Type definition in Documents (lines 20-21)
  2. Runtime mapping in documents object (lines 67-68)
  3. Type-safe overload functions (lines 137-144)

The additions follow the existing pattern and maintain consistency. Since this is auto-generated code (indicated by /* eslint-disable */), any future changes should be made by regenerating from the GraphQL schema rather than manual edits.

Also applies to: 67-68, 137-144

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
api/src/unraid-api/config/api-config.module.ts (1)

88-98: Consider adding error handling and logging.

The shutdown logic is correct and properly handles edge cases (undefined currentOsVersion or apiConfig). However, consider the following optional improvements:

  1. Add error handling for the persist() call to log failures explicitly
  2. Add logging to indicate when lastSeenOsVersion is successfully updated or skipped

Apply this diff to add error handling and logging:

 async onApplicationShutdown() {
     if (!this.currentOsVersion) {
         return;
     }

     const apiConfig = this.configService.get<ApiConfig>('api');
     if (apiConfig) {
         apiConfig.lastSeenOsVersion = this.currentOsVersion;
-        await this.persist(apiConfig);
+        try {
+            await this.persist(apiConfig);
+            console.log(`Updated lastSeenOsVersion to ${this.currentOsVersion}`);
+        } catch (error) {
+            console.error('Failed to persist lastSeenOsVersion:', error);
+        }
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3539b2 and 4022578.

📒 Files selected for processing (1)
  • api/src/unraid-api/config/api-config.module.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/config/api-config.module.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.module.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.module.ts
🧬 Code graph analysis (1)
api/src/unraid-api/config/api-config.module.ts (2)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • ApiConfig (132-139)
web/src/composables/gql/graphql.ts (1)
  • ApiConfig (132-139)
🔇 Additional comments (3)
api/src/unraid-api/config/api-config.module.ts (3)

1-1: LGTM: Lifecycle hook correctly implemented.

The addition of OnApplicationShutdown to both imports and implements clause follows NestJS patterns for lifecycle management.

Also applies to: 51-51


53-53: LGTM: Field type correctly allows undefined.

The currentOsVersion field is properly typed to allow undefined, which is handled correctly in the shutdown method.


85-86: LGTM: OS version capture added to bootstrap.

The bootstrap method correctly captures the OS version from the config store for later persistence during shutdown.

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)
api/src/unraid-api/config/api-config.module.ts (1)

14-21: Ensure GraphQL schema/types include lastSeenOsVersion.

Adding this field requires updating the ApiConfig GraphQL type and regenerating types.

🧹 Nitpick comments (2)
api/src/unraid-api/config/os-version-tracker.module.ts (1)

70-73: Enable fsyncWait for safer writes to config storage.

Atomic writes to flash/config benefit from fsync. Set fsyncWait: true.

-            await writeFile(this.trackerPath, JSON.stringify(state, null, 2), { mode: 0o644 });
+            await writeFile(this.trackerPath, JSON.stringify(state, null, 2), {
+                mode: 0o644,
+                fsyncWait: true,
+            });

Based on learnings

api/src/unraid-api/config/api-config.test.ts (1)

14-21: Remove unused mocks to reduce noise.

Mocks for file-exists appear unused in this file; consider deleting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4022578 and a0ec3ce.

📒 Files selected for processing (3)
  • api/src/unraid-api/config/api-config.module.ts (3 hunks)
  • api/src/unraid-api/config/api-config.test.ts (2 hunks)
  • api/src/unraid-api/config/os-version-tracker.module.ts (1 hunks)
🧰 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

Files:

  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/os-version-tracker.module.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/os-version-tracker.module.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/os-version-tracker.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/unraid-api/config/api-config.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/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
🧬 Code graph analysis (2)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (78-80)
  • loadApiConfig (29-41)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/os-version-tracker.module.ts (2)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (114-119)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
🔇 Additional comments (3)
api/src/unraid-api/config/api-config.module.ts (2)

10-10: ESM import extension looks good.

Using .js in TS imports matches project ESM guidelines.


115-118: Module wiring LGTM.

Importing and exporting OsVersionTrackerModule via ApiConfigModule is coherent and avoids cycles.

api/src/unraid-api/config/api-config.test.ts (1)

124-167: Tracker behavior tests are solid.

Good coverage for first-run persist, no‑rewrite on same version, and error handling.

Also applies to: 169-179

Comment on lines 55 to 56
this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`);
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix Nest Logger argument order (message first, stack/context second).

Current calls pass the error object as the message. Swap to message-first, include stack/message from the error.

@@
-        } catch (error) {
-            this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`);
+        } catch (error) {
+            this.logger.error(
+                `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`,
+                (error as Error)?.stack
+            );
             return undefined;
         }
@@
-        } catch (error) {
-            this.logger.debug(error, `Unable to read OS version tracker state at ${this.trackerPath}`);
+        } catch (error) {
+            this.logger.debug(
+                `Unable to read OS version tracker state at ${this.trackerPath}: ${(error as Error)?.message}`
+            );
             return undefined;
         }
@@
-        } catch (error) {
-            this.logger.error(error, 'Failed to persist OS version tracker state');
+        } catch (error) {
+            this.logger.error('Failed to persist OS version tracker state', (error as Error)?.stack);
         }

Also applies to: 65-66, 74-75

🤖 Prompt for AI Agents
In api/src/unraid-api/config/os-version-tracker.module.ts around lines 55-56
(also update 65-66 and 74-75), the Nest logger calls pass the Error object as
the first argument; change them to pass a descriptive message string first
(e.g., "Failed to read current OS version from <path>") and pass the error
object or an object containing its stack/message as the second argument so the
logger receives message-first and error/context-second.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (10)
web/src/components/Activation/store/upgradeOnboarding.ts (2)

6-9: Use .js extension in TS import specifiers for ESM.

Update import specifiers to include .js to match our ESM guideline. As per coding guidelines.

-import type { ReleaseStepConfig } from '~/components/Activation/releaseConfigs';
-import { getUpgradeSteps } from '~/components/Activation/releaseConfigs';
-import { UPGRADE_INFO_QUERY } from '~/components/Activation/upgradeInfo.query';
+import type { ReleaseStepConfig } from '~/components/Activation/releaseConfigs.js';
+import { getUpgradeSteps } from '~/components/Activation/releaseConfigs.js';
+import { UPGRADE_INFO_QUERY } from '~/components/Activation/upgradeInfo.query.js';

35-45: Guard async watch against race conditions and handle errors.

Consecutive getUpgradeSteps calls can resolve out of order and overwrite newer state; also no error handling. Prefer a requestId guard + try/catch.

-  watch(
+  let requestId = 0;
+  watch(
     [isUpgrade, previousVersion, currentVersion],
-    async ([isUpgradeValue, prevVersion, currVersion]) => {
-      if (isUpgradeValue && prevVersion && currVersion) {
-        allUpgradeSteps.value = await getUpgradeSteps(prevVersion, currVersion);
-      } else {
-        allUpgradeSteps.value = [];
-      }
-    },
+    async ([isUpgradeValue, prevVersion, currVersion]) => {
+      const id = ++requestId;
+      try {
+        if (isUpgradeValue && prevVersion && currVersion) {
+          const steps = await getUpgradeSteps(prevVersion, currVersion);
+          if (id === requestId) allUpgradeSteps.value = steps;
+        } else {
+          if (id === requestId) allUpgradeSteps.value = [];
+        }
+      } catch (e) {
+        if (id === requestId) allUpgradeSteps.value = [];
+        console.error('getUpgradeSteps failed', e);
+      }
+    },
     { immediate: true }
   );
web/src/components/Activation/completeUpgradeStep.mutation.ts (1)

1-1: Add .js extension to import path.

Align with ESM import guideline; import the re-export that exists in your tree (index.js or gql.js).

As per coding guidelines.

-import { graphql } from '~/composables/gql';
+import { graphql } from '~/composables/gql/index.js';

If there’s no index re-export, use the concrete file:

-import { graphql } from '~/composables/gql';
+import { graphql } from '~/composables/gql/gql.js';

Please confirm which path exists in the repo.

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

3-9: Validate and constrain stepId (enum or validators).

Prevent unknown/empty IDs at the schema boundary. Consider a GraphQL enum (e.g., timezone | plugins) or add class-validator decorators (IsNotEmpty, Length) if ValidationPipe is enabled.

Would you like me to propose an enum + registerEnumType and updates across web/API?

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

16-22: Deduplicate UpgradeInfo construction and avoid stale reads.

buildUpgradeInfo duplicates VersionsResolver.upgrade logic and reads via ConfigService right after markStepCompleted; if the tracker writes asynchronously, results may be stale. Prefer a single shared builder (service/utility) used by both resolvers, and source completedSteps directly from OnboardingTracker to guarantee freshness.

Do you want a small UpgradeInfoService extracted so both resolvers use it? I can provide a diff.

Also applies to: 24-53

api/generated-schema.graphql (2)

1996-2010: Prefer DateTime scalar for currentTime

SystemTime.currentTime is a String. Consider using DateTime for consistency with Info.time and better type safety. Update the code-first model; this file will regenerate.


2539-2555: Enforce ntpServers length (max 4) in resolver validation

Schema cannot express max list length; the description says “up to four”. Validate and reject longer arrays in the mutation handler.

web/src/components/Activation/ActivationModal.vue (1)

155-167: Verify upgrade step advancement logic

After marking a step completed, index is clamped, not incremented. This assumes upgradeSteps removes the completed step. If it doesn’t, users may get stuck on the same step. Confirm store behavior or increment index when appropriate.

api/src/unraid-api/config/onboarding-tracker.module.ts (2)

186-193: Ensure directory exists and improve write durability

  • Create parent directory before writing to avoid ENOENT on first run.
  • Use fsyncWait for stronger durability guarantees.

Apply this diff:

-import { writeFile } from 'atomically';
+import { writeFile } from 'atomically';
+import { mkdir } from 'fs/promises';
+import { dirname } from 'path';
@@
     private async writeTrackerState(state: TrackerState): Promise<void> {
         try {
-            await writeFile(this.trackerPath, JSON.stringify(state, null, 2), { mode: 0o644 });
+            await mkdir(dirname(this.trackerPath), { recursive: true });
+            await writeFile(this.trackerPath, JSON.stringify(state, null, 2), {
+                mode: 0o644,
+                // Ensure data hits disk (slower but safer for config-state)
+                fsyncWait: true,
+            } as any);
             this.state = state;
         } catch (error) {
             this.logger.error(error, 'Failed to persist onboarding tracker state');
         }
     }

Note: atomically’s writeFile supports fsyncWait; cast if TS types don’t include it.


104-140: Optional: serialize writes to avoid racing updates

Concurrent markStepCompleted calls can race and lose updates. Consider a simple in‑memory queue/lock around ensureStateLoaded + writeTrackerState.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0ec3ce and 5f4153c.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (17)
  • api/generated-schema.graphql (8 hunks)
  • api/src/unraid-api/config/api-config.module.ts (3 hunks)
  • api/src/unraid-api/config/api-config.test.ts (2 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (3 hunks)
  • web/src/components/Activation/ActivationModal.vue (4 hunks)
  • web/src/components/Activation/completeUpgradeStep.mutation.ts (1 hunks)
  • web/src/components/Activation/store/upgradeOnboarding.ts (1 hunks)
  • web/src/components/Activation/upgradeInfo.query.ts (1 hunks)
  • web/src/composables/gql/gql.ts (4 hunks)
  • web/src/composables/gql/graphql.ts (13 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • web/src/components/Activation/upgradeInfo.query.ts
🧰 Additional context used
📓 Path-based instructions (11)
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/config/api-config.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/config/api-config.test.ts
  • web/src/components/Activation/store/upgradeOnboarding.ts
  • web/src/components/Activation/completeUpgradeStep.mutation.ts
  • web/src/composables/gql/graphql.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/config/api-config.test.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationModal.vue
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/config/api-config.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/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
**/{store,stores}/**/*.ts

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

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • web/src/components/Activation/store/upgradeOnboarding.ts
web/**/*.mutation.ts

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

GraphQL mutations must be placed in .mutation.ts files

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

Files:

  • web/src/components/Activation/completeUpgradeStep.mutation.ts
web/**/*.{query,mutation}.ts

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

All GraphQL under web/ must follow this naming convention

Files:

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

Applied to files:

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

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Ensure Vue reactivity imports (computed, ref, watchEffect) are added to store source files

Applied to files:

  • web/src/components/Activation/ActivationModal.vue
🧬 Code graph analysis (9)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (1)
  • Resolver (13-81)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (14-50)
web/src/composables/gql/graphql.ts (3)
  • OnboardingMutations (1519-1523)
  • UpgradeInfo (2341-2351)
  • CompleteUpgradeStepInput (439-442)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (4)
  • ObjectType (5-15)
  • ObjectType (17-42)
  • ObjectType (44-60)
  • ObjectType (62-72)
web/src/composables/gql/graphql.ts (2)
  • OnboardingMutations (1519-1523)
  • UpgradeInfo (2341-2351)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • OnboardingMutations (1519-1523)
  • UpgradeInfo (2341-2351)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2)
web/src/composables/gql/graphql.ts (2)
  • Mutation (1221-1265)
  • OnboardingMutations (1519-1523)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • Mutation (1221-1265)
  • OnboardingMutations (1519-1523)
api/src/unraid-api/config/onboarding-tracker.module.ts (2)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (114-119)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (2)
web/src/composables/gql/graphql.ts (1)
  • CompleteUpgradeStepInput (439-442)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • CompleteUpgradeStepInput (439-442)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (78-80)
  • loadApiConfig (29-41)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
web/src/components/Activation/store/upgradeOnboarding.ts (2)
web/src/components/Activation/upgradeInfo.query.ts (1)
  • UPGRADE_INFO_QUERY (3-18)
web/src/components/Activation/releaseConfigs.ts (2)
  • ReleaseStepConfig (3-7)
  • getUpgradeSteps (58-82)
web/src/components/Activation/completeUpgradeStep.mutation.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (312-314)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (11)
  • Maybe (3-3)
  • Scalars (11-75)
  • CompleteUpgradeStepInput (439-442)
  • UpgradeInfo (2341-2351)
  • OnboardingMutations (1519-1523)
  • SystemTime (2098-2108)
  • MutationUpdateSystemTimeArgs (1378-1380)
  • UpdateSystemTimeInput (2330-2339)
  • OnboardingMutationsCompleteUpgradeStepArgs (1527-1529)
  • InputMaybe (4-4)
  • Exact (5-5)
🔇 Additional comments (13)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)

46-49: Onboarding mutation surface wiring LGTM; confirm resolver is provided.

The onboarding() namespacing pattern matches existing groups.

Ensure OnboardingMutationsResolver is included in the GraphQL module providers and exported model types are compiled into the schema.

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

3-3: Onboarding mutations wiring looks good; confirm resolver exposure

  • Import uses .js extension (ESM‑safe).
  • Type/field definitions align with existing mutation model patterns.

Please confirm OnboardingMutationsResolver is registered and RootMutations resolver exposes onboarding as expected.

Also applies to: 44-52, 74-75

api/src/unraid-api/config/api-config.test.ts (3)

55-62: Solid test coverage and alignment with config versioning changes

  • Validates fileName/configKey/defaults and migration correctly (incl. lastSeenOsVersion).
  • Ensures bootstrap sets api.version to API_VERSION.

No change requested.

Also applies to: 75-99, 101-105


107-154: OnboardingTracker tests are thorough

Covers upgrade version deferral, idempotent writes, snapshot semantics, missing version handling, and per‑step completion. Good use of mocks and no brittle error message assertions.

Also applies to: 155-194, 195-225, 226-239, 240-289


296-307: loadApiConfig default/version behavior validated

Ensures API_VERSION override and default shape including lastSeenOsVersion.

Also applies to: 309-321

web/src/components/Activation/ActivationModal.vue (1)

45-46: Confirm close behavior for upgrade mode

showModal depends on shouldShowUpgradeOnboarding, but closeModal sets isHidden. Ensure setIsHidden updates shouldShowUpgradeOnboarding (or have showModal consider isHidden) so the modal actually closes.

Also applies to: 134-141

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

2-2: Import missing .js extension.

The import statement is missing the .js extension required for ESM compatibility per the coding guidelines. However, since this is an auto-generated file, the fix should be applied to the code generator configuration rather than manual editing.

As per coding guidelines

Consider updating the GraphQL Code Generator configuration to output imports with .js extensions:

import * as types from './graphql.js';

17-17: LGTM: GraphQL operations correctly added.

The new GraphQL operations (CompleteUpgradeStep mutation, UpdateSystemTime mutation, and UpgradeInfo query) are properly wired into the type system and runtime document mappings. The overloads follow the existing pattern and maintain type safety for the new onboarding and system time features.

Also applies to: 21-22, 65-65, 69-70, 127-130, 143-146, 147-150

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

135-135: New field added to ApiConfig.

The lastSeenOsVersion field is added to track the last OS version seen by the API, which aligns with the upgrade tracking functionality introduced in this PR.


438-442: CompleteUpgradeStep mutation types properly defined.

The mutation input, resolver type, and operation types are correctly structured for marking upgrade onboarding steps as completed. The stepId string parameter and UpgradeInfo return type align with the PR's upgrade onboarding objectives.

Also applies to: 1518-1529, 2644-2649


2097-2108: SystemTime types and mutation correctly implemented.

The SystemTime type and UpdateSystemTimeInput properly model system time configuration with:

  • Current time (ISO-8601 UTC)
  • Timezone (IANA identifier)
  • NTP enable/disable flag
  • NTP servers array
  • Optional manual date/time with documented format

The mutation is correctly wired into the schema. Field documentation clearly describes expected formats and behavior.

Also applies to: 2330-2339, 1738-1739, 1262-1263, 1377-1380, 2666-2671


2341-2351: UpgradeInfo type and query properly defined.

The UpgradeInfo type captures upgrade state with:

  • isUpgrade flag indicating version change
  • Previous and current version strings
  • Array of completed onboarding step IDs

The integration into InfoVersions and the corresponding query operation are correctly structured for tracking upgrade onboarding progress.

Also applies to: 1096-1097, 2673-2676


2937-2942: Document definitions correctly generated.

The GraphQL document definitions for the new operations (CompleteUpgradeStepDocument, UpdateSystemTimeDocument, UpgradeInfoDocument) are properly structured with correct operation types and variable definitions. These enable type-safe GraphQL client usage.

Comment on lines 165 to 262
private async readCurrentVersion(): Promise<string | undefined> {
try {
const contents = await readFile(OS_VERSION_FILE_PATH, 'utf8');
const match = contents.match(/^\s*version\s*=\s*"([^"]+)"\s*$/m);
return match?.[1]?.trim() || undefined;
} catch (error) {
this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`);
return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix Nest Logger usage (message/trace ordering)

Logger.error/debug expect (message, trace?). Current calls pass error as message. Swap to meaningful message + error stack for better observability.

Apply this diff:

-            this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`);
+            this.logger.error(
+                `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`,
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
+            this.logger.debug(
+                `Unable to read onboarding tracker state at ${this.trackerPath}: ${error instanceof Error ? error.message : String(error)}`
+            );
@@
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );

Also applies to: 176-183, 186-193

🤖 Prompt for AI Agents
In api/src/unraid-api/config/onboarding-tracker.module.ts around lines 165-173
(and likewise at 176-183 and 186-193), the Nest Logger calls currently pass the
Error object as the first argument and the message second; change these to pass
a clear string message first (e.g., `Failed to read current OS version from
${OS_VERSION_FILE_PATH}`) and the error stack or message as the second argument
(e.g., `error?.stack ?? String(error)`), and do the same pattern for the other
logger.error/debug invocations in the noted ranges so message and trace/stack
ordering follow Nest Logger conventions.

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/config/onboarding-tracker.module.ts (3)

171-179: Fix Nest Logger usage (message/trace ordering).

This issue was previously flagged and remains unaddressed. The Nest Logger expects (message, trace?) but the current code passes the error object as the first argument.

Apply this diff:

     private async readCurrentVersion(): Promise<string | undefined> {
         try {
             const contents = await readFile(this.versionFilePath, 'utf8');
             const match = contents.match(/^\s*version\s*=\s*"([^"]+)"\s*$/m);
             return match?.[1]?.trim() || undefined;
         } catch (error) {
-            this.logger.error(error, `Failed to read current OS version from ${this.versionFilePath}`);
+            this.logger.error(
+                `Failed to read current OS version from ${this.versionFilePath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
             return undefined;
         }
     }

182-189: Fix Nest Logger usage (message/trace ordering).

This issue was previously flagged and remains unaddressed. The Nest Logger expects (message, trace?) but the current code passes the error object as the first argument.

Apply this diff:

     private async readTrackerState(): Promise<TrackerState | undefined> {
         try {
             const content = await readFile(this.trackerPath, 'utf8');
             return JSON.parse(content) as TrackerState;
         } catch (error) {
-            this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
+            this.logger.debug(
+                `Unable to read onboarding tracker state at ${this.trackerPath}: ${error instanceof Error ? error.message : String(error)}`
+            );
             return undefined;
         }
     }

192-199: Fix Nest Logger usage (message/trace ordering).

This issue was previously flagged and remains unaddressed. The Nest Logger expects (message, trace?) but the current code passes the error object as the first argument.

Apply this diff:

     private async writeTrackerState(state: TrackerState): Promise<void> {
         try {
             await writeFile(this.trackerPath, JSON.stringify(state, null, 2), { mode: 0o644 });
             this.state = state;
         } catch (error) {
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );
         }
     }
🧹 Nitpick comments (1)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)

162-169: Document legacy compatibility for dual writes in syncConfig
Add a comment above lines 162–169 in api/src/unraid-api/config/onboarding-tracker.module.ts explaining that both onboardingTracker.currentVersion and store.emhttp.var.version are set for backward compatibility with existing consumers (resolvers, plugin connection service).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f4153c and 223004c.

📒 Files selected for processing (2)
  • api/src/unraid-api/config/api-config.test.ts (2 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
🧰 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

Files:

  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/onboarding-tracker.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/unraid-api/config/api-config.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/config/api-config.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
🧬 Code graph analysis (2)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (78-80)
  • loadApiConfig (29-41)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.module.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • Injectable (49-111)
  • Module (114-119)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
🔇 Additional comments (2)
api/src/unraid-api/config/api-config.test.ts (2)

107-307: LGTM! Comprehensive test coverage.

The OnboardingTracker test suite is well-structured and covers:

  • Lifecycle behavior (bootstrap/shutdown)
  • Version change detection and persistence
  • Error handling for missing files
  • Step completion tracking
  • Upgrade flag preservation
  • Config synchronization

The use of a configStore to simulate ConfigService behavior is a clean approach that makes assertions straightforward.


310-340: LGTM! loadApiConfig tests updated correctly.

The tests correctly include lastSeenOsVersion: undefined in the expected default config and use the API_VERSION constant consistently.


import { Node } from '@unraid/shared/graphql.model.js';

import { ActivationOnboardingStepId } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import ActivationOnboardingStepId.

Copilot Autofix

AI 3 days ago

The best way to fix the problem is to remove the unused import. In file api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts, delete the import statement for ActivationOnboardingStepId on line 5. No other changes or definitions are needed, and the removal will avoid confusion and potential maintenance issues caused by unused imports.


Suggested changeset 1
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts b/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
--- a/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
+++ b/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
@@ -2,7 +2,6 @@
 
 import { Node } from '@unraid/shared/graphql.model.js';
 
-import { ActivationOnboardingStepId } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js';
 
 @ObjectType()
 export class CoreVersions {
EOF
@@ -2,7 +2,6 @@

import { Node } from '@unraid/shared/graphql.model.js';

import { ActivationOnboardingStepId } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js';

@ObjectType()
export class CoreVersions {
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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

2547-2547: Authorization directives still missing on state-changing mutations.

Past review already identified that updateSystemTime and onboarding.completeUpgradeStep lack authorization directives. These mutations modify system state and must enforce authorization to prevent unauthorized access.

Also applies to: 2558-2559

web/public/test-pages/all-components.html (1)

486-502: Undefined function call: updateDarkModeButton().

Line 498 calls updateDarkModeButton(), but this function is not defined in this file. The correct function name is updateThemeButton() (defined on line 420). This will cause a runtime error when the "Toggle Theme" test button is clicked.

Replace the undefined function call with the correct function:

         window.themeStore.theme.name = newTheme;
         window.themeStore.setCssVars();
-        updateDarkModeButton();
+        updateThemeButton();
         
         log('Theme changed to: ' + newTheme);

This issue was previously identified in an earlier review comment on lines 487-502.

🧹 Nitpick comments (10)
api/src/unraid-api/main.ts (1)

24-24: Add cleanup lifecycle hooks in new modules/services

  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts

None implement onModuleDestroy or onApplicationShutdown; if these classes manage timers, connections, or other resources, implement the appropriate NestJS lifecycle hooks.

web/src/components/Activation/ActivationTimezoneStep.vue (1)

77-90: Consider a non-blocking notification for failed updates (optional).

While the non-blocking error handling aligns with the design goal of not preventing Unraid access, users are currently unaware when the timezone update fails. Consider adding a toast or banner notification that doesn't block progression but informs users they can configure the timezone later in settings.

Example approach:

  try {
    await updateSystemTime({
      input: {
        timeZone: selectedTimeZone.value,
      },
    });
  } catch (err) {
-   console.warn('Failed to update timezone, continuing anyway:', err);
+   console.warn('Failed to update timezone, continuing anyway:', err);
+   // Show non-blocking notification (e.g., toast or banner)
+   // Example: toast.warning(t('Timezone update failed. You can configure it later in settings.'));
  } finally {
    isSaving.value = false;
  }
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)

154-204: Consider naming clarity between activation and upgrade onboarding (optional).

There's conceptual overlap between ActivationOnboardingStep (this file) and UpgradeStep (versions.model.ts). Both relate to onboarding/setup flows:

  • ActivationOnboardingStep: tracks completion state, uses strongly-typed enum
  • UpgradeStep: includes display metadata (title, description, icon), uses string id

While they serve different modules and purposes, the naming could be clearer to distinguish the activation flow from the OS upgrade flow. Consider whether these could be unified or more explicitly named to avoid confusion.

web/src/components/Activation/ActivationWelcomeStep.vue (2)

59-67: Consider using Vue Router for navigation instead of window.location.href.

Using window.location.href = '/login' (line 62) bypasses Vue Router, which means losing benefits like navigation guards, smooth transitions, and state preservation. If the application has Vue Router configured, prefer using router.push('/login').

Apply this diff to use Vue Router:

 <script lang="ts" setup>
 import { computed } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
 
 import { BrandButton } from '@unraid/ui';
 
 export interface Props {
   // Version context
   currentVersion?: string;
   previousVersion?: string;
   // Partner context
   partnerName?: string;
   // Common props
   onComplete: () => void;
   onSkip?: () => void;
   onBack?: () => void;
   showSkip?: boolean;
   showBack?: boolean;
   // For redirecting to login page after welcome
   redirectToLogin?: boolean;
 }
 
 const props = defineProps<Props>();
 const { t } = useI18n();
+const router = useRouter();
 
 const modalTitle = computed<string>(() => {
   // Partner context
   if (props.partnerName) {
     return t('Welcome to your new {0} system, powered by Unraid!', [props.partnerName]);
   }
 
   // Version context
   if (props.currentVersion) {
     return t('Welcome to Unraid {0}!', [props.currentVersion]);
   }
 
   return t('Welcome to Unraid!');
 });
 
 const modalDescription = computed<string>(() => {
   // Upgrade context (has both previous and current version)
   if (props.previousVersion && props.currentVersion) {
     return t("You've upgraded from {0} to {1}", [props.previousVersion, props.currentVersion]);
   }
 
   // Current version context (has current version but no previous)
   if (props.currentVersion) {
     return t('Welcome to your Unraid {0} system', [props.currentVersion]);
   }
 
   // Default context
   return t('Get started with your new Unraid system');
 });
 
 const buttonText = computed<string>(() => {
   return t('Get Started');
 });
 
 const handleComplete = () => {
   if (props.redirectToLogin) {
-    // Redirect to login page for password creation
-    window.location.href = '/login';
+    // Navigate to login page for password creation
+    router.push('/login');
   } else {
     // Normal completion flow
     props.onComplete();
   }
 };
 </script>

If Vue Router is not available in this context or if a hard refresh is intentionally required, the current implementation is acceptable.


77-83: Consider adding ARIA labels for improved accessibility.

The buttons lack explicit ARIA labels, which could enhance screen reader support. While the visible text is localized, adding aria-label attributes would improve accessibility.

Apply this diff to add ARIA labels:

     <div class="flex space-x-4">
-      <BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />
+      <BrandButton 
+        v-if="showBack" 
+        :text="t('Back')" 
+        :aria-label="t('Go back to previous step')"
+        variant="outline" 
+        @click="onBack" 
+      />
 
-      <BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />
+      <BrandButton 
+        v-if="showSkip" 
+        :text="t('Skip')" 
+        :aria-label="t('Skip this step')"
+        variant="outline" 
+        @click="onSkip" 
+      />
 
-      <BrandButton :text="buttonText" @click="handleComplete" />
+      <BrandButton 
+        :text="buttonText" 
+        :aria-label="t('Continue to next step')"
+        @click="handleComplete" 
+      />
     </div>

Note: Verify that the BrandButton component supports the aria-label prop before applying this change.

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

5-5: Drop unused import

ActivationOnboardingStepId isn’t referenced in this file; please remove it to satisfy CodeQL and keep the surface lean.

web/src/components/Activation/ActivationModal.vue (1)

289-289: Simplify boolean condition.

The expression isHidden === false is verbose and less idiomatic than using boolean negation.

Apply this diff:

-    :show-close-button="isHidden === false || shouldShowUpgradeOnboarding"
+    :show-close-button="!isHidden || shouldShowUpgradeOnboarding"
web/src/components/Activation/ActivationSteps.vue (3)

26-26: Type assertion could be made safer.

The type assertion as Record<ActivationOnboardingStepId, StepItem> assumes stepMetadata contains all possible step IDs. While the fallback at line 39 handles missing entries, this assertion bypasses TypeScript's type checking.

Consider adding a runtime check or using a type guard function:

 const dynamicSteps = computed(() => {
-  const metadataLookup = stepMetadata as Record<ActivationOnboardingStepId, StepItem>;
+  const metadataLookup = stepMetadata;

Or if the assertion is necessary, add a comment explaining why it's safe:

 const dynamicSteps = computed(() => {
+  // Safe assertion: fallback at line 39 handles missing entries
   const metadataLookup = stepMetadata as Record<ActivationOnboardingStepId, StepItem>;

28-35: Hardcoded fallback steps could drift from registry.

The fallback when props.steps.length === 0 duplicates step IDs as hardcoded strings. If the step IDs or order change in the registry, this fallback won't automatically update.

Extract the default step IDs to a shared constant or derive them from the registry:

// In stepRegistry.ts or nearby
export const DEFAULT_STEP_ORDER: ActivationOnboardingStepId[] = [
  'WELCOME',
  'TIMEZONE', 
  'PLUGINS',
  'ACTIVATION',
];

Then use it here:

   if (props.steps.length === 0) {
-    return [
-      metadataLookup.WELCOME,
-      metadataLookup.TIMEZONE,
-      metadataLookup.PLUGINS,
-      metadataLookup.ACTIVATION,
-    ];
+    return DEFAULT_STEP_ORDER.map(id => metadataLookup[id]);
   }

71-75: Extract repeated offset calculation to reduce duplication.

The offset calculation appears in both currentStepIndex (lines 72-73) and handleStepClick (line 103). Extracting this to a computed property would improve maintainability.

+const stepOffset = computed(() => (includeInitialStep.value ? 1 : 0));
+
 const currentStepIndex = computed(() => {
-  const offset = includeInitialStep.value ? 1 : 0;
-  const targetIndex = (props.activeStepIndex ?? 0) + offset;
+  const targetIndex = (props.activeStepIndex ?? 0) + stepOffset.value;
   return Math.min(Math.max(targetIndex, 0), timelineSteps.value.length - 1);
 });

And in handleStepClick:

-  const offset = includeInitialStep.value ? 1 : 0;
-  const actualStepIndex = Math.max(0, stepIndex - offset);
+  const actualStepIndex = Math.max(0, stepIndex - stepOffset.value);

Also applies to: 103-104

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 223004c and c414f39.

⛔ Files ignored due to path filters (2)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (40)
  • @tailwind-shared/css-variables.css (1 hunks)
  • api/generated-schema.graphql (9 hunks)
  • api/src/unraid-api/config/api-config.module.ts (2 hunks)
  • api/src/unraid-api/config/api-config.test.ts (2 hunks)
  • api/src/unraid-api/config/onboarding-tracker.model.ts (1 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts (8 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1 hunks)
  • api/src/unraid-api/main.ts (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (6 hunks)
  • web/__test__/components/Activation/WelcomeModal.test.ts (4 hunks)
  • web/components.d.ts (2 hunks)
  • web/public/test-pages/all-components.html (11 hunks)
  • web/src/components/Activation/ActivationLicenseStep.vue (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (2 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/components/Activation/ActivationSteps.vue (1 hunks)
  • web/src/components/Activation/ActivationTimezoneStep.vue (1 hunks)
  • web/src/components/Activation/ActivationWelcomeStep.vue (1 hunks)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md (1 hunks)
  • web/src/components/Activation/WelcomeModal.standalone.vue (2 hunks)
  • web/src/components/Activation/activationOnboarding.query.ts (1 hunks)
  • web/src/components/Activation/completeUpgradeStep.mutation.ts (1 hunks)
  • web/src/components/Activation/stepRegistry.ts (1 hunks)
  • web/src/components/Activation/steps/timezone.ts (1 hunks)
  • web/src/components/Activation/store/upgradeOnboarding.ts (1 hunks)
  • web/src/components/DevModalTest.standalone.vue (2 hunks)
  • web/src/composables/gql/gql.ts (4 hunks)
  • web/src/composables/gql/graphql.ts (14 hunks)
✅ Files skipped from review due to trivial changes (1)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/src/components/Activation/store/upgradeOnboarding.ts
  • api/src/unraid-api/config/api-config.module.ts
  • web/src/composables/gql/gql.ts
  • web/src/components/Activation/ActivationPluginsStep.vue
🧰 Additional context used
📓 Path-based instructions (14)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/src/components/Activation/steps/timezone.ts
  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
  • web/src/components/Activation/completeUpgradeStep.mutation.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • web/src/components/Activation/activationOnboarding.query.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • web/src/composables/gql/graphql.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • web/components.d.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
  • web/src/components/Activation/stepRegistry.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/Activation/ActivationTimezoneStep.vue
  • web/src/components/Activation/ActivationLicenseStep.vue
  • web/src/components/DevModalTest.standalone.vue
  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/Activation/ActivationWelcomeStep.vue
web/**/*.mutation.ts

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

GraphQL mutations must be placed in .mutation.ts files

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

Files:

  • web/src/components/Activation/completeUpgradeStep.mutation.ts
web/**/*.{query,mutation}.ts

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

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Activation/completeUpgradeStep.mutation.ts
  • web/src/components/Activation/activationOnboarding.query.ts
web/**/*.query.ts

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

GraphQL queries must be placed in .query.ts files

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

Files:

  • web/src/components/Activation/activationOnboarding.query.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/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.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/config/api-config.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
🧠 Learnings (14)
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-10-11T02:09:16.853Z
Learnt from: elibosley
PR: unraid/api#1746
File: web/src/components/Activation/ActivationTimezoneStep.vue:78-91
Timestamp: 2025-10-11T02:09:16.853Z
Learning: In Unraid setup/activation flows (e.g., ActivationTimezoneStep.vue), don't block user progression when non-critical configuration steps like timezone updates fail. The design prioritizes allowing users to access Unraid over enforcing complete setup step success.

Applied to files:

  • web/src/components/Activation/ActivationTimezoneStep.vue
📚 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/customization/onboarding.service.spec.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

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

Applied to files:

  • web/components.d.ts
📚 Learning: 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 : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

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

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
🧬 Code graph analysis (15)
api/src/unraid-api/config/onboarding-tracker.model.ts (1)
api/src/unraid-api/config/onboarding-tracker.module.ts (2)
  • UpgradeStepState (293-293)
  • UpgradeProgressSnapshot (292-292)
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (3)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • Module (285-289)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (5)
  • ObjectType (7-17)
  • ObjectType (19-44)
  • ObjectType (46-74)
  • ObjectType (76-98)
  • ObjectType (100-110)
web/src/composables/gql/graphql.ts (2)
  • ActivationOnboardingStep (136-146)
  • ActivationOnboarding (122-134)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • ActivationOnboardingStep (136-146)
  • ActivationOnboarding (122-134)
web/src/components/Activation/completeUpgradeStep.mutation.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (312-314)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (1)
  • Resolver (15-60)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1)
  • Resolver (9-20)
web/src/composables/gql/graphql.ts (4)
  • Theme (2150-2166)
  • ActivationOnboarding (122-134)
  • ActivationOnboardingStep (136-146)
  • PublicPartnerInfo (1702-1710)
web/src/components/Activation/activationOnboarding.query.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (312-314)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (3)
api/src/unraid-api/config/onboarding-tracker.model.ts (1)
  • UpgradeProgressSnapshot (20-25)
web/src/composables/gql/graphql.ts (1)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • UpgradeInfo (2374-2386)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (4)
  • Maybe (3-3)
  • Scalars (11-75)
  • InputMaybe (4-4)
  • Exact (5-5)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (5)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • Resolver (19-99)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1)
  • Resolver (9-20)
web/src/composables/gql/graphql.ts (2)
  • InfoVersions (1121-1130)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • InfoVersions (1120-1129)
  • UpgradeInfo (2374-2386)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)
  • buildUpgradeInfoFromSnapshot (4-22)
api/src/unraid-api/config/onboarding-tracker.module.ts (4)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)
  • resolveActivationStepDefinitions (79-89)
  • ActivationStepDefinition (47-52)
  • ActivationStepContext (42-45)
  • findActivationCodeFile (20-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.model.ts (4)
  • TrackerState (8-12)
  • UpgradeProgressSnapshot (20-25)
  • CompletedStepState (3-6)
  • UpgradeStepState (14-18)
api/src/store/index.ts (1)
  • getters (18-23)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (1)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • Injectable (37-283)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (3)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (5)
  • ObjectType (33-60)
  • ObjectType (73-143)
  • ObjectType (145-152)
  • ObjectType (154-174)
  • ObjectType (176-204)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (8)
  • ObjectType (14-15)
  • ObjectType (17-18)
  • ObjectType (20-21)
  • ObjectType (23-26)
  • ObjectType (28-31)
  • ObjectType (33-42)
  • ObjectType (44-52)
  • ObjectType (54-76)
web/src/composables/gql/graphql.ts (2)
  • UpgradeStep (2389-2403)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (3)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (14-50)
web/src/composables/gql/graphql.ts (3)
  • OnboardingMutations (1551-1555)
  • UpgradeInfo (2375-2387)
  • CompleteUpgradeStepInput (471-474)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)
  • buildUpgradeInfoFromSnapshot (4-22)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (2)
web/src/composables/gql/graphql.ts (1)
  • CompleteUpgradeStepInput (471-474)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • CompleteUpgradeStepInput (470-473)
🪛 GitHub Actions: CI - Main (API)
packages/unraid-shared/package.json

[error] 1-1: Specifiers in the lockfile don't match package.json: 1 dependencies were removed: [email protected].

🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts

[notice] 5-5: Unused variable, import, function or class
Unused import ActivationOnboardingStepId.

🔇 Additional comments (30)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)

4-4: LGTM!

The OnboardingTrackerModule integration is clean and follows TypeScript ESM conventions with the .js extension.

Also applies to: 18-18

web/src/components/Activation/activationOnboarding.query.ts (1)

1-18: LGTM!

The GraphQL query follows the naming convention for query files and correctly retrieves all necessary activation onboarding fields.

As per coding guidelines

web/src/components/Activation/steps/timezone.ts (1)

1-8: LGTM!

Clean step registration with proper type checking via satisfies.

web/src/components/Activation/stepRegistry.ts (1)

1-40: LGTM!

The centralized step registry provides type-safe mapping between step identifiers, components, and metadata, improving maintainability.

web/src/components/DevModalTest.standalone.vue (1)

2-2: LGTM!

The Welcome modal integration in the dev test component is implemented correctly with proper ref typing and method invocation.

Also applies to: 7-7, 10-10, 27-31, 36-40

web/src/components/Activation/WelcomeModal.standalone.vue (1)

6-6: LGTM!

The refactor to use ActivationWelcomeStep improves component composition. The empty steps array for ActivationSteps is acceptable for a single-step modal where the timeline UI is not needed.

Also applies to: 10-10, 89-97

web/src/components/Activation/ActivationTimezoneStep.vue (1)

1-99: LGTM with optional UX improvement suggestion.

The component logic is correct, and the browser timezone detection is well-implemented with fallback matching. The non-blocking error handling (lines 83-84) is intentional per the design decision to avoid blocking Unraid usage.

Based on learnings

api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)

62-71: LGTM with a note on naming clarity.

The new GraphQL types are correctly implemented with proper decorators and field definitions. The ActivationOnboardingStepId enum provides type safety.

Also applies to: 154-204

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

3-9: LGTM!

Clean refactoring to use OnboardingService and integrate OnboardingTrackerModule. The module structure is correct and providers are properly registered.

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

1-62: LGTM!

Well-structured test that verifies the mapping from UpgradeProgressSnapshot to ActivationOnboarding. The test properly covers:

  • Upgrade detection (version comparison)
  • Completed vs pending steps
  • All relevant fields in the response
web/src/components/Activation/completeUpgradeStep.mutation.ts (1)

1-19: LGTM!

Mutation follows the required naming convention (*.mutation.ts) and correctly requests all necessary fields from the UpgradeInfo type.

api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)

4-22: LGTM!

The mapping logic correctly:

  • Detects upgrades by comparing versions
  • Sets previousVersion only when an upgrade is detected
  • Handles undefined/falsy values properly
  • Extracts only the necessary fields from upgrade steps
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (1)

1-16: LGTM!

Clean input type definition with proper validation. The @IsEnum decorator ensures only valid ActivationOnboardingStepId values are accepted.

web/src/components/Activation/ActivationLicenseStep.vue (1)

1-46: LGTM!

Well-structured Vue component with:

  • Proper TypeScript prop definitions
  • Correct i18n usage
  • Conditional rendering for the Back button
  • Clean layout with dynamic button rendering
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2)

6-20: LGTM! Clean integration of upgrade tracking.

The dependency injection and resolver implementation follow NestJS best practices. The OnboardingTracker is properly injected, and the upgrade snapshot retrieval is cleanly implemented.


55-59: LGTM! Proper async resolver implementation.

The upgrade resolver correctly retrieves the snapshot and transforms it using the utility function. The async/await pattern is appropriate here.

web/__test__/components/Activation/WelcomeModal.test.ts (2)

47-63: LGTM! Mock component correctly implements the new welcome step behavior.

The ActivationWelcomeStep mock properly handles both navigation paths (redirect to login vs. calling onComplete callback), which aligns with the actual component implementation.


184-210: LGTM! Test correctly verifies redirect behavior.

The test properly mocks window.location and verifies that clicking the button triggers navigation to '/login'. The test structure follows Vitest best practices.

api/src/unraid-api/config/onboarding-tracker.model.ts (1)

1-25: LGTM! Well-structured type definitions.

The type definitions are clear, properly typed, and align with the onboarding tracking requirements. The use of Record<ActivationOnboardingStepId, CompletedStepState> provides type safety for the step completion tracking.

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

20-40: LGTM! Robust file search utility with proper error handling.

The findActivationCodeFile function correctly handles ENOENT errors separately from other errors, providing appropriate logging for each case. The async error handling is well-implemented.


42-89: LGTM! Clean step definition and resolution logic.

The activation step definitions are well-structured with clear required/optional flags and version tracking. The conditional ACTIVATION step logic (line 74-75) appropriately checks for activation code presence and registration state.

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

98-146: LGTM! Test setup correctly updated for OnboardingService.

The test module setup properly references the renamed OnboardingService class, and all mocking is correctly configured.


356-361: LGTM! Log message assertions updated to match implementation.

The test expectations correctly reflect the updated terminology ("activation code" instead of "JSON file"), aligning with the service's actual log messages.

web/src/components/Activation/ActivationModal.vue (1)

61-66: Past review comment may be outdated.

A previous review flagged SSR issues with window access in this area, but the current code (lines 61-66) shows computed properties with no window references. The code appears to have been refactored since that comment was made.

If debug logging with window access is needed, it should be added within a guard as originally suggested, but the current implementation doesn't require it.

web/__test__/components/Activation/ActivationModal.test.ts (2)

47-84: LGTM: Mock components properly updated.

The mock components for ActivationSteps and the new step components (ActivationPluginsStep, ActivationTimezoneStep, ActivationWelcomeStep, ActivationLicenseStep) correctly reflect the component's new step-driven architecture. The props definitions align with the actual component implementations.


113-151: LGTM: Upgrade onboarding mocks are comprehensive.

The mock step definitions and upgrade onboarding store setup provide appropriate test fixtures for the new upgrade flow. The mock includes all necessary fields (id, required, completed, introducedIn, title, description, icon) and properly simulates the upgrade state.

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

122-153: LGTM: Generated GraphQL types are well-formed.

The new types for activation onboarding (ActivationOnboarding, ActivationOnboardingStep, ActivationOnboardingStepId, CompleteUpgradeStepInput, OnboardingMutations) and system time configuration (SystemTime, UpdateSystemTimeInput) are correctly generated from the GraphQL schema. The type definitions are consistent and complete.

Also applies to: 470-474, 1550-1561, 2131-2142, 2364-2403

web/src/components/Activation/ActivationSteps.vue (3)

1-6: Past review comment about PuzzlePieceIcon appears resolved.

The previous review comment mentioned PuzzlePieceIcon imports from heroicons that needed test mocks. However, the current code doesn't import any heroicons icons—it uses string-based icon identifiers (e.g., 'i-heroicons-lock-closed') that are part of Nuxt UI's icon system. The line numbers in the past comment also don't align with the current file, suggesting the code has been refactored. If the test failures have been resolved, this comment can be closed.


77-92: LGTM: SSR-safe mobile detection.

The window access is correctly placed inside onMounted, which only runs on the client side. The isMobile ref initializes to false as a safe default for SSR, and the resize listener is properly cleaned up in onUnmounted. This is the correct pattern for Nuxt SSR compatibility.


94-110: LGTM: Robust click handler with proper validation.

The handler correctly:

  • Guards against missing onStepClick callback
  • Handles both string and number step indices with proper type conversion
  • Validates for NaN after parsing
  • Accounts for the offset when mapping UI indices to logical step indices
  • Bounds-checks before invoking the callback

Comment on lines 96 to 98
--ui-text-highlighted: white;
--ui-text-inverted: var(--ui-color-neutral-900);
--ui-text-inverted: var(--ui-color-neutral-300);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore contrast for inverted text in dark mode.

Switching --ui-text-inverted to var(--ui-color-neutral-300) makes inverted text nearly indistinguishable on its paired --ui-bg-inverted (still white), breaking readability and failing WCAG contrast. Keep a dark value (e.g., var(--ui-color-neutral-900)) for inverted text.

🤖 Prompt for AI Agents
In @tailwind-shared/css-variables.css around lines 96 to 98, the variable
--ui-text-inverted was changed to var(--ui-color-neutral-300) which reduces
contrast against the inverted background (still white); revert or set
--ui-text-inverted to a dark neutral (for example var(--ui-color-neutral-900))
so inverted text remains dark and meets WCAG contrast requirements. Ensure the
selected value provides sufficient contrast with --ui-bg-inverted (white) and
update the variable accordingly.

"@types/bun": "1.2.21",
"@types/lodash-es": "4.17.12",
"@types/node": "22.18.0",
"@types/semver": "7.7.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CI failing: lockfile/spec mismatch for semver.

The build is red (Specifiers in the lockfile don't match package.json: ... [email protected]). Please re-run the package manager (e.g., pnpm install) so the lockfile picks up this change—or restore the runtime semver entry if it’s still required.

🤖 Prompt for AI Agents
In packages/unraid-shared/package.json around line 39, the CI is failing due to
a semver specifier mismatch between package.json and the lockfile (lock
references [email protected] while package.json lists @types/semver 7.7.0); fix by
syncing the package manager state—either re-run your package manager (pnpm
install) to update the lockfile to match package.json, or if you intended to
depend on runtime semver restore/add the correct semver entry with the desired
version and then run pnpm install so the lockfile is regenerated and the
specifiers match.

Comment on lines +39 to +47
(async () => {
try {
await themeStore.setTheme();
// Ensure CSS variables are applied
themeStore.setCssVars();
} catch (error) {
console.error('Error setting theme:', error);
}
})();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move theme initialization to component lifecycle hook.

The IIFE executes at module load time, before the Vue component is mounted and potentially before the Pinia store is properly initialized. This can cause race conditions, SSR issues, and unpredictable behavior.

Move the theme initialization to onMounted:

+import { computed, ref, watch, onMounted } from 'vue';
-import { computed, ref, watch } from 'vue';
 
 const themeStore = useThemeStore();
 
-// Apply theme when modal opens
-(async () => {
-  try {
-    await themeStore.setTheme();
-    // Ensure CSS variables are applied
-    themeStore.setCssVars();
-  } catch (error) {
-    console.error('Error setting theme:', error);
-  }
-})();
+onMounted(async () => {
+  try {
+    await themeStore.setTheme();
+    // Ensure CSS variables are applied
+    themeStore.setCssVars();
+  } catch (error) {
+    console.error('Error setting theme:', error);
+  }
+});
🤖 Prompt for AI Agents
In web/src/components/Activation/ActivationModal.vue around lines 39 to 47, the
Immediately-Invoked Async Function runs at module load and can execute before
the Vue component and Pinia store are initialized; move this theme
initialization into the component's lifecycle by removing the IIFE and invoking
the same async logic inside an onMounted hook within the component's setup
(import onMounted if not present), e.g., wrap await themeStore.setTheme() and
themeStore.setCssVars() in an async onMounted callback (or call an async helper
from onMounted) and keep the try/catch that logs errors.

…m time configuration

- Introduced a new GraphQL type `SystemTime` to manage system time settings, including current time, timezone, NTP status, and NTP servers.
- Added `systemTime` query to retrieve current system time configuration.
- Implemented `updateSystemTime` mutation to modify system time settings.
- Created corresponding service and resolver for handling system time logic.
- Added input validation for updating system time, including manual date/time handling.
- Integrated new module into the main resolver module for accessibility.

This update enhances the API's capability to manage and retrieve system time configurations effectively.
…d system time mutation

- Added a new `ActivationTimezoneStep` component for selecting time zones during the activation process.
- Integrated timezone selection with the `updateSystemTime` mutation to update the system's timezone settings.
- Updated the `ActivationModal` and `ActivationSteps` components to accommodate the new timezone step.
- Improved visibility logic for the activation modal based on the presence of an activation code.
- Introduced a new GraphQL mutation for updating system time, enhancing the API's capability to manage time settings.

This update streamlines the activation process by allowing users to set their timezone, ensuring accurate timestamps across the system.
- Enhanced `ActivationModal.test.ts` to verify rendering of the new `ActivationTimezoneStep` when an activation code is present.
- Modified `ActivationSteps.test.ts` to reflect the addition of a fourth step and updated titles/descriptions accordingly.
- Adjusted visibility logic in `activationCodeModal.test.ts` to ensure the modal is visible during fresh installs when the activation code is missing, aligning with the new user flow.

These changes improve test coverage and ensure the activation process behaves as expected with the recent updates.
- Introduced a new `ActivationPluginsStep` component for selecting and installing essential plugins during the activation process.
- Updated the `ActivationModal` to include the new plugins step, allowing users to navigate to it after the timezone selection.
- Enhanced the `ActivationSteps` component to reflect the new step in the activation flow.
- Added a button in the test pages to facilitate testing of the new plugins step functionality.
- Improved visibility logic and step handling in the activation modal to accommodate the new workflow.

This update enhances the user experience by allowing for plugin installation as part of the activation process, streamlining setup and configuration.
- Updated the error logging in the `readCsrfTokenFromVarIni` function to log the error object directly, enhancing the clarity and detail of the logs when CSRF token retrieval fails.

This change improves debugging capabilities by providing more structured error information.
- Added `UpgradeInfo` type to track OS version changes, including current and previous versions.
- Enhanced `InfoVersions` and GraphQL resolvers to expose upgrade information.
- Introduced `upgradeOnboarding` store to manage visibility and steps for users upgrading their OS.
- Updated `ActivationModal` to handle both fresh installs and upgrade onboarding, displaying relevant steps based on the user's upgrade path.
- Created configuration for defining upgrade steps and conditions in `releaseConfigs.ts`.
- Added new components and logic to facilitate the upgrade onboarding experience, improving user guidance during OS upgrades.

This update streamlines the upgrade process, ensuring users receive contextual onboarding steps when upgrading their Unraid OS, enhancing overall user experience.
- Added `ActivationPluginsStep` component to the activation process, allowing users to select and install essential plugins.
- Updated `ActivationModal` and related test files to incorporate the new plugins step, ensuring proper navigation and functionality.
- Improved state management for selected plugins in `ActivationPluginsStep.vue` to optimize performance.
- Mocked new upgrade onboarding store in tests to support the latest activation flow.

This update improves the activation experience by integrating plugin selection, streamlining the setup process for users.
- Added `OnApplicationShutdown` lifecycle hook to `ApiConfigPersistence` for managing OS version tracking during application shutdown.
- Removed the previous method for tracking OS version upgrades and streamlined the logic to set the last seen OS version directly during shutdown.
- Introduced a private variable to store the current OS version for improved state management.

This update improves the reliability of OS version tracking by ensuring it is properly handled during application shutdown, enhancing overall configuration persistence.
…cker module

- Introduced `OsVersionTracker` to manage and persist the current OS version during application bootstrap.
- Updated `ApiConfigPersistence` to remove shutdown handling for OS version tracking, streamlining the logic.
- Enhanced tests for `ApiConfigPersistence` and added tests for `OsVersionTracker` to ensure proper functionality.
- Registered `OsVersionTrackerModule` in `ApiConfigModule` to facilitate OS version management.

This update improves the reliability of OS version tracking and simplifies the configuration persistence logic.
- Introduced `OnboardingMutations` to manage the completion of upgrade onboarding steps.
- Added `completeUpgradeStep` mutation to mark specific onboarding steps as completed, returning relevant upgrade information.
- Enhanced `UpgradeInfo` type to include completed steps for better tracking of user progress during OS upgrades.
- Updated `OsVersionTracker` to persist completed onboarding steps alongside OS version tracking.
- Integrated new GraphQL queries and mutations in the frontend to support the onboarding process.

This update improves the user experience by providing a structured way to track and manage onboarding steps during OS upgrades, ensuring users receive appropriate guidance throughout the process.
- Moved RClone related types and inputs back into the generated schema for better structure and clarity.
- Removed legacy references to RClone tracking in the onboarding tracker module to streamline the codebase.
- Updated API configuration to ensure proper formatting and consistency.

This update enhances the organization of the API schema and improves maintainability by removing outdated code references.
…ion path

- Refactored `OnboardingTracker` to utilize a configurable OS version file path based on the data directory.
- Enhanced tests to verify behavior when the data directory is unavailable, ensuring fallback to the default version path.
- Updated related logic to improve version tracking and persistence during application bootstrap.

This update improves flexibility in OS version management and enhances the reliability of onboarding processes.
@elibosley elibosley force-pushed the feat/timezone-control branch from c414f39 to 4e0b831 Compare October 13, 2025 21:00
@coderabbitai coderabbitai bot requested a review from zackspear October 13, 2025 21:02
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: 7

Caution

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

⚠️ Outside diff range comments (4)
web/src/components/Wrapper/mount-engine.ts (1)

7-13: Add .js extensions to TypeScript import specifiers.

Per the coding guidelines, TypeScript import specifiers should include .js extensions for ESM compatibility. The following imports are missing extensions:

  • Line 7: @/components/Wrapper/component-registry
  • Line 8: ~/helpers/create-apollo-client
  • Line 9: ~/helpers/i18n-loader
  • Line 12: ~/store/globalPinia
  • Line 13: ~/store/theme

Apply this diff to add .js extensions:

-import { componentMappings } from '@/components/Wrapper/component-registry';
-import { client } from '~/helpers/create-apollo-client';
-import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
+import { componentMappings } from '@/components/Wrapper/component-registry.js';
+import { client } from '~/helpers/create-apollo-client.js';
+import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js';

 // Import Pinia for use in Vue apps
-import { globalPinia } from '~/store/globalPinia';
-import { useThemeStore } from '~/store/theme';
+import { globalPinia } from '~/store/globalPinia.js';
+import { useThemeStore } from '~/store/theme.js';

As per coding guidelines.

api/src/core/utils/clients/emcmd.ts (1)

116-123: Avoid any in catch; narrow unknown and log safely

Replace any with unknown, narrow for code === 'ENOENT', and use structured logging.

As per coding guidelines

-    } catch (error: any) {
-        if (error.code === 'ENOENT') {
-            appLogger.error('emhttpd socket unavailable.', error);
-            throw new Error('emhttpd socket unavailable.');
-        }
-        appLogger.error(`emcmd execution failed: ${error.message}`, error);
-        throw error;
-    }
+    } catch (error: unknown) {
+        const code =
+            typeof error === 'object' && error !== null && 'code' in error
+                ? (error as { code?: unknown }).code
+                : undefined;
+        const logError = error instanceof Error ? error : { message: String(error) };
+
+        if (code === 'ENOENT') {
+            appLogger.error({ error: logError }, 'emhttpd socket unavailable.');
+            throw new AppError('emhttpd socket unavailable.');
+        }
+
+        const msg = error instanceof Error ? error.message : String(error);
+        appLogger.error({ error: logError }, `emcmd execution failed: ${msg}`);
+        throw error;
+    }

If you prefer a helper, add this near the top of the file:

function getErrCode(e: unknown): string | undefined {
  return typeof e === 'object' && e !== null && 'code' in e
    ? (e as { code?: unknown }).code as string | undefined
    : undefined;
}

Then use const code = getErrCode(error);.

web/src/components/Activation/WelcomeModal.standalone.vue (1)

57-65: Bug: assigning to ref variable in template; use v-model or .value

The handler sets showModal (a Ref) instead of showModal.value. Use v-model (preferred) to wire modelValue/update correctly.

-    <Dialog
+    <Dialog
       :to="modalContainer"
-      :model-value="showModal"
+      v-model="showModal"
       :show-footer="false"
       :show-close-button="isLoginPage"
       size="full"
       class="bg-background"
-      @update:model-value="(value) => (showModal = value)"
     >
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (1)

339-347: Fix banner flag mismatch between write and read paths

applyDisplaySettings writes banner='image', but getTheme reads banner === 'yes'. This will misreport showBannerImage.

Apply this diff to align behavior:

-        const banner = getters.dynamix()!.display!.banner;
+        const banner = getters.dynamix()!.display!.banner;
@@
-            showBannerImage: banner === 'yes',
+            // 'image' indicates a custom image; many configs also treat any non-'no' as enabled
+            showBannerImage: banner === 'image' || banner === 'yes',

Also applies to: 508-519

♻️ Duplicate comments (9)
api/src/core/utils/clients/emcmd.ts (1)

62-65: Fix: log unknown safely in retry catch (TS2769) and keep context structured

The logger still receives a raw unknown as a positional arg, causing overload mismatch in CI. Wrap/narrow before logging.

Apply:

-    ).catch((error) => {
-        appLogger.error('Failed to load CSRF token after multiple retries', error);
-        throw new AppError('Failed to load CSRF token after multiple retries');
-    });
+    ).catch((error) => {
+        const logError = error instanceof Error ? error : { message: String(error) };
+        appLogger.error(
+            { error: logError },
+            'Failed to load CSRF token after multiple retries'
+        );
+        throw new AppError('Failed to load CSRF token after multiple retries');
+    });
packages/unraid-shared/package.json (1)

39-39: CI still failing: lockfile out of sync.

The pipeline continues to fail with "Lockfile out of date with package.json when using --frozen-lockfile." Please run pnpm install to regenerate the lockfile and commit the updated lock file.

web/src/components/Activation/ActivationPluginsStep.vue (1)

53-61: Set reactivity fix LGTM

Cloning and reassigning Set ensures updates propagate. Thanks for addressing the prior feedback.

web/public/test-pages/all-components.html (1)

487-501: Undefined function call: updateDarkModeButton()

This will throw when clicking “Toggle Theme”. Use the defined updateThemeButton().

         window.themeStore.theme.name = newTheme;
         window.themeStore.setCssVars();
-        updateDarkModeButton();
+        updateThemeButton();
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (1)

86-93: loadStateFiles errors are swallowed; unwrap and return optimistic result on failure

Redux Toolkit dispatch won’t throw without .unwrap(); current try/catch won’t run on thunk rejection, and returning getSystemTime() can expose stale state. Use unwrap() and fall back to optimistic values.

-        try {
-            await store.dispatch(loadStateFiles());
-        } catch (error) {
-            this.logger.warn('Failed to reload emhttp state after updating system time', error as Error);
-        }
-
-        return this.getSystemTime();
+        try {
+            await store.dispatch(loadStateFiles()).unwrap();
+        } catch (error) {
+            this.logger.warn(
+                'Failed to reload emhttp state after updating system time',
+                error instanceof Error ? error : undefined
+            );
+            // Avoid returning a stale snapshot; return optimistic result.
+            return {
+                currentTime: new Date().toISOString(),
+                timeZone: desiredTimeZone,
+                useNtp: desiredUseNtp,
+                ntpServers: desiredServers,
+            };
+        }
+        return this.getSystemTime();
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (1)

5-6: Remove unused import (CodeQL finding)

ActivationOnboardingStepId is unused here; safe to remove.

-import { ActivationOnboardingStepId } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js';

Based on static analysis hints

api/src/unraid-api/config/onboarding-tracker.module.ts (1)

200-201: Swap Logger arguments: message first, stack as trace

Current calls pass error first. Use message-first with error.stack for trace. This improves observability.

-            this.logger.error(error, 'Failed to evaluate activation onboarding steps');
+            this.logger.error(
+                'Failed to evaluate activation onboarding steps',
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error(error, `Failed to read current OS version from ${this.versionFilePath}`);
+            this.logger.error(
+                `Failed to read current OS version from ${this.versionFilePath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
+            this.logger.debug(
+                `Unable to read onboarding tracker state at ${this.trackerPath}: ${
+                    error instanceof Error ? error.message : String(error)
+                }`
+            );
@@
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );

Also applies to: 260-261, 270-271, 280-281

web/src/components/Activation/ActivationModal.vue (1)

38-47: Move theme initialization to onMounted (avoids SSR/load-order issues)

Running the IIFE at module eval can predate store init and break SSR. Use onMounted.

As per coding guidelines

-import { computed, ref, watch } from 'vue';
+import { computed, ref, watch, onMounted } from 'vue';
@@
-// Apply theme when modal opens
-(async () => {
-  try {
-    await themeStore.setTheme();
-    // Ensure CSS variables are applied
-    themeStore.setCssVars();
-  } catch (error) {
-    console.error('Error setting theme:', error);
-  }
-})();
+// Apply theme when modal opens
+onMounted(async () => {
+  try {
+    await themeStore.setTheme();
+    themeStore.setCssVars();
+  } catch (error) {
+    console.error('Error setting theme:', error);
+  }
+});
api/generated-schema.graphql (1)

1173-1178: Add auth directives to state-changing mutations

completeUpgradeStep and updateSystemTime mutate system state but lack @usePermissions. Guard them consistently with your permissions model.

#!/bin/bash
# Show existing auth-decorated fields to mirror conventions
rg -n -C2 '@usePermissions' api/generated-schema.graphql

# Show the two mutations in context
rg -n -C3 'completeUpgradeStep|updateSystemTime' api/generated-schema.graphql

Also applies to: 2558-2560

🧹 Nitpick comments (24)
web/src/components/Activation/steps/timezone.ts (1)

1-1: Use .js extension in TS import and verify id casing

  • Change the type import to .js for ESM compatibility. As per coding guidelines.
  • Confirm the lowercase 'timezone' id matches StepComponentRegistry keys and other step-id conventions (else align casing).

Apply:

-import type { StepComponentRegistry } from '~/components/Activation/steps/types';
+import type { StepComponentRegistry } from '~/components/Activation/steps/types.js';

Also applies to: 5-8

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

20-61: Good coverage; add a couple more cases

  • Add tests for: all steps completed (hasPendingSteps: false) and no upgrade (previous == current or snapshot indicates no change).
web/src/components/Activation/WelcomeModal.standalone.vue (1)

35-37: Optional: react to late store initialization

If isInitialSetup resolves after mount, the current initialization won’t auto-open. Consider deriving or watching.

Example:

// Option A: computed source of truth
const showModal = ref(false);
watchEffect(() => {
  if (isLoginPage.value || isInitialSetup.value) showModal.value = true;
});
web/src/components/Activation/stepRegistry.ts (2)

1-1: Use .js extension in TS import

For ESM compatibility in TS files, use .js. As per coding guidelines.

-import type { ActivationOnboardingStepId } from '~/composables/gql/graphql';
+import type { ActivationOnboardingStepId } from '~/composables/gql/graphql.js';

20-40: Localize UI strings

Hard-coded titles/descriptions bypass i18n. Replace with translation keys or runtime t() lookup.

web/src/components/Activation/ActivationWelcomeStep.vue (1)

26-57: Prefer translation keys over inline English strings

Use named i18n keys with params instead of inline English sentences to enable localization.

Also applies to: 71-83

web/src/components/Activation/ActivationPluginsStep.vue (1)

75-81: Await plugin installs (if async) to avoid racing the completion

If install(...) returns a Promise, await it to ensure sequential completion before continuing.

-    for (const plugin of pluginsToInstall) {
-      install({
+    for (const plugin of pluginsToInstall) {
+      await install({
         pluginUrl: plugin.url,
         modalTitle: `Installing ${plugin.name}`,
       });
-      await new Promise((resolve) => setTimeout(resolve, 1000));
+      // optional spacing between installs
+      await new Promise((resolve) => setTimeout(resolve, 1000));
     }

If install is intentionally fire‑and‑forget, ignore and keep current flow. Please confirm the composable’s contract.

Also applies to: 83-90

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

49-51: Use Nest Logger instead of console.error for server logs

Replace console.error with Logger to keep consistent, structured logging.

-        } catch (error) {
-            console.error('Failed to get package versions:', error);
+        } catch (error) {
+            // optionally inject a logger: private readonly logger = new Logger(VersionsResolver.name);
+            // this.logger.warn('Failed to get package versions', error as Error);
+            console.error('Failed to get package versions:', error); // interim until Logger is wired
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (3)

54-66: Validate manualDateTime format when NTP is disabled

Add a simple format check (e.g., YYYY-MM-DD HH:mm:ss) to catch typos early before sending emcmd.

// Example helper
private isValidManualDateTime(s: string): boolean {
  return /^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(s);
}

// Before setting commands.newDateTime:
if (manualDateTime && !this.isValidManualDateTime(manualDateTime)) {
  throw new BadRequestException('manualDateTime must be in YYYY-MM-DD HH:mm:ss format.');
}

74-80: Improve error logging detail

Logger.error’s second argument is typically stack/trace. Prefer passing error.stack or using this.logger.error(message, (error as Error)?.stack).

-        } catch (error) {
-            this.logger.error('Failed to update system time via emcmd', error as Error);
+        } catch (error) {
+            const err = error as Error;
+            this.logger.error(`Failed to update system time via emcmd: ${err.message}`, err.stack);
             throw error;
         }

95-98: Avoid broad type assertions

Casting to Partial hides shape drift. Prefer tightening store getter types so getters.emhttp().var is typed, or guard fields individually.

web/src/components/Activation/ActivationSteps.vue (2)

49-69: includeInitialStep is always true with current logic

dynamicSteps never returns an empty array (falls back to defaults), so includeInitialStep will always be true. You can inline the offset as 1 or simplify the computed.


94-110: Minor: simplify click index normalization

Use Number() and Number.isNaN for brevity; keeps behavior identical.

-  const stepIndex =
-    typeof clickedStepIndex === 'string' ? parseInt(clickedStepIndex, 10) : clickedStepIndex;
-  if (isNaN(stepIndex)) return;
+  const stepIndex = Number(clickedStepIndex);
+  if (Number.isNaN(stepIndex)) return;
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)

449-493: Guard against undefined filePath in updateCfgFile

If this.configFile is undefined, fs.readFile/write will throw. Short-circuit safely.

Apply this diff:

     private async updateCfgFile(
         filePath: string,
         section: string | null,
         updates: Record<string, string>
     ) {
+        if (!filePath) {
+            this.logger.warn('updateCfgFile called without a valid filePath; skipping update.');
+            return;
+        }

292-295: Use proper Logger patterns (no %o; stringify objects; include error.stack)

Nest Logger doesn’t format %o and treats the 2nd param as context. Prefer JSON.stringify for objects and pass error.stack for traces.

Apply this diff:

-        this.logger.debug('Current display settings from store:', currentDisplaySettings);
+        this.logger.debug(`Current display settings from store: ${JSON.stringify(currentDisplaySettings)}`);
@@
-        this.logger.log('Updating display settings:', settingsToUpdate);
+        this.logger.log(`Updating display settings: ${JSON.stringify(settingsToUpdate)}`);
@@
-            this.logger.log(`Calling emcmd with params: %o`, updateParams);
+            this.logger.log(`Calling emcmd with params: ${JSON.stringify(updateParams)}`);
             await emcmd(updateParams, { waitForToken: true });
@@
-        } catch (error: unknown) {
-            this.logger.error('Error applying server identity: %o', error);
+        } catch (error: unknown) {
+            this.logger.error(
+                'Error applying server identity',
+                error instanceof Error ? error.stack : String(error)
+            );

Also applies to: 360-361, 439-445


31-37: Prefer message-first with error.stack for Logger.error/debug

Ensure Logger.error/debug receive message first and a string trace (error.stack). Avoid passing raw objects.

Example fixes:

-            logger?.error?.('Error accessing activation directory or reading its content.', error);
+            logger?.error?.(
+                'Error accessing activation directory or reading its content.',
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error(`Error processing activation file ${activationJsonPath}:`, error);
+            this.logger.error(
+                `Error processing activation file ${activationJsonPath}:`,
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error('Error setting up partner banner:', error);
+            this.logger.error(
+                'Error setting up partner banner:',
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error('Error applying display settings:', error);
+            this.logger.error(
+                'Error applying display settings:',
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error('Error applying case model:', error);
+            this.logger.error(
+                'Error applying case model:',
+                error instanceof Error ? error.stack : String(error)
+            );

Also applies to: 218-221, 281-283, 365-367, 391-392


211-213: Use plainToInstance instead of plainToClass

plainToClass is deprecated in class-transformer. Switch to plainToInstance for forward-compatibility.

Apply this diff:

-import { plainToClass } from 'class-transformer';
+import { plainToInstance } from 'class-transformer';
@@
-            const activationDataDto = plainToClass(ActivationCode, activationDataRaw);
+            const activationDataDto = plainToInstance(ActivationCode, activationDataRaw);
web/__test__/components/Activation/WelcomeModal.test.ts (1)

208-218: Rename test to reflect “Get Started” button

The test name still says “Create a password” but the UI uses “Get Started”.

Apply this diff:

-  it('disables the Create a password button when loading', async () => {
+  it('does not disable the Get Started button by default', async () => {
web/src/components/Activation/ActivationModal.vue (1)

9-9: Add .js extensions to local imports for ESM compatibility

Per repo guidelines, TS imports should use .js extensions.

As per coding guidelines

-import { DOCS_URL_ACCOUNT, DOCS_URL_LICENSING_FAQ } from '~/consts';
+import { DOCS_URL_ACCOUNT, DOCS_URL_LICENSING_FAQ } from '~/consts.js';
@@
-import type { ActivationOnboardingQuery } from '~/composables/gql/graphql';
+import type { ActivationOnboardingQuery } from '~/composables/gql/graphql.js';
@@
-import { COMPLETE_UPGRADE_STEP_MUTATION } from '~/components/Activation/completeUpgradeStep.mutation';
+import { COMPLETE_UPGRADE_STEP_MUTATION } from '~/components/Activation/completeUpgradeStep.mutation.js';
-import { stepComponents } from '~/components/Activation/stepRegistry';
+import { stepComponents } from '~/components/Activation/stepRegistry.js';
@@
-import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
-import { useActivationCodeModalStore } from '~/components/Activation/store/activationCodeModal';
-import { useUpgradeOnboardingStore } from '~/components/Activation/store/upgradeOnboarding';
+import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData.js';
+import { useActivationCodeModalStore } from '~/components/Activation/store/activationCodeModal.js';
+import { useUpgradeOnboardingStore } from '~/components/Activation/store/upgradeOnboarding.js';
@@
-import { usePurchaseStore } from '~/store/purchase';
-import { useThemeStore } from '~/store/theme';
+import { usePurchaseStore } from '~/store/purchase.js';
+import { useThemeStore } from '~/store/theme.js';

Also applies to: 12-14, 17-23

web/__test__/components/Activation/ActivationModal.test.ts (2)

204-211: Restore window.addEventListener after tests

You replace window.addEventListener globally and never restore it; this can leak across tests. Spy and restore in hooks.

-const originalAddEventListener = window.addEventListener;
-window.addEventListener = vi.fn((event: string, handler: EventListenerOrEventListenerObject) => {
-  if (event === 'keydown') {
-    handleKeydown = handler as unknown as (e: KeyboardEvent) => void;
-  }
-  return originalAddEventListener(event, handler);
-});
+let originalAddEventListener: typeof window.addEventListener;
+beforeEach(() => {
+  originalAddEventListener = window.addEventListener;
+  window.addEventListener = vi.fn((event: string, handler: EventListenerOrEventListenerObject) => {
+    if (event === 'keydown') handleKeydown = handler as (e: KeyboardEvent) => void;
+    return originalAddEventListener(event, handler);
+  }) as unknown as typeof window.addEventListener;
+});
+afterEach(() => {
+  window.addEventListener = originalAddEventListener;
+});

As per coding guidelines


46-48: Align ActivationSteps stub with new activeStepIndex usage

Stub sets a custom attr active-step while comment states “uses activeStepIndex prop”. Prefer a data attribute for test assertions.

- template: '<div data-testid="activation-steps" :active-step="activeStepIndex"></div>',
+ template: '<div data-testid="activation-steps" :data-active-step-index="activeStepIndex"></div>',

Optionally assert it where you check the active step:

- expect(activationSteps.exists()).toBe(true);
+ expect(activationSteps.attributes('data-active-step-index')).toBeDefined();

Also applies to: 343-346

api/generated-schema.graphql (2)

2057-2069: Use DateTime for currentTime

SystemTime.currentTime is a String; prefer the DateTime scalar for correctness and client typing.

 type SystemTime {
-  """Current server time in ISO-8601 format (UTC)"""
-  currentTime: String!
+  """Current server time in ISO-8601 format (UTC)"""
+  currentTime: DateTime!
   ...
 }

2602-2619: Tighten manualDateTime typing and clarify timezone semantics

manualDateTime is a free-form String with a format hint. Consider using DateTime for validation and clarify if the value is assumed in the provided timeZone, local server time, or UTC.

 input UpdateSystemTimeInput {
-  """Manual date/time to apply when disabling NTP, expected format YYYY-MM-DD HH:mm:ss"""
-  manualDateTime: String
+  """Manual date/time to apply when disabling NTP. If not UTC, specify relation to timeZone."""
+  manualDateTime: DateTime
 }
web/src/composables/gql/gql.ts (1)

1-8: Optional: emit .js extension for ESM local imports

If you run native ESM without bundler path resolution, configure codegen to import './graphql.js' to avoid resolution issues.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c414f39 and 4e0b831.

⛔ Files ignored due to path filters (2)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (59)
  • @tailwind-shared/css-variables.css (1 hunks)
  • api/generated-schema.graphql (9 hunks)
  • api/src/core/utils/clients/emcmd.ts (3 hunks)
  • api/src/unraid-api/config/api-config.module.ts (2 hunks)
  • api/src/unraid-api/config/api-config.test.ts (2 hunks)
  • api/src/unraid-api/config/onboarding-tracker.model.ts (1 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts (8 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (1 hunks)
  • api/src/unraid-api/main.ts (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (6 hunks)
  • web/__test__/components/Activation/ActivationSteps.test.ts (0 hunks)
  • web/__test__/components/Activation/WelcomeModal.test.ts (4 hunks)
  • web/__test__/store/activationCodeModal.test.ts (1 hunks)
  • web/components.d.ts (2 hunks)
  • web/package.json (1 hunks)
  • web/public/test-pages/all-components.html (11 hunks)
  • web/public/test-pages/shared-header.js (2 hunks)
  • web/src/assets/main.css (1 hunks)
  • web/src/components/Activation/ActivationLicenseStep.vue (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (2 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/components/Activation/ActivationSteps.vue (1 hunks)
  • web/src/components/Activation/ActivationTimezoneStep.vue (1 hunks)
  • web/src/components/Activation/ActivationWelcomeStep.vue (1 hunks)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md (1 hunks)
  • web/src/components/Activation/WelcomeModal.standalone.vue (2 hunks)
  • web/src/components/Activation/activationOnboarding.query.ts (1 hunks)
  • web/src/components/Activation/completeUpgradeStep.mutation.ts (1 hunks)
  • web/src/components/Activation/stepRegistry.ts (1 hunks)
  • web/src/components/Activation/steps/timezone.ts (1 hunks)
  • web/src/components/Activation/store/activationCodeModal.ts (2 hunks)
  • web/src/components/Activation/store/upgradeOnboarding.ts (1 hunks)
  • web/src/components/Activation/updateSystemTime.mutation.ts (1 hunks)
  • web/src/components/ColorSwitcher.standalone.vue (1 hunks)
  • web/src/components/DevModalTest.standalone.vue (2 hunks)
  • web/src/components/TestThemeSwitcher.standalone.vue (1 hunks)
  • web/src/components/Wrapper/mount-engine.ts (2 hunks)
  • web/src/composables/gql/gql.ts (4 hunks)
  • web/src/composables/gql/graphql.ts (14 hunks)
💤 Files with no reviewable changes (1)
  • web/test/components/Activation/ActivationSteps.test.ts
🚧 Files skipped from review as they are similar to previous changes (23)
  • api/src/unraid-api/config/api-config.module.ts
  • @tailwind-shared/css-variables.css
  • web/src/components/Activation/updateSystemTime.mutation.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • web/src/components/ColorSwitcher.standalone.vue
  • web/package.json
  • web/src/components/Activation/UPGRADE_ONBOARDING.md
  • web/src/components/Activation/store/upgradeOnboarding.ts
  • web/test/store/activationCodeModal.test.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • web/src/components/Activation/completeUpgradeStep.mutation.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • web/src/assets/main.css
  • web/public/test-pages/shared-header.js
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • web/src/components/TestThemeSwitcher.standalone.vue
  • web/src/components/Activation/ActivationTimezoneStep.vue
  • web/src/components/Activation/activationOnboarding.query.ts
🧰 Additional context used
📓 Path-based instructions (12)
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/config/api-config.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • web/src/components/Activation/stepRegistry.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • web/src/components/Activation/steps/timezone.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/core/utils/clients/emcmd.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • web/src/composables/gql/graphql.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/config/api-config.test.ts
  • web/src/composables/gql/gql.ts
  • web/src/components/Wrapper/mount-engine.ts
  • web/components.d.ts
  • web/src/components/Activation/store/activationCodeModal.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/core/utils/clients/emcmd.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/config/api-config.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/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationWelcomeStep.vue
  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/Activation/ActivationLicenseStep.vue
  • web/src/components/DevModalTest.standalone.vue
  • web/src/components/Activation/WelcomeModal.standalone.vue
  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/Activation/ActivationPluginsStep.vue
{**/*.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:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • api/src/unraid-api/config/api-config.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
**/{store,stores}/**/*.ts

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

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • web/src/components/Activation/store/activationCodeModal.ts
🧠 Learnings (13)
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 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 : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

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

Applied to files:

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

Applied to files:

  • web/components.d.ts
🧬 Code graph analysis (16)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (5)
  • ObjectType (7-17)
  • ObjectType (19-44)
  • ObjectType (46-74)
  • ObjectType (76-98)
  • ObjectType (100-110)
web/src/composables/gql/graphql.ts (2)
  • OnboardingMutations (1551-1555)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • OnboardingMutations (1550-1554)
  • UpgradeInfo (2374-2386)
api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts (1)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (3)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • Module (285-289)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-38)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (5)
  • ObjectType (7-17)
  • ObjectType (19-44)
  • ObjectType (46-74)
  • ObjectType (76-98)
  • ObjectType (100-110)
web/src/composables/gql/graphql.ts (2)
  • ActivationOnboardingStep (136-146)
  • ActivationOnboarding (122-134)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • ActivationOnboardingStep (136-146)
  • ActivationOnboarding (122-134)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (1)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • Injectable (37-283)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (5)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (71-124)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadStateFiles (145-164)
api/src/core/types/states/var.ts (1)
  • Var (12-242)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (2)
web/src/composables/gql/graphql.ts (1)
  • CompleteUpgradeStepInput (471-474)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • CompleteUpgradeStepInput (470-473)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (3)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (5)
  • ObjectType (33-60)
  • ObjectType (73-143)
  • ObjectType (145-152)
  • ObjectType (154-174)
  • ObjectType (176-204)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (8)
  • ObjectType (14-15)
  • ObjectType (17-18)
  • ObjectType (20-21)
  • ObjectType (23-26)
  • ObjectType (28-31)
  • ObjectType (33-42)
  • ObjectType (44-52)
  • ObjectType (54-76)
web/src/composables/gql/graphql.ts (2)
  • UpgradeStep (2389-2403)
  • UpgradeInfo (2375-2387)
api/src/core/utils/clients/emcmd.ts (3)
api/src/core/log.ts (1)
  • appLogger (83-83)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadSingleStateFile (107-141)
api/src/unraid-api/config/onboarding-tracker.module.ts (5)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)
  • resolveActivationStepDefinitions (79-89)
  • ActivationStepDefinition (47-52)
  • ActivationStepContext (42-45)
  • findActivationCodeFile (20-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.model.ts (4)
  • TrackerState (8-12)
  • UpgradeProgressSnapshot (20-25)
  • CompletedStepState (3-6)
  • UpgradeStepState (14-18)
api/src/store/index.ts (1)
  • getters (18-23)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (5)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • Resolver (19-99)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1)
  • Resolver (9-20)
web/src/composables/gql/graphql.ts (2)
  • InfoVersions (1121-1130)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • InfoVersions (1120-1129)
  • UpgradeInfo (2374-2386)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)
  • buildUpgradeInfoFromSnapshot (4-22)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (13)
  • ActivationOnboarding (122-134)
  • Maybe (3-3)
  • Scalars (11-75)
  • ActivationOnboardingStep (136-146)
  • CompleteUpgradeStepInput (470-473)
  • UpgradeInfo (2374-2386)
  • OnboardingMutations (1550-1554)
  • SystemTime (2131-2141)
  • UpdateSystemTimeInput (2363-2372)
  • OnboardingMutationsCompleteUpgradeStepArgs (1558-1560)
  • InputMaybe (4-4)
  • UpgradeStep (2388-2402)
  • Exact (5-5)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (3)
api/src/store/index.ts (2)
  • getters (18-23)
  • store (6-12)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (71-124)
api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts (1)
  • MANUAL_TIME_REGEX (58-58)
api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts (1)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/store/theme.ts (1)
  • useThemeStore (51-242)
🪛 GitHub Actions: CI - Main (API)
packages/unraid-shared/package.json

[error] 1-1: Lockfile out of date with package.json when using --frozen-lockfile. Please update dependencies or run 'pnpm install' without --frozen-lockfile.

🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts

[notice] 5-5: Unused variable, import, function or class
Unused import ActivationOnboardingStepId.

Comment on lines 46 to 98
@ObjectType()
export class UpgradeStep {
@Field(() => String, { description: 'Identifier of the onboarding step' })
id!: string;

@Field(() => Boolean, {
description: 'Whether the step is required to continue',
defaultValue: false,
})
required!: boolean;

@Field(() => String, {
nullable: true,
description: 'Version of Unraid when this step was introduced',
})
introducedIn?: string;

@Field(() => String, { description: 'Display title for the onboarding step' })
title!: string;

@Field(() => String, { description: 'Display description for the onboarding step' })
description!: string;

@Field(() => String, {
nullable: true,
description: 'Icon identifier for the onboarding step',
})
icon?: string;
}

@ObjectType()
export class UpgradeInfo {
@Field(() => Boolean, { description: 'Whether the OS version has changed since last boot' })
isUpgrade!: boolean;

@Field(() => String, { nullable: true, description: 'Previous OS version before upgrade' })
previousVersion?: string;

@Field(() => String, { nullable: true, description: 'Current OS version' })
currentVersion?: string;

@Field(() => [String], {
description: 'Onboarding step identifiers completed for the current OS version',
defaultValue: [],
})
completedSteps!: string[];

@Field(() => [UpgradeStep], {
description: 'Onboarding step definitions applicable to the current upgrade path',
defaultValue: [],
})
steps!: UpgradeStep[];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Schema requires non-null UpgradeStep.title/description but resolver doesn’t supply them

As implemented, buildUpgradeInfoFromSnapshot returns steps without title/description, causing GraphQL non-null violations when requested. Either:

  • Make these fields nullable, or
  • Ensure the resolver populates them (e.g., defaults or a server-side registry).

Recommended: add safe defaults in the converter to avoid a schema change. Example patch:

diff --git a/api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts b/api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
@@
 export const buildUpgradeInfoFromSnapshot = (snapshot: UpgradeProgressSnapshot): UpgradeInfo => {
   const { currentVersion, lastTrackedVersion, completedSteps, steps } = snapshot;
@@
   return {
     isUpgrade,
     previousVersion: isUpgrade ? lastTrackedVersion : undefined,
     currentVersion: currentVersion ?? undefined,
     completedSteps,
-    steps: steps.map((step) => ({
-        id: step.id,
-        required: step.required,
-        introducedIn: step.introducedIn,
-    })),
+    steps: steps.map((step) => ({
+        id: step.id,
+        required: step.required,
+        introducedIn: step.introducedIn,
+        // Provide safe defaults to satisfy non-null schema until metadata is available
+        title: step.id,
+        description: '',
+    })),
   };
 };
📝 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
@ObjectType()
export class UpgradeStep {
@Field(() => String, { description: 'Identifier of the onboarding step' })
id!: string;
@Field(() => Boolean, {
description: 'Whether the step is required to continue',
defaultValue: false,
})
required!: boolean;
@Field(() => String, {
nullable: true,
description: 'Version of Unraid when this step was introduced',
})
introducedIn?: string;
@Field(() => String, { description: 'Display title for the onboarding step' })
title!: string;
@Field(() => String, { description: 'Display description for the onboarding step' })
description!: string;
@Field(() => String, {
nullable: true,
description: 'Icon identifier for the onboarding step',
})
icon?: string;
}
@ObjectType()
export class UpgradeInfo {
@Field(() => Boolean, { description: 'Whether the OS version has changed since last boot' })
isUpgrade!: boolean;
@Field(() => String, { nullable: true, description: 'Previous OS version before upgrade' })
previousVersion?: string;
@Field(() => String, { nullable: true, description: 'Current OS version' })
currentVersion?: string;
@Field(() => [String], {
description: 'Onboarding step identifiers completed for the current OS version',
defaultValue: [],
})
completedSteps!: string[];
@Field(() => [UpgradeStep], {
description: 'Onboarding step definitions applicable to the current upgrade path',
defaultValue: [],
})
steps!: UpgradeStep[];
}
// File: api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
export const buildUpgradeInfoFromSnapshot = (snapshot: UpgradeProgressSnapshot): UpgradeInfo => {
const { currentVersion, lastTrackedVersion, completedSteps, steps } = snapshot;
const isUpgrade = /* existing logic */;
return {
isUpgrade,
previousVersion: isUpgrade ? lastTrackedVersion : undefined,
currentVersion: currentVersion ?? undefined,
completedSteps,
steps: steps.map((step) => ({
id: step.id,
required: step.required,
introducedIn: step.introducedIn,
// Provide safe defaults to satisfy non-null schema until metadata is available
title: step.id,
description: '',
})),
};
};
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts around
lines 46 to 98, the GraphQL UpgradeStep type marks title and description as
non-nullable but the resolver buildUpgradeInfoFromSnapshot is returning
UpgradeStep objects without those fields, causing GraphQL non-null violations;
fix by updating the converter so each returned step always includes non-null
title and description (populate from a server-side registry when available, or
set safe defaults like a short human-readable id-based title and an
empty/placeholder description) before returning UpgradeInfo.steps, ensuring
required fields are never undefined.

Comment on lines +55 to +59
@ResolveField(() => UpgradeInfo)
async upgrade(): Promise<UpgradeInfo> {
const snapshot = await this.onboardingTracker.getUpgradeSnapshot();
return buildUpgradeInfoFromSnapshot(snapshot);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Upgrade steps likely missing required fields (title/description) → GraphQL non-null risk

UpgradeInfo.steps from buildUpgradeInfoFromSnapshot only returns id/required/introducedIn. UpgradeStep schema requires non-null title and description; queries requesting them will error. Either:

  • Populate these fields in the converter, or
  • Make them nullable in the schema, or
  • Add field resolvers to derive them.

Recommended: extend the converter with safe defaults so schema stays stable. See proposed util change in a separate comment.

Comment on lines 279 to 285
it('renders timezone step initially when activation code is present', async () => {
const wrapper = mountComponent();
const button = wrapper.find('[data-testid="brand-button"]');

expect(button.exists()).toBe(true);

await button.trigger('click');

expect(mockPurchaseStore.activate).toHaveBeenCalledTimes(1);
// The component now renders steps dynamically based on the step registry
// Check that the activation steps component is rendered
expect(wrapper.html()).toContain('data-testid="activation-steps"');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test title vs assertion mismatch

Title says “renders timezone step initially” but you only assert ActivationSteps presence. Either assert the timezone step or rename the test.

-  it('renders timezone step initially when activation code is present', async () => {
+  it('renders activation steps when activation code is present', async () => {
     const wrapper = mountComponent();
-    // The component now renders steps dynamically based on the step registry
-    // Check that the activation steps component is rendered
     expect(wrapper.html()).toContain('data-testid="activation-steps"');
   });

Or, if the step renders here, assert the element:

-    expect(wrapper.html()).toContain('data-testid="activation-steps"');
+    expect(wrapper.html()).toContain('data-testid="timezone-step"');
📝 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
it('renders timezone step initially when activation code is present', async () => {
const wrapper = mountComponent();
const button = wrapper.find('[data-testid="brand-button"]');
expect(button.exists()).toBe(true);
await button.trigger('click');
expect(mockPurchaseStore.activate).toHaveBeenCalledTimes(1);
// The component now renders steps dynamically based on the step registry
// Check that the activation steps component is rendered
expect(wrapper.html()).toContain('data-testid="activation-steps"');
});
it('renders activation steps when activation code is present', async () => {
const wrapper = mountComponent();
expect(wrapper.html()).toContain('data-testid="activation-steps"');
});
Suggested change
it('renders timezone step initially when activation code is present', async () => {
const wrapper = mountComponent();
const button = wrapper.find('[data-testid="brand-button"]');
expect(button.exists()).toBe(true);
await button.trigger('click');
expect(mockPurchaseStore.activate).toHaveBeenCalledTimes(1);
// The component now renders steps dynamically based on the step registry
// Check that the activation steps component is rendered
expect(wrapper.html()).toContain('data-testid="activation-steps"');
});
it('renders timezone step initially when activation code is present', async () => {
const wrapper = mountComponent();
expect(wrapper.html()).toContain('data-testid="timezone-step"');
});
🤖 Prompt for AI Agents
In web/__test__/components/Activation/ActivationModal.test.ts around lines
279–285, the test title says “renders timezone step initially” but the assertion
only checks for the ActivationSteps component; either change the assertion to
specifically verify the timezone step is rendered (check for the timezone step
DOM element / data-testid used by that step) or rename the test title to reflect
that it asserts ActivationSteps presence (e.g., “renders activation steps
initially when activation code is present”); update the expectation accordingly
so title and assertion match.

Comment on lines 78 to 83
<BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />

<BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />

<BrandButton :text="buttonText" @click="handleComplete" />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Guard optional handlers; align with Plugins step

Avoid rendering Back/Skip when handlers are absent and prevent calling undefined. Match the pattern used in ActivationPluginsStep.

-      <BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />
+      <BrandButton v-if="onBack && showBack" :text="t('Back')" variant="outline" @click="onBack?.()" />
@@
-      <BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />
+      <BrandButton v-if="onSkip && showSkip" :text="t('Skip')" variant="outline" @click="onSkip?.()" />
📝 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
<BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />
<BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />
<BrandButton :text="buttonText" @click="handleComplete" />
</div>
<BrandButton v-if="onBack && showBack" :text="t('Back')" variant="outline" @click="onBack?.()" />
<BrandButton v-if="onSkip && showSkip" :text="t('Skip')" variant="outline" @click="onSkip?.()" />
<BrandButton :text="buttonText" @click="handleComplete" />
</div>
🤖 Prompt for AI Agents
In web/src/components/Activation/ActivationWelcomeStep.vue around lines 78 to
83, the Back and Skip buttons are rendered and invoked even when their handler
props may be undefined; update the template to only render the Back and Skip
BrandButton components when the corresponding handler props exist (e.g.,
showBack && typeof onBack === 'function', showSkip && typeof onSkip ===
'function') and ensure the click handlers call the props defensively (e.g.,
check existence before invoking) to match the pattern used in
ActivationPluginsStep so undefined handlers are neither rendered nor called.

Comment on lines +22 to +23
themeStore: ReturnType<typeof useThemeStore>;
LOCALE_DATA?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify LOCALE_DATA usage and LOCALE removal.

The addition of LOCALE_DATA and the removal of LOCALE (per the AI summary) suggest a refactoring of how locale information is accessed globally. Ensure that:

  1. LOCALE_DATA is actually populated and used in the codebase
  2. No remaining references to the removed LOCALE property exist that might cause runtime errors

Run the following script to verify the changes:


🏁 Script executed:

#!/bin/bash
# Verify LOCALE_DATA usage and check for stale LOCALE references

echo "=== Searching for LOCALE_DATA usage ==="
rg -nP '\bLOCALE_DATA\b' --type=ts --type=tsx

echo -e "\n=== Searching for potentially stale window.LOCALE references ==="
rg -nP '\bwindow\.LOCALE\b(?!_DATA)' --type=ts --type=tsx

echo -e "\n=== Searching for Window.LOCALE interface references ==="
rg -nP 'interface\s+Window.*LOCALE[^_]' --type=ts --type=tsx

Length of output: 247


🏁 Script executed:

#!/bin/bash
echo "=== Searching for LOCALE_DATA usage ==="
rg -n '\bLOCALE_DATA\b' -g '*.ts' -g '*.tsx'

echo -e "\n=== Searching for window.LOCALE references ==="
rg -n 'window\.LOCALE\b' -g '*.ts' -g '*.tsx'

echo -e "\n=== Searching for Window interface LOCALE references ==="
rg -nP '(?s)interface\s+Window.*?\{.*?\bLOCALE\b' -g '*.ts' -g '*.tsx'

Length of output: 276


Remove stale window.LOCALE usage and integrate LOCALE_DATA

  • web/src/helpers/i18n-loader.ts:121 still returns window.LOCALE; update this to consume the newly declared LOCALE_DATA source.
  • LOCALE_DATA (mount-engine.ts:23) is never referenced elsewhere—ensure it’s correctly populated on mount and used throughout locale loading.
🤖 Prompt for AI Agents
In web/src/components/Wrapper/mount-engine.ts around lines 22–23, the new
LOCALE_DATA property is declared but never populated or used, while
web/src/helpers/i18n-loader.ts (line ~121) still reads window.LOCALE; change
i18n-loader to consume LOCALE_DATA instead of window.LOCALE, and populate
LOCALE_DATA during the Wrapper mount sequence (set it from whatever
server/bootstrap locale source you already use for window.LOCALE) so all locale
loading reads from this property; ensure any callers or imports that previously
relied on window.LOCALE are updated to reference the wrapper’s LOCALE_DATA or
accept it as an argument to the loader.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
api/src/core/utils/clients/emcmd.ts (1)

75-75: Replace dynamic import with static import
Add getters to the top‐level import and remove the await import('@app/store/index.js') at line 75—no circular dependency exists.

♻️ Duplicate comments (10)
@tailwind-shared/css-variables.css (1)

97-97: Revert inverted text color to maintain contrast.

Setting --ui-text-inverted to var(--ui-color-neutral-300) (#d4d4d4) leaves it nearly indistinguishable on --ui-bg-inverted (white). Contrast drops below WCAG 2.1 AA for normal text, so inverted text becomes illegible in dark mode. Please restore a dark neutral (e.g., var(--ui-color-neutral-900) or equivalent) for this token.

api/src/core/utils/clients/emcmd.ts (1)

63-63: Inconsistent error handling pattern.

Line 63 uses appLogger.error('message', error) while Line 22 uses appLogger.debug({ error }, 'message'). These should follow the same pattern. Based on the past review comment, this line still has the TS2769 type error because error is unknown in the catch clause.

Apply the same fix as suggested for Line 22:

-    ).catch((error) => {
-        appLogger.error('Failed to load CSRF token after multiple retries', error);
+    ).catch((error) => {
+        appLogger.error(
+            { error: error instanceof Error ? error : { message: String(error) } },
+            'Failed to load CSRF token after multiple retries'
+        );
         throw new AppError('Failed to load CSRF token after multiple retries');
     });
web/public/test-pages/shared-header.js (1)

368-400: Loading theme CSS from raw.githubusercontent.com (test-only) previously discussed

Noting prior review: acceptable for test pages; consider onerror handlers and pinning to a tag/commit if stability is needed later.

Also applies to: 406-423, 427-443

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

86-90: Handle loadStateFiles rejections correctly.

Redux Toolkit's createAsyncThunk dispatch returns a fulfilled promise containing the rejected action when the thunk fails, so this try/catch never executes. The failure is silently ignored, and stale state may be returned.

Apply this diff to fix the rejection handling:

 try {
-    await store.dispatch(loadStateFiles());
+    await store.dispatch(loadStateFiles()).unwrap();
 } catch (error) {
     this.logger.warn('Failed to reload emhttp state after updating system time', error as Error);
 }
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1)

13-19: Enforce authorization on state-changing mutation.

This mutation modifies system state by marking onboarding steps as completed but lacks authorization enforcement. Without protection, unauthorized callers can manipulate onboarding progress.

Apply this diff to add authorization:

+import { UsePermissions } from '@app/unraid-api/auth/decorators/use-permissions.decorator.js';
+import { AuthAction, Resource } from '@app/unraid-api/auth/permission.model.js';
+
 @Resolver(() => OnboardingMutations)
 export class OnboardingMutationsResolver {
     constructor(private readonly onboardingTracker: OnboardingTracker) {}

     @ResolveField(() => UpgradeInfo, {
         description: 'Marks an upgrade onboarding step as completed for the current OS version',
     })
+    @UsePermissions({
+        action: AuthAction.UPDATE,
+        resource: Resource.CUSTOMIZATIONS,
+    })
     async completeUpgradeStep(@Args('input') input: CompleteUpgradeStepInput): Promise<UpgradeInfo> {
web/public/test-pages/all-components.html (1)

486-502: Undefined function: verify the fix was applied.

Line 499 calls updateDarkModeButton(), but only updateThemeButton() is defined (line 420). This was flagged in a previous review and marked as addressed, but the code still shows the incorrect function name.

Verify that the intended fix from commits fabe6a2 to 4e0b831 was properly applied:

-        updateDarkModeButton();
+        updateThemeButton();
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (1)

5-5: Remove unused import.

ActivationOnboardingStepId is imported but never used in this file. Remove the import to keep the code clean.

-import { ActivationOnboardingStepId } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js';
-
web/src/components/Activation/ActivationModal.vue (1)

2-2: Move theme initialization to onMounted (avoid module-load side effects/SSR races).

Run-once IIFE executes before component mount and store readiness. Use onMounted instead.

Apply:

-import { computed, ref, watch } from 'vue';
+import { computed, ref, watch, onMounted } from 'vue';
@@
-// Apply theme when modal opens
-(async () => {
-  try {
-    await themeStore.setTheme();
-    // Ensure CSS variables are applied
-    themeStore.setCssVars();
-  } catch (error) {
-    console.error('Error setting theme:', error);
-  }
-})();
+// Apply theme when modal opens
+onMounted(async () => {
+  try {
+    await themeStore.setTheme();
+    // Ensure CSS variables are applied
+    themeStore.setCssVars();
+  } catch (error) {
+    console.error('Error setting theme:', error);
+  }
+});

Also applies to: 38-47

api/src/unraid-api/config/onboarding-tracker.module.ts (1)

200-201: Fix Nest Logger argument order (message first, stack/trace second).

Current calls pass Error first; swap to message-first for proper logs/traces.

Apply:

-        } catch (error) {
-            this.logger.error(error, 'Failed to evaluate activation onboarding steps');
+        } catch (error) {
+            this.logger.error(
+                'Failed to evaluate activation onboarding steps',
+                error instanceof Error ? error.stack : String(error)
+            );
         }
@@
-        } catch (error) {
-            this.logger.error(error, `Failed to read current OS version from ${this.versionFilePath}`);
+        } catch (error) {
+            this.logger.error(
+                `Failed to read current OS version from ${this.versionFilePath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
             return undefined;
         }
@@
-        } catch (error) {
-            this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
+        } catch (error) {
+            this.logger.debug(
+                `Unable to read onboarding tracker state at ${this.trackerPath}: ${
+                    error instanceof Error ? error.message : String(error)
+                }`
+            );
             return undefined;
         }
@@
-        } catch (error) {
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+        } catch (error) {
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );
         }

Also applies to: 260-261, 270-271, 280-281

api/generated-schema.graphql (1)

1172-1179: Add @UsePermissions to the completeUpgradeStep resolver

The completeUpgradeStep field in api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts is unguarded—add the appropriate @UsePermissions(...) decorator to enforce authorization. updateSystemTime already has @UsePermissions applied.

🧹 Nitpick comments (13)
api/src/core/utils/clients/emcmd.ts (1)

58-61: Document retry configuration values.

The retry timeouts and attempts use magic numbers without explanation. Consider extracting these as named constants with comments explaining the total wait time (up to ~100 seconds worst case).

+// Retry configuration for CSRF token loading
+// With exponential backoff between 5-10s, max 10 retries = ~75-100s total
+const CSRF_TOKEN_RETRY_CONFIG = {
+    minTimeout: 5000,
+    maxTimeout: 10000,
+    retries: 10,
+} as const;
+
     return retry(
         async (retries) => {
             // ...
         },
-        {
-            minTimeout: 5000,
-            maxTimeout: 10000,
-            retries: 10,
-        }
+        CSRF_TOKEN_RETRY_CONFIG
     ).catch((error) => {
web/src/components/Activation/stepRegistry.ts (1)

9-14: Add “satisfies” to lock mappings; consider ESM .js import policy

  • Use “satisfies” so keys are checked without widening and to prevent drift between step IDs and mappings.
  • Per repo guidelines, TypeScript imports should use .js extensions for ESM; if your web build enforces this, adjust the graphql import accordingly. If Nuxt/Vite handles TS path aliases without extensions, you can defer.

Example:

-export const stepComponents: Record<ActivationOnboardingStepId, Component> = {
+export const stepComponents = {
   WELCOME: ActivationWelcomeStep,
   TIMEZONE: ActivationTimezoneStep,
   PLUGINS: ActivationPluginsStep,
   ACTIVATION: ActivationLicenseStep,
-};
+} satisfies Record<ActivationOnboardingStepId, Component>;
 
-export const stepMetadata: Record<
-  ActivationOnboardingStepId,
-  { title: string; description: string; icon: string }
-> = {
+export const stepMetadata = {
   WELCOME: { title: 'Welcome to Unraid', description: 'Get started with your new Unraid system', icon: 'i-heroicons-sparkles' },
   TIMEZONE: { title: 'Set Time Zone', description: 'Configure system time', icon: 'i-heroicons-clock' },
   PLUGINS: { title: 'Install Essential Plugins', description: 'Add helpful plugins', icon: 'i-heroicons-puzzle-piece' },
   ACTIVATION: { title: 'Activate License', description: 'Create an Unraid.net account and activate your key', icon: 'i-heroicons-key' },
-};
+} satisfies Record<ActivationOnboardingStepId, { title: string; description: string; icon: string }>;

If needed by your build policy:

-import type { ActivationOnboardingStepId } from '~/composables/gql/graphql';
+import type { ActivationOnboardingStepId } from '~/composables/gql/graphql.js';

As per coding guidelines

Also applies to: 16-40

web/src/components/Activation/ActivationLicenseStep.vue (1)

39-43: LGTM; ensure v-for key uniqueness

Using button.text as key is fine if guaranteed unique; otherwise prefer a stable unique key (e.g., an id).

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

148-151: Use Intl.DateTimeFormat for date formatting.

The custom date formatter is error-prone and doesn't handle locale or timezone nuances. Intl.DateTimeFormat provides robust, locale-aware formatting.

Apply this diff to use Intl.DateTimeFormat:

 private formatManualDateTime(date: Date): string {
-    const pad = (value: number) => value.toString().padStart(2, '0');
-    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
+    const formatter = new Intl.DateTimeFormat('en-CA', {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit',
+        hour12: false,
+        timeZone: 'UTC'
+    });
+    return formatter.format(date).replace(',', '');
 }
web/src/components/Activation/ActivationWelcomeStep.vue (2)

26-38: Use i18n keys instead of literal strings

Current code passes English strings to t(), which bypasses translations and complicates l10n. Switch to namespaced keys (e.g., activation.welcome.*) with params.

Example:

-    return t('Welcome to your new {0} system, powered by Unraid!', [props.partnerName]);
+    return t('activation.welcome.partnerTitle', [props.partnerName]);
...
-    return t('Welcome to Unraid {0}!', [props.currentVersion]);
+    return t('activation.welcome.versionTitle', [props.currentVersion]);
...
-  return t('Welcome to Unraid!');
+  return t('activation.welcome.defaultTitle');
...
-    return t("You've upgraded from {0} to {1}", [props.previousVersion, props.currentVersion]);
+    return t('activation.welcome.upgradeDescription', [props.previousVersion, props.currentVersion]);
...
-    return t('Welcome to your Unraid {0} system', [props.currentVersion]);
+    return t('activation.welcome.versionDescription', [props.currentVersion]);
...
-  return t('Get started with your new Unraid system');
+  return t('activation.welcome.defaultDescription');
...
-  return t('Get Started');
+  return t('activation.welcome.getStarted');

Also applies to: 40-53, 55-57


59-67: Prefer router-based navigation over window.location

For SPA consistency and SSR safety, use useRouter().push('/login') (Nuxt: navigateTo('/login')) instead of window.location.

+import { useRouter } from 'vue-router';
...
-const handleComplete = () => {
-  if (props.redirectToLogin) {
-    window.location.href = '/login';
-  } else {
-    props.onComplete();
-  }
-};
+const router = useRouter();
+const handleComplete = () => {
+  if (props.redirectToLogin) return router.push('/login');
+  props.onComplete();
+};
web/__test__/components/Activation/WelcomeModal.test.ts (1)

51-64: Keep stub props aligned with real component

Stub lists isInitialSetup/onComplete/redirectToLogin, but the real ActivationWelcomeStep exposes showBack/showSkip/onBack/onSkip too. Consider mirroring props to reduce drift and false positives in future refactors.

web/src/components/Activation/ActivationSteps.vue (1)

25-45: Type cast may hide missing metadata.

Line 26 casts stepMetadata as Record<ActivationOnboardingStepId, StepItem>, which bypasses TypeScript's type checking. If stepMetadata doesn't have all required keys, you'll get undefined at runtime (caught by the fallback on lines 39-43, but still a concern).

Consider defining stepMetadata with a more specific type in stepRegistry.ts to ensure all step IDs are present at compile time, or add a runtime assertion here.

web/src/components/Activation/ActivationModal.vue (2)

57-58: Verify source for availableSteps (likely upgradeSteps, not allUpgradeSteps).

Navigation should reflect currently applicable steps. Confirm whether this should map from upgradeSteps to avoid hidden/irrelevant steps being included in the flow.

If appropriate:

-const availableSteps = computed<StepId[]>(() => allUpgradeSteps.value.map((step) => step.id as StepId));
+const availableSteps = computed<StepId[]>(() => upgradeSteps.value.map((step) => step.id as StepId));

179-182: Remove or gate debug logs behind DEV.

Console logs in shipped code add noise.

Apply:

-const handleTimezoneComplete = async () => {
-  console.log('[ActivationModal] Timezone complete, moving to next step');
-  await goToNextStep();
-};
+const handleTimezoneComplete = async () => {
+  if (import.meta.env.DEV) {
+    console.log('[ActivationModal] Timezone complete, moving to next step');
+  }
+  await goToNextStep();
+};
@@
-      console.log('[ActivationModal] WELCOME step props:', {
-        currentVersion: currentVersion.value,
-        previousVersion: previousVersion.value,
-      });
+      if (import.meta.env.DEV) {
+        console.log('[ActivationModal] WELCOME step props:', {
+          currentVersion: currentVersion.value,
+          previousVersion: previousVersion.value,
+        });
+      }

Also applies to: 219-223

api/src/unraid-api/config/onboarding-tracker.module.ts (2)

187-203: Use fromVersion to filter steps (reduce noise, honor upgrade path).

currently unused _fromVersion means all steps are always returned. Filter by introducedIn relative to from/to versions.

Apply:

-    private async computeStepsForUpgrade(
-        _fromVersion: string | undefined,
+    private async computeStepsForUpgrade(
+        fromVersion: string | undefined,
         toVersion: string | undefined
     ): Promise<UpgradeStepState[]> {
         if (!toVersion) {
             return [];
         }
 
         try {
             const context = await this.buildStepContext();
             const stepConfigs = await resolveActivationStepDefinitions(context);
-            return stepConfigs.map((step) => this.normalizeStep(step, toVersion));
+            const normalized = stepConfigs.map((step) => this.normalizeStep(step, toVersion));
+            // Only include steps introduced up to the current version, and (if known) after the last tracked version
+            return normalized.filter((step) => {
+                const introduced = step.introducedIn ?? toVersion;
+                try {
+                    const notFuture = compare(introduced, toVersion) <= 0;
+                    const afterFrom = fromVersion ? compare(introduced, fromVersion) > 0 : true;
+                    return notFuture && afterFrom;
+                } catch {
+                    // Fallback to string compare when semver fails
+                    return (!fromVersion || introduced > fromVersion) && introduced <= toVersion;
+                }
+            });
         } catch (error) {
-            this.logger.error(error, 'Failed to evaluate activation onboarding steps');
+            this.logger.error(
+                'Failed to evaluate activation onboarding steps',
+                error instanceof Error ? error.stack : String(error)
+            );
             return [];
         }
     }

275-279: Harden persistence: ensure directory exists and fsync writes.

Create parent dir if missing and enable fsyncWait for durability.

Apply:

-    private async writeTrackerState(state: TrackerState): Promise<void> {
+    private async writeTrackerState(state: TrackerState): Promise<void> {
         try {
-            await writeFile(this.trackerPath, JSON.stringify(state, null, 2), { mode: 0o644 });
+            // Ensure destination directory exists
+            await (await import('fs/promises')).mkdir(path.dirname(this.trackerPath), { recursive: true });
+            await writeFile(this.trackerPath, JSON.stringify(state, null, 2), {
+                mode: 0o644,
+                // Improve durability on power loss
+                fsyncWait: true,
+            } as unknown as Record<string, unknown>);
             this.state = state;
         } catch (error) {
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );
         }
     }

Based on learnings

web/__test__/components/Activation/ActivationModal.test.ts (1)

112-140: Align mock step IDs with enum values (prevents false positives).

Use ActivationOnboardingStepId values ('TIMEZONE','PLUGINS','ACTIVATION') to match runtime mapping.

Apply:

-  {
-    id: 'timezone',
+  {
+    id: 'TIMEZONE',
@@
-  {
-    id: 'plugins',
+  {
+    id: 'PLUGINS',
@@
-  {
-    id: 'activation',
+  {
+    id: 'ACTIVATION',

Optionally, assert the initial step component renders (e.g., expect wrapper.find('[data-testid="timezone-step"]').exists()). As per coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c414f39 and 4e0b831.

⛔ Files ignored due to path filters (2)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (59)
  • @tailwind-shared/css-variables.css (1 hunks)
  • api/generated-schema.graphql (9 hunks)
  • api/src/core/utils/clients/emcmd.ts (3 hunks)
  • api/src/unraid-api/config/api-config.module.ts (2 hunks)
  • api/src/unraid-api/config/api-config.test.ts (2 hunks)
  • api/src/unraid-api/config/onboarding-tracker.model.ts (1 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts (8 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (1 hunks)
  • api/src/unraid-api/main.ts (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (6 hunks)
  • web/__test__/components/Activation/ActivationSteps.test.ts (0 hunks)
  • web/__test__/components/Activation/WelcomeModal.test.ts (4 hunks)
  • web/__test__/store/activationCodeModal.test.ts (1 hunks)
  • web/components.d.ts (2 hunks)
  • web/package.json (1 hunks)
  • web/public/test-pages/all-components.html (11 hunks)
  • web/public/test-pages/shared-header.js (2 hunks)
  • web/src/assets/main.css (1 hunks)
  • web/src/components/Activation/ActivationLicenseStep.vue (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (2 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/components/Activation/ActivationSteps.vue (1 hunks)
  • web/src/components/Activation/ActivationTimezoneStep.vue (1 hunks)
  • web/src/components/Activation/ActivationWelcomeStep.vue (1 hunks)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md (1 hunks)
  • web/src/components/Activation/WelcomeModal.standalone.vue (2 hunks)
  • web/src/components/Activation/activationOnboarding.query.ts (1 hunks)
  • web/src/components/Activation/completeUpgradeStep.mutation.ts (1 hunks)
  • web/src/components/Activation/stepRegistry.ts (1 hunks)
  • web/src/components/Activation/steps/timezone.ts (1 hunks)
  • web/src/components/Activation/store/activationCodeModal.ts (2 hunks)
  • web/src/components/Activation/store/upgradeOnboarding.ts (1 hunks)
  • web/src/components/Activation/updateSystemTime.mutation.ts (1 hunks)
  • web/src/components/ColorSwitcher.standalone.vue (1 hunks)
  • web/src/components/DevModalTest.standalone.vue (2 hunks)
  • web/src/components/TestThemeSwitcher.standalone.vue (1 hunks)
  • web/src/components/Wrapper/mount-engine.ts (2 hunks)
  • web/src/composables/gql/gql.ts (4 hunks)
  • web/src/composables/gql/graphql.ts (14 hunks)
💤 Files with no reviewable changes (1)
  • web/test/components/Activation/ActivationSteps.test.ts
✅ Files skipped from review due to trivial changes (1)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md
🚧 Files skipped from review as they are similar to previous changes (20)
  • web/test/store/activationCodeModal.test.ts
  • web/src/components/Activation/store/upgradeOnboarding.ts
  • packages/unraid-shared/package.json
  • web/src/assets/main.css
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/config/api-config.module.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • web/src/components/Activation/updateSystemTime.mutation.ts
  • web/src/components/DevModalTest.standalone.vue
  • web/package.json
  • web/src/components/Activation/completeUpgradeStep.mutation.ts
  • api/src/unraid-api/main.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.resolver.ts
  • web/src/components/Activation/WelcomeModal.standalone.vue
  • web/src/components/TestThemeSwitcher.standalone.vue
  • web/src/components/Activation/activationOnboarding.query.ts
  • web/components.d.ts
🧰 Additional context used
📓 Path-based instructions (12)
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts
  • web/src/composables/gql/gql.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • web/src/components/Wrapper/mount-engine.ts
  • web/src/components/Activation/store/activationCodeModal.ts
  • web/src/components/Activation/stepRegistry.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • web/src/composables/gql/graphql.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • web/src/components/Activation/steps/timezone.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/core/utils/clients/emcmd.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts
  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/core/utils/clients/emcmd.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/ColorSwitcher.standalone.vue
  • web/src/components/Activation/ActivationPluginsStep.vue
  • web/src/components/Activation/ActivationTimezoneStep.vue
  • web/src/components/Activation/ActivationLicenseStep.vue
  • web/src/components/Activation/ActivationWelcomeStep.vue
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/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.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/config/api-config.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
**/{store,stores}/**/*.ts

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

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • web/src/components/Activation/store/activationCodeModal.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
🧠 Learnings (10)
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
PR: unraid/api#972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • web/public/test-pages/shared-header.js
📚 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:

  • web/__test__/components/Activation/ActivationModal.test.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:

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

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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 : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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 : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
📚 Learning: 2025-10-11T02:09:16.853Z
Learnt from: elibosley
PR: unraid/api#1746
File: web/src/components/Activation/ActivationTimezoneStep.vue:78-91
Timestamp: 2025-10-11T02:09:16.853Z
Learning: In Unraid setup/activation flows (e.g., ActivationTimezoneStep.vue), don't block user progression when non-critical configuration steps like timezone updates fail. The design prioritizes allowing users to access Unraid over enforcing complete setup step success.

Applied to files:

  • web/src/components/Activation/ActivationTimezoneStep.vue
🧬 Code graph analysis (16)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (5)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • Resolver (19-99)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (1)
  • Resolver (15-60)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (14-50)
web/src/composables/gql/graphql.ts (3)
  • OnboardingMutations (1551-1555)
  • UpgradeInfo (2375-2387)
  • CompleteUpgradeStepInput (471-474)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)
  • buildUpgradeInfoFromSnapshot (4-22)
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (4)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • Module (285-289)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-38)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
web/public/test-pages/shared-header.js (1)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • theme (96-98)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (4)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • Resolver (19-99)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1)
  • Resolver (9-20)
web/src/composables/gql/graphql.ts (2)
  • InfoVersions (1121-1130)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)
  • buildUpgradeInfoFromSnapshot (4-22)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (3)
api/src/unraid-api/config/onboarding-tracker.model.ts (1)
  • UpgradeProgressSnapshot (20-25)
web/src/composables/gql/graphql.ts (1)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • UpgradeInfo (2374-2386)
api/src/unraid-api/graph/resolvers/system-time/system-time.module.ts (1)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/store/theme.ts (1)
  • useThemeStore (51-242)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (5)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (71-124)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadStateFiles (145-164)
api/src/core/types/states/var.ts (1)
  • Var (12-242)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
api/src/unraid-api/config/onboarding-tracker.module.ts (5)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)
  • resolveActivationStepDefinitions (79-89)
  • ActivationStepDefinition (47-52)
  • ActivationStepContext (42-45)
  • findActivationCodeFile (20-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.model.ts (4)
  • TrackerState (8-12)
  • UpgradeProgressSnapshot (20-25)
  • CompletedStepState (3-6)
  • UpgradeStepState (14-18)
api/src/store/index.ts (1)
  • getters (18-23)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (13)
  • ActivationOnboarding (122-134)
  • Maybe (3-3)
  • Scalars (11-75)
  • ActivationOnboardingStep (136-146)
  • CompleteUpgradeStepInput (470-473)
  • UpgradeInfo (2374-2386)
  • OnboardingMutations (1550-1554)
  • SystemTime (2131-2141)
  • MutationUpdateSystemTimeArgs (1409-1411)
  • UpdateSystemTimeInput (2363-2372)
  • OnboardingMutationsCompleteUpgradeStepArgs (1558-1560)
  • InputMaybe (4-4)
  • UpgradeStep (2388-2402)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (2)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (5)
  • ObjectType (7-17)
  • ObjectType (19-44)
  • ObjectType (46-74)
  • ObjectType (76-98)
  • ObjectType (100-110)
web/src/composables/gql/graphql.ts (2)
  • OnboardingMutations (1551-1555)
  • UpgradeInfo (2375-2387)
api/src/unraid-api/config/onboarding-tracker.model.ts (1)
api/src/unraid-api/config/onboarding-tracker.module.ts (2)
  • UpgradeStepState (293-293)
  • UpgradeProgressSnapshot (292-292)
api/src/core/utils/clients/emcmd.ts (3)
api/src/core/log.ts (1)
  • appLogger (83-83)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadSingleStateFile (107-141)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (2)
web/src/composables/gql/graphql.ts (1)
  • CompleteUpgradeStepInput (471-474)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • CompleteUpgradeStepInput (470-473)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (5)
  • ObjectType (7-17)
  • ObjectType (19-44)
  • ObjectType (46-74)
  • ObjectType (76-98)
  • ObjectType (100-110)
web/src/composables/gql/graphql.ts (2)
  • ActivationOnboardingStep (136-146)
  • ActivationOnboarding (122-134)
api/src/unraid-api/cli/generated/graphql.ts (2)
  • ActivationOnboardingStep (136-146)
  • ActivationOnboarding (122-134)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts

[notice] 5-5: Unused variable, import, function or class
Unused import ActivationOnboardingStepId.

🔇 Additional comments (38)
web/src/components/ColorSwitcher.standalone.vue (1)

53-54: LGTM! Label update improves clarity.

The updated labels 'White' and 'Black' directly reflect the actual colors and align with the other color-based labels ('Azure', 'Gray') in the list, making the UI more explicit and consistent.

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

22-23: LGTM! Clear and well-typed interface additions.

The Window interface updates are well-structured:

  • themeStore uses ReturnType<typeof useThemeStore> for precise typing
  • LOCALE_DATA naming is more descriptive than the removed LOCALE property (as indicated in the AI summary)

97-99: LGTM! Consistent pattern for debugging access.

The themeStore exposure follows the established pattern used for globalPinia (lines 27-29), with proper browser environment checking for SSR safety.

api/src/core/utils/clients/emcmd.ts (3)

22-22: Verify error handling resolves TS2769.

The error handling has been updated to wrap the error in an object ({ error }), which differs from the past review's suggestion. This should resolve the TS2769 type error if the logger accepts an object as the first parameter. Please confirm that this pattern matches your logger's signature and that CI passes.


110-112: Verify response body error handling logic.

The code throws an error if the response body is non-empty (after trimming). Confirm that the emhttp /update endpoint returns an empty body on success and non-empty body only for errors, as this pattern is unusual for HTTP APIs.


94-94: Do not send an empty CSRF token. Replace params.append('csrf_token', csrfToken ?? '') with an explicit error or rejection when csrfToken is undefined, or ensure ensureCsrfToken never returns undefined. Confirm how the backend handles missing CSRF tokens.

web/src/components/Activation/store/activationCodeModal.ts (2)

13-15: Decoupling from activationCode looks good

Using only isFreshInstall simplifies the store and avoids unnecessary coupling.


38-38: Visibility condition update LGTM

Clear logic: explicit show beats default; otherwise show only on fresh install without callbackData.

Please verify tests cover:

  • isHidden === false => true
  • isHidden === null + fresh install + no callbackData => true
  • Any callbackData present => false
api/src/unraid-api/config/onboarding-tracker.model.ts (1)

1-25: Types and ESM import look good

Clean model types and .js import path align with ESM policy.

As per coding guidelines

web/src/components/Activation/ActivationPluginsStep.vue (1)

51-52: No changes requireduseInstallPlugin is the default export of web/src/composables/installPlugin.ts and its install() method is synchronous (void‐returning), so there’s no import/name mismatch or need to await it.

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

1-9: LGTM!

Module structure is correct and follows NestJS conventions.

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

8-61: LGTM!

Test structure is correct and validates the snapshot-to-response mapping logic. The mocks are properly typed and assertions cover the expected behavior.

web/src/components/Activation/steps/timezone.ts (1)

1-8: LGTM!

Step registration is correct and type-safe. The satisfies check ensures the step conforms to the registry type.

api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)

4-22: LGTM!

The upgrade detection logic is correct, and the snapshot-to-UpgradeInfo mapping preserves all required fields appropriately.

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

20-20: LGTM!

Module wiring is correct and follows established patterns for integrating new resolvers and modules.

Also applies to: 28-28, 56-56, 67-67

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

17-18: LGTM!

GraphQL document mappings and function overloads are correctly structured and maintain type safety. The additions follow existing patterns in the file.

Also applies to: 22-22, 65-66, 70-70, 127-134, 147-150

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

7-16: LGTM; confirm enum registration for GraphQL

Input type and validation look correct, and ESM imports use .js as required. Please verify that ActivationOnboardingStepId is registered with GraphQL (registerEnumType) so schema generation works as expected.

As per coding guidelines

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

44-52: Onboarding mutations wiring looks correct

New OnboardingMutations object and RootMutations.onboarding field align with UpgradeInfo-based schema. Ensure resolver method signature matches (args/input name) but otherwise good.

As per coding guidelines

Also applies to: 74-75

web/__test__/components/Activation/WelcomeModal.test.ts (2)

48-50: ActivationSteps stub matches new API

activeStepIndex wiring and assertion look correct.


180-206: Redirect test setup is robust

Mocking window.location with configurable descriptor avoids JSDOM immutability issues. Assertion is concise.

web/public/test-pages/all-components.html (1)

528-644: LGTM!

The activation modal test handlers are well-implemented with proper dependency checks, error logging, and defensive programming. The approach to manipulate Apollo cache for testing scenarios and use debug hooks for step navigation is appropriate for a test page.

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

46-109: LGTM!

The UpgradeStep and UpgradeInfo GraphQL types are well-structured with clear field descriptions, appropriate default values, and proper nullable field markers. The integration into InfoVersions is clean.

web/src/components/Activation/ActivationTimezoneStep.vue (3)

40-59: Verify fallback behavior when timezone not in list.

Line 54 returns the detected timezone even when it's not found in the timeZones array (no exact or group match). This could result in selectedTimeZone.value being set to a value that doesn't exist in timeZoneItems, potentially causing the Select component to display incorrectly or fail validation.

Consider returning null instead when no match is found:

     const groupMatch = timeZones.find((tz) => tz.group.includes(detectedTz));
     if (groupMatch) {
       return groupMatch.name;
     }
 
-    return detectedTz;
+    return null;

Alternatively, verify that the Select component gracefully handles values not in its items list.


68-90: LGTM!

The submit flow properly validates input, manages loading state, and follows the intentional design pattern of not blocking progression when non-critical configuration steps fail.

Based on learnings.


101-144: LGTM!

The template is well-structured with proper reactive bindings, conditional rendering based on props, and appropriate disabled/loading states.

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

55-59: LGTM!

The upgrade() resolver is cleanly implemented, properly using the injected OnboardingTracker service and transformation utility to return upgrade information.

api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (2)

62-71: LGTM!

The ActivationOnboardingStepId enum is properly defined and registered with GraphQL, following standard patterns.


154-204: LGTM!

The ActivationOnboardingStep and ActivationOnboarding types are well-structured with clear field descriptions and proper nullable field markers, following GraphQL best practices.

api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (3)

20-40: LGTM!

The findActivationCodeFile utility properly handles file system errors with appropriate logging levels (debug for ENOENT, error for other issues) and returns null on failure.


42-89: LGTM!

The step definitions and resolution logic are well-structured. The conditional inclusion of the ACTIVATION step based on activation code presence and registration state is properly implemented.


91-168: LGTM!

The service rename from CustomizationService to OnboardingService is consistent, and the refactoring to use the extracted findActivationCodeFile utility maintains the original functionality while improving code organization.

web/src/components/Activation/ActivationSteps.vue (3)

71-92: LGTM!

The active step index calculation properly handles the offset for the initial step, and the responsive orientation logic correctly manages window resize events with proper cleanup.


94-110: LGTM!

The step click handler properly parses the index, accounts for the initial step offset, and validates bounds before invoking the callback.


114-122: LGTM!

The template correctly binds the computed values to the UStepper component and handles the click event.

api/src/unraid-api/config/api-config.test.ts (4)

48-120: LGTM!

The ApiConfigPersistence test suite properly validates default configuration, legacy migration, and bootstrap behavior with appropriate assertions for the new lastSeenOsVersion field.


153-178: LGTM!

The test correctly validates that lastTrackedVersion persistence is deferred until shutdown, confirming the intended design pattern.


180-448: LGTM!

The OnboardingTracker integration test suite is comprehensive, covering upgrade scenarios, version tracking, step completion, and edge cases with clear test names and appropriate assertions.


451-481: LGTM!

The loadApiConfig tests properly validate default configuration values including the new lastSeenOsVersion field.

const readCsrfTokenFromVarIni = async (): Promise<string | undefined> => {
try {
const iniContents = await readFile(VAR_INI_PATH, 'utf-8');
const parsed = ini.parse(iniContents) as { csrf_token?: string };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace type assertion with proper type definition.

The type assertion as { csrf_token?: string } violates the coding guideline "Avoid type casting; model proper types from the start."

Define an interface and let TypeScript infer the type:

+interface VarIniConfig {
+    csrf_token?: string;
+}
+
 const readCsrfTokenFromVarIni = async (): Promise<string | undefined> => {
     try {
         const iniContents = await readFile(VAR_INI_PATH, 'utf-8');
-        const parsed = ini.parse(iniContents) as { csrf_token?: string };
+        const parsed: VarIniConfig = ini.parse(iniContents);
         return parsed?.csrf_token;
📝 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 parsed = ini.parse(iniContents) as { csrf_token?: string };
// Define a proper type instead of using a type assertion
interface VarIniConfig {
csrf_token?: string;
}
const readCsrfTokenFromVarIni = async (): Promise<string | undefined> => {
try {
const iniContents = await readFile(VAR_INI_PATH, 'utf-8');
const parsed: VarIniConfig = ini.parse(iniContents);
return parsed?.csrf_token;
} catch (error) {
// …existing error handling…
}
// …rest of function…
}
🤖 Prompt for AI Agents
In api/src/core/utils/clients/emcmd.ts around line 19, replace the type
assertion on parsed ini data with a proper interface and let TypeScript infer
the type: declare an interface (e.g., IniConfig { csrf_token?: string }), then
use that interface when calling ini.parse or when typing the parsed variable
(for example ini.parse<IniConfig>(iniContents) or const parsed: IniConfig =
ini.parse(iniContents)); remove the "as { csrf_token?: string }" cast so the
value is type-checked without casting.


appLogger.debug('emcmd executed successfully');
return response;
} catch (error: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace any type with unknown.

The catch clause uses error: any, which violates the coding guideline "Never use the any type; prefer precise typing."

Apply this diff:

-    } catch (error: any) {
+    } catch (error: unknown) {
+        const message = error instanceof Error ? error.message : String(error);
         if (error.code === 'ENOENT') {
-            appLogger.error('emhttpd socket unavailable.', error);
+            appLogger.error({ error }, 'emhttpd socket unavailable.');
             throw new Error('emhttpd socket unavailable.');
         }
-        appLogger.error(`emcmd execution failed: ${error.message}`, error);
+        appLogger.error({ error }, `emcmd execution failed: ${message}`);
         throw error;
     }

Note: You'll also need to check for the code property safely:

-    } catch (error: any) {
+    } catch (error: unknown) {
+        const message = error instanceof Error ? error.message : String(error);
-        if (error.code === 'ENOENT') {
+        if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
-            appLogger.error('emhttpd socket unavailable.', error);
+            appLogger.error({ error }, 'emhttpd socket unavailable.');
             throw new Error('emhttpd socket unavailable.');
         }
-        appLogger.error(`emcmd execution failed: ${error.message}`, error);
+        appLogger.error({ error }, `emcmd execution failed: ${message}`);
         throw error;
     }

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

🤖 Prompt for AI Agents
In api/src/core/utils/clients/emcmd.ts around line 116, replace the catch
parameter type from "any" to "unknown" and then narrow the error before using
it: change to catch(error: unknown), check if error is an instance of Error to
read message, and safely check for a "code" property using a type guard (e.g.,
typeof (error as { code?: unknown }).code === 'string' or the 'in' operator)
before accessing it; update logging/handling to use the narrowed types
accordingly.

Comment on lines +3 to 10
import { OnboardingTrackerModule } from '@app/unraid-api/config/onboarding-tracker.module.js';
import { CustomizationResolver } from '@app/unraid-api/graph/resolvers/customization/customization.resolver.js';
import { CustomizationService } from '@app/unraid-api/graph/resolvers/customization/customization.service.js';
import { OnboardingService } from '@app/unraid-api/graph/resolvers/customization/onboarding.service.js';

@Module({
providers: [CustomizationService, CustomizationResolver],
imports: [OnboardingTrackerModule],
providers: [OnboardingService, CustomizationResolver],
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Export OnboardingService to avoid DI failure

ResolversModule provides OnboardingMutationsResolver and imports CustomizationModule, but OnboardingService isn’t exported here. In Nest, a provider must be exported by its module to be injected elsewhere. Without export, DI will fail at runtime.

Apply this diff:

 @Module({
-    imports: [OnboardingTrackerModule],
-    providers: [OnboardingService, CustomizationResolver],
+    imports: [OnboardingTrackerModule],
+    providers: [OnboardingService, CustomizationResolver],
+    exports: [OnboardingService],
 })
 export class CustomizationModule {}
📝 Committable suggestion

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

Suggested change
import { OnboardingTrackerModule } from '@app/unraid-api/config/onboarding-tracker.module.js';
import { CustomizationResolver } from '@app/unraid-api/graph/resolvers/customization/customization.resolver.js';
import { CustomizationService } from '@app/unraid-api/graph/resolvers/customization/customization.service.js';
import { OnboardingService } from '@app/unraid-api/graph/resolvers/customization/onboarding.service.js';
@Module({
providers: [CustomizationService, CustomizationResolver],
imports: [OnboardingTrackerModule],
providers: [OnboardingService, CustomizationResolver],
})
import { OnboardingTrackerModule } from '@app/unraid-api/config/onboarding-tracker.module.js';
import { CustomizationResolver } from '@app/unraid-api/graph/resolvers/customization/customization.resolver.js';
import { OnboardingService } from '@app/unraid-api/graph/resolvers/customization/onboarding.service.js';
@Module({
imports: [OnboardingTrackerModule],
providers: [OnboardingService, CustomizationResolver],
exports: [OnboardingService],
})
export class CustomizationModule {}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/customization/customization.module.ts
around lines 3 to 10, the module declares OnboardingService as a provider but
does not export it, causing dependency injection failures when other modules
(like ResolversModule) try to use the service; update the @Module metadata to
add an exports array that includes OnboardingService so the service is available
to importing modules (i.e., add exports: [OnboardingService]) while keeping the
existing imports and providers.

Comment on lines +31 to +35
@Field({ nullable: true, description: 'Enable or disable NTP-based synchronization' })
@IsOptional()
@IsBoolean()
useNtp?: boolean;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Enforce cross-field rules between useNtp and manualDateTime

Currently, manualDateTime can be provided regardless of useNtp, and is not required when useNtp is false. Add conditional validators to:

  • Require manualDateTime when useNtp === false
  • Forbid manualDateTime when useNtp === true

Apply:

-import { ArrayMaxSize, IsArray, IsBoolean, IsOptional, IsString, Matches } from 'class-validator';
+import { ArrayMaxSize, IsArray, IsBoolean, IsEmpty, IsNotEmpty, IsOptional, IsString, Matches, ValidateIf } from 'class-validator';

@@
-    @Field({
+    @Field({
         nullable: true,
         description: 'Manual date/time to apply when disabling NTP, expected format YYYY-MM-DD HH:mm:ss',
     })
-    @IsOptional()
-    @IsString()
-    @Matches(MANUAL_TIME_PATTERN, {
-        message: 'manualDateTime must be formatted as YYYY-MM-DD HH:mm:ss',
-    })
-    manualDateTime?: string;
+    // When NTP is disabled, manualDateTime is required and must match the pattern
+    @ValidateIf(o => o.useNtp === false)
+    @IsNotEmpty()
+    @IsString()
+    @Matches(MANUAL_TIME_PATTERN, { message: 'manualDateTime must be formatted as YYYY-MM-DD HH:mm:ss' })
+    // When NTP is enabled, manualDateTime must be omitted
+    @ValidateIf(o => o.useNtp === true)
+    @IsEmpty({ message: 'manualDateTime must be omitted when useNtp is enabled' })
+    manualDateTime?: string;

Optionally clarify the timezone assumption in the description (interpreted in selected timeZone vs. server local).

Also applies to: 46-56

🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts around
lines 31-35 (and similarly for the manualDateTime-related block at lines 46-56),
add conditional validation rules so manualDateTime is required when useNtp ===
false and forbidden when useNtp === true: update the decorators on
manualDateTime to use class-validator conditional validators (e.g., ValidateIf
or IsDefined combined with ValidateIf/IsOptional) so that when useNtp is false
manualDateTime is validated/required, and when useNtp is true manualDateTime is
rejected/ignored (e.g., use ValidateIf(o => o.useNtp === false) + appropriate
IsDefined/IsString/IsDate validators, and a complementary ValidateIf(o =>
o.useNtp === true) that enforces absence or throws validation error); also
update the GraphQL/Field description to optionally clarify the timezone
assumption (interpreted in selected timeZone vs server local).

Comment on lines 77 to 83
<div class="flex space-x-4">
<BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />

<BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />

<BrandButton :text="buttonText" @click="handleComplete" />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard optional callbacks to prevent runtime errors

showBack/showSkip can be true while onBack/onSkip are undefined, causing a crash on click. Call safely.

-      <BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />
+      <BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack && onBack()" />
 
-      <BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />
+      <BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip && onSkip()" />
📝 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
<div class="flex space-x-4">
<BrandButton v-if="showBack" :text="t('Back')" variant="outline" @click="onBack" />
<BrandButton v-if="showSkip" :text="t('Skip')" variant="outline" @click="onSkip" />
<BrandButton :text="buttonText" @click="handleComplete" />
</div>
<div class="flex space-x-4">
<BrandButton
v-if="showBack"
:text="t('Back')"
variant="outline"
@click="onBack && onBack()"
/>
<BrandButton
v-if="showSkip"
:text="t('Skip')"
variant="outline"
@click="onSkip && onSkip()"
/>
<BrandButton
:text="buttonText"
@click="handleComplete"
/>
</div>
🤖 Prompt for AI Agents
In web/src/components/Activation/ActivationWelcomeStep.vue around lines 77 to
83, the template renders BrandButton when showBack/showSkip are true but may
call undefined onBack/onSkip handlers, causing runtime errors; update the click
bindings to call the callbacks safely (e.g., call them only if defined or
dispatch to a no-op fallback) or provide default no-op methods for onBack/onSkip
in the component props/options so clicking the buttons cannot trigger undefined
function calls.

@elibosley elibosley changed the title feat(system-time): add SystemTime type and update resolvers for syste… feat(onboarding): add new onboarding flows for Unraid OS Oct 14, 2025
- Removed `title`, `description`, and `icon` fields from the `UpgradeStep` type in the GraphQL schema and related TypeScript definitions to simplify the onboarding process.
- Updated documentation to reflect changes in how onboarding steps are tracked and displayed.
- Adjusted related tests and components to ensure compatibility with the updated schema.

This update streamlines the onboarding experience by focusing on essential information for upgrade steps.
- Replaced direct state access with ConfigService in SystemTimeService to improve dependency management and testability.
- Updated unit tests to mock ConfigService for retrieving system time settings, enhancing test isolation.
- Removed unnecessary getters and store dispatch calls, streamlining the service logic.

This update enhances the maintainability and clarity of the SystemTimeService by leveraging NestJS's configuration management capabilities.
- Introduced permission checks for the `completeUpgradeStep` mutation in the onboarding process.
- Utilized `UsePermissions` directive to enforce authorization based on `AuthAction.UPDATE_ANY` for the `Resource.WELCOME`.
- This update enhances security by ensuring only authorized users can mark onboarding steps as completed.
…mplete snapshot versions

- Introduced a new test case in `customization.resolver.spec.ts` to verify that upgrade metadata is omitted when snapshot versions are incomplete.
- Updated the `CustomizationResolver` logic to handle cases where either `currentVersion` or `lastTrackedVersion` is undefined, ensuring accurate response structure.
- This update enhances the robustness of the onboarding process by ensuring correct behavior under incomplete version scenarios.
…ponent

- Eliminated the unused `useI18n` import from the `WelcomeModal.standalone.vue` file to clean up the code.
- This update enhances the clarity and maintainability of the component by removing unnecessary dependencies.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/Activation/WelcomeModal.standalone.vue (1)

30-34: Guard window access to avoid SSR crashes; initialize safely.

Direct use of window in setup will throw during SSR and in tests. Guard it.

Apply this diff:

-const isLoginPage = computed(() => window.location.pathname.includes('login'));
+const isLoginPage = computed(
+  () => typeof window !== 'undefined' && window.location.pathname.includes('login')
+);
 
 // Initialize showModal based on conditions
-const showModal = ref(isLoginPage.value || isInitialSetup.value);
+const showModal = ref((isLoginPage.value || isInitialSetup.value) as boolean);
🧹 Nitpick comments (11)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (4)

60-60: Consider removing the as any type cast.

The mock return value uses as any, which violates the "Never use the any type" coding guideline. Since tests only need emcmd to resolve successfully without throwing, consider simplifying this to avoid the type cast:

-        vi.mocked(emcmd).mockResolvedValue({ ok: true } as any);
+        vi.mocked(emcmd).mockResolvedValue(undefined as never);

Alternatively, define a minimal interface that matches what the service expects from the emcmd response, if any properties are actually used.

As per coding guidelines.


140-142: Consider using .rejects.toThrow() for error tests.

The tests use .rejects.toBeInstanceOf(BadRequestException) which is more specific than the coding guideline recommendation. While checking the exception type is useful, the guidelines state: "For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test."

If the specific exception type is not critical to the behavior being tested, consider simplifying to:

-        await expect(service.updateSystemTime({ timeZone: 'Not/AZone' })).rejects.toBeInstanceOf(
-            BadRequestException
-        );
+        await expect(service.updateSystemTime({ timeZone: 'Not/AZone' })).rejects.toThrow();

Apply the same pattern to line 147.

As per coding guidelines.

Also applies to: 147-149


72-72: Replace magic number with a named constant.

The test expects an array of length 4 for NTP servers, but this is a magic number. Consider extracting this to a constant to improve maintainability and make the intent clearer.

+const MAX_NTP_SERVERS = 4;
+
 it('returns system time from store state', async () => {
     const result = await service.getSystemTime();
     expect(result.timeZone).toBe('UTC');
     expect(result.useNtp).toBe(true);
-    expect(result.ntpServers).toEqual(['time1.google.com', 'time2.google.com', '', '']);
+    expect(result.ntpServers).toHaveLength(MAX_NTP_SERVERS);
+    expect(result.ntpServers.slice(0, 2)).toEqual(['time1.google.com', 'time2.google.com']);
     expect(typeof result.currentTime).toBe('string');
 });

68-191: Consider adding tests for emcmd failure scenarios.

The current test suite covers success paths and input validation errors well. However, based on the emcmd implementation in the relevant code snippets, there are additional failure scenarios that could be tested:

  • Network errors (e.g., ENOENT when socket is unavailable)
  • HTTP errors (status >= 400)
  • Response body errors (trimmed body content)

Adding tests for these scenarios would ensure the service handles emcmd failures gracefully and provides appropriate error messages to callers. Example test structure:

it('handles emcmd socket unavailability', async () => {
    vi.mocked(emcmd).mockRejectedValue(new Error('emhttpd socket unavailable.'));
    await expect(service.updateSystemTime({ useNtp: true })).rejects.toThrow();
});

it('handles emcmd HTTP errors', async () => {
    vi.mocked(emcmd).mockRejectedValue(new Error('emcmd request failed with status 500'));
    await expect(service.updateSystemTime({ useNtp: true })).rejects.toThrow();
});
api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts (2)

9-16: Improve mock type safety.

The mock objects use as unknown as Type which bypasses type checking. Consider using Partial<> or creating properly typed test doubles to catch type mismatches at compile time.

Apply this diff to improve type safety:

-    const onboardingService = {
+    const onboardingService: Partial<OnboardingService> = {
         getActivationData: vi.fn(),
         getPublicPartnerInfo: vi.fn(),
         getTheme: vi.fn(),
-    } as unknown as OnboardingService;
-    const onboardingTracker = {
+    };
+    const onboardingTracker: Partial<OnboardingTracker> = {
         getUpgradeSnapshot: vi.fn(),
-    } as unknown as OnboardingTracker;
+    };

-    const resolver = new CustomizationResolver(onboardingService, onboardingTracker);
+    const resolver = new CustomizationResolver(
+        onboardingService as OnboardingService,
+        onboardingTracker as OnboardingTracker
+    );

20-93: Consider additional test coverage.

The current tests cover the basic happy path and one edge case (undefined lastTrackedVersion). Consider adding tests for:

  • Empty steps array
  • All steps completed (hasPendingSteps = false)
  • Error handling when getUpgradeSnapshot throws
  • Edge case when currentVersion is undefined but lastTrackedVersion is defined
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)

49-77: Consider error handling for getUpgradeSnapshot.

The method doesn't handle potential errors from onboardingTracker.getUpgradeSnapshot(). If the tracker throws (e.g., file system errors, invalid data), the GraphQL query will fail without a clear error message.

Consider wrapping the call in try-catch if graceful degradation is needed, or ensure upstream error handling is sufficient.

web/src/components/Activation/WelcomeModal.standalone.vue (2)

54-62: Optional: simplify with v-model.

You can drop the manual update handler and bind v-model for clarity.

-    <Dialog
-      :to="modalContainer"
-      :model-value="showModal"
-      :show-footer="false"
-      :show-close-button="isLoginPage"
-      size="full"
-      class="bg-background"
-      @update:model-value="(value) => (showModal = value)"
-    >
+    <Dialog
+      :to="modalContainer"
+      v-model="showModal"
+      :show-footer="false"
+      :show-close-button="isLoginPage"
+      size="full"
+      class="bg-background"
+    >

92-93: Confirm non-interactive timeline when steps is empty.

Passing steps=[] results in a static timeline (ActivationSteps doesn’t invoke onStepClick in this case). If that’s intended here, ignore; otherwise consider passing real steps or enabling clicks for fallback steps.

web/src/components/Activation/ActivationSteps.vue (1)

118-134: Allow clicks when using fallback steps (when props.steps is empty).

Currently, clicks are ignored if props.steps.length === 0. If you want the fallback steps to be navigable, gate against the effective timeline length instead.

-  // Allow clicking on any step that exists (completed or incomplete)
-  if (actualStepIndex < props.steps.length) {
-    props.onStepClick(actualStepIndex);
-  }
+  // Allow clicking on any step that exists (completed or incomplete)
+  const effectiveLength =
+    props.steps.length > 0 ? props.steps.length : dynamicSteps.value.length;
+  if (actualStepIndex < effectiveLength) {
+    props.onStepClick(actualStepIndex);
+  }

If the non-clickable fallback is intentional, feel free to keep as-is.

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

90-103: Remove unreachable padding loop.

The while loop at lines 98-100 is dead code and never executes because the servers array is guaranteed to have exactly 4 elements after the map operation. The initial array has 4 elements (lines 91-95), and map() always returns an array of the same length.

Apply this diff to remove the dead code:

     const servers = [
         varState.ntpServer1 ?? '',
         varState.ntpServer2 ?? '',
         varState.ntpServer3 ?? '',
         varState.ntpServer4 ?? '',
     ].map((value) => value?.trim() ?? '');
 
-    while (servers.length < MAX_NTP_SERVERS) {
-        servers.push('');
-    }
-
     return servers;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df8c096 and c56c3fd.

📒 Files selected for processing (7)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (1 hunks)
  • web/src/components/Activation/ActivationSteps.vue (1 hunks)
  • web/src/components/Activation/WelcomeModal.standalone.vue (3 hunks)
🧰 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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.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/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts
  • api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/Activation/WelcomeModal.standalone.vue
🧠 Learnings (3)
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.

Applied to files:

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

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.

Applied to files:

  • web/src/components/Activation/WelcomeModal.standalone.vue
🧬 Code graph analysis (4)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (2)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (71-124)
api/src/unraid-api/graph/resolvers/system-time/system-time.model.ts (1)
  • MANUAL_TIME_REGEX (58-58)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (3)
api/src/core/types/states/var.ts (1)
  • Var (12-242)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (71-124)
api/src/core/utils/plugins/php-loader.ts (1)
  • phpLoader (38-66)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (6)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • Resolver (19-97)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (1)
  • Resolver (15-60)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (14-50)
api/src/unraid-api/cli/generated/graphql.ts (3)
  • OnboardingMutations (1551-1555)
  • UpgradeInfo (2375-2387)
  • CompleteUpgradeStepInput (471-474)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.ts (1)
  • buildUpgradeInfoFromSnapshot (4-22)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (4)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (1)
  • Resolver (12-27)
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts (1)
  • Resolver (15-60)
api/src/unraid-api/cli/generated/graphql.ts (4)
  • Customization (625-630)
  • Query (1712-1782)
  • ActivationOnboarding (122-134)
  • ActivationOnboardingStep (136-146)
api/src/unraid-api/auth/public.decorator.ts (1)
  • Public (4-4)
🔇 Additional comments (13)
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts (3)

1-10: LGTM! Imports follow ESM conventions.

All TypeScript imports correctly use .js extensions for ESM compatibility, as required by the coding guidelines.


16-22: Authorization properly enforced.

The @UsePermissions decorator correctly protects this state-changing mutation with AuthAction.UPDATE_ANY and Resource.WELCOME. This addresses the critical authorization concern raised in previous reviews.


23-26: LGTM! Clean and consistent implementation.

The mutation logic is straightforward and correct:

  • Properly typed parameters and return value
  • Reuses the buildUpgradeInfoFromSnapshot utility for consistency with VersionsResolver.upgrade()
  • Error handling appropriately delegated to the NestJS/GraphQL framework
api/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.ts (1)

15-19: LGTM! Mock setup follows best practices.

The mock declarations are correctly placed at the top level, and the factory function pattern for module mocks is appropriate. The spy on phpLoader allows for selective mocking while preserving the module structure.

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

56-77: Logic for currentVersion field may hide partial data.

Line 73 returns currentVersion: undefined whenever hasBothVersions is false, even when snapshot.currentVersion exists. This means if lastTrackedVersion is undefined (e.g., fresh install) but currentVersion is available, the API returns currentVersion: undefined instead of the actual version.

Verify whether this is intentional. If consumers need to know the current OS version regardless of upgrade state, consider returning snapshot.currentVersion directly without the guard.

Proposed alternative if current version should always be returned:

         const hasBothVersions = snapshot.lastTrackedVersion != null && snapshot.currentVersion != null;

         return {
             isUpgrade: hasBothVersions && snapshot.lastTrackedVersion !== snapshot.currentVersion,
             previousVersion:
                 hasBothVersions && snapshot.lastTrackedVersion !== snapshot.currentVersion
                     ? snapshot.lastTrackedVersion
                     : undefined,
-            currentVersion: hasBothVersions ? snapshot.currentVersion : undefined,
+            currentVersion: snapshot.currentVersion,
             hasPendingSteps: steps.some((step) => !step.completed),
             steps,
         };
web/src/components/Activation/ActivationSteps.vue (2)

2-2: Good: explicit Vue imports.

Explicit computed/ref/onMounted/onUnmounted imports ensure tests/SSR don’t rely on auto-imports.

As per coding guidelines


89-94: Defensive clamping looks good.

Offset handling and bounds clamping for currentStepIndex are correct and avoid out-of-range errors.

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

1-14: LGTM!

Imports follow TypeScript ESM conventions with .js extensions as required by the coding guidelines, and the MAX_NTP_SERVERS constant is appropriately defined.


21-31: LGTM!

The getSystemTime method correctly retrieves system time configuration from the var state, extracts NTP servers, and returns a well-typed SystemTime object with appropriate defaults.


105-120: LGTM!

The normalizeNtpServers method correctly handles both cases: using current state when no override is provided, and sanitizing/padding the override array to exactly 4 elements when provided.


129-136: LGTM!

The timezone validation using Intl.DateTimeFormat is a robust, standard approach that leverages the JavaScript runtime's built-in IANA timezone database. Error handling is appropriate with clear messaging.


138-157: LGTM!

Both helper methods are implemented correctly:

  • formatManualDateTime properly formats the date in the expected YYYY-MM-DD HH:MM:SS format.
  • resetTimezoneWatcher appropriately calls the PHP script with tolerant error handling, logging warnings without failing the operation, which is correct for this non-critical task.

122-127: Verify 40-character limit in sanitizeNtpServer (system-time.service.ts:126)
DNS hostnames can reach 253 characters per RFC 1123; slicing to 40 chars may truncate valid NTP server names. Confirm this limit with Unraid’s emcmd interface or document the rationale.

const resolver = new CustomizationResolver(onboardingService, onboardingTracker);

it('maps onboarding tracker snapshot into activation onboarding response', async () => {
(onboardingTracker.getUpgradeSnapshot as any).mockResolvedValue({
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove any type cast.

Casting to any violates the coding guideline "Never use the any type". Vitest's vi.fn() returns a properly typed mock that accepts mockResolvedValue without needing a cast.

As per coding guidelines.

Apply this diff:

-        (onboardingTracker.getUpgradeSnapshot as any).mockResolvedValue({
+        vi.mocked(onboardingTracker.getUpgradeSnapshot).mockResolvedValue({
📝 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
(onboardingTracker.getUpgradeSnapshot as any).mockResolvedValue({
vi.mocked(onboardingTracker.getUpgradeSnapshot).mockResolvedValue({
🤖 Prompt for AI Agents
In
api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
around line 21, remove the unnecessary "as any" cast on
onboardingTracker.getUpgradeSnapshot; instead rely on Vitest's properly typed
mock (or explicitly type the mock as vi.MockedFunction<typeof
onboardingTracker.getUpgradeSnapshot> if needed) and call mockResolvedValue
directly on the mocked function so the test conforms to the "never use any"
guideline.

});

it('omits upgrade metadata when snapshot versions are incomplete', async () => {
(onboardingTracker.getUpgradeSnapshot as any).mockResolvedValue({
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove any type cast.

Same issue as line 21—avoid any casts.

As per coding guidelines.

Apply this diff:

-        (onboardingTracker.getUpgradeSnapshot as any).mockResolvedValue({
+        vi.mocked(onboardingTracker.getUpgradeSnapshot).mockResolvedValue({
📝 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
(onboardingTracker.getUpgradeSnapshot as any).mockResolvedValue({
vi.mocked(onboardingTracker.getUpgradeSnapshot).mockResolvedValue({
🤖 Prompt for AI Agents
In
api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
around line 64, remove the "any" cast on onboardingTracker.getUpgradeSnapshot;
instead spy on or properly type the function mock. Replace the cast expression
with either jest.spyOn(onboardingTracker,
'getUpgradeSnapshot').mockResolvedValue({...}) or cast to a typed Jest mock such
as (onboardingTracker.getUpgradeSnapshot as jest.MockedFunction<typeof
onboardingTracker.getUpgradeSnapshot>).mockResolvedValue({...}) so the test
avoids using any.

Comment on lines +33 to +88
public async updateSystemTime(input: UpdateSystemTimeInput): Promise<SystemTime> {
const current = this.configService.get<Partial<Var>>('store.emhttp.var', {});

const desiredTimeZone = (input.timeZone ?? current.timeZone)?.trim();
if (!desiredTimeZone) {
throw new BadRequestException('A valid time zone is required.');
}
this.validateTimeZone(desiredTimeZone);

const desiredUseNtp = input.useNtp ?? Boolean(current.useNtp);
const desiredServers = this.normalizeNtpServers(input.ntpServers, current);

const commands: Record<string, string> = {
setDateTime: 'apply',
timeZone: desiredTimeZone,
USE_NTP: desiredUseNtp ? 'yes' : 'no',
};

desiredServers.forEach((server, index) => {
commands[`NTP_SERVER${index + 1}`] = server;
});

const switchingToManual = desiredUseNtp === false && Boolean(current.useNtp);
if (desiredUseNtp === false) {
let manualDateTime = input.manualDateTime?.trim();
if (switchingToManual && !manualDateTime) {
throw new BadRequestException(
'manualDateTime is required when disabling NTP synchronization.'
);
}
if (!manualDateTime) {
manualDateTime = this.formatManualDateTime(new Date());
}
commands.newDateTime = manualDateTime;
}

const timezoneChanged = desiredTimeZone !== (current.timeZone ?? '');

this.logger.log(
`Updating system time settings (zone=${desiredTimeZone}, useNtp=${desiredUseNtp}, timezoneChanged=${timezoneChanged})`
);

try {
await emcmd(commands, { waitForToken: true });
this.logger.log('emcmd executed successfully for system time update.');
} catch (error) {
this.logger.error('Failed to update system time via emcmd', error as Error);
throw error;
}

if (timezoneChanged) {
await this.resetTimezoneWatcher();
}

return this.getSystemTime();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify store freshness after emcmd execution.

The method logic is well-structured with proper validation and error handling. However, at line 87, getSystemTime() is called immediately after emcmd() to return updated state, which reads from ConfigService store data.

Please verify that the store is updated synchronously after emcmd() completes, or if there's an automatic mechanism that refreshes the store. If the store remains stale, consider returning the values that were just applied (optimistic response) instead of reading from the store.

Run the following verification to check if there's a store update mechanism after emcmd:


🏁 Script executed:

#!/bin/bash
# Description: Search for store update or state reload mechanisms after emcmd calls

# Search for patterns that might update the store after emcmd
rg -n -C3 --type=ts 'emcmd.*await|\.dispatch.*load|store.*update' api/src/

Length of output: 11016


Reload store state after emcmd before calling getSystemTime()
getSystemTime() reads from ConfigService which isn’t refreshed by emcmd; after await emcmd(...), dispatch loadSingleStateFile(StateFileKey.var) or return the applied settings directly to avoid stale data.

🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts around
lines 33 to 88, after the await emcmd(...) call the code calls getSystemTime()
which reads from ConfigService but ConfigService is not refreshed by emcmd;
update the function to refresh the in-memory store before reading by dispatching
loadSingleStateFile(StateFileKey.var) (importing StateFileKey and the loader if
needed) immediately after the successful emcmd and before
resetTimezoneWatcher/getSystemTime, or alternatively skip getSystemTime() and
return a SystemTime object constructed from the applied values (desiredTimeZone,
desiredUseNtp, desiredServers and manualDateTime) to avoid stale data.

- Added a test to infer config type from file extension when not provided, improving flexibility in config loading.
- Introduced a normalization function for file extensions to ensure consistent validation of config types.
- Refactored state file update handling in the StateManager to streamline event processing and improve error logging.
- Enhanced OnboardingTracker to handle cases where the version is unavailable, ensuring onboarding steps are still surfaced.

This update improves the robustness and maintainability of the configuration and onboarding processes.
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/config/onboarding-tracker.module.ts (1)

196-196: Fix Nest Logger usage (message/trace ordering)

The Nest Logger calls currently pass the Error object as the first argument and the message second. These should be swapped to pass a clear string message first and the error stack or message as the second argument.

This issue was already flagged in a previous review. Please apply the suggested diff from the earlier comment to fix all instances.

Also applies to: 255-255, 265-265, 275-275

🧹 Nitpick comments (2)
api/src/__test__/core/utils/misc/parse-config.test.ts (1)

64-68: LGTM! Verifies type inference behavior.

The test correctly validates that parseConfig can infer the configuration type from the file extension when the type parameter is omitted.

Optional: Consider additional test cases for edge cases.

While the current test covers the happy path, you could add test cases for:

  • .cfg extension inference
  • Case-insensitive extensions (.INI, .Cfg)
  • Uppercase extensions without dots (INI, CFG)

Example:

test('it handles case-insensitive extensions when type not provided', () => {
    // Would need a test file with .INI or similar
    // Or mock fileExistsSync to return true
});

test('it infers cfg type from file extension', () => {
    // Would need a .cfg test file
});

This would provide more thorough coverage of the normalization logic, but is not critical since the existing test confirms the core functionality works.

api/src/unraid-api/config/onboarding-tracker.module.ts (1)

114-156: Consider validating stepId parameter

The markStepCompleted method accepts any ActivationOnboardingStepId but doesn't validate whether it's a known/valid step before processing. While the current implementation handles unknown steps gracefully (they won't match any definition), adding explicit validation could improve error messages and debugging.

Example validation:

 async markStepCompleted(stepId: ActivationOnboardingStepId): Promise<UpgradeProgressSnapshot> {
+    if (!Object.values(ActivationOnboardingStepId).includes(stepId)) {
+        this.logger.warn(`Unknown onboarding step ID: ${stepId}`);
+        return this.getUpgradeSnapshot();
+    }
+
     const currentVersion =
         this.currentVersion ??
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c56c3fd and 56cfc87.

📒 Files selected for processing (5)
  • api/src/__test__/core/utils/misc/parse-config.test.ts (1 hunks)
  • api/src/core/utils/misc/parse-config.ts (2 hunks)
  • api/src/store/watch/state-watch.ts (1 hunks)
  • api/src/unraid-api/config/api-config.test.ts (3 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/{store,stores}/**/*.ts

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

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • api/src/store/watch/state-watch.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/store/watch/state-watch.ts
  • api/src/core/utils/misc/parse-config.ts
  • api/src/__test__/core/utils/misc/parse-config.test.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/store/watch/state-watch.ts
  • api/src/core/utils/misc/parse-config.ts
  • api/src/__test__/core/utils/misc/parse-config.test.ts
  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/onboarding-tracker.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__/core/utils/misc/parse-config.test.ts
  • api/src/unraid-api/config/api-config.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__/core/utils/misc/parse-config.test.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/__test__/core/utils/misc/parse-config.test.ts
  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/__test__/core/utils/misc/parse-config.test.ts
  • api/src/unraid-api/config/api-config.test.ts
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
  • api/src/unraid-api/config/onboarding-tracker.module.ts
🧬 Code graph analysis (4)
api/src/store/watch/state-watch.ts (3)
api/src/core/log.ts (1)
  • emhttpLogger (86-86)
api/src/store/index.ts (2)
  • store (6-12)
  • getters (18-23)
api/src/store/modules/emhttp.ts (1)
  • loadSingleStateFile (107-141)
api/src/__test__/core/utils/misc/parse-config.test.ts (1)
api/src/core/utils/misc/parse-config.ts (1)
  • parseConfig (156-213)
api/src/unraid-api/config/api-config.test.ts (2)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.module.ts (8)
api/src/unraid-api/config/api-config.module.ts (2)
  • Injectable (48-110)
  • Module (113-118)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (5)
  • Injectable (91-525)
  • resolveActivationStepDefinitions (79-89)
  • ActivationStepDefinition (47-52)
  • ActivationStepContext (42-45)
  • findActivationCodeFile (20-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.model.ts (4)
  • TrackerState (8-12)
  • UpgradeProgressSnapshot (20-25)
  • CompletedStepState (3-6)
  • UpgradeStepState (14-18)
api/src/store/index.ts (1)
  • getters (18-23)
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1)
  • Module (7-11)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-38)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
🔇 Additional comments (12)
api/src/store/watch/state-watch.ts (2)

48-66: LGTM! Excellent refactor that eliminates duplication.

The new handleStateFileUpdate method properly encapsulates the shared logic for handling both 'add' and 'change' events. The implementation includes:

  • Appropriate early return when state file key cannot be resolved
  • Proper async/await usage for the dispatch call
  • Comprehensive error handling that logs but doesn't propagate (correct for file watchers to prevent cascading failures)
  • Well-chosen logging levels (trace for resolution failures, debug for normal ops, error for failures)

75-76: LGTM! Clean delegation pattern.

Both event handlers now correctly delegate to the shared handleStateFileUpdate method, eliminating code duplication while maintaining consistent behavior.

api/src/core/utils/misc/parse-config.ts (2)

127-132: LGTM! Clean normalization logic.

The normalizeExtension helper correctly handles extensions with or without leading dots and different casing, ensuring consistent validation.


151-154: LGTM! Robust validation with normalization.

The updated validation correctly normalizes extensions before checking, making it handle both dotted (.ini) and case-insensitive (INI, Ini) variations consistently.

api/src/unraid-api/config/onboarding-tracker.module.ts (5)

165-184: LGTM: Proper version comparison for completion tracking

The logic correctly uses isCompletionUpToDate with semver comparison to determine if a step's completion is still valid for the current step definition version. This handles patch upgrades gracefully while re-prompting for steps when definitions are upgraded.


228-238: LGTM: Robust version comparison with fallback

The version comparison properly uses semver's compare with a try-catch fallback to string equality. This handles both semantic versions and any potential non-semver version strings safely.


270-278: LGTM: Atomic writes with proper permissions

The use of atomically package's writeFile with mode 0o644 ensures safe, atomic writes to the tracker state file, preventing partial writes or corruption.


53-67: LGTM: Bootstrap logic correctly handles version unavailability

The bootstrap method properly handles the case where version cannot be read, clearing state and syncing undefined to config, which prevents stale data from causing issues.


69-87: LGTM: Shutdown only persists when version changes

The shutdown logic correctly skips writing when lastTrackedVersion already matches currentVersion, preventing unnecessary disk I/O on restarts without upgrades.

api/src/unraid-api/config/api-config.test.ts (3)

23-47: LGTM: Proper mock setup with factory functions

The mocks are correctly set up using vi.mock with factory functions at the top level, and mock store implementation properly provides test data via getters. This follows Vitest best practices and avoids hoisting issues.

As per coding guidelines: "Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls."


121-459: LGTM: Comprehensive test coverage for OnboardingTracker

The test suite thoroughly covers:

  • Lifecycle behavior (bootstrap/shutdown)
  • Version tracking and upgrade detection
  • Step completion with version comparison
  • Edge cases (missing files, version unavailability, activation code presence)
  • State persistence and deferred writes

The tests properly use mocks, handle async operations with await, and assert on observable behavior rather than implementation details.


129-150: LGTM: Proper mock reset in beforeEach

The beforeEach correctly resets all mocks using mockReset() and sets up fresh test state, ensuring test isolation.

As per coding guidelines: "Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation."

- Added functionality to read and write an upgrade marker file, allowing the OnboardingTracker to manage version states more effectively.
- Enhanced the OnboardingTracker to infer the last tracked version from the upgrade marker, improving the onboarding experience during version upgrades.
- Updated tests to verify the correct behavior of the upgrade marker handling, ensuring that version information is accurately persisted and retrieved.

This update enhances the onboarding process by providing a reliable mechanism for tracking version upgrades, improving user experience during transitions between versions.
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/config/onboarding-tracker.module.ts (1)

188-188: Fix Nest Logger argument order (message first, then stack).

NestJS Logger methods expect (message, trace?) but these calls pass the error object first. This was flagged in a previous review and remains unaddressed.

Apply this diff to correct all Logger calls:

-            this.logger.debug(error, `Unable to read upgrade marker at ${UPGRADE_MARKER_PATH}`);
+            this.logger.debug(
+                `Unable to read upgrade marker at ${UPGRADE_MARKER_PATH}`,
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.warn(error, 'Failed to persist onboarding upgrade marker');
+            this.logger.warn(
+                'Failed to persist onboarding upgrade marker',
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error(error, 'Failed to evaluate activation onboarding steps');
+            this.logger.error(
+                'Failed to evaluate activation onboarding steps',
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error(error, `Failed to read current OS version from ${this.versionFilePath}`);
+            this.logger.error(
+                `Failed to read current OS version from ${this.versionFilePath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
+            this.logger.debug(
+                `Unable to read onboarding tracker state at ${this.trackerPath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
@@
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );

Also applies to: 200-200, 235-235, 294-294, 304-304, 314-314

🧹 Nitpick comments (2)
api/src/unraid-api/config/onboarding-tracker.module.ts (2)

193-202: Consider atomic write for consistency and durability.

The upgrade marker write at line 198 uses standard writeFileFs while writeTrackerState (line 311) uses atomic writeFile from the atomically library. For consistency and durability guarantees (especially in case of power loss during write), consider using atomic writes here as well.

Apply this diff:

 private async writeUpgradeMarker(version: string | undefined): Promise<void> {
     try {
         if (!version) {
             return;
         }
-        await writeFileFs(UPGRADE_MARKER_PATH, version, 'utf8');
+        await writeFile(UPGRADE_MARKER_PATH, version, { encoding: 'utf8' });
     } catch (error) {
         this.logger.warn(error, 'Failed to persist onboarding upgrade marker');
     }
 }

Based on learnings: atomically provides atomic file writes with durability guarantees.


143-144: Optional: Use nullish coalescing for cleaner default.

The type assertion works but could be cleaner with nullish coalescing.

Apply this diff:

-        const completedSteps =
-            this.state.completedSteps ?? ({} as Record<ActivationOnboardingStepId, CompletedStepState>);
+        const completedSteps = this.state.completedSteps ?? {};

Note: This assumes TrackerState.completedSteps is properly typed as Record<ActivationOnboardingStepId, CompletedStepState> | undefined. If not, consider updating the type definition instead of using type assertions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56cfc87 and 11b541f.

📒 Files selected for processing (2)
  • api/src/unraid-api/config/api-config.test.ts (3 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
🧰 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

Files:

  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/config/api-config.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/config/api-config.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/unraid-api/config/api-config.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/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
🧬 Code graph analysis (2)
api/src/unraid-api/config/onboarding-tracker.module.ts (7)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)
  • resolveActivationStepDefinitions (79-89)
  • ActivationStepDefinition (47-52)
  • ActivationStepContext (42-45)
  • findActivationCodeFile (20-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.model.ts (4)
  • TrackerState (8-12)
  • UpgradeProgressSnapshot (20-25)
  • CompletedStepState (3-6)
  • UpgradeStepState (14-18)
api/src/store/index.ts (1)
  • getters (18-23)
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1)
  • Module (7-11)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-38)
api/src/unraid-api/config/api-config.test.ts (3)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • UPGRADE_MARKER_PATH (36-36)
🔇 Additional comments (1)
api/src/unraid-api/config/api-config.test.ts (1)

126-538: Excellent test coverage for OnboardingTracker.

The test suite comprehensively covers the OnboardingTracker lifecycle and behavior:

  • Version persistence and upgrade tracking
  • Edge cases (missing files, errors, version changes)
  • Step completion and state management
  • Conditional step inclusion based on activation code presence

Tests follow best practices with proper mock isolation, clear assertions, and meaningful test names.

- Introduced unlink functionality to manage the upgrade marker file, allowing for better cleanup during onboarding processes.
- Updated the OnboardingTracker to conditionally write the upgrade marker based on the completion of required steps, improving version tracking accuracy.
- Added logic to clear the upgrade marker when all onboarding steps are completed, ensuring a clean state for future upgrades.
- Enhanced tests to verify the correct behavior of version persistence and upgrade marker management during application shutdown.

This update improves the onboarding experience by ensuring accurate version tracking and state management during upgrades.
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/config/onboarding-tracker.module.ts (1)

214-214: Critical: Fix Nest Logger parameter order (flagged in previous review)

This issue was identified in a previous review but remains unaddressed. The Nest Logger methods expect (message, trace?) but these calls pass the error object first. This breaks observability and makes logs harder to search.

Apply these fixes:

-            this.logger.debug(error, `Unable to read upgrade marker at ${UPGRADE_MARKER_PATH}`);
+            this.logger.debug(
+                `Unable to read upgrade marker at ${UPGRADE_MARKER_PATH}`,
+                error instanceof Error ? error.stack : String(error)
+            );
-            this.logger.warn(error, 'Failed to persist onboarding upgrade marker');
+            this.logger.warn(
+                'Failed to persist onboarding upgrade marker',
+                error instanceof Error ? error.stack : String(error)
+            );
-            this.logger.debug(error, 'Failed to remove onboarding upgrade marker');
+            this.logger.debug(
+                'Failed to remove onboarding upgrade marker',
+                error instanceof Error ? error.stack : String(error)
+            );
-            this.logger.error(error, 'Failed to evaluate activation onboarding steps');
+            this.logger.error(
+                'Failed to evaluate activation onboarding steps',
+                error instanceof Error ? error.stack : String(error)
+            );
-            this.logger.error(error, `Failed to read current OS version from ${this.versionFilePath}`);
+            this.logger.error(
+                `Failed to read current OS version from ${this.versionFilePath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
-            this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
+            this.logger.debug(
+                `Unable to read onboarding tracker state at ${this.trackerPath}`,
+                error instanceof Error ? error.stack : String(error)
+            );
-            this.logger.error(error, 'Failed to persist onboarding tracker state');
+            this.logger.error(
+                'Failed to persist onboarding tracker state',
+                error instanceof Error ? error.stack : String(error)
+            );

Also applies to: 226-226, 237-237, 289-289, 348-348, 358-358, 368-368

🧹 Nitpick comments (4)
api/src/unraid-api/config/onboarding-tracker.module.ts (3)

99-100: Refactor: Extract completedSteps initialization to reduce duplication

The pattern this.state.completedSteps ?? ({} as Record<ActivationOnboardingStepId, CompletedStepState>) appears four times. Extract to a helper method.

Add this helper method:

private getCompletedSteps(): Record<ActivationOnboardingStepId, CompletedStepState> {
    return this.state.completedSteps ?? {};
}

Then replace all occurrences:

-        const completedEntries =
-            this.state.completedSteps ?? ({} as Record<ActivationOnboardingStepId, CompletedStepState>);
+        const completedEntries = this.getCompletedSteps();

Also applies to: 162-163, 242-243, 334-335


69-75: Clarify: Complex version inference logic needs documentation

The conditional logic for inferring lastTrackedVersion from the marker is subtle. Add a comment explaining when the marker takes precedence.

Add explanatory comment:

+        // Use marker version if:
+        // 1. Marker exists and differs from current version, AND
+        // 2. State has no lastTrackedVersion OR state's version matches current
+        // This handles the case where the tracker was updated to current version
+        // but a marker from the previous version still exists
         if (
             markerVersion &&
             markerVersion !== this.currentVersion &&
             (inferredLastTrackedVersion == null || inferredLastTrackedVersion === this.currentVersion)
         ) {
             inferredLastTrackedVersion = markerVersion;
         }

333-340: Document: Explain why multiple config.set calls are needed

Setting four config values individually could be batched or explained. Clarify whether order matters or if ConfigService lacks a batch API.

Add a comment:

     private syncConfig(currentVersion: string | undefined) {
         const completedStepsMap = this.getCompletedSteps();
+        // Sync multiple config paths to keep state consistent across stores
+        // (ConfigService doesn't support batched updates)
         this.configService.set(`${CONFIG_PREFIX}.currentVersion`, currentVersion);
api/src/unraid-api/config/api-config.test.ts (1)

412-417: Clarify: Magic version '7.0.0' in assertion

Line 415 asserts that the completed TIMEZONE step has version '7.0.0', but the test setup uses version '7.2.0'. Explain where '7.0.0' comes from (likely the step's introducedIn value).

Add a comment:

         expect(setMock).toHaveBeenCalledWith(
             'onboardingTracker.completedSteps',
             expect.objectContaining({
+                // TIMEZONE step was introduced in 7.0.0 per step definitions
                 TIMEZONE: expect.objectContaining({ version: '7.0.0' }),
             })
         );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11b541f and 4165575.

📒 Files selected for processing (2)
  • api/src/unraid-api/config/api-config.test.ts (3 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
🧰 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

Files:

  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/config/api-config.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/onboarding-tracker.module.ts
  • api/src/unraid-api/config/api-config.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/unraid-api/config/api-config.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/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/config/api-config.test.ts
🧬 Code graph analysis (2)
api/src/unraid-api/config/onboarding-tracker.module.ts (8)
api/src/unraid-api/config/api-config.module.ts (1)
  • Module (113-118)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)
  • resolveActivationStepDefinitions (79-89)
  • ActivationStepDefinition (47-52)
  • ActivationStepContext (42-45)
  • findActivationCodeFile (20-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.model.ts (4)
  • TrackerState (8-12)
  • CompletedStepState (3-6)
  • UpgradeProgressSnapshot (20-25)
  • UpgradeStepState (14-18)
api/src/store/index.ts (1)
  • getters (18-23)
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1)
  • Module (7-11)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-38)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (39-80)
api/src/unraid-api/config/api-config.test.ts (3)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • UPGRADE_MARKER_PATH (36-36)
🔇 Additional comments (5)
api/src/unraid-api/config/onboarding-tracker.module.ts (3)

326-330: Verify: Empty catch block might hide semver comparison errors

The try-catch silently falls back to string comparison if compare() throws. This could hide issues like malformed version strings that should be logged.

Consider logging comparison failures:

         try {
             return compare(existingVersion, requiredVersion) >= 0;
-        } catch {
+        } catch (error) {
+            this.logger.debug(
+                `Semver comparison failed for versions ${existingVersion} and ${requiredVersion}, falling back to string equality`,
+                error instanceof Error ? error.stack : String(error)
+            );
             return existingVersion === requiredVersion;
         }

54-86: LGTM: Lifecycle hooks correctly manage upgrade state

The bootstrap and shutdown hooks properly coordinate version tracking, marker management, and state persistence. The logic correctly defers final version updates until all required steps complete.

Also applies to: 88-120


147-196: LGTM: Step completion logic handles all cases correctly

The markStepCompleted method properly validates versions, updates state atomically, and clears the upgrade marker when appropriate. The fallback behavior when version is unavailable is appropriate.

api/src/unraid-api/config/api-config.test.ts (2)

128-627: LGTM: Comprehensive test coverage for OnboardingTracker

The test suite thoroughly covers:

  • Bootstrap and shutdown lifecycle
  • Version file handling (present, missing, permission errors)
  • Upgrade marker semantics
  • Step completion and persistence
  • Activation code conditional logic
  • Version upgrade scenarios

The mocking strategy properly isolates file I/O and external dependencies.


78-95: LGTM: ApiConfigPersistence tests verify migration and defaults

The tests correctly verify:

  • Default config generation with API_VERSION
  • Legacy config migration (origin parsing, ssoSubIds parsing)
  • Bootstrap version setting

Also applies to: 122-126

…ingTracker

- Added functionality to the OnboardingTracker to manage first boot completion state, ensuring accurate onboarding flow.
- Introduced tests to verify the behavior of first boot completion logic, including scenarios for both new and existing states.
- Updated the OnboardingService to utilize the new first boot completion check, enhancing the onboarding process by preventing redundant setups.

This update improves the onboarding experience by accurately tracking first boot completion, ensuring a smoother initialization process for users.
- Changed the time format in the logging configuration from 'HH:mm:ss' to 'SYS:HH:MM:ss' to ensure compatibility with system time settings.
- This update improves the accuracy of log timestamps, enhancing the overall logging functionality.
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/src/unraid-api/config/api-config.test.ts (1)

34-35: Consider more precise typing for mock objects.

While as any is used here for test mocks, consider defining more precise types or interfaces for mockEmhttpState and mockPathsState to improve type safety and catch potential issues during test development.

For example:

-const mockEmhttpState = { var: { regState: 'PRO' } } as any;
-const mockPathsState = { activationBase: '/activation' } as any;
+interface MockEmhttpState {
+    var: {
+        regState: string;
+    };
+}
+
+interface MockPathsState {
+    activationBase: string;
+}
+
+const mockEmhttpState: MockEmhttpState = { var: { regState: 'PRO' } };
+const mockPathsState: MockPathsState = { activationBase: '/activation' };
api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts (1)

61-62: Consider simplifying the condition expression.

The Boolean() wrapper is redundant since startsWith() already returns a boolean, and the optional chaining handles undefined correctly.

Apply this diff to simplify:

-        condition: (context) =>
-            context.hasActivationCode && Boolean(context.regState?.startsWith('ENOKEYFILE')),
+        condition: (context) =>
+            context.hasActivationCode && (context.regState?.startsWith('ENOKEYFILE') ?? 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 4165575 and b5d084b.

📒 Files selected for processing (7)
  • api/src/core/log.ts (1 hunks)
  • api/src/unraid-api/config/api-config.test.ts (3 hunks)
  • api/src/unraid-api/config/onboarding-tracker.model.ts (1 hunks)
  • api/src/unraid-api/config/onboarding-tracker.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts (13 hunks)
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/src/unraid-api/config/onboarding-tracker.model.ts
  • api/src/unraid-api/config/onboarding-tracker.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

Files:

  • api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/core/log.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts
  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/core/log.ts
  • api/src/unraid-api/config/api-config.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/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/config/api-config.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
  • api/src/unraid-api/config/api-config.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/config/api-config.test.ts
🧠 Learnings (3)
📚 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/customization/onboarding.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} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

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

Applied to files:

  • api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts
🧬 Code graph analysis (3)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (2)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • Injectable (38-392)
api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts (1)
  • findActivationCodeFile (7-27)
api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts (1)
packages/unraid-shared/src/util/file.ts (1)
  • fileExists (12-15)
api/src/unraid-api/config/api-config.test.ts (3)
api/src/unraid-api/config/api-config.module.ts (2)
  • defaultConfig (77-79)
  • loadApiConfig (28-40)
api/src/environment.ts (1)
  • PATHS_CONFIG_MODULES (108-109)
api/src/unraid-api/config/onboarding-tracker.module.ts (1)
  • UPGRADE_MARKER_PATH (36-36)
🔇 Additional comments (14)
api/src/unraid-api/config/api-config.test.ts (4)

78-125: LGTM!

The ApiConfigPersistence tests provide good coverage of the basic functionality including file naming, configuration keys, default config generation, legacy migration, and bootstrap behavior. The test setup is clean and properly isolated.


468-473: Verify expected completion version.

The test expects the completed step to be recorded with version '7.0.0' (line 471), but the current version in this test scenario is '7.2.0' (line 421).

If this is intentional (e.g., recording the version when the step was first introduced rather than when it was completed), consider adding a comment to clarify this behavior. Otherwise, verify that the expected version is correct.

Could you confirm whether completed steps should record:

  1. The version when the step was completed (current version '7.2.0'), or
  2. The version when the step was introduced (definition version '7.0.0')?

163-683: Excellent test coverage for OnboardingTracker.

The OnboardingTracker test suite comprehensively covers critical scenarios including:

  • First boot detection and completion
  • Version tracking across boot/shutdown cycles
  • Upgrade path handling with the upgrade marker
  • Step completion and persistence
  • Edge cases (missing files, permission errors)
  • Conditional step inclusion (activation)

The tests effectively validate both in-memory state management and disk persistence, ensuring the onboarding tracking system works correctly across various scenarios.


686-714: LGTM!

The loadApiConfig tests correctly validate that the function returns the default configuration with API_VERSION as the version, both in the success path and when handling errors. This aligns with the implementation shown in the relevant code snippets where version: API_VERSION overrides any disk config version.

api/src/unraid-api/graph/resolvers/customization/activation-steps.util.ts (3)

7-27: LGTM!

The function correctly handles directory access, file searching, and error cases. The optional logger parameter provides flexibility, and returning null on errors is a safe defensive approach.


29-39: LGTM!

The type definitions are clean and well-structured. Supporting both synchronous and asynchronous conditions provides good flexibility for different evaluation scenarios.


66-76: LGTM!

The function correctly filters step definitions by evaluating their conditions, properly handling both synchronous and asynchronous condition functions.

api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts (4)

13-13: LGTM!

The dependency injection and imports are correctly implemented. The OnboardingTracker integration follows NestJS patterns and provides cleaner separation of concerns.

Also applies to: 18-18, 32-32


34-43: LGTM!

The method correctly delegates first-boot tracking to OnboardingTracker, providing cleaner separation of concerns. The boolean return value clearly indicates whether first boot was already completed.


54-57: LGTM!

The early guard for missing configFile is good defensive programming. It prevents undefined errors downstream and provides a clear error message for debugging.


100-102: LGTM!

The delegation to findActivationCodeFile removes code duplication and provides a cleaner, more maintainable implementation. The utility function is properly parameterized with directory, extension, and logger.

api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts (3)

99-101: LGTM!

The OnboardingTracker mock is properly set up with correct typing, default behavior, and cleanup between tests. The dependency injection in the testing module correctly provides the mock.

Also applies to: 144-158, 161-165


227-238: LGTM!

The test correctly verifies the new behavior where first boot completion is tracked via OnboardingTracker instead of direct file operations. The expectations align with the service implementation.


103-104: LGTM!

All describe blocks and type annotations have been consistently updated from CustomizationService to OnboardingService. The naming is clear and reflects the service's refactored purpose.

Also applies to: 879-879, 1062-1063

ignore: 'hostname,pid',
destination: logDestination,
translateTime: 'HH:mm:ss',
translateTime: 'SYS:HH:MM:ss',
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect translateTime format string.

The translateTime value 'SYS:HH:MM:ss' mixes a system timezone prefix with a custom format string incorrectly. According to pino-pretty documentation, valid formats are either:

  • Format strings: 'HH:MM:ss', 'yyyy-mm-dd HH:MM:ss'
  • System timezone specifiers: 'SYS:standard', 'UTC:standard'

The current format will likely produce malformed timestamps.

Apply this diff to use the correct format:

-            translateTime: 'SYS:HH:MM:ss',
+            translateTime: 'HH:MM:ss',

If system timezone formatting is required, use:

-            translateTime: 'SYS:HH:MM:ss',
+            translateTime: 'SYS:standard',
📝 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
translateTime: 'SYS:HH:MM:ss',
translateTime: 'HH:MM:ss',
🤖 Prompt for AI Agents
In api/src/core/log.ts around line 33 the translateTime value 'SYS:HH:MM:ss' is
invalid for pino-pretty; replace it with a valid format—either a pure format
like 'HH:MM:ss' (for local time without SYS prefix) or a system timezone
specifier like 'SYS:standard' (if you need system timezone behavior); update the
translateTime string accordingly to one of these valid options.

Comment on lines +436 to +437
mockReaddir.mockResolvedValue(['pending.activationcode'] as unknown as ReaddirResult);
mockEmhttpState.var.regState = 'ENOKEYFILE_PENDING';
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 15, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent mock usage pattern.

Line 436 uses mockResolvedValue (persistent mock) while a similar setup at line 665 uses mockResolvedValueOnce (single-use mock). Since beforeEach at line 151 already sets a default empty array for mockReaddir, using mockResolvedValue here will persist beyond this test and potentially affect subsequent tests.

Apply this diff to use the single-call pattern consistently:

-        mockReaddir.mockResolvedValue(['pending.activationcode'] as unknown as ReaddirResult);
+        mockReaddir.mockResolvedValueOnce(['pending.activationcode'] as unknown as ReaddirResult);
📝 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
mockReaddir.mockResolvedValue(['pending.activationcode'] as unknown as ReaddirResult);
mockEmhttpState.var.regState = 'ENOKEYFILE_PENDING';
mockReaddir.mockResolvedValueOnce(['pending.activationcode'] as unknown as ReaddirResult);
mockEmhttpState.var.regState = 'ENOKEYFILE_PENDING';
🤖 Prompt for AI Agents
In api/src/unraid-api/config/api-config.test.ts around lines 436 to 437, the
test sets mockReaddir.mockResolvedValue which creates a persistent mock and can
leak into other tests; change this to
mockReaddir.mockResolvedValueOnce(['pending.activationcode'] as unknown as
ReaddirResult) to make it a single-call mock consistent with the pattern used at
line 665, keeping the existing mockEmhttpState.var.regState =
'ENOKEYFILE_PENDING' unchanged.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reverted back to mockResolvedValue - This test needs the mock to persist for multiple calls since markStepCompleted() likely calls readdir again

Copy link
Contributor

Choose a reason for hiding this comment

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

@elibosley, you're absolutely right! Your reasoning is sound. If markStepCompleted() makes multiple calls to readdir during the test execution, then mockResolvedValue is the correct choice to ensure all calls return the expected value.

Additionally, the concern about the mock persisting into other tests isn't an issue here because the beforeEach at line 154 calls mockReaddir.mockReset(), which clears all mock implementations before each test runs. This ensures test isolation.

Your approach is correct for this scenario. If you want to make the intent even clearer for future maintainers, you could consider adding a brief comment explaining that the mock needs to persist for multiple readdir calls within this test, but that's entirely optional.

…nused pages

- Removed the `onComplete` prop from the `ActivationModal` component to streamline its functionality.
- Deleted several unused pages including `apikeys.vue`, `changelog.vue`, `flashbackup.vue`, `index.vue`, `login.query.ts`, `login.vue`, `redirect.vue`, `welcome.vue`, `apikeyauthorize.vue`, and `test-update-modal.vue` to improve codebase maintainability.
- This update enhances the clarity and performance of the application by eliminating unnecessary files and simplifying the component structure.
- Updated the `ActivationModal` component to include a new method for completing pending upgrade steps, improving the onboarding experience.
- Modified the `closeModal` function to ensure pending steps are marked as complete before closing the modal.
- Enhanced tests to verify the correct behavior of upgrade step completion and modal interactions.
- Updated documentation to reflect changes in upgrade onboarding behavior.

This update improves the user experience by ensuring that all pending upgrade steps are properly handled during the activation process.
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)
web/src/components/Activation/ActivationModal.vue (1)

36-47: Theme initialization still executes at module load time.

This IIFE remains problematic as previously noted. Module-load execution can cause SSR issues, race conditions, and unpredictable behavior before the component and stores are fully initialized.

As previously suggested, move the theme initialization to onMounted:

-// Apply theme when modal opens
-(async () => {
-  try {
-    await themeStore.setTheme();
-    // Ensure CSS variables are applied
-    themeStore.setCssVars();
-  } catch (error) {
-    console.error('Error setting theme:', error);
-  }
-})();
+import { computed, ref, watch, onMounted } from 'vue';

+onMounted(async () => {
+  try {
+    await themeStore.setTheme();
+    themeStore.setCssVars();
+  } catch (error) {
+    console.error('Error setting theme:', error);
+  }
+});
🧹 Nitpick comments (5)
web/src/components/Activation/ActivationModal.vue (5)

177-180: Remove or guard debug console.log in production.

Debug logging should be removed or wrapped in a DEV check to prevent noise in production.

Apply this diff:

 const handleTimezoneComplete = async () => {
-  console.log('[ActivationModal] Timezone complete, moving to next step');
+  if (import.meta.env.DEV) {
+    console.log('[ActivationModal] Timezone complete, moving to next step');
+  }
   await goToNextStep();
 };

Or simply remove the log if it's no longer needed.


217-220: Remove or guard debug console.log in production.

Debug logging should be removed or wrapped in a DEV check.

Apply this diff:

     case 'WELCOME':
-      console.log('[ActivationModal] WELCOME step props:', {
-        currentVersion: currentVersion.value,
-        previousVersion: previousVersion.value,
-      });
+      if (import.meta.env.DEV) {
+        console.log('[ActivationModal] WELCOME step props:', {
+          currentVersion: currentVersion.value,
+          previousVersion: previousVersion.value,
+        });
+      }
       return {

130-139: Consider adding user-facing error feedback.

Mutation failures are currently only logged to the console. Users won't be notified if a step fails to complete, which could cause confusion about their progress.

Consider adding user-visible error handling:

 const markUpgradeStepCompleted = async (stepId: StepId | null) => {
   if (!stepId) return;

   try {
     await completeUpgradeStepMutation({ input: { stepId } });
     await refetchActivationOnboarding();
   } catch (error) {
     console.error('[ActivationModal] Failed to mark upgrade step completed', error);
+    // TODO: Show user-facing error notification
+    // Consider using a toast/notification system here
   }
 };

68-70: Type assertion is safe but could be more defensive.

The type assertion on stepComponents assumes it matches Record<StepId, Component>. The nullish coalescing operator provides a fallback, making this safe. However, consider adding a runtime check for clarity.

If desired, you could add a runtime check for additional safety:

 const currentStepComponent = computed<Component | null>(() =>
-  currentStep.value ? ((stepComponents as Record<StepId, Component>)[currentStep.value] ?? null) : null
+  currentStep.value && stepComponents[currentStep.value]
+    ? (stepComponents as Record<StepId, Component>)[currentStep.value]
+    : null
 );

Or improve the typing in stepRegistry.ts to export stepComponents as Record<StepId, Component>.


286-286: Simplify boolean comparison.

isHidden === false can be simplified to !isHidden for better readability.

Apply this diff:

-    :show-close-button="isHidden === false || shouldShowUpgradeOnboarding"
+    :show-close-button="!isHidden || shouldShowUpgradeOnboarding"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5d084b and e6b42d9.

📒 Files selected for processing (11)
  • web/src/components/Activation/ActivationModal.vue (2 hunks)
  • web/src/pages/apikeys.vue (0 hunks)
  • web/src/pages/changelog.vue (0 hunks)
  • web/src/pages/flashbackup.vue (0 hunks)
  • web/src/pages/index.vue (0 hunks)
  • web/src/pages/login.query.ts (0 hunks)
  • web/src/pages/login.vue (0 hunks)
  • web/src/pages/redirect.vue (0 hunks)
  • web/src/pages/tools/apikeyauthorize.vue (0 hunks)
  • web/src/pages/tools/test-update-modal.vue (0 hunks)
  • web/src/pages/welcome.vue (0 hunks)
💤 Files with no reviewable changes (10)
  • web/src/pages/tools/apikeyauthorize.vue
  • web/src/pages/apikeys.vue
  • web/src/pages/changelog.vue
  • web/src/pages/login.query.ts
  • web/src/pages/index.vue
  • web/src/pages/login.vue
  • web/src/pages/welcome.vue
  • web/src/pages/redirect.vue
  • web/src/pages/tools/test-update-modal.vue
  • web/src/pages/flashbackup.vue
🧰 Additional context used
📓 Path-based instructions (1)
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationModal.vue
🔇 Additional comments (5)
web/src/components/Activation/ActivationModal.vue (5)

261-278: LGTM! Watch logic preserves user navigation.

The watch correctly initializes the step index only when at index 0, preventing unwanted resets if the user has manually navigated. The fallback to the last step when all are complete is a good UX choice.


141-173: LGTM! Navigation logic handles all cases correctly.

The step navigation implementation properly handles:

  • Marking steps as completed before advancing
  • Boundary checks for forward/backward navigation
  • Modal closure on final step completion
  • Direct step navigation within bounds

281-308: LGTM! Template structure is clean and correct.

The template properly:

  • Conditionally renders the modal based on showModal
  • Dynamically renders step components with bound props
  • Hides partner logo during upgrade onboarding
  • Integrates the stepper component with correct props

1-24: LGTM! Imports follow best practices.

The imports are well-organized and follow the coding guidelines by explicitly importing Vue composables (ref, computed, watch), which ensures tests work correctly without relying on auto-imports.


28-28: No consistency issue; isVisible is a computed property derived from isHidden in the store.

…s in ActivationModal tests

- Enhanced the test cases for the `ActivationModal` component by adding titles, descriptions, and icons for the upgrade steps.
- This update improves the clarity and context of the upgrade steps in the tests, facilitating better understanding and maintenance of the onboarding process.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
web/src/components/Activation/ActivationModal.vue (1)

39-47: Move theme initialization to component lifecycle hook.

The IIFE executes at module load time, before the Vue component is mounted and potentially before the Pinia store is properly initialized. This can cause race conditions, SSR issues, and unpredictable behavior.

Move the theme initialization to onMounted:

+import { computed, ref, watch, onMounted } from 'vue';
-import { computed, ref, watch } from 'vue';
 
 const themeStore = useThemeStore();
 
-// Apply theme when modal opens
-(async () => {
-  try {
-    await themeStore.setTheme();
-    // Ensure CSS variables are applied
-    themeStore.setCssVars();
-  } catch (error) {
-    console.error('Error setting theme:', error);
-  }
-})();
+onMounted(async () => {
+  try {
+    await themeStore.setTheme();
+    // Ensure CSS variables are applied
+    themeStore.setCssVars();
+  } catch (error) {
+    console.error('Error setting theme:', error);
+  }
+});
web/__test__/components/Activation/ActivationModal.test.ts (1)

297-303: Test title vs assertion mismatch.

Title says "renders timezone step initially" but you only assert ActivationSteps presence. Either assert the timezone step or rename the test.

-  it('renders timezone step initially when activation code is present', async () => {
+  it('renders activation steps when activation code is present', async () => {
     const wrapper = mountComponent();
-    // The component now renders steps dynamically based on the step registry
-    // Check that the activation steps component is rendered
     expect(wrapper.html()).toContain('data-testid="activation-steps"');
   });

Or, if the step renders here, assert the element:

-    expect(wrapper.html()).toContain('data-testid="activation-steps"');
+    expect(wrapper.html()).toContain('data-testid="timezone-step"');
🧹 Nitpick comments (1)
web/src/components/Activation/ActivationModal.vue (1)

206-206: Remove or guard debug console.log statements.

Debug console.log statements should be removed before production or guarded with import.meta.env.DEV checks to prevent noise in production logs.

Apply this diff to guard the console.log statements:

+  if (import.meta.env.DEV) {
+    console.log('[ActivationModal] Timezone complete, moving to next step');
+  }
-  console.log('[ActivationModal] Timezone complete, moving to next step');
     case 'WELCOME':
+      if (import.meta.env.DEV) {
-      console.log('[ActivationModal] WELCOME step props:', {
-        currentVersion: currentVersion.value,
-        previousVersion: previousVersion.value,
-      });
+        console.log('[ActivationModal] WELCOME step props:', {
+          currentVersion: currentVersion.value,
+          previousVersion: previousVersion.value,
+        });
+      }
       return {

Also applies to: 245-248

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6b42d9 and 0a9620b.

📒 Files selected for processing (4)
  • web/__test__/components/Activation/ActivationModal.test.ts (11 hunks)
  • web/src/components/Activation/ActivationModal.vue (2 hunks)
  • web/src/components/Activation/UPGRADE_ONBOARDING.md (1 hunks)
  • web/src/components/Activation/store/upgradeOnboarding.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Activation/store/upgradeOnboarding.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationModal.vue
{**/*.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:

  • web/__test__/components/Activation/ActivationModal.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/ActivationModal.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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:

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

Applied to files:

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

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.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 : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
🪛 LanguageTool
web/src/components/Activation/UPGRADE_ONBOARDING.md

[grammar] ~11-~11: There might be a mistake here.
Context: ...ackend (API) 1. Version Tracking - api/src/unraid-api/config/onboarding-tracker.module.ts - On boot, compares current OS version wit...

(QB_NEW_EN)


[grammar] ~45-~45: There might be a mistake here.
Context: ... ``` 2. Upgrade Onboarding Store - store/upgradeOnboarding.ts - Queries `activationOnboarding` - Uses...

(QB_NEW_EN)


[grammar] ~46-~46: There might be a mistake here.
Context: ...tore/upgradeOnboarding.ts - QueriesactivationOnboarding` - Uses returned step metadata to decide wh...

(QB_NEW_EN)


[grammar] ~51-~51: There might be a mistake here.
Context: ...esh install and upgrade onboarding modes - Automatically detects which mode based o...

(QB_NEW_EN)


[grammar] ~52-~52: There might be a mistake here.
Context: ...detects which mode based on system state - Displays relevant steps for each mode ...

(QB_NEW_EN)


[grammar] ~53-~53: There might be a mistake here.
Context: ... - Displays relevant steps for each mode - Reuses existing step components (timezon...

(QB_NEW_EN)


[grammar] ~54-~54: There might be a mistake here.
Context: ...ting step components (timezone, plugins) - Relies on recorded completion status fro...

(QB_NEW_EN)


[grammar] ~128-~128: There might be a mistake here.
Context: ...tart the API 4. The modal should appear on next page load with relevant steps from...

(QB_NEW_EN)

- Introduced a new `UnraidPlugins` module to manage plugin installations, including tracking installation progress and status updates.
- Added GraphQL types and mutations for installing plugins, retrieving installation operations, and subscribing to installation updates.
- Enhanced the `ActivationPluginsStep` component to support real-time feedback during plugin installations, displaying logs and status messages.
- Updated localization files to include messages related to plugin installation processes.
- Implemented unit tests for the new plugin installation service and component interactions, ensuring reliability and correctness.

This update significantly enhances the user experience by providing a robust mechanism for managing plugin installations, improving the overall onboarding process.
- Updated the `ActivationPluginsStep` component to improve the handling of plugin installations, including better state management for installation completion.
- Introduced a new primary action button that dynamically updates based on installation status and selected plugins.
- Enhanced unit tests to cover new checkbox interactions and verify the correct behavior of the installation process.
- Updated localization files to include new messages related to the installation process.

This update significantly improves user experience by providing clearer feedback during plugin installations and ensuring proper state management throughout the process.
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: 5

♻️ Duplicate comments (1)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (1)

45-53: Wiring looks good; ensure resolver guards are in place

Imports use .js specifiers and new mutation groups are correctly exposed on RootMutations. Please verify the corresponding resolvers apply the project’s permission guard for state‑changing operations.

As per coding guidelines.

Also applies to: 55-63, 85-89

🧹 Nitpick comments (9)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (5)

68-73: Timeout should be configurable; 5 minutes may be too short

Longer installs may exceed 5m. Read from config/env with a sensible default (e.g., 15–30m).

Based on learnings


200-207: Preserve leading whitespace in trailing buffer

Using trim() removes leading spaces. Use trimEnd() to only drop trailing whitespace/newline.

-        const buffered = operation.bufferedOutput.trim();
+        const buffered = operation.bufferedOutput.trimEnd();

146-150: Consider preserving blank lines in output

Dropping empty lines changes log fidelity. If UI expects raw logs, avoid filtering by length.

-        return lines.map((line) => line.trimEnd()).filter((line) => line.length > 0);
+        return lines.map((line) => line.trimEnd());

41-42: Add post-completion cleanup to avoid unbounded memory growth

Operations stay in memory forever. Delete them after a retention window (e.g., 10 minutes) when sending final event.

-    private readonly MAX_OUTPUT_LINES = 500;
+    private readonly MAX_OUTPUT_LINES = 500;
+    private readonly OPERATION_RETENTION_MS = 10 * 60 * 1000;
-        if (final) {
-            // no-op placeholder for future cleanup hooks
-        }
+        if (final) {
+            setTimeout(() => {
+                this.operations.delete(operation.id);
+            }, this.OPERATION_RETENTION_MS);
+        }

Also applies to: 292-295


68-83: Optional: capture stdout/stderr end for a final flush

Consider listening to 'end' on the combined stream to ensure the last bytes are flushed exactly once before close-handling.

Based on learnings

web/__test__/components/Activation/ActivationPluginsStep.test.ts (1)

51-82: Consider verifying URL pattern more precisely.

Line 78 checks that the URL contains "community.applications", but this is a partial substring match that could lead to false positives. Consider using a more specific pattern or exact URL check.

-    expect(firstCallArgs?.url).toContain('community.applications');
+    expect(firstCallArgs?.url).toBe('https://raw.githubusercontent.com/unraid/community.applications/master/plugins/community.applications.plg');

Alternatively, use a regex pattern if the URL format is expected to vary:

expect(firstCallArgs?.url).toMatch(/github\.com\/unraid\/community\.applications.*\.plg$/);
web/src/components/Activation/ActivationPluginsStep.vue (1)

28-47: Consider moving plugin list to configuration.

The hardcoded availablePlugins list could be moved to a configuration file or fetched from the API to make it easier to maintain and update without code changes.

Example:

// ~/config/plugins.ts
export const DEFAULT_PLUGINS = [
  {
    id: 'community-apps',
    name: 'Community Apps',
    // ...
  },
  // ...
];

Then import in the component:

import { DEFAULT_PLUGINS } from '~/config/plugins';
const availablePlugins: Plugin[] = DEFAULT_PLUGINS;
web/src/components/Activation/usePluginInstaller.ts (1)

56-103: Handle subscription completion to avoid hangs; add a settle guard

If the operation finishes between mutate and subscribe, the stream can complete without emitting an event, leaving the Promise unresolved. Add a complete handler and a settled guard to resolve by fetching the final operation state; also guard against double settle.

   return new Promise<InstallPluginResult>((resolve, reject) => {
-      const observable = apolloClient.subscribe({
+      let settled = false;
+      const observable = apolloClient.subscribe({
         query: PLUGIN_INSTALL_UPDATES_SUBSCRIPTION,
         variables: { operationId: operation.id },
       });
 
-      const subscription = observable.subscribe({
+      const subscription = observable.subscribe({
         next: ({ data: subscriptionData }) => {
           const event = subscriptionData?.pluginInstallUpdates;
           if (!event) {
             return;
           }
 
           if (event.output?.length) {
             trackedOutput.push(...event.output);
           }
 
           onEvent?.(event);
 
           if (isFinalStatus(event.status)) {
-            void apolloClient
+            void apolloClient
               .query({
                 query: PLUGIN_INSTALL_OPERATION_QUERY,
                 variables: { operationId: operation.id },
                 fetchPolicy: 'network-only',
               })
               .then((result) => {
                 const operationResult = result.data?.pluginInstallOperation;
-                subscription.unsubscribe();
-                resolve({
+                subscription.unsubscribe();
+                if (settled) return;
+                settled = true;
+                resolve({
                   operationId: operation.id,
-                  status: event.status,
+                  status: event.status,
                   output: operationResult?.output ?? trackedOutput,
                 });
               })
               .catch((error) => {
-                subscription.unsubscribe();
-                reject(error);
+                subscription.unsubscribe();
+                if (settled) return;
+                settled = true;
+                reject(error);
               });
           }
         },
         error: (error) => {
-          subscription.unsubscribe();
-          reject(error);
+          subscription.unsubscribe();
+          if (settled) return;
+          settled = true;
+          reject(error);
+        },
+        complete: () => {
+          // Stream ended without a final event being observed; fetch latest state and resolve.
+          void apolloClient
+            .query({
+              query: PLUGIN_INSTALL_OPERATION_QUERY,
+              variables: { operationId: operation.id },
+              fetchPolicy: 'network-only',
+            })
+            .then((result) => {
+              subscription.unsubscribe();
+              if (settled) return;
+              settled = true;
+              const op = result.data?.pluginInstallOperation;
+              resolve({
+                operationId: operation.id,
+                status: op?.status ?? operation.status,
+                output: op?.output ?? trackedOutput,
+              });
+            })
+            .catch((error) => {
+              subscription.unsubscribe();
+              if (settled) return;
+              settled = true;
+              reject(error);
+            });
         },
       });
     });
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts (1)

21-29: Confirm URL constraints for plugin installation

URL is restricted to http/https. If local file paths or file:// URLs are valid plugin sources in your environment, this will reject them.

Optionally extend validation (e.g., allow file protocol or accept absolute paths via a custom validator) if local sources must be supported. Based on learnings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9620b and a4b4757.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (19)
  • api/generated-schema.graphql (11 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (1 hunks)
  • web/__test__/components/Activation/ActivationPluginsStep.test.ts (1 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/components/Activation/graphql/installPlugin.mutation.ts (1 hunks)
  • web/src/components/Activation/graphql/pluginInstallOperation.query.ts (1 hunks)
  • web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts (1 hunks)
  • web/src/components/Activation/usePluginInstaller.ts (1 hunks)
  • web/src/composables/gql/gql.ts (4 hunks)
  • web/src/composables/gql/graphql.ts (20 hunks)
  • web/src/locales/en.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/locales/en.json
🧰 Additional context used
📓 Path-based instructions (14)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts
  • web/src/composables/gql/gql.ts
  • web/src/components/Activation/graphql/pluginInstallOperation.query.ts
  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
  • web/src/components/Activation/usePluginInstaller.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • web/src/components/Activation/graphql/installPlugin.mutation.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts
  • web/src/composables/gql/graphql.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts
api/src/unraid-api/**

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

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

Files:

  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts
web/**/*.query.ts

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

GraphQL queries must be placed in .query.ts files

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

Files:

  • web/src/components/Activation/graphql/pluginInstallOperation.query.ts
web/**/*.{query,mutation}.ts

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

All GraphQL under web/ must follow this naming convention

Files:

  • web/src/components/Activation/graphql/pluginInstallOperation.query.ts
  • web/src/components/Activation/graphql/installPlugin.mutation.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:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/components/**/*.vue

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

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

Files:

  • web/src/components/Activation/ActivationPluginsStep.vue
web/**/*.mutation.ts

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

GraphQL mutations must be placed in .mutation.ts files

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

Files:

  • web/src/components/Activation/graphql/installPlugin.mutation.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/unraid-plugins/unraid-plugins.service.spec.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

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

Applied to files:

  • web/src/composables/gql/gql.ts
📚 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/mutation/mutation.resolver.ts
🧬 Code graph analysis (14)
web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (324-326)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts (4)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (15-56)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts (1)
  • Resolver (12-54)
web/src/composables/gql/graphql.ts (3)
  • UnraidPluginsMutations (2407-2411)
  • PluginInstallOperation (1700-1718)
  • InstallPluginInput (1144-1151)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.ts (1)
api/src/unraid-api/graph/resolvers/resolvers.module.ts (1)
  • Module (40-82)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (2)
web/src/composables/gql/graphql.ts (3)
  • PluginInstallEvent (1687-1697)
  • InstallPluginInput (1144-1151)
  • PluginInstallOperation (1700-1718)
api/src/core/pubsub.ts (2)
  • createSubscription (19-23)
  • pubsub (12-12)
web/src/components/Activation/graphql/pluginInstallOperation.query.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (324-326)
web/__test__/components/Activation/ActivationPluginsStep.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/src/components/Activation/usePluginInstaller.ts (6)
web/src/composables/gql/graphql.ts (1)
  • PluginInstallEvent (1687-1697)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts (1)
  • installPlugin (24-26)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (1)
  • installPlugin (43-98)
web/src/components/Activation/graphql/installPlugin.mutation.ts (1)
  • INSTALL_PLUGIN_MUTATION (3-18)
web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts (1)
  • PLUGIN_INSTALL_UPDATES_SUBSCRIPTION (3-12)
web/src/components/Activation/graphql/pluginInstallOperation.query.ts (1)
  • PLUGIN_INSTALL_OPERATION_QUERY (3-16)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (2)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts (2)
  • ObjectType (48-94)
  • ObjectType (96-114)
api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (5)
  • ObjectType (7-17)
  • ObjectType (19-44)
  • ObjectType (46-62)
  • ObjectType (64-86)
  • ObjectType (88-98)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
web/src/composables/gql/graphql.ts (3)
  • Mutation (1263-1308)
  • OnboardingMutations (1562-1566)
  • UnraidPluginsMutations (2407-2411)
web/src/components/Activation/graphql/installPlugin.mutation.ts (1)
web/src/composables/gql/gql.ts (1)
  • graphql (324-326)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts (1)
api/src/core/pubsub.ts (1)
  • pubsub (12-12)
web/src/composables/gql/graphql.ts (1)
api/src/unraid-api/cli/generated/graphql.ts (4)
  • Maybe (3-3)
  • Scalars (11-75)
  • InputMaybe (4-4)
  • Exact (5-5)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts (4)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (15-56)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.ts (1)
  • Resolver (13-27)
web/src/composables/gql/graphql.ts (4)
  • Query (1765-1839)
  • PluginInstallOperation (1700-1718)
  • Subscription (2174-2187)
  • PluginInstallEvent (1687-1697)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.ts (3)
web/src/composables/gql/graphql.ts (3)
  • InstallPluginInput (1144-1151)
  • PluginInstallOperation (1700-1718)
  • PluginInstallEvent (1687-1697)
api/src/unraid-api/cli/generated/graphql.ts (3)
  • InstallPluginInput (1144-1151)
  • PluginInstallOperation (1700-1718)
  • PluginInstallEvent (1687-1697)
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (9)
  • ObjectType (15-16)
  • ObjectType (18-19)
  • ObjectType (21-22)
  • ObjectType (24-27)
  • ObjectType (29-32)
  • ObjectType (34-43)
  • ObjectType (45-53)
  • ObjectType (55-63)
  • ObjectType (65-90)
🔇 Additional comments (23)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.ts (1)

7-11: Module wiring looks good

Providers/export are correct; ESM imports use .js.

web/src/components/Activation/graphql/installPlugin.mutation.ts (1)

3-18: Mutation shape matches backend types

Fields requested align with PluginInstallOperation.

web/src/components/Activation/graphql/pluginInstallOperation.query.ts (1)

3-16: Query shape matches backend types

Requested fields align with PluginInstallOperation.

web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts (1)

3-12: Subscription document is included in generated documents. The PluginInstallUpdates subscription appears in web/src/composables/gql/gql.ts, so graphql() will return the correct document.

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

123-129: Confirm correct flag for forced plugin install
Verify whether Unraid’s plugin install command accepts a positional forced argument or requires a flag (e.g., -f/--force), then update buildPluginArgs accordingly.

web/__test__/components/Activation/ActivationPluginsStep.test.ts (2)

1-29: LGTM! Mock setup follows best practices.

The module mocks are correctly placed at the top level with factory functions, and beforeEach properly resets the mock between tests. This aligns with the coding guidelines for test file structure.


84-96: Good: Error test doesn't assert exact message.

The test correctly verifies the error UI is rendered without checking the exact error message format. This follows the coding guideline to avoid brittle assertions on error wording.

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

1-27: LGTM! Mutation resolver follows established patterns.

The resolver correctly:

  • Uses @ResolveField for namespaced mutations (consistent with other mutation resolvers like ApiKeyMutations)
  • Applies appropriate UPDATE_ANY permissions on CONFIG resource for installation operations
  • Delegates business logic to the service layer

This aligns with the NestJS architecture in the codebase.

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

17-25: LGTM! Generated GraphQL documents are correctly wired.

These additions extend the GraphQL operation surface for the new onboarding and plugin installation features. The duplicate query/mutation strings are intentional for the GraphQL CodeGen type system to function properly. Based on learnings.

Also applies to: 67-75, 131-166

api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts (3)

1-33: LGTM! Mock setup follows Vitest best practices.

The mock declarations are correctly placed at the top level with factory functions, and beforeEach properly resets state between tests. The MockExecaProcess class appropriately extends EventEmitter to simulate subprocess behavior.


47-88: Good test coverage for success path.

The test correctly:

  • Verifies the execa call with expected arguments (including 'forced' flag)
  • Checks status transitions (RUNNING → SUCCEEDED)
  • Validates output capture
  • Confirms pubsub events are published

The test follows coding guidelines by focusing on behavior rather than implementation details.


90-111: Good test coverage for failure path.

The test correctly verifies that a non-zero exit code transitions the operation to FAILED status and captures error output. The assertion on line 103 checks for content inclusion without being overly specific about the exact message format.

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

1-28: LGTM! Query resolver follows established patterns.

The pluginInstallOperation query correctly:

  • Uses nullable return type for optional lookup
  • Applies READ_ANY permissions on CONFIG resource
  • Delegates to the service layer

This aligns with other query resolvers in the codebase.


41-53: LGTM! Subscription resolver properly configured.

The subscription correctly:

  • Uses the resolve function to extract the payload
  • Guards access with appropriate permissions
  • Returns an AsyncIterableIterator for streaming updates
  • Delegates to the service's subscribe method

This follows NestJS GraphQL subscription patterns.

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

20-20: LGTM! Module wiring correctly extends resolver surface.

The additions properly register the new modules and resolvers:

  • SystemTimeModule and UnraidPluginsModule are imported as dependencies
  • OnboardingMutationsResolver is added to providers for dependency injection

This follows standard NestJS module configuration patterns.

Also applies to: 28-29, 57-57, 59-59, 69-69

web/src/components/Activation/ActivationPluginsStep.vue (5)

1-27: LGTM! Imports and Props interface are well-defined.

The component properly imports Vue composables, UI components, and GraphQL types. The Props interface clearly defines the component's contract with optional callbacks and display flags.


64-72: Good: Set reactivity handled correctly.

The togglePlugin function properly creates a new Set and reassigns it to trigger Vue's reactivity system. This addresses the past review comment about Set mutations not being reactive.


74-115: Good error handling and sequential installation.

The handleInstall function correctly:

  • Handles the case when no plugins are selected
  • Installs plugins sequentially with proper error handling
  • Captures output via the onEvent callback
  • Uses i18n for user-facing messages
  • Handles both success and failure paths

The translation keys are properly used (not props.t as in past issues).


126-197: LGTM! Template structure is clean and accessible.

The template correctly:

  • Uses semantic HTML with proper labels and input associations
  • Binds disabled states to prevent interaction during installation
  • Conditionally renders logs and error messages
  • Properly passes props to BrandButton components
  • Uses v-if for conditional rendering of Back/Skip buttons

The checkbox inputs are properly associated with labels using :for and :id attributes.


87-88: Translation keys verified
Found in web/src/locales/en.json:

  • activation.pluginsStep.installingPluginMessage
  • activation.pluginsStep.pluginInstalledMessage
  • activation.pluginsStep.installFailed
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)

7-7: LGTM! Mutation resolver correctly exposes new namespaced mutations.

The additions follow the established pattern in this file:

  • Import the mutation types from mutation.model.js
  • Add factory methods decorated with @Mutation
  • Return new instances of the mutation classes

This enables the onboarding and unraidPlugins namespaces in the GraphQL schema. As per learnings, error handling is properly delegated to the service level rather than in these resolver methods.

Also applies to: 11-11, 47-55

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

122-153: Generated GraphQL types/docs look consistent with schema

No issues spotted; keep regenerating after schema/auth updates.

If you add auth directives on the server, re-run codegen to ensure client documents remain valid.

Also applies to: 1421-1424, 1686-1726, 2195-2211, 2771-2825, 3080-3089

api/generated-schema.graphql (1)

1184-1190: Auth guards are in place
completeUpgradeStep, installPlugin, and updateSystemTime resolvers already include @UsePermissions decorators; no missing guards.

Likely an incorrect or invalid review comment.

Comment on lines +43 to +57
async installPlugin(input: InstallPluginInput): Promise<PluginInstallOperation> {
const id = randomUUID();
const createdAt = new Date();

const operation: OperationState = {
id,
url: input.url,
name: input.name,
status: PluginInstallStatus.RUNNING,
createdAt,
updatedAt: createdAt,
output: [],
bufferedOutput: '',
forced: input.forced ?? true,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate URL input (protocol + .plg) before spawning the plugin command

Currently accepts any string. Add strict validation to reduce SSRF/path mis-use and ensure only http/https .plg URLs are allowed.

Apply this diff to enforce validation:

 async installPlugin(input: InstallPluginInput): Promise<PluginInstallOperation> {
+        this.assertValidInstallInput(input);
         const id = randomUUID();
         const createdAt = new Date();

Add this helper (outside the shown range):

private assertValidInstallInput(input: InstallPluginInput) {
    try {
        const u = new URL(input.url);
        if (!['http:', 'https:'].includes(u.protocol)) {
            throw new Error('Only http/https URLs are allowed');
        }
        if (!u.pathname.toLowerCase().endsWith('.plg')) {
            throw new Error('Only .plg plugin URLs are supported');
        }
    } catch {
        throw new Error('Invalid plugin URL');
    }
}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts
around lines 43 to 57, the installPlugin method accepts any string for
input.url; add strict validation to ensure only http/https URLs with a .plg path
are allowed. Implement a private helper method assertValidInstallInput(input:
InstallPluginInput) (placed outside the shown range in the class) that parses
the URL, checks protocol is http: or https:, and checks pathname ends with .plg,
throwing on failure; then call this.assertValidInstallInput(input) at the start
of installPlugin before any operation creation or spawning to prevent
invalid/unsafe URLs.

@@ -0,0 +1,18 @@
import { graphql } from '~/composables/gql';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use .js extension for ESM imports

Update import to comply with policy.

As per coding guidelines

-import { graphql } from '~/composables/gql';
+import { graphql } from '~/composables/gql/index.js';
📝 Committable suggestion

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

Suggested change
import { graphql } from '~/composables/gql';
import { graphql } from '~/composables/gql/index.js';
🤖 Prompt for AI Agents
In web/src/components/Activation/graphql/installPlugin.mutation.ts around line
1, the ESM import lacks a file extension; update the import from the bare module
to include the .js extension (e.g. change import { graphql } from
'~/composables/gql' to import { graphql } from '~/composables/gql.js') so it
complies with the ESM policy and build tooling.

@@ -0,0 +1,16 @@
import { graphql } from '~/composables/gql';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use .js extension for ESM imports

Update import to comply with policy.

As per coding guidelines

-import { graphql } from '~/composables/gql';
+import { graphql } from '~/composables/gql/index.js';
📝 Committable suggestion

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

Suggested change
import { graphql } from '~/composables/gql';
import { graphql } from '~/composables/gql/index.js';
🤖 Prompt for AI Agents
In web/src/components/Activation/graphql/pluginInstallOperation.query.ts around
line 1, the ESM import uses a bare module path without the .js extension; update
the import statement to use the .js extension (e.g. import { graphql } from
'~/composables/gql.js';) so it complies with the ESM policy and project coding
guidelines.

@@ -0,0 +1,12 @@
import { graphql } from '~/composables/gql';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use .js extension for ESM imports

Update to comply with project ESM import policy.

As per coding guidelines

-import { graphql } from '~/composables/gql';
+import { graphql } from '~/composables/gql/index.js';
📝 Committable suggestion

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

Suggested change
import { graphql } from '~/composables/gql';
import { graphql } from '~/composables/gql/index.js';
🤖 Prompt for AI Agents
In web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts
around line 1, the ESM import lacks the .js extension; update the import to use
the .js file extension (e.g. change the module specifier from
'~/composables/gql' to '~/composables/gql.js') so it conforms to the project's
ESM import policy and ensure any build/tsconfig path mappings still resolve
correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
web/__test__/components/Activation/ActivationPluginsStep.test.ts (2)

26-28: Consider using vi.clearAllMocks() for comprehensive mock cleanup.

While mockReset() works for this specific mock, vi.clearAllMocks() is recommended in the coding guidelines to ensure all mocks are cleared between tests, providing better isolation.

Apply this diff:

   beforeEach(() => {
-    installPluginMock.mockReset();
+    vi.clearAllMocks();
   });

95-98: Use exact assertion for callback invocation.

The current assertion would pass even if onComplete is called multiple times. If verifying that clicking Continue triggers onComplete exactly once, use a more precise assertion.

Apply this diff:

-    const callsBeforeContinue = props.onComplete.mock.calls.length;
     await continueButton!.trigger('click');
 
-    expect(props.onComplete.mock.calls.length).toBeGreaterThanOrEqual(callsBeforeContinue + 1);
+    expect(props.onComplete).toHaveBeenCalledTimes(1);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4b4757 and a2d9d7a.

📒 Files selected for processing (3)
  • web/__test__/components/Activation/ActivationPluginsStep.test.ts (1 hunks)
  • web/src/components/Activation/ActivationPluginsStep.vue (1 hunks)
  • web/src/locales/en.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Activation/ActivationPluginsStep.vue
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.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:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Activation/ActivationPluginsStep.test.ts
🧬 Code graph analysis (1)
web/__test__/components/Activation/ActivationPluginsStep.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)

Comment on lines +88 to +89
expect(wrapper.html()).toContain('installation started');
expect(wrapper.html()).toContain('installed successfully');
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Prefer wrapper.text() over wrapper.html() for text assertions.

Using wrapper.html().toContain() is brittle as it checks raw HTML strings. Per coding guidelines, use wrapper.text() to assert rendered text, which is more robust and ignores markup.

Apply this diff:

-    expect(wrapper.html()).toContain('installation started');
-    expect(wrapper.html()).toContain('installed successfully');
+    expect(wrapper.text()).toContain('installation started');
+    expect(wrapper.text()).toContain('installed successfully');
📝 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
expect(wrapper.html()).toContain('installation started');
expect(wrapper.html()).toContain('installed successfully');
expect(wrapper.text()).toContain('installation started');
expect(wrapper.text()).toContain('installed successfully');
🤖 Prompt for AI Agents
In web/__test__/components/Activation/ActivationPluginsStep.test.ts around lines
88-89, the test uses wrapper.html() to assert visible text which is brittle;
change both assertions to use wrapper.text() so they read
expect(wrapper.text()).toContain('installation started') and
expect(wrapper.text()).toContain('installed successfully'), ensuring text-based
assertions ignore markup and are more robust.

await flushPromises();

expect(props.onComplete).not.toHaveBeenCalled();
expect(wrapper.html()).toContain('Failed to install plugins. Please try again.');
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Prefer wrapper.text() over wrapper.html() for text assertions.

Same as the earlier comment: use wrapper.text() instead of wrapper.html() for asserting rendered text to avoid brittle HTML string matching.

Apply this diff:

-    expect(wrapper.html()).toContain('Failed to install plugins. Please try again.');
+    expect(wrapper.text()).toContain('Failed to install plugins. Please try again.');
📝 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
expect(wrapper.html()).toContain('Failed to install plugins. Please try again.');
expect(wrapper.text()).toContain('Failed to install plugins. Please try again.');
🤖 Prompt for AI Agents
In web/__test__/components/Activation/ActivationPluginsStep.test.ts around line
121, the test uses wrapper.html() to assert rendered text which is brittle;
replace the assertion to use wrapper.text() instead of wrapper.html() so the
test checks plain rendered text (e.g., change
expect(wrapper.html()).toContain('Failed to install plugins. Please try again.')
to expect(wrapper.text()).toContain('Failed to install plugins. Please try
again.')).

…ation

- Updated the `UnraidPluginsService` to ensure that error and success handling for plugin installations only occurs when the operation status is RUNNING.
- Added checks in the `handleFailure` and `handleSuccess` methods to prevent state changes if the operation is not in progress.
- This change improves the reliability of the plugin installation process by preventing unintended state updates after the operation has completed or failed.
Copy link
Contributor

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

43-57: Validate URL input (protocol + .plg) before spawning the plugin command

The previous review comment about URL validation has not been addressed. Currently accepts any string without validation, creating SSRF/path traversal risks. Add strict validation to ensure only http/https .plg URLs are allowed before creating the operation.

Apply this diff to enforce validation:

 async installPlugin(input: InstallPluginInput): Promise<PluginInstallOperation> {
+        this.assertValidInstallInput(input);
         const id = randomUUID();
         const createdAt = new Date();

Add this helper method to the class:

private assertValidInstallInput(input: InstallPluginInput) {
    try {
        const u = new URL(input.url);
        if (!['http:', 'https:'].includes(u.protocol)) {
            throw new Error('Only http/https URLs are allowed');
        }
        if (!u.pathname.toLowerCase().endsWith('.plg')) {
            throw new Error('Only .plg plugin URLs are supported');
        }
    } catch {
        throw new Error('Invalid plugin URL');
    }
}
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (2)

40-40: Consider operation cleanup strategy for production.

The in-memory Map will accumulate operations indefinitely and lose state on restart. For production, consider:

  • Implementing TTL-based eviction for completed operations
  • Documenting the ephemeral nature of operation history
  • Or adding explicit cleanup endpoints

68-73: Document plugin executable requirement. The service already catches ENOENT and returns “Plugin command not found on this system.” (lines 234–236), so no upfront check is needed. Add a note in the README or API docs that the plugin binary must be installed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2d9d7a and 771840c.

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

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

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

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts
🧬 Code graph analysis (1)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (2)
api/src/unraid-api/cli/generated/graphql.ts (3)
  • PluginInstallEvent (1687-1697)
  • InstallPluginInput (1144-1151)
  • PluginInstallOperation (1700-1718)
api/src/core/pubsub.ts (2)
  • createSubscription (19-23)
  • pubsub (12-12)
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts (3)

85-101: LGTM! Double finalization now prevented.

The guards at lines 86 and 92 correctly prevent race conditions between 'error' and 'close' handlers. The same pattern is applied in handleSuccess (lines 159-161) and handleFailure (lines 181-183), ensuring operations finalize only once.


223-284: LGTM! Comprehensive error handling.

The error normalization logic handles multiple error types, ExecaError structure, and edge cases (ENOENT, buffers, arrays) correctly. The coercion logic is type-safe and thorough.


106-327: LGTM! Well-structured helper methods.

The helper methods are well-organized with clear responsibilities:

  • Query/subscription methods handle access patterns correctly
  • Output buffering and line extraction logic is sound
  • Event publishing uses pubsub appropriately
  • Data conversion creates defensive copies (line 320)
  • All edge cases (empty buffers, array trimming, null coalescing) are handled properly

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant