-
Notifications
You must be signed in to change notification settings - Fork 10
feat(onboarding): add new onboarding flows for Unraid OS #1746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Claude encountered an error —— View job I'll analyze this and get back to you. |
WalkthroughAdds 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
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)
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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 guidelinePlease 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
📒 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)
api/src/unraid-api/graph/resolvers/system-time/system-time.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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. CallupdateDarkModeButton()
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 precisionDividing minutes by 60 prints offsets like
UTC+5.5
orUTC-9.75
, which is confusing. Users expectUTC+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
⛔ 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
filesPlace 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 forUpdateSystemTime
mutation is properly typed and follows the established pattern for GraphQL operation handling.
} catch (error) { | ||
appLogger.debug(`Unable to read CSRF token from ${VAR_INI_PATH}: %o`, error); | ||
return undefined; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
There was a problem hiding this 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
: Removeany
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
anderror.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 isunknown
. 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 simplifyingLogic 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
andhandlePluginsSkip
are identical. Collapse into oneadvanceFromPlugins()
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 clarityCurrent
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 valueIf 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 advancesIf 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 logicYou 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 expressionBoth 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
📒 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
Theinstall
function inuseInstallPlugin
returns void and catches errors internally, soawait install()
won’t capture failures or block until completion. To enable proper sequencing and error handling, refactorinstall
to return a Promise that resolves on success (and rejects on failure), then useawait install(...)
.Likely an incorrect or invalid review comment.
There was a problem hiding this 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 forwindow.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
andActivationTimezoneStep
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 mocksuseUpgradeOnboardingStore
but never asserts cases whenshouldShowUpgradeOnboarding
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
⛔ 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
filesPlace 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 comparingcurrentVersion
withlastSeenVersion
. 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-nullableisUpgrade
field with nullable version fields provides a clear contract for upgrade detection.
64-65
: LGTM!The
upgrade
field addition toInfoVersions
is correct and consistent with the newUpgradeInfo
type.api/generated-schema.graphql (1)
1673-1682
: LGTM!The generated GraphQL schema for
UpgradeInfo
and theupgrade
field onInfoVersions
are correct and consistent with the TypeScript models defined inapi/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
ThelastSeenOsVersion
field is added toApiConfig
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 usesimportOriginal
with an async factory to preserve original exports while overriding only theUStepper
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 andUpgradeInfo
query are correctly added in all three required locations:
- Type definition in
Documents
(lines 20-21)- Runtime mapping in
documents
object (lines 67-68)- 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
There was a problem hiding this 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
orapiConfig
). However, consider the following optional improvements:
- Add error handling for the
persist()
call to log failures explicitly- Add logging to indicate when
lastSeenOsVersion
is successfully updated or skippedApply 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
📒 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 allowundefined
, 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.
There was a problem hiding this 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
📒 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
this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`); | ||
return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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 currentTimeSystemTime.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 validationSchema 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 logicAfter 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 updatesConcurrent 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
⛔ 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
filesPlace 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 thoroughCovers 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 validatedEnsures 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 modeshowModal 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 andUpgradeInfo
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.
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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 bothonboardingTracker.currentVersion
andstore.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
📒 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 theAPI_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
Show autofix suggestion
Hide autofix suggestion
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.
@@ -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 { |
There was a problem hiding this 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
andonboarding.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 isupdateThemeButton()
(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
oronApplicationShutdown
; 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) andUpgradeStep
(versions.model.ts). Both relate to onboarding/setup flows:
ActivationOnboardingStep
: tracks completion state, uses strongly-typed enumUpgradeStep
: includes display metadata (title, description, icon), uses string idWhile 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 usingrouter.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>
assumesstepMetadata
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) andhandleStepClick
(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
⛔ 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
filesPlace 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
filesPlace 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 integrateOnboardingTrackerModule
. 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
toActivationOnboarding
. 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 theUpgradeInfo
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 validActivationOnboardingStepId
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 nowindow
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. TheisMobile
ref initializes tofalse
as a safe default for SSR, and the resize listener is properly cleaned up inonUnmounted
. 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
--ui-text-highlighted: white; | ||
--ui-text-inverted: var(--ui-color-neutral-900); | ||
--ui-text-inverted: var(--ui-color-neutral-300); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
Outdated
Show resolved
Hide resolved
"@types/bun": "1.2.21", | ||
"@types/lodash-es": "4.17.12", | ||
"@types/node": "22.18.0", | ||
"@types/semver": "7.7.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
(async () => { | ||
try { | ||
await themeStore.setTheme(); | ||
// Ensure CSS variables are applied | ||
themeStore.setCssVars(); | ||
} catch (error) { | ||
console.error('Error setting theme:', error); | ||
} | ||
})(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
c414f39
to
4e0b831
Compare
There was a problem hiding this 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
: Avoidany
in catch; narrowunknown
and log safelyReplace
any
withunknown
, narrow forcode === '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 .valueThe 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 pathsapplyDisplaySettings 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 structuredThe 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 LGTMCloning 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 failureRedux 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 traceCurrent 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 mutationscompleteUpgradeStep 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.graphqlAlso 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 initializationIf 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 importFor 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 stringsHard-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 stringsUse 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 completionIf 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 logsReplace 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 wiredapi/src/unraid-api/graph/resolvers/system-time/system-time.service.ts (3)
54-66
: Validate manualDateTime format when NTP is disabledAdd 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 detailLogger.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 assertionsCasting 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 logicdynamicSteps 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 normalizationUse 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 updateCfgFileIf 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/debugEnsure 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 plainToClassplainToClass 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” buttonThe 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 compatibilityPer 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 testsYou 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 usageStub 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 currentTimeSystemTime.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 semanticsmanualDateTime 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 importsIf 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
⛔ 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.
@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[]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@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.
@ResolveField(() => UpgradeInfo) | ||
async upgrade(): Promise<UpgradeInfo> { | ||
const snapshot = await this.onboardingTracker.getUpgradeSnapshot(); | ||
return buildUpgradeInfoFromSnapshot(snapshot); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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"'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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"'); | |
}); |
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.
<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> |
There was a problem hiding this comment.
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.
<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.
themeStore: ReturnType<typeof useThemeStore>; | ||
LOCALE_DATA?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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:
LOCALE_DATA
is actually populated and used in the codebase- 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 declaredLOCALE_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.
There was a problem hiding this 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
Addgetters
to the top‐level import and remove theawait 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
tovar(--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 usesappLogger.debug({ error }, 'message')
. These should follow the same pattern. Based on the past review comment, this line still has the TS2769 type error becauseerror
isunknown
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 discussedNoting 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
: HandleloadStateFiles
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 onlyupdateThemeButton()
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 resolverThe 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 uniquenessUsing 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
: UseIntl.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 stringsCurrent 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.locationFor 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 componentStub 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
asRecord<ActivationOnboardingStepId, StepItem>
, which bypasses TypeScript's type checking. IfstepMetadata
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 instepRegistry.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
⛔ 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
usesReturnType<typeof useThemeStore>
for precise typingLOCALE_DATA
naming is more descriptive than the removedLOCALE
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. Replaceparams.append('csrf_token', csrfToken ?? '')
with an explicit error or rejection whencsrfToken
is undefined, or ensureensureCsrfToken
never returnsundefined
. Confirm how the backend handles missing CSRF tokens.web/src/components/Activation/store/activationCodeModal.ts (2)
13-15
: Decoupling from activationCode looks goodUsing only isFreshInstall simplifies the store and avoids unnecessary coupling.
38-38
: Visibility condition update LGTMClear 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 goodClean 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 required –useInstallPlugin
is the default export ofweb/src/composables/installPlugin.ts
and itsinstall()
method is synchronous (void‐returning), so there’s no import/name mismatch or need toawait
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 GraphQLInput 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 correctNew 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 APIactiveStepIndex wiring and assertion look correct.
180-206
: Redirect test setup is robustMocking 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
andUpgradeInfo
GraphQL types are well-structured with clear field descriptions, appropriate default values, and proper nullable field markers. The integration intoInfoVersions
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 inselectedTimeZone.value
being set to a value that doesn't exist intimeZoneItems
, 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 injectedOnboardingTracker
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
andActivationOnboarding
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
toOnboardingService
is consistent, and the refactoring to use the extractedfindActivationCodeFile
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 newlastSeenOsVersion
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 newlastSeenOsVersion
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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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], | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
@Field({ nullable: true, description: 'Enable or disable NTP-based synchronization' }) | ||
@IsOptional() | ||
@IsBoolean() | ||
useNtp?: boolean; | ||
|
There was a problem hiding this comment.
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).
<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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
<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.
- 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.
There was a problem hiding this 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 theas 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 usingPartial<>
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 butlastTrackedVersion
is definedapi/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 theservers
array is guaranteed to have exactly 4 elements after the map operation. The initial array has 4 elements (lines 91-95), andmap()
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
📒 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 withAuthAction.UPDATE_ANY
andResource.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 withVersionsResolver.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
wheneverhasBothVersions
is false, even whensnapshot.currentVersion
exists. This means iflastTrackedVersion
is undefined (e.g., fresh install) butcurrentVersion
is available, the API returnscurrentVersion: 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 theMAX_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-typedSystemTime
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 expectedYYYY-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({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
(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({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
(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.
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(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
There was a problem hiding this 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 thetype
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 parameterThe
markStepCompleted
method accepts anyActivationOnboardingStepId
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
📒 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 trackingThe 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 fallbackThe 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 permissionsThe use of
atomically
package'swriteFile
with mode0o644
ensures safe, atomic writes to the tracker state file, preventing partial writes or corruption.
53-67
: LGTM: Bootstrap logic correctly handles version unavailabilityThe 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 changesThe shutdown logic correctly skips writing when
lastTrackedVersion
already matchescurrentVersion
, 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 functionsThe 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 OnboardingTrackerThe 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 beforeEachThe
beforeEach
correctly resets all mocks usingmockReset()
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.
There was a problem hiding this 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
whilewriteTrackerState
(line 311) uses atomicwriteFile
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 asRecord<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
📒 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.
There was a problem hiding this 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 duplicationThe 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 documentationThe 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 neededSetting 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 assertionLine 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
📒 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 errorsThe 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 stateThe 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 correctlyThe
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 OnboardingTrackerThe 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 defaultsThe 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.
There was a problem hiding this 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 formockEmhttpState
andmockPathsState
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 sincestartsWith()
already returns a boolean, and the optional chaining handlesundefined
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
📒 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:
- The version when the step was completed (current version
'7.2.0'
), or- 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 withAPI_VERSION
as the version, both in the success path and when handling errors. This aligns with the implementation shown in the relevant code snippets whereversion: 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
toOnboardingService
. 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', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
mockReaddir.mockResolvedValue(['pending.activationcode'] as unknown as ReaddirResult); | ||
mockEmhttpState.var.regState = 'ENOKEYFILE_PENDING'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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 matchesRecord<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 exportstepComponents
asRecord<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
📒 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 fromisHidden
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.
There was a problem hiding this 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
📒 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 - Queries
activationOnboarding` - 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.
There was a problem hiding this 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 placeImports 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 shortLonger 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 bufferUsing 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 outputDropping 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 growthOperations 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 flushConsider 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 guardIf 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 installationURL 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
⛔ 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
filesPlace 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
filesPlace 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 goodProviders/export are correct; ESM imports use .js.
web/src/components/Activation/graphql/installPlugin.mutation.ts (1)
3-18
: Mutation shape matches backend typesFields requested align with PluginInstallOperation.
web/src/components/Activation/graphql/pluginInstallOperation.query.ts (1)
3-16
: Query shape matches backend typesRequested fields align with PluginInstallOperation.
web/src/components/Activation/graphql/pluginInstallUpdates.subscription.ts (1)
3-12
: Subscription document is included in generated documents. ThePluginInstallUpdates
subscription appears inweb/src/composables/gql/gql.ts
, sographql()
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’splugin install
command accepts a positionalforced
argument or requires a flag (e.g.,-f
/--force
), then updatebuildPluginArgs
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 onCONFIG
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. TheMockExecaProcess
class appropriately extendsEventEmitter
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 onCONFIG
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
andUnraidPluginsModule
are imported as dependenciesOnboardingMutationsResolver
is added to providers for dependency injectionThis 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
andunraidPlugins
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 schemaNo 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.
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, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts
Show resolved
Hide resolved
@@ -0,0 +1,18 @@ | |||
import { graphql } from '~/composables/gql'; |
There was a problem hiding this comment.
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.
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'; |
There was a problem hiding this comment.
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.
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'; |
There was a problem hiding this comment.
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.
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.
There was a problem hiding this 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 usingvi.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 triggersonComplete
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
📒 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)
expect(wrapper.html()).toContain('installation started'); | ||
expect(wrapper.html()).toContain('installed successfully'); |
There was a problem hiding this comment.
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.
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.'); |
There was a problem hiding this comment.
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.
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.
This plugin has been deployed to Cloudflare R2 and is available for testing.
|
There was a problem hiding this 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 commandThe 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
: Documentplugin
executable requirement. The service already catchesENOENT
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 theplugin
binary must be installed.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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) andhandleFailure
(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
…m time configuration
SystemTime
to manage system time settings, including current time, timezone, NTP status, and NTP servers.systemTime
query to retrieve current system time configuration.updateSystemTime
mutation to modify system time settings.This update enhances the API's capability to manage and retrieve system time configurations effectively.
Summary by CodeRabbit
New Features
Tests
Documentation