-
Notifications
You must be signed in to change notification settings - Fork 17
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. |
WalkthroughThis PR introduces a comprehensive onboarding system overhaul, replacing the legacy Activation flow with a unified Onboarding architecture. It expands the GraphQL API with system time configuration, plugin installation workflows, onboarding state tracking, and network information. Backend services handle onboarding persistence and override testing. Frontend mirrors this with new Onboarding components, multi-step workflows, and state stores. Localization keys migrate from activation to onboarding namespace. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant OnboardingModal
participant OnboardingSteps
participant OnboardingStep as Step Component
participant Store as Pinia Store
participant GraphQL as GraphQL API
participant Backend as Backend Service
User->>OnboardingModal: Open modal / Show
OnboardingModal->>GraphQL: Fetch theme, onboarding status
GraphQL->>Backend: Query publicTheme, onboarding
Backend-->>GraphQL: Return current state
GraphQL-->>OnboardingModal: Update modal state
OnboardingModal->>OnboardingSteps: Render steps from registry
loop For Each Step
User->>OnboardingStep: Interact (enter data, select, etc.)
OnboardingStep->>Store: Update draft state
Store-->>OnboardingStep: Confirm update
OnboardingStep->>OnboardingStep: Validate input
User->>OnboardingStep: Click Next/Complete
end
OnboardingStep->>GraphQL: Apply settings mutation
GraphQL->>Backend: Execute mutation (updateSystemTime, setTheme, etc.)
Backend-->>GraphQL: Return updated state
GraphQL-->>Store: Update store with result
Store-->>OnboardingModal: Notify completion
rect rgba(100, 200, 50, 0.5)
OnboardingModal->>GraphQL: Install plugins mutation
GraphQL->>Backend: Start plugin install operation
Backend-->>GraphQL: Return operation ID
GraphQL-->>OnboardingModal: Subscribe to updates
loop Output Streaming
Backend->>GraphQL: Emit status & output events
GraphQL-->>OnboardingModal: Forward real-time updates
OnboardingModal->>OnboardingConsole: Display logs
end
end
OnboardingModal->>GraphQL: Complete onboarding mutation
GraphQL->>Backend: Mark onboarding complete
Backend->>Backend: Write tracker state to disk
Backend-->>GraphQL: Return updated onboarding
GraphQL-->>OnboardingModal: Confirm completion
OnboardingModal->>User: Close modal, redirect
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1746 +/- ##
==========================================
+ Coverage 46.49% 46.91% +0.41%
==========================================
Files 954 993 +39
Lines 59788 63344 +3556
Branches 5552 6039 +487
==========================================
+ Hits 27799 29718 +1919
- Misses 31870 33503 +1633
- Partials 119 123 +4 ☔ 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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.model.tsapi/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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.model.tsapi/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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.model.tsapi/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
globalPiniaon 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.5orUTC-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.yamlis 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.tsweb/src/components/Activation/store/activationCodeModal.tsweb/src/composables/gql/graphql.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/src/components/Wrapper/mount-engine.tsweb/__test__/store/activationCodeModal.test.tsapi/src/core/utils/clients/emcmd.tsweb/src/components/Activation/updateSystemTime.mutation.tsweb/src/composables/gql/gql.tsweb/__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.tsweb/__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.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.tsweb/__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.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.vueweb/src/components/Activation/ActivationTimezoneStep.vueweb/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.tsfilesPlace 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
UpdateSystemTimemutation 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
graphqlfunction overload forUpdateSystemTimemutation 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: Removeanytype 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.codeanderror.messageto 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
errorin 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
handlePluginsCompleteandhandlePluginsSkipare 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
advanceFromPluginsto both on-complete/on-skip.web/src/components/Activation/ActivationTimezoneStep.vue (3)
28-37: Format UTC offsets as ±HH:MM for clarityCurrent
offsetprints 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.vueweb/src/components/Activation/ActivationModal.vueweb/src/components/Activation/ActivationPluginsStep.vueweb/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19
❯ setupStatefulComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7906:25
❯ setupComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7867:36
❯ mountComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5198:7
❯ processComponent ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5164:9
❯ patch ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:4691:11
❯ ReactiveEffect.componentUpdateFn [as fn] ../node_modules/.pnpm/@VUE+runtime-core@3.5.20/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_tokenfalls back to''if undefined. Does the emhttp/updateendpoint 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
Theinstallfunction inuseInstallPluginreturns void and catches errors internally, soawait install()won’t capture failures or block until completion. To enable proper sequencing and error handling, refactorinstallto 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
DOMContentLoadedpath (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
setIntervalfor waiting forwindow.themeStoredoesn'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
ActivationPluginsStepandActivationTimezoneStepsuggest 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
activationCodeis absent or null- Behavior when
isFreshInstallis 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 mocksuseUpgradeOnboardingStorebut never asserts cases whenshouldShowUpgradeOnboardingis 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.tsis 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.tsfilesPlace 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.tspackages/unraid-shared/src/services/api-config.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsweb/src/components/Activation/store/upgradeOnboarding.tsweb/src/components/Activation/store/activationCodeModal.tsweb/src/composables/gql/graphql.tsapi/src/unraid-api/config/api-config.module.tsweb/src/components/Activation/releaseConfigs.tsweb/src/composables/gql/gql.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsweb/__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.tsapi/src/unraid-api/config/api-config.module.tsapi/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.tsapi/src/unraid-api/config/api-config.module.tsapi/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.tsweb/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.vueweb/src/components/Activation/ActivationSteps.vueweb/src/components/TestThemeSwitcher.standalone.vueweb/src/components/Activation/ActivationTimezoneStep.vueweb/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: undefinedto 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 comparingcurrentVersionwithlastSeenVersion. 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
upgradeOnboardingstore 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
UpgradeInfomodel is properly structured with appropriate GraphQL decorators, field types, and descriptions. The non-nullableisUpgradefield with nullable version fields provides a clear contract for upgrade detection.
64-65: LGTM!The
upgradefield addition toInfoVersionsis correct and consistent with the newUpgradeInfotype.api/generated-schema.graphql (1)
1673-1682: LGTM!The generated GraphQL schema for
UpgradeInfoand theupgradefield onInfoVersionsare 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
_smap (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__activationModalDebugare 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
ThelastSeenOsVersionfield is added toApiConfigbut 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/uimock correctly usesimportOriginalwith an async factory to preserve original exports while overriding only theUSteppercomponent. 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
UpdateSystemTimemutation andUpgradeInfoquery are correctly added in all three required locations:
- Type definition in
Documents(lines 20-21)- Runtime mapping in
documentsobject (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
currentOsVersionorapiConfig). However, consider the following optional improvements:
- Add error handling for the
persist()call to log failures explicitly- Add logging to indicate when
lastSeenOsVersionis 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
OnApplicationShutdownto 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
currentOsVersionfield 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.tsapi/src/unraid-api/config/api-config.test.tsapi/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.tsapi/src/unraid-api/config/api-config.test.tsapi/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.tsapi/src/unraid-api/config/api-config.test.tsapi/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.tsis 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.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/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.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsweb/src/composables/gql/gql.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/src/unraid-api/config/api-config.test.tsweb/src/components/Activation/store/upgradeOnboarding.tsweb/src/components/Activation/completeUpgradeStep.mutation.tsweb/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.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/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.tsfilesPlace 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
.jsextension 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
.jsextensions: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
lastSeenOsVersionfield 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
stepIdstring parameter andUpgradeInforeturn 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:
isUpgradeflag 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.currentVersionandstore.emhttp.var.versionare 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.tsapi/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.tsapi/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.tsapi/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
configStoreto simulate ConfigService behavior is a clean approach that makes assertions straightforward.
310-340: LGTM! loadApiConfig tests updated correctly.The tests correctly include
lastSeenOsVersion: undefinedin the expected default config and use theAPI_VERSIONconstant consistently.
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
updateSystemTimeandonboarding.completeUpgradeSteplack 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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsNone implement
onModuleDestroyoronApplicationShutdown; 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-labelattributes 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-labelprop before applying this change.api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts (1)
5-5: Drop unused import
ActivationOnboardingStepIdisn’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 === falseis 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>assumesstepMetadatacontains 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 === 0duplicates 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.tsis excluded by!**/generated/**pnpm-lock.yamlis 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.tsapi/src/unraid-api/config/onboarding-tracker.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/main.tsapi/src/unraid-api/graph/resolvers/customization/activation-code.model.tsweb/src/components/Activation/completeUpgradeStep.mutation.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.tsapi/src/unraid-api/graph/resolvers/info/info.module.tsweb/src/components/Activation/activationOnboarding.query.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsapi/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.tsweb/src/composables/gql/graphql.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsweb/components.d.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/config/api-config.test.tsweb/src/components/Activation/stepRegistry.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.tsweb/__test__/components/Activation/WelcomeModal.test.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsweb/__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.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/main.tsapi/src/unraid-api/graph/resolvers/customization/activation-code.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.tsapi/src/unraid-api/graph/resolvers/info/info.module.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsapi/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/config/api-config.test.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/main.tsapi/src/unraid-api/graph/resolvers/customization/activation-code.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.tsapi/src/unraid-api/graph/resolvers/info/info.module.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsapi/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/config/api-config.test.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.tsapi/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.vueweb/src/components/Activation/ActivationModal.vueweb/src/components/Activation/ActivationTimezoneStep.vueweb/src/components/Activation/ActivationLicenseStep.vueweb/src/components/DevModalTest.standalone.vueweb/src/components/Activation/ActivationSteps.vueweb/src/components/Activation/ActivationWelcomeStep.vue
web/**/*.mutation.ts
📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)
GraphQL mutations must be placed in
.mutation.tsfilesPlace 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.tsweb/src/components/Activation/activationOnboarding.query.ts
web/**/*.query.ts
📄 CodeRabbit inference engine (.cursor/rules/web-graphql.mdc)
GraphQL queries must be placed in
.query.tsfilesPlace 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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/config/api-config.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__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.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__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.tsweb/__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.tsweb/__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.tsweb/__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.tsweb/__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.tsweb/__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: semver@7.7.2.
🪛 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
.jsextension.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
stepsarray 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
ActivationOnboardingStepIdenum 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
OnboardingServiceand 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
UpgradeProgressSnapshottoActivationOnboarding. 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 theUpgradeInfotype.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
previousVersiononly 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
@IsEnumdecorator ensures only validActivationOnboardingStepIdvalues 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
findActivationCodeFilefunction 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
OnboardingServiceclass, 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
windowaccess in this area, but the current code (lines 61-66) shows computed properties with nowindowreferences. The code appears to have been refactored since that comment was made.If debug logging with
windowaccess 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
ActivationStepsand 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
PuzzlePieceIconimports 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. TheisMobileref initializes tofalseas 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
onStepClickcallback- 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
api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.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: ... semver@7.7.2). 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 semver@7.7.2 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.
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/themeApply 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: Avoidanyin catch; narrowunknownand log safelyReplace
anywithunknown, 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 installto 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.tsis excluded by!**/generated/**pnpm-lock.yamlis 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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.resolver.tsapi/src/unraid-api/graph/resolvers/info/info.module.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/graph/resolvers/customization/activation-code.model.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.resolver.tsapi/src/unraid-api/graph/resolvers/info/info.module.tsweb/src/components/Activation/stepRegistry.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/graph/resolvers/customization/activation-code.model.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsweb/src/components/Activation/steps/timezone.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/core/utils/clients/emcmd.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsweb/src/composables/gql/graphql.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/src/unraid-api/config/api-config.test.tsweb/src/composables/gql/gql.tsweb/src/components/Wrapper/mount-engine.tsweb/components.d.tsweb/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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.resolver.tsapi/src/unraid-api/graph/resolvers/info/info.module.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/graph/resolvers/customization/activation-code.model.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/core/utils/clients/emcmd.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/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.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.spec.tsapi/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.vueweb/src/components/Activation/ActivationSteps.vueweb/src/components/Activation/ActivationLicenseStep.vueweb/src/components/DevModalTest.standalone.vueweb/src/components/Activation/WelcomeModal.standalone.vueweb/src/components/Activation/ActivationModal.vueweb/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.tsweb/__test__/components/Activation/ActivationModal.test.tsapi/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.tsweb/__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.tsweb/__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.tsweb/__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.tsweb/__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.
api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.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: 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
Addgettersto 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-invertedtovar(--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 becauseerrorisunknownin 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: HandleloadStateFilesrejections correctly.Redux Toolkit's
createAsyncThunkdispatch 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.
ActivationOnboardingStepIdis 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.DateTimeFormatfor date formatting.The custom date formatter is error-prone and doesn't handle locale or timezone nuances.
Intl.DateTimeFormatprovides 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
stepMetadataasRecord<ActivationOnboardingStepId, StepItem>, which bypasses TypeScript's type checking. IfstepMetadatadoesn'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
stepMetadatawith a more specific type instepRegistry.tsto 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
_fromVersionmeans 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.tsis excluded by!**/generated/**pnpm-lock.yamlis 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.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.model.tsapi/src/unraid-api/config/api-config.test.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/unraid-api/config/onboarding-tracker.model.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/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.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.model.tsweb/src/composables/gql/gql.tsapi/src/unraid-api/config/api-config.test.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsweb/src/components/Wrapper/mount-engine.tsweb/src/components/Activation/store/activationCodeModal.tsweb/src/components/Activation/stepRegistry.tsweb/__test__/components/Activation/ActivationModal.test.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/config/onboarding-tracker.module.tsweb/src/composables/gql/graphql.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsweb/src/components/Activation/steps/timezone.tsweb/__test__/components/Activation/WelcomeModal.test.tsapi/src/unraid-api/config/onboarding-tracker.model.tsapi/src/core/utils/clients/emcmd.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/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.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.model.tsapi/src/unraid-api/config/api-config.test.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsapi/src/unraid-api/graph/resolvers/info/versions/upgrade-info.util.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.module.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/config/onboarding-tracker.module.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/info/versions/versions.model.tsapi/src/unraid-api/config/onboarding-tracker.model.tsapi/src/core/utils/clients/emcmd.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.model.tsapi/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.vueweb/src/components/Activation/ActivationModal.vueweb/src/components/ColorSwitcher.standalone.vueweb/src/components/Activation/ActivationPluginsStep.vueweb/src/components/Activation/ActivationTimezoneStep.vueweb/src/components/Activation/ActivationLicenseStep.vueweb/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.tsapi/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.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__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.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__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.tsweb/__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.tsweb/__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.tsweb/__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.tsweb/__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:
themeStoreusesReturnType<typeof useThemeStore>for precise typingLOCALE_DATAnaming is more descriptive than the removedLOCALEproperty (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
/updateendpoint 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 whencsrfTokenis undefined, or ensureensureCsrfTokennever 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 –useInstallPluginis the default export ofweb/src/composables/installPlugin.tsand itsinstall()method is synchronous (void‐returning), so there’s no import/name mismatch or need toawaitit.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
satisfiescheck 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
UpgradeStepandUpgradeInfoGraphQL types are well-structured with clear field descriptions, appropriate default values, and proper nullable field markers. The integration intoInfoVersionsis 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
timeZonesarray (no exact or group match). This could result inselectedTimeZone.valuebeing set to a value that doesn't exist intimeZoneItems, potentially causing the Select component to display incorrectly or fail validation.Consider returning
nullinstead 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 injectedOnboardingTrackerservice and transformation utility to return upgrade information.api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (2)
62-71: LGTM!The
ActivationOnboardingStepIdenum is properly defined and registered with GraphQL, following standard patterns.
154-204: LGTM!The
ActivationOnboardingStepandActivationOnboardingtypes 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
findActivationCodeFileutility 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
CustomizationServicetoOnboardingServiceis consistent, and the refactoring to use the extractedfindActivationCodeFileutility 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
ApiConfigPersistencetest suite properly validates default configuration, legacy migration, and bootstrap behavior with appropriate assertions for the newlastSeenOsVersionfield.
153-178: LGTM!The test correctly validates that
lastTrackedVersionpersistence is deferred until shutdown, confirming the intended design pattern.
180-448: LGTM!The
OnboardingTrackerintegration 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
loadApiConfigtests properly validate default configuration values including the newlastSeenOsVersionfield.
| 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.
api/src/unraid-api/graph/resolvers/customization/customization.module.ts
Show resolved
Hide resolved
| @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.
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
♻️ Duplicate comments (3)
web/src/components/Activation/ActivationWelcomeStep.vue (1)
80-86: Guard optional Back/Skip handlers to avoid undefined calls.Render buttons only when handlers exist and invoke safely. This prevents click-on-undefined at runtime and aligns with Timezone step.
- <BrandButton v-if="showBack" :text="t('common.back')" variant="outline" @click="onBack" /> + <BrandButton + v-if="onBack && showBack" + :text="t('common.back')" + variant="outline" + @click="onBack?.()" + /> @@ - <BrandButton v-if="showSkip" :text="t('common.skip')" variant="outline" @click="onSkip" /> + <BrandButton + v-if="onSkip && showSkip" + :text="t('common.skip')" + variant="outline" + @click="onSkip?.()" + />web/src/components/Activation/ActivationModal.vue (1)
39-47: Theme initialization IIFE still executes at module load.Despite the past review comment being marked as "Addressed in commit c414f39", the IIFE remains at module scope and will execute before Vue component initialization, which can cause SSR crashes and race conditions with Pinia store setup.
Move the theme initialization into
onMounted:-import { computed, ref, watch } from 'vue'; +import { computed, ref, watch, onMounted } 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(); + themeStore.setCssVars(); + } catch (error) { + console.error('Error setting theme:', error); + } +});web/__test__/components/Activation/ActivationModal.test.ts (1)
283-289: Test title vs assertion mismatch.The test title mentions "renders timezone step initially" but the assertion only verifies that the activation-steps component is present, not the timezone step itself.
Either assert the timezone step specifically:
it('renders timezone step initially 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"'); + expect(wrapper.html()).toContain('data-testid="timezone-step"'); });Or update the title to match the assertion:
- 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"'); });
🧹 Nitpick comments (9)
web/__test__/store/activationCodeModal.test.ts (1)
61-72: Add a test for the full Konami sequence (assert hide + redirect).Harness is solid (Pinia + mounted host triggers lifecycle). Consider adding a test that sends the entire sequence to assert isHidden becomes true and window.location.href updates per store behavior.
Also applies to: 152-174
web/src/components/Activation/ActivationWelcomeStep.vue (1)
1-71: Optional: prevent unknown attrs from leaking to DOM.Add inheritAttrs: false as a safety net so accidental non-prop attrs don’t land on the root element.
<script lang="ts" setup> +defineOptions({ inheritAttrs: false });Also applies to: 73-88
web/src/components/Activation/ActivationTimezoneStep.vue (2)
29-37: Format UTC offsets as HH:mm and sort items for readability.Current labels show decimal offsets (e.g., UTC+5.5). Prefer HH:mm and a stable sort by offset then name for quicker scanning.
-const timeZoneItems = computed(() => { - 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}`, - }; - }); -}); +const timeZoneItems = computed(() => { + const toOffsetHHmm = (minutes: number) => { + const sign = minutes >= 0 ? '+' : '-'; + const abs = Math.abs(minutes); + const h = String(Math.floor(abs / 60)).padStart(2, '0'); + const m = String(abs % 60).padStart(2, '0'); + return `${sign}${h}:${m}`; + }; + return [...timeZones] + .sort((a, b) => a.currentTimeOffsetInMinutes - b.currentTimeOffsetInMinutes || a.name.localeCompare(b.name)) + .map((tz) => ({ + value: tz.name, + label: `${tz.alternativeName} (${tz.name}) UTC${toOffsetHHmm(tz.currentTimeOffsetInMinutes)}`, + })); +});
77-90: Non-blocking failure handling is OK; consider surfacing a toast.Continuing on failure matches the product decision. Optionally add a non-blocking notification so users know timezone wasn’t applied.
Based on learnings
web/__test__/components/Activation/WelcomeModal.test.ts (2)
194-201: Don’t pass unused props to WelcomeModal.WelcomeModal has no t prop. Remove it to avoid leaking attrs and keep the mount minimal.
- const wrapper = mount(WelcomeModal, { - props: { t: mockT as unknown as ComposerTranslation }, + const wrapper = mount(WelcomeModal, { global: { stubs: mockComponents, }, });
369-387: Align test name with button text (“Get Started”).Rename the test to avoid confusion with old copy.
- it('disables the Create a password button when loading', async () => { + it('ensures the Get Started button is enabled by default', async () => {web/src/components/Activation/ActivationModal.vue (1)
217-220: Remove debug console.log from production code.The console.log statement should be removed or wrapped in a DEV guard before release.
Apply this diff:
case 'WELCOME': - console.log('[ActivationModal] WELCOME step props:', { - currentVersion: currentVersion.value, - previousVersion: previousVersion.value, - }); return {Or guard it for development only:
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 {web/src/components/Activation/ActivationSteps.vue (1)
112-128: Click handler correctly maps UI indices to internal step indices.The offset calculation properly accounts for the contextual "Create Device Password" step. The boundary checks and type handling are appropriate.
Consider adding a comment to clarify the offset calculation:
const handleStepClick = (clickedStepIndex: string | number | undefined) => { if (!props.onStepClick || clickedStepIndex === undefined) return; const stepIndex = typeof clickedStepIndex === 'string' ? parseInt(clickedStepIndex, 10) : clickedStepIndex; if (isNaN(stepIndex)) return; - // Map the clicked step index to the actual step index - // Account for the "Create Device Password" step that's added at the beginning + // Map UI step index to internal step index. + // The timeline includes a prepended "Create Device Password" step, + // so we subtract 1 when that step is present. const offset = includeInitialStep.value ? 1 : 0; const actualStepIndex = Math.max(0, stepIndex - offset);web/__test__/components/Activation/ActivationModal.test.ts (1)
216-351: Test coverage is solid for core scenarios.The tests cover i18n, visibility, partner logo, and the Konami code easter egg. Mock isolation via
clearAllMocksis properly implemented.Consider adding tests for:
- Step navigation (goToNextStep, goToPreviousStep, goToStep)
- Mutation calls when completing steps
- Error handling in markUpgradeStepCompleted
- Step-specific props passed to each step component
Based on coding guidelines.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
web/__test__/components/Activation/ActivationModal.test.ts(8 hunks)web/__test__/components/Activation/WelcomeModal.test.ts(6 hunks)web/__test__/store/activationCodeModal.test.ts(4 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/WelcomeModal.standalone.vue(3 hunks)web/src/components/Activation/stepRegistry.ts(1 hunks)web/src/components/Activation/steps/timezone.ts(1 hunks)web/src/locales/en.json(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- web/src/components/Activation/steps/timezone.ts
- web/src/components/Activation/ActivationPluginsStep.vue
- web/src/components/Activation/ActivationLicenseStep.vue
🧰 Additional context used
📓 Path-based instructions (9)
{**/*.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.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.tsweb/__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/WelcomeModal.test.tsweb/src/components/Activation/stepRegistry.tsweb/__test__/store/activationCodeModal.test.tsweb/__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/WelcomeModal.test.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.tsweb/__test__/store/activationCodeModal.test.tsweb/__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.tsweb/__test__/store/activationCodeModal.test.tsweb/__test__/components/Activation/ActivationModal.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/ActivationSteps.vueweb/src/components/Activation/WelcomeModal.standalone.vueweb/src/components/Activation/ActivationModal.vueweb/src/components/Activation/ActivationWelcomeStep.vueweb/src/components/Activation/ActivationTimezoneStep.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
**/{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/__test__/store/activationCodeModal.test.ts
🧠 Learnings (19)
📚 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.tsweb/__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 : For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed
Applied to files:
web/__test__/store/activationCodeModal.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} : For store tests, use createPinia() and setActivePinia
Applied to files:
web/__test__/store/activationCodeModal.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__/store/activationCodeModal.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__/store/activationCodeModal.test.ts
📚 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/__test__/store/activationCodeModal.test.ts
📚 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/__test__/store/activationCodeModal.test.tsweb/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. 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/__test__/store/activationCodeModal.test.tsweb/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/__test__/store/activationCodeModal.test.tsweb/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/__test__/store/activationCodeModal.test.tsweb/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.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Applied to files:
web/__test__/store/activationCodeModal.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 : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Applied to files:
web/__test__/store/activationCodeModal.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__/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} : 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-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 (2)
web/__test__/components/Activation/WelcomeModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/store/activationCodeModal.test.ts (1)
web/src/components/Activation/store/activationCodeModal.ts (1)
useActivationCodeModalStore(10-85)
🔇 Additional comments (5)
web/src/components/Activation/stepRegistry.ts (1)
1-43: LGTM! Clean registry pattern for step mapping.The centralized registry provides strong typing between step IDs, components, and metadata. The structure is maintainable and the i18n keys follow a consistent pattern.
web/src/components/Activation/ActivationModal.vue (2)
128-196: Step navigation and mutation logic is well-structured.The mutation-driven completion flow properly handles errors, refetches data, and includes boundary checks for step navigation. The separation of concerns between completion marking and navigation is clear.
262-279: Initial step selection logic handles edge cases correctly.The watcher properly initializes to the first incomplete step or the last step when all are complete, and avoids overwriting manual navigation by checking
currentStepIndex === 0.web/src/components/Activation/ActivationSteps.vue (1)
40-93: Dynamic step computation is well-structured.The metadata-driven approach with fallbacks and proper clamping ensures the timeline is always valid. The offset logic correctly accounts for the optional initial step when mapping to internal indices.
web/__test__/components/Activation/ActivationModal.test.ts (1)
153-206: Mocks follow best practices for Vitest.Factory functions, top-level placement, and proper typing with
importOriginalprevent hoisting issues and maintain type safety.Based on coding guidelines.
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 anytype 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 Typewhich 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
getUpgradeSnapshotthrows- Edge case when
currentVersionis undefined butlastTrackedVersionis 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
whileloop at lines 98-100 is dead code and never executes because theserversarray 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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.tsapi/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.tsapi/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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.tsapi/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.tsapi/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.tsapi/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.tsapi/src/unraid-api/graph/resolvers/system-time/system-time.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.tsapi/src/unraid-api/graph/resolvers/customization/customization.resolver.tsapi/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.vueweb/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
.jsextensions for ESM compatibility, as required by the coding guidelines.
16-22: Authorization properly enforced.The
@UsePermissionsdecorator correctly protects this state-changing mutation withAuthAction.UPDATE_ANYandResource.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
buildUpgradeInfoFromSnapshotutility 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: undefinedwheneverhasBothVersionsis false, even whensnapshot.currentVersionexists. This means iflastTrackedVersionis undefined (e.g., fresh install) butcurrentVersionis available, the API returnscurrentVersion: undefinedinstead 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.currentVersiondirectly 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
.jsextensions as required by the coding guidelines, and theMAX_NTP_SERVERSconstant is appropriately defined.
21-31: LGTM!The
getSystemTimemethod correctly retrieves system time configuration from the var state, extracts NTP servers, and returns a well-typedSystemTimeobject with appropriate defaults.
105-120: LGTM!The
normalizeNtpServersmethod 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.DateTimeFormatis 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:
formatManualDateTimeproperly formats the date in the expectedYYYY-MM-DD HH:MM:SSformat.resetTimezoneWatcherappropriately 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.
api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts
Outdated
Show resolved
Hide resolved
| 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.
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
parseConfigcan infer the configuration type from the file extension when thetypeparameter is omitted.Optional: Consider additional test cases for edge cases.
While the current test covers the happy path, you could add test cases for:
.cfgextension 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
markStepCompletedmethod accepts anyActivationOnboardingStepIdbut 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.tsapi/src/core/utils/misc/parse-config.tsapi/src/__test__/core/utils/misc/parse-config.test.tsapi/src/unraid-api/config/api-config.test.tsapi/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.tsapi/src/core/utils/misc/parse-config.tsapi/src/__test__/core/utils/misc/parse-config.test.tsapi/src/unraid-api/config/api-config.test.tsapi/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.tsapi/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.tsapi/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.tsapi/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.tsapi/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.tsapi/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
handleStateFileUpdatemethod 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
handleStateFileUpdatemethod, eliminating code duplication while maintaining consistent behavior.api/src/core/utils/misc/parse-config.ts (2)
127-132: LGTM! Clean normalization logic.The
normalizeExtensionhelper 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
isCompletionUpToDatewith 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
comparewith 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
atomicallypackage'swriteFilewith mode0o644ensures 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
lastTrackedVersionalready 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.mockwith 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
beforeEachcorrectly 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."
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
writeFileFswhilewriteTrackerState(line 311) uses atomicwriteFilefrom 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.completedStepsis 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.tsapi/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.tsapi/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.tsapi/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.
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
lastTrackedVersionfrom 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
introducedInvalue).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.tsapi/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.tsapi/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.tsapi/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
markStepCompletedmethod 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
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 anyis used here for test mocks, consider defining more precise types or interfaces formockEmhttpStateandmockPathsStateto 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 handlesundefinedcorrectly.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.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsapi/src/core/log.tsapi/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.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.tsapi/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsapi/src/core/log.tsapi/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.tsapi/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.tsapi/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.tsapi/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
loadApiConfigtests correctly validate that the function returns the default configuration withAPI_VERSIONas the version, both in the success path and when handling errors. This aligns with the implementation shown in the relevant code snippets whereversion: API_VERSIONoverrides 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
nullon 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
configFileis good defensive programming. It prevents undefined errors downstream and provides a clear error message for debugging.
100-102: LGTM!The delegation to
findActivationCodeFileremoves 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
CustomizationServicetoOnboardingService. 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.
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
stepComponentsassumes 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.tsto exportstepComponentsasRecord<StepId, Component>.
286-286: Simplify boolean comparison.
isHidden === falsecan be simplified to!isHiddenfor 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;isVisibleis a computed property derived fromisHiddenin the store.
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.DEVchecks 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 - QueriesactivationOnboarding` - Uses returned step metadata to decide wh...
(QB_NEW_EN)
[grammar] ~51-~51: There might be a mistake here.
Context: ...esh install and upgrade onboarding modes - Automatically detects which mode based o...
(QB_NEW_EN)
[grammar] ~52-~52: There might be a mistake here.
Context: ...detects which mode based on system state - Displays relevant steps for each mode ...
(QB_NEW_EN)
[grammar] ~53-~53: There might be a mistake here.
Context: ... - Displays relevant steps for each mode - Reuses existing step components (timezon...
(QB_NEW_EN)
[grammar] ~54-~54: There might be a mistake here.
Context: ...ting step components (timezone, plugins) - Relies on recorded completion status fro...
(QB_NEW_EN)
[grammar] ~128-~128: There might be a mistake here.
Context: ...tart the API 4. The modal should appear on next page load with relevant steps from...
(QB_NEW_EN)
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
availablePluginslist 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.tsis 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.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.mutation.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.tsweb/src/composables/gql/gql.tsweb/src/components/Activation/graphql/pluginInstallOperation.query.tsweb/__test__/components/Activation/ActivationPluginsStep.test.tsweb/src/components/Activation/usePluginInstaller.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsweb/src/components/Activation/graphql/installPlugin.mutation.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.tsweb/src/composables/gql/graphql.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.tsapi/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.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.tsapi/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.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.module.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.tsapi/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.tsapi/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.tsfilesPlace 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.tsweb/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.tsapi/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.tsfilesPlace 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. ThePluginInstallUpdatessubscription 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 installcommand accepts a positionalforcedargument or requires a flag (e.g.,-f/--force), then updatebuildPluginArgsaccordingly.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
beforeEachproperly 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
@ResolveFieldfor namespaced mutations (consistent with other mutation resolvers like ApiKeyMutations)- Applies appropriate
UPDATE_ANYpermissions onCONFIGresource 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
beforeEachproperly resets state between tests. TheMockExecaProcessclass appropriately extendsEventEmitterto simulate subprocess behavior.
47-88: Good test coverage for success path.The test correctly:
- Verifies the
execacall 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
pluginInstallOperationquery correctly:
- Uses nullable return type for optional lookup
- Applies
READ_ANYpermissions onCONFIGresource- 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
resolvefunction to extract the payload- Guards access with appropriate permissions
- Returns an
AsyncIterableIteratorfor 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:
SystemTimeModuleandUnraidPluginsModuleare imported as dependenciesOnboardingMutationsResolveris 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
togglePluginfunction 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
handleInstallfunction correctly:
- Handles the case when no plugins are selected
- Installs plugins sequentially with proper error handling
- Captures output via the
onEventcallback- Uses i18n for user-facing messages
- Handles both success and failure paths
The translation keys are properly used (not
props.tas 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
:forand:idattributes.
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
onboardingandunraidPluginsnamespaces 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
onCompleteis called multiple times. If verifying that clicking Continue triggersonCompleteexactly 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.')).
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: Documentpluginexecutable requirement. The service already catchesENOENTand 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 thepluginbinary 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
4c62b10 to
a88e9b8
Compare
|
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 (2)
web/__test__/components/Activation/ActivationPluginsStep.test.ts (2)
107-109: Preferwrapper.text()overwrapper.html()for text assertions.Using
wrapper.html().toContain()checks raw HTML strings and is brittle. Per coding guidelines, usewrapper.text()to assert rendered text.🔎 Proposed fix
- expect(wrapper.html()).toContain('installation started'); - expect(wrapper.html()).toContain('Installed'); - expect(wrapper.html()).toContain('installed successfully'); + expect(wrapper.text()).toContain('installation started'); + expect(wrapper.text()).toContain('Installed'); + expect(wrapper.text()).toContain('installed successfully');As per coding guidelines for
**/__test__/components/**/*.ts: Assert on rendered text content withwrapper.text().
141-142: Preferwrapper.text()overwrapper.html()for text assertions.Same issue: use
wrapper.text()instead ofwrapper.html()for asserting rendered text to avoid brittle HTML string matching.🔎 Proposed fix
- expect(wrapper.html()).toContain('Failed to install plugins. Please try again.'); - expect(wrapper.html()).toContain('Install failed'); + expect(wrapper.text()).toContain('Failed to install plugins. Please try again.'); + expect(wrapper.text()).toContain('Install failed');As per coding guidelines for
**/__test__/components/**/*.ts: Assert on rendered text content withwrapper.text().
🧹 Nitpick comments (1)
web/__test__/components/Activation/ActivationPluginsStep.test.ts (1)
115-118: Simplify mock assertion to avoid testing implementation details.Checking
mock.calls.lengthis testing implementation details. Use Vitest's built-in matcher to verify the callback was invoked.🔎 Proposed fix
- const callsBeforeContinue = props.onComplete.mock.calls.length; - await continueButton!.trigger('click'); - - expect(props.onComplete.mock.calls.length).toBeGreaterThanOrEqual(callsBeforeContinue + 1); + await continueButton!.trigger('click'); + + expect(props.onComplete).toHaveBeenCalled();As per coding guidelines: Test component behavior and output, not implementation details.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/__test__/components/Activation/ActivationPluginsStep.test.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use.rejects.toThrow()without arguments to test that functions throw errors, not exact error message strings
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
web/__test__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries likefind('button')rather than data-test IDs
UsecreatePinia()andsetActivePiniawhen testing Store files, only usecreateTestingPiniaif you specifically need its testing features
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/*.test.ts: Use.rejects.toThrow()without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Usevi.mock()for module-level mocks
Specify return values for component methods withvi.spyOn()
Reset mocks between tests withvi.clearAllMocks()
Always await async operations before making assertions
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Mock external dependencies and services in Vue component tests
Test component behavior and output, not implementation details
Verify that the expected elements are rendered in Vue components
Test component interactions such as clicks and inputs
Check for expected prop handling and event emissions in Vue components
UsecreateTestingPinia()for mocking stores in Vue components
Use semantic queries likefind('button')orfind('[data-test="id"]')but prefer not to use data test ID's when finding elements
Find Vue components withfindComponent(ComponentName)
UsefindAllto check for multiple elements in Vue components
Assert on rendered text content withwrapper.text()
Assert on element attributes withelement.attributes()
Verify element existence withexpect(element.exists()).toBe(true)
Check component state through rendered output
Trigger events withawait element.trigger('click')
Set input values withawait input.setValue('value')
Test emitted events withwrapper.emitted()
Useawait nextTick()for DOM updates in Vue component tests
UseflushPromises()for complex promise chains in Vue component tests
Files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
🧠 Learnings (21)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 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.
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions such as clicks and inputs
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Find Vue components with `findComponent(ComponentName)`
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Stub complex child components that aren't the focus of the test
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Assert on rendered text content with `wrapper.text()`
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test emitted events with `wrapper.emitted()`
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Test what the code does, not implementation details like exact error message wording
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check component state through rendered output
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use semantic queries like `find('button')` or `find('[data-test="id"]')` but prefer not to use data test ID's when finding elements
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Assert on element attributes with `element.attributes()`
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Trigger events with `await element.trigger('click')`
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: This is a Nuxt.js app but tests are run with vitest outside of the Nuxt environment
Applied to files:
web/__test__/components/Activation/ActivationPluginsStep.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `flushPromises()` for complex promise chains in Vue component tests
Applied to 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(106-115)
🔇 Additional comments (2)
web/__test__/components/Activation/ActivationPluginsStep.test.ts (2)
10-36: Good mock organization withvi.hoisted().The mock setup follows best practices by using
vi.hoisted()to ensure mocks are available before module imports, and properly preserves actual Apollo composable implementations while mocking only what's needed.
69-143: Comprehensive test coverage for success and failure scenarios.The tests properly cover both the happy path (successful installation with progress streaming) and error handling (failed installation). The use of
mockRejectedValuefor consistent failure behavior and proper async/await handling aligns with best practices.
|
@Ajit-Mehrotra - let's set this PR up for onboarding and make sure it works correctly on fresh systems / updated systems. You can test this on the Tools -> Onboarding testing page that I built, which we should remove before a production release. We are also trying to build a flow to allow a user to add a file so they can see a logo + "Welcome to your BLAH System, Powered by Unraid". You can see the basis for this system https://github.com/unraid/api/blob/main/web/src/components/Activation/WelcomeModal.standalone.vue |
(cherry picked from commit f7fe959)
(cherry picked from commit a32dd21)
* Automated fresh builds: Added `predocker:build-and-run` to automatically run `pnpm install` and rebuild `api`, `web`, and [ui] artifacts from source before starting the container. * Automation: Updated `docker:run` to automatically execute `pnpm build` inside the container before dropping into a shell. * Optimization: Updated [dc.sh] to respect `SKIP_HOST_BUILD` to prevent redundant builds when running via npm scripts. * Convenience: Added `docker:build-and-run` aliases to root, `api`, and `web` workspaces for easy access from anywhere in the monorepo. * Test: Added `test:modifications:update` script to `api` for easily refreshing file modification fixtures and snapshots. (cherry picked from commit 41aa09f)
…rds and animations
Refactors the onboarding wizard to accumulate state in a central store and execute changes in a unified summary step, while simultaneously upgrading the test harness for better developer experience. **Frontend Changes:** - **State Management**: Introduced `onboardingDraft` store to temporarily hold user choices (server name, description, theme, language, plugins, ssh) as they navigate the wizard, replacing scattered component logic. - **Summary & Execution**: Rewrote `ActivationSummaryStep.vue` to serve as the execution engine. It now sequentially applies server identity, installs the selected language, installs plugins (Community Apps, Connect, etc.), and updates SSH settings. - **Console Feedback**: Added [OnboardingConsole.vue](cci:7://file:///Users/ajitmehrotra/Projects/Unraid/feat-timezone-control/web/src/components/Activation/OnboardingConsole.vue:0:0-0:0) to the summary step to provide real-time log feedback during the multi-step installation process. - **Test Harness**: Overhauled [OnboardingTestHarness.standalone.vue](cci:7://file:///Users/ajitmehrotra/Projects/Unraid/feat-timezone-control/web/src/components/Activation/OnboardingTestHarness.standalone.vue:0:0-0:0): - Renamed activation action to "Open" for clarity. - Added visual selection state for active presets. - Implemented semantic color tokens (`bg-card`, `border-border`) for proper theming. - Added ability to create custom presets from the current editor state on the fly. - **Core Steps**: Updated `ActivationCoreSettingsStep` and `ActivationPluginsStep` to write to `onboardingDraft` instead of handling their own mutations. **Backend Changes:** - **Refactor**: Extracted logic from [OnboardingTrackerModule](cci:2://file:///Users/ajitmehrotra/Projects/Unraid/feat-timezone-control/api/src/unraid-api/config/onboarding-tracker.module.ts:11:0-16:39) into a dedicated [OnboardingTrackerService](cci:2://file:///Users/ajitmehrotra/Projects/Unraid/feat-timezone-control/api/src/unraid-api/config/onboarding-tracker.service.ts:24:0-364:1) to improve maintainability and testability. - **Schema**: Updated GraphQL schema to support new tracking and customization inputs. **Other:** - **Linting**: Applied linting and type fixes across the modified files.
BREAKING CHANGE: Renamed OnboardingScenario enum to OnboardingStatus API Changes: - Rename OnboardingScenario → OnboardingStatus - Rename FRESH_INSTALL → INCOMPLETE status - Rename scenario → status field in Onboarding type - Remove currentVersion from Onboarding (use info.versions.core.unraid) - Remove isInitialSetup from OnboardingState (use isFreshInstall) - Simplify onboarding-tracker service to focus on completion state Frontend Changes: - Rename useUpgradeOnboardingStore → useOnboardingStore - Rename UPGRADE_ONBOARDING_QUERY → ONBOARDING_QUERY - Rename COMPLETE_UPGRADE_STEP_MUTATION → COMPLETE_ONBOARDING_MUTATION - Update stores and components to use new API field names - Add OnboardingAdminPanel with dev presets and data source reference - Update WelcomeModal to use isFreshInstall instead of isInitialSetup Test updates for new API schema
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
| await completePendingOnboarding(); | ||
| } | ||
| stepSaveState.value = 'idle'; | ||
| if (stepSaveTimeout) { |
Check warning
Code scanning / CodeQL
Useless conditional Warning
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, to fix a useless conditional like if (stepSaveTimeout), ensure the variable can actually hold the intended truthy values, and that the check tests for the correct condition. For timeout handles in TypeScript, that typically means declaring them as a type that can store what setTimeout returns (e.g., ReturnType<typeof setTimeout> | null) and then checking if (stepSaveTimeout !== null) before calling clearTimeout.
Concretely for this file, the conditional in closeModal should check explicitly for a non‑null timeout handle, and the variable should be declared so its type includes the timeout handle returned by setTimeout. Since CodeQL says the condition is always false, the most robust minimal change (within the shown snippet) is to change if (stepSaveTimeout) to a null-check if (stepSaveTimeout !== null) so that both the type system and static analysis understand that the branch is reachable when a timeout has been set. This doesn’t change runtime behavior (because any non‑null timeout handle is truthy) but removes the “always false” condition by aligning with the actual possible values. No new imports are needed; we just adjust the conditional in the closeModal function where CodeQL flagged the issue. We do not alter other logic or behavior.
-
Copy modified line R188
| @@ -185,7 +185,7 @@ | ||
| await completePendingOnboarding(); | ||
| } | ||
| stepSaveState.value = 'idle'; | ||
| if (stepSaveTimeout) { | ||
| if (stepSaveTimeout !== null) { | ||
| clearTimeout(stepSaveTimeout); | ||
| stepSaveTimeout = null; | ||
| } |
| @@ -0,0 +1,47 @@ | |||
| import { Field, Int, ObjectType } from '@nestjs/graphql'; | |||
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, unused imports should be removed from the import list to keep the code clean and avoid confusion about what is actually used. Here, we only need Field and ObjectType from @nestjs/graphql, so we can safely remove Int from the import statement.
Concretely, in api/src/unraid-api/graph/resolvers/info/network/network.model.ts, update line 1 to remove Int from the destructuring import. No other code changes are necessary, and no additional methods, imports, or definitions are required, since Int is not used anywhere in the shown class.
-
Copy modified line R1
| @@ -1,4 +1,4 @@ | ||
| import { Field, Int, ObjectType } from '@nestjs/graphql'; | ||
| import { Field, ObjectType } from '@nestjs/graphql'; | ||
|
|
||
| import { Node } from '@unraid/shared/graphql.model.js'; | ||
|
|
| @@ -0,0 +1,26 @@ | |||
| import { Query, ResolveField, Resolver } from '@nestjs/graphql'; | |||
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
- In general, unused imports should be removed to improve readability and avoid confusion.
- Here, the fix is to adjust the import statement on line 1 to only import the decorators that are actually used:
ResolveFieldandResolver. RemovingQuerywill not change runtime behavior because it is never referenced in the file. - Specifically, in
api/src/unraid-api/graph/resolvers/info/network/network.resolver.ts, edit the first import line to deleteQueryfrom the destructuring import. - No additional methods, imports, or definitions are needed; we are only simplifying the existing import.
-
Copy modified line R1
| @@ -1,4 +1,4 @@ | ||
| import { Query, ResolveField, Resolver } from '@nestjs/graphql'; | ||
| import { ResolveField, Resolver } from '@nestjs/graphql'; | ||
|
|
||
| import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; | ||
| import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; |
| @@ -0,0 +1,26 @@ | |||
| import { Query, ResolveField, Resolver } from '@nestjs/graphql'; | |||
|
|
|||
| import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; | |||
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
To fix unused imports, they should be removed when they are not referenced anywhere in the file. This reduces clutter and prevents confusion about unused concepts (like permissions) that are not actually applied.
In this file, the best fix is to delete the entire import line on line 3:
import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';No other code changes are needed because these symbols are not used anywhere in InfoNetworkResolver. We do not need additional methods, imports, or definitions; we are only cleaning up an unused import in api/src/unraid-api/graph/resolvers/info/network/network.resolver.ts.
| @@ -1,6 +1,5 @@ | ||
| import { Query, ResolveField, Resolver } from '@nestjs/graphql'; | ||
|
|
||
| import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; | ||
| import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; | ||
|
|
||
| import { Info } from '@app/unraid-api/graph/resolvers/info/info.model.js'; |
| import { Query, ResolveField, Resolver } from '@nestjs/graphql'; | ||
|
|
||
| import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; | ||
| import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
To fix the issue, remove the unused UsePermissions import from this resolver file. This does not change any existing functionality because the symbol is not referenced anywhere in the class or its decorators.
Concretely:
- In
api/src/unraid-api/graph/resolvers/info/network/network.resolver.ts, delete the line importingUsePermissionsfrom@unraid/shared/use-permissions.directive.js. - No additional imports, methods, or definitions are needed.
| @@ -1,7 +1,6 @@ | ||
| import { Query, ResolveField, Resolver } from '@nestjs/graphql'; | ||
|
|
||
| import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; | ||
| import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; | ||
|
|
||
| import { Info } from '@app/unraid-api/graph/resolvers/info/info.model.js'; | ||
| import { InfoNetworkInterface } from '@app/unraid-api/graph/resolvers/info/network/network.model.js'; |
|
|
||
| import { networkInterfaces } from 'systeminformation'; | ||
|
|
||
| import { getters } from '@app/store/index.js'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, unused imports should be removed to improve readability and avoid confusion about dependencies. They can also slightly impact compilation and tooling performance and may indicate outdated or incomplete refactors.
For this specific case, the best fix is to delete the unused named import of getters from @app/store/index.js in api/src/unraid-api/graph/resolvers/info/network/network.service.ts. This does not change any existing functionality, because getters is not referenced anywhere in the code shown. No additional methods, imports, or definitions are required. Only the single import line should be removed; all other imports remain unchanged.
| @@ -2,7 +2,6 @@ | ||
|
|
||
| import { networkInterfaces } from 'systeminformation'; | ||
|
|
||
| import { getters } from '@app/store/index.js'; | ||
| import { InfoNetworkInterface } from '@app/unraid-api/graph/resolvers/info/network/network.model.js'; | ||
|
|
||
| @Injectable() |
| @@ -1,6 +1,7 @@ | |||
| import { Field, ID, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; | |||
| import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; | |||
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
To fix the problem, remove the unused ID symbol from the named import list from @nestjs/graphql. This keeps the import of all actually used decorators/types while eliminating the dead import.
Concretely, in api/src/unraid-api/graph/resolvers/vars/vars.model.ts, on the first line, change the import from:
import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';to:
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';No other code changes, methods, or additional imports are required.
-
Copy modified line R1
| @@ -1,4 +1,4 @@ | ||
| import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; | ||
| import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; | ||
|
|
||
| import { Node } from '@unraid/shared/graphql.model.js'; | ||
| import { IsBoolean, IsInt, Max, Min } from 'class-validator'; |
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: 13
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web/scripts/deploy-dev.sh (1)
76-83:⚠️ Potential issue | 🟠 MajorRetries won’t re-run the heredoc script after the first failure.
The heredoc is consumed on the first attempt, so subsequent retries execute with empty stdin. Wrap the SSH call in a function so each retry replays the script.
🐛 Proposed fix
update_auth_request() { local server_name="$1" - retry 3 2 ssh $SSH_OPTS "root@${server_name}" /bin/bash -s << 'EOF' + run_update_auth_request() { + ssh $SSH_OPTS "root@${server_name}" /bin/bash -s << 'EOF' set -euo pipefail set -o errtrace AUTH_REQUEST_FILE='/usr/local/emhttp/auth-request.php' UNRAID_COMPS_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/' @@ -EOF +EOF + } + + retry 3 2 run_update_auth_request }api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
123-131:⚠️ Potential issue | 🟠 MajorPrevent
showBannerGradientfrom overwriting settings when absent.The default
trueplus the current transform will setshowBannerGradienteven when the activation data doesn’t specify it, which can overwrite existing user settings. Returnundefinedfor missing values and drop the default initializer.🐛 Proposed fix
`@Field`(() => Boolean, { nullable: true }) `@IsOptional`() `@Transform`(({ value }) => { if (typeof value === 'boolean') return value; + if (value === undefined || value === null) return undefined; const sanitized = sanitizeString(value); return sanitized === 'yes'; }) -showBannerGradient?: boolean = true; +showBannerGradient?: boolean;
🤖 Fix all issues with AI agents
In `@api/src/unraid-api/config/onboarding-tracker.service.ts`:
- Around line 177-184: The writeTrackerState method swallows file write errors
causing callers (markCompleted/reset) to proceed with syncConfig() and return an
unchanged getState(); update writeTrackerState to propagate failure so callers
can react: change writeTrackerState to re-throw the caught error (or return a
boolean success) instead of only logging, and update markCompleted and reset to
handle the thrown error (or check the boolean) before calling syncConfig() and
returning getState(); reference the methods writeTrackerState, markCompleted,
reset, syncConfig, getState, and the this.state assignment to locate and
implement the change.
In `@api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts`:
- Around line 148-151: The GraphQL field decorator for hasPartnerLogo is
declared non-nullable while the TypeScript type allows null/undefined; update
the `@Field`() on hasPartnerLogo to include { nullable: true } so the GraphQL
schema matches the TypeScript type (e.g., change `@Field`(() => Boolean, {
description: 'Indicates if a partner logo exists' }) to include nullable: true)
ensuring ActivationCodeModel/hasPartnerLogo can be omitted or null without
serialization errors.
In `@api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts`:
- Around line 66-78: The upgrade branch misclassifies completed systems when
getCurrentVersion() returns the 'unknown' sentinel; change the logic in the
resolver around onboardingTracker.getCurrentVersion(), state.completedAtVersion
and the OnboardingStatus assignment so the UPGRADE case only runs if a real
currentVersion is present (e.g., currentVersion is truthy and not the 'unknown'
sentinel) and state.completedAtVersion !== currentVersion; otherwise treat it as
COMPLETED. Update the conditional that sets OnboardingStatus.UPGRADE to include
the extra guard against the 'unknown' sentinel.
In `@api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts`:
- Around line 176-188: The tests are failing because fs/promises isn't
module-mocked and there are unnecessary "as any" casts; add a top-level
vi.mock('fs/promises') next to the existing vi.mock('file-exists') and then
remove the "as any" casts in the mock setup (replace
vi.mocked(fs.mkdir).mockResolvedValue(undefined as any) etc. with properly typed
mockResolvedValue calls), ensuring you use vi.mocked(...) for fs.mkdir,
fs.access, fs.readdir, fs.readFile, fs.writeFile, fs.copyFile and for fileExists
as shown elsewhere in this test file (see other mocks around lines ~1000) so
typings align.
In `@api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts`:
- Around line 125-135: The code currently spreads activationData.branding into a
plain object and casts it with "branding as any"; replace this with a real
BrandingConfig instance: create a new BrandingConfig(), copy relevant properties
from activationData.branding (or use a constructor/initializer if available),
set hasPartnerLogo using await fileExists(paths.activation.logo) and set logoUrl
to paths.webgui.logo.assetPath, then return branding typed as BrandingConfig (no
"as any"). Update the block around getters.paths(), activationData and the
return object so the returned branding is an actual BrandingConfig instance
rather than a mutated plain object.
In `@api/src/unraid-api/graph/resolvers/info/display/display.service.ts`:
- Around line 110-138: setLocale and setTheme currently accept arbitrary strings
and persist them without validation; update the GraphQL arg types to use enums
(e.g., change the resolver `@Args` to `@Args`('theme', { type: () => ThemeName })
and similarly for locale), and add input validation inside the service methods
(setLocale and setTheme) before calling updateCfgFile by reusing the existing
validateEnumValue pattern (or a new validateEnumValue call) to ensure the
provided theme/locale is in the allowed enum; if validation fails, throw a clear
error instead of writing to dynamix.cfg, then proceed to call updateCfgFile and
refresh the in-memory config as before.
In `@api/src/unraid-api/graph/resolvers/servers/server.resolver.ts`:
- Around line 52-62: The updateServerIdentity resolver is returning a partial
Server (id, name, comment) via serverService.updateServerIdentity and casting it
to any, which breaks the GraphQL ServerModel contract; change the resolver to
return a complete ServerModel by either (a) having
serverService.updateServerIdentity construct and return the full Server object
(including owner, guid, apikey, status, wanip, lanip, localurl, remoteurl, etc.)
or (b) calling serverService.getLocalServer(updatedId) (or equivalent) after the
identity update and returning that full model instead of the partial result, and
remove the as any cast. Ensure updateServerIdentity and getLocalServer
signatures/types reflect the proper Server model.
In `@api/src/unraid-api/graph/resolvers/servers/server.service.ts`:
- Around line 70-91: The returned object in the emcmd success path currently
uses an unsafe cast ("as any") and only includes id, name, comment; replace this
by returning a complete Server object (no any) containing all required fields
(owner, guid, apikey, status, wanip, lanip, localurl, remoteurl, etc.) — either
by calling ServerResolver.getLocalServer() or by refetching the local server
from the same source the resolver uses and returning that full Server instance;
while doing so remove the large block of redundant comments and keep just a
brief clarifying comment if needed.
In `@web/__test__/store/activationCodeData.test.ts`:
- Around line 180-202: The test uses a non-existent enum member 'REGISTERED' for
RegistrationState; replace the cast with a real enum value (e.g.,
RegistrationState.BASIC or any valid non-ENOKEYFILE state) in the mocked
response for ACTIVATION_CODE_QUERY so the useQuery mock returns a valid
onboardingState.registrationState; ensure the test still simulates
backend-determined isFreshInstall and that
useActivationCodeDataStore().isFreshInstall assertion remains unchanged.
In `@web/scripts/deploy-dev.sh`:
- Around line 35-38: The script currently runs "pnpm run clean" and "pnpm run
build" without failing fast and omits required code generation; update the
deploy script to run "pnpm codegen" in web/ before building and ensure the
script fails immediately on any command error (e.g., enable "set -e" or check
exit codes) so that if "pnpm run clean" or "pnpm run build" fails the deploy
stops and no stale artifacts are used.
In `@web/src/components/Onboarding/components/OnboardingPartnerLogo.vue`:
- Around line 14-26: The current template uses a redundant outer <template
v-if="partnerInfo?.partner?.url"> which makes the inner v-else unreachable;
remove the outer conditional and apply a single conditional around the anchor vs
direct image rendering so the component renders an <a> wrapper when
partnerInfo?.partner?.url is present and otherwise renders
<OnboardingPartnerLogoImg> directly—locate the OnboardingPartnerLogo.vue
template and adjust the conditional logic around the <a> element and
<OnboardingPartnerLogoImg> (referencing partnerInfo?.partner?.url and the
OnboardingPartnerLogoImg component) to implement this behavior.
In `@web/src/components/Onboarding/graphql/activationOnboarding.query.ts`:
- Line 1: The import for the graphql symbol in activationOnboarding.query.ts
needs to use the explicit ESM module entry like other .query.ts files (e.g.,
public-oidc-providers.query.ts); update the import statement so it references
the .js entry used by the project's graphql composable to ensure ESM
compatibility. Locate the top-level import of graphql and change it to match the
explicit .js composable import pattern used elsewhere in the codebase.
In `@web/src/components/Onboarding/standalone/WelcomeModal.standalone.vue`:
- Around line 2-55: showModal is initialized before isFreshInstall resolves so
the modal can remain closed on fresh installs; change initialization to react to
isFreshInstall (and isLoginPage) by setting showModal when isFreshInstall.value
becomes true (use a watcher/effect on isFreshInstall and isLoginPage) and add a
local dismissed flag that dropdownHide/hideWelcomeModal set to true to prevent
auto re-open; update showWelcomeModal to respect the dismissed flag and
reference the existing showModal, isFreshInstall, isLoginPage,
dropdownHide/hideWelcomeModal, and showWelcomeModal symbols when implementing
the watcher and dismissal logic.
🟡 Minor comments (34)
web/src/components/Onboarding/components/TypographyCloud.vue-20-28 (1)
20-28:⚠️ Potential issue | 🟡 MinorRemove non-essential template comments.
The template comments are descriptive but not necessary and add noise.
As per coding guidelines, Never add comments unless they are needed for clarity of function.
api/src/unraid-api/graph/resolvers/servers/server.service.ts-20-86 (1)
20-86:⚠️ Potential issue | 🟡 MinorTrim redundant inline comments.
Several comments restate the code and add noise; keep only essential doc comments.
As per coding guidelines, Never add comments unless they are needed for clarity of function; Never add comments for obvious things, and avoid commenting when starting and ending code blocks.
web/src/components/Onboarding/components/TypographyCloud.vue-18-45 (1)
18-45:⚠️ Potential issue | 🟡 MinorMark the decorative word cloud as aria-hidden.
This background is purely decorative, but screen readers will announce each word. Please add
aria-hidden="true"to the wrapper.🔧 Proposed fix
- <div class="bg-background pointer-events-none absolute inset-0 z-0 overflow-hidden"> + <div + class="bg-background pointer-events-none absolute inset-0 z-0 overflow-hidden" + aria-hidden="true" + >web/src/components/Onboarding/steps/OnboardingWelcomeStep.vue-9-121 (1)
9-121:⚠️ Potential issue | 🟡 MinorRemove non-essential comments in script/template.
Several comments (e.g., section labels) restate the markup and add noise.
As per coding guidelines, Never add comments unless they are needed for clarity of function; Never add comments for obvious things, and avoid commenting when starting and ending code blocks.
web/src/components/Onboarding/store/activationCodeModal.ts-5-8 (1)
5-8:⚠️ Potential issue | 🟡 MinorAdd
.jsextensions on internal imports.Internal imports must use
.jsextensions for ESM compatibility:🔧 Proposed fix
-import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts'; +import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts.js'; -import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData'; +import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData.js'; -import { useCallbackActionsStore } from '~/store/callbackActions'; +import { useCallbackActionsStore } from '~/store/callbackActions.js';web/src/components/Onboarding/graphql/completeUpgradeStep.mutation.ts-1-14 (1)
1-14:⚠️ Potential issue | 🟡 MinorUse
.jsfor internal ESM imports and runpnpm codegento register the mutation document.The import path should include the
.jsextension for ESM compatibility:🔧 Proposed fix
-import { graphql } from '~/composables/gql'; +import { graphql } from '~/composables/gql/gql.js';After adding this mutation, run
pnpm codegento register the typed document in the GraphQL generated types.web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue-145-152 (1)
145-152:⚠️ Potential issue | 🟡 MinorMissing guard for optional
onBackcallback.When
showBackistruebutonBackisundefined, clicking the Back button will attempt to invokeundefined, causing a runtime error.🐛 Proposed fix
<button v-if="showBack" - `@click`="props.onBack" + `@click`="props.onBack?.()" class="text-muted hover:text-toned group flex w-full items-center justify-center gap-2 font-medium transition-colors sm:w-auto sm:justify-start" >web/src/store/server.ts-43-43 (1)
43-43:⚠️ Potential issue | 🟡 MinorAdd
.jsextension to the activationCodeData store import.This import must include the
.jsextension for ESM compatibility.Proposed fix
-import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData'; +import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData.js';web/src/components/Onboarding/graphql/timeZoneOptions.query.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorAdd
.jsextension to the GraphQL helper import.The file should import explicitly from the module with a
.jsextension for ESM compatibility, rather than relying on the barrel export.♻️ Proposed fix
-import { graphql } from '~/composables/gql'; +import { graphql } from '~/composables/gql/gql.js';web/src/components/Onboarding/graphql/updateSystemTime.mutation.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorUse a
.jsextension in the graphql helper import for ESM compatibility.-import { graphql } from '~/composables/gql'; +import { graphql } from '~/composables/gql.js';Per coding guidelines, TypeScript imports must include
.jsextensions for ESM compatibility, referencing the compiled JavaScript output.api/src/unraid-api/graph/resolvers/vars/vars.model.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRemove unused
IDimport.Static analysis indicates
IDis imported but not used in this file.Proposed fix
-import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';api/src/unraid-api/graph/resolvers/vars/vars.service.ts-18-18 (1)
18-18:⚠️ Potential issue | 🟡 MinorAvoid using
anytype.The
formatValhelper usesanywhich violates coding guidelines.Proposed fix
- const formatVal = (val: any) => (val !== undefined && val !== null ? String(val) : ''); + const formatVal = (val: string | number | boolean | undefined | null) => (val !== undefined && val !== null ? String(val) : '');api/src/unraid-api/graph/resolvers/info/network/network.resolver.ts-1-5 (1)
1-5:⚠️ Potential issue | 🟡 MinorRemove unused imports flagged by CodeQL.
Query,AuthAction,Resource, andUsePermissionsaren’t used.🧹 Suggested fix
-import { Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { ResolveField, Resolver } from '@nestjs/graphql'; @@ -import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; -import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';api/src/unraid-api/graph/resolvers/info/network/network.service.ts-5-5 (1)
5-5:⚠️ Potential issue | 🟡 MinorRemove unused
gettersimport (CodeQL).It’s unused and flagged by static analysis.
🧹 Suggested fix
-import { getters } from '@app/store/index.js';web/src/components/Onboarding/standalone/WelcomeModal.standalone.vue-10-11 (1)
10-11:⚠️ Potential issue | 🟡 MinorAdd
.jsextensions to store imports.Internal TS ESM imports should include
.jsextensions.🔧 Suggested fix
-import { useWelcomeModalDataStore } from '~/components/Onboarding/store/welcomeModalData'; -import { useThemeStore } from '~/store/theme'; +import { useWelcomeModalDataStore } from '~/components/Onboarding/store/welcomeModalData.js'; +import { useThemeStore } from '~/store/theme.js';web/src/components/Onboarding/steps/OnboardingSummaryStep.vue-94-99 (1)
94-99:⚠️ Potential issue | 🟡 MinorLocalize remaining user-facing strings in the summary step.
Labels like “Trial Ready”, “Activation”, “Configuration”, “Theme”, “Language”, “View Selected/Hide Selected”, “No plugins selected”, and “System Setup Log” should go through i18n for consistency.
🧹 Example fix (apply similarly to other literals)
- <span class="text-muted text-xs uppercase">Description</span> + <span class="text-muted text-xs uppercase"> + {{ t('onboarding.summaryStep.descriptionLabel') }} + </span> @@ - <span class="text-muted">Activation</span> + <span class="text-muted">{{ t('onboarding.summaryStep.activationLabel') }}</span>Also applies to: 280-336, 357-385, 401-404
api/src/unraid-api/graph/resolvers/info/info.module.ts-33-35 (1)
33-35:⚠️ Potential issue | 🟡 MinorRemove the duplicate
VersionsServiceprovider.Registering the same provider twice is redundant and can complicate overrides.
🧹 Suggested fix
- VersionsService, - VersionsService, + VersionsService,web/__test__/components/Onboarding/OnboardingModal.test.ts-8-8 (1)
8-8:⚠️ Potential issue | 🟡 MinorRestore
window.addEventListenerafter tests.The global override can leak into other test files; reset it in an
afterEach.🧹 Suggested fix
-import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ describe('Onboarding/OnboardingModal.vue', () => { beforeEach(() => { @@ }); + + afterEach(() => { + window.addEventListener = originalAddEventListener; + });Also applies to: 229-235, 237-262
api/src/unraid-api/graph/resolvers/info/network/network.model.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRemove unused
Intimport.Static analysis flagged
Intas unused; dropping it avoids lint noise.🧹 Suggested fix
-import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { Field, ObjectType } from '@nestjs/graphql';web/src/components/Onboarding/graphql/activationCode.query.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorAdd
.jsextension to the localgraphqlimport for ESM compatibility.-import { graphql } from '~/composables/gql/gql'; +import { graphql } from '~/composables/gql/gql.js';web/__test__/components/Onboarding/OnboardingModal.test.ts-10-12 (1)
10-12:⚠️ Potential issue | 🟡 MinorUse
.jsextension for the local i18n import.The codebase consistently uses
.jsextensions in imports throughoutweb/srcand in some test files (e.g.,web/__test__/composables/useAuthorizationLink.test.ts). This is required for proper ESM module resolution.Suggested fix
-import { createTestI18n, testTranslate } from '../../utils/i18n'; +import { createTestI18n, testTranslate } from '../../utils/i18n.js';web/src/components/Onboarding/steps/OnboardingPluginsStep.vue-65-71 (1)
65-71:⚠️ Potential issue | 🟡 MinorPreserve an explicit “no plugins selected” choice.
initialSelectionusessize > 0, so an intentionally empty draft (from Skip) is treated as “first visit” and resets to defaults. Consider tracking an explicit “selectionInitialized” flag in the draft store or allowing an empty set to be honored when it was previously saved.Also applies to: 144-152
web/src/components/Onboarding/OnboardingModal.vue-97-103 (1)
97-103:⚠️ Potential issue | 🟡 Minor
stepSaveTimeoutis never set.Static analysis indicates the cleanup condition is always false. Either wire the timeout where step saving occurs or remove the dead state to avoid misleading maintenance.
Also applies to: 188-191
web/src/components/Onboarding/steps/OnboardingPluginsStep.vue-61-65 (1)
61-65:⚠️ Potential issue | 🟡 MinorTrim non-essential inline comments.
Several comments read like in-progress notes/TODOs rather than clarifying non-obvious logic. Please remove or move them to a task tracker.
As per coding guidelines: **/*: Never add comments unless they are needed for clarity of function.Also applies to: 97-101, 145-147
web/src/components/Onboarding/OnboardingSteps.vue-108-121 (1)
108-121:⚠️ Potential issue | 🟡 MinorAllow step clicks when default steps are used.
When
props.stepsis empty, you render the default list but the click handler never fires because it checksprops.steps.length. UsetimelineSteps.value.length(or default steps length) so clicks work in the fallback path.🛠️ Suggested fix
- if (actualStepIndex < props.steps.length) { + if (actualStepIndex < timelineSteps.value.length) { props.onStepClick(actualStepIndex); }web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue-9-20 (1)
9-20:⚠️ Potential issue | 🟡 MinorTrim section header comments that don’t clarify non-obvious logic.
Several comment headers (e.g., “Theme Images”, “Language Logic”, “Submit Logic”) restate the code and can be removed.
As per coding guidelines: **/*: Never add comments unless they are needed for clarity of function.web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue-67-105 (1)
67-105:⚠️ Potential issue | 🟡 MinorPreserve explicit empty-string draft values.
Using truthy checks (
if (d.serverDescription)) means an intentionally cleared value is ignored and replaced with server data. Prefer!== undefinedchecks and a proper “draft initialized” indicator.🛠️ Suggested fix
- if (d.serverName) { + if (d.serverName !== undefined) { serverName.value = d.serverName; } else if (res.data?.server || res.data?.vars) { serverName.value = res.data?.server?.name || res.data?.vars?.name || ''; } - if (d.serverDescription) { + if (d.serverDescription !== undefined) { serverDescription.value = d.serverDescription; } else if (res.data?.server) { serverDescription.value = res.data.server.comment || ''; } - const hasDraft = !!d.serverName; + const hasDraft = + d.serverName !== undefined || + d.serverDescription !== undefined || + d.selectedTimeZone !== undefined || + d.selectedTheme !== undefined || + d.selectedLanguage !== undefined || + d.useSsh !== undefined;web/src/components/Onboarding/OnboardingSteps.vue-5-8 (1)
5-8:⚠️ Potential issue | 🟡 MinorAdd
.jsextensions to local ESM imports.The imports from
~/components/Onboarding/stepRegistry(a TypeScript file) must include.jsextensions for ESM compatibility, consistent with the codebase pattern seen in other Vue files likePermissionCounter.vue.🛠️ Suggested fix
-import type { StepMetadataEntry } from '~/components/Onboarding/stepRegistry'; +import type { StepMetadataEntry } from '~/components/Onboarding/stepRegistry.js'; -import { stepMetadata } from '~/components/Onboarding/stepRegistry'; +import { stepMetadata } from '~/components/Onboarding/stepRegistry.js';web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue-10-20 (1)
10-20:⚠️ Potential issue | 🟡 MinorAdd
.jsextensions to TypeScript imports for ESM compatibility.Only the
.query.tsand.tsstore imports need.jsextensions. PNG assets and Vue components remain unchanged.🛠️ Suggested fix
-import { GET_AVAILABLE_LANGUAGES_QUERY } from '@/components/Onboarding/graphql/availableLanguages.query'; -import { GET_CORE_SETTINGS_QUERY } from '@/components/Onboarding/graphql/getCoreSettings.query'; -import { TIME_ZONE_OPTIONS_QUERY } from '@/components/Onboarding/graphql/timeZoneOptions.query'; -import { useOnboardingDraftStore } from '@/components/Onboarding/store/onboardingDraft'; +import { GET_AVAILABLE_LANGUAGES_QUERY } from '@/components/Onboarding/graphql/availableLanguages.query.js'; +import { GET_CORE_SETTINGS_QUERY } from '@/components/Onboarding/graphql/getCoreSettings.query.js'; +import { TIME_ZONE_OPTIONS_QUERY } from '@/components/Onboarding/graphql/timeZoneOptions.query.js'; +import { useOnboardingDraftStore } from '@/components/Onboarding/store/onboardingDraft.js';Per codebase guidelines: **/*.{ts,tsx,js,jsx} must use
.jsextensions in imports for ESM compatibility.web/src/components/Onboarding/OnboardingModal.vue-9-24 (1)
9-24:⚠️ Potential issue | 🟡 MinorAdd
.jsextensions to local ESM imports for TypeScript files.Per the codebase's ESM requirements, all imports from TypeScript files (including
.mutation.ts,.query.ts, and store files) must include.jsextensions, even though TypeScript's import resolution allows omitting them.🛠️ Suggested fix
-import { COMPLETE_ONBOARDING_MUTATION } from '@/components/Onboarding/graphql/completeUpgradeStep.mutation'; +import { COMPLETE_ONBOARDING_MUTATION } from '@/components/Onboarding/graphql/completeUpgradeStep.mutation.js'; -import { stepComponents } from '~/components/Onboarding/stepRegistry'; -import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData'; -import { useActivationCodeModalStore } from '~/components/Onboarding/store/activationCodeModal'; -import { useUpgradeOnboardingStore } from '~/components/Onboarding/store/upgradeOnboarding'; +import { stepComponents } from '~/components/Onboarding/stepRegistry.js'; +import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData.js'; +import { useActivationCodeModalStore } from '~/components/Onboarding/store/activationCodeModal.js'; +import { useUpgradeOnboardingStore } from '~/components/Onboarding/store/upgradeOnboarding.js';api/src/unraid-api/graph/resolvers/info/display/display.service.ts-151-155 (1)
151-155:⚠️ Potential issue | 🟡 MinorRemove type assertions for ini.parse() and response.json(); add runtime validation for external data and input constraints.
Type assertions at line 154 bypass shape validation on config file parsing. The
inilibrary can be used directly without casting—see howparseConfighandles it. Line 264 similarly masks data integrity by casting external fetch results; validate the response shape at runtime instead. Additionally,setLocale()andsetTheme()accept bare strings without enum validation; constrain these inputs to match allowed theme/locale values (unlike thecustomization.mutations.resolvervariant which properly usesThemeNameenum).As per coding guidelines: **/*.{ts,tsx}: Never use the
anytype. Always prefer proper typing. Avoid using casting whenever possible, prefer proper typing from the start.web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue-224-226 (1)
224-226:⚠️ Potential issue | 🟡 MinorRemove the cast and use generated GraphQL types.
The
GetAvailableLanguagesQuerytype from codegen already defines the correct shape for the query result. UselanguagesResult.value?.availableLanguagesdirectly without casting, or update the localAvailableLanguageinterface to match the generated type (note thaturlis optional and nullable in the actual GraphQL type, not required).web/__test__/components/Onboarding/WelcomeModal.test.ts-269-279 (1)
269-279:⚠️ Potential issue | 🟡 MinorAlign the “loading” test with actual behavior.
The test name says it disables the button on loading, but no loading state is set and the assertion checks the opposite. Either set loading and assert disabled, or rename the test and drop the explanatory comments.As per coding guidelines: Never add comments unless they are needed for clarity of function.🔧 Suggested adjustment
- it('disables the Create a password button when loading', async () => { - // The WelcomeModal component doesn't use the loading state from the store - // Instead, it uses its own internal state. For now, we'll test that the button exists - // and can be clicked (the actual loading behavior would need to be implemented) + it('renders the Get Started button by default', async () => { const wrapper = await mountComponent(); const button = wrapper.find('button'); expect(button.exists()).toBe(true); - // The button should not be disabled by default since loading state is not implemented expect(button.attributes('disabled')).toBeUndefined(); });web/__test__/components/Onboarding/WelcomeModal.test.ts-290-294 (1)
290-294:⚠️ Potential issue | 🟡 MinorAwait the async mount before asserting.
mountComponent()is an async function that needs to be awaited; the test must be marked as async and the call awaited before the assertion runs.🔧 Suggested fix
- it('calls fetchTheme on mount', () => { - mountComponent(); + it('calls fetchTheme on mount', async () => { + await mountComponent(); expect(mockThemeStore.fetchTheme).toHaveBeenCalled(); });
🧹 Nitpick comments (16)
plugin/source/dynamix.unraid.net/install/doinst.sh (1)
34-39: Remove the duplicated corepack/npm/npx relink block.Lines 34-39 repeat the exact commands from Lines 28-33, adding redundant work and maintenance noise without changing behavior.
♻️ Proposed cleanup
-( cd usr/local/bin ; rm -rf corepack ) -( cd usr/local/bin ; ln -sf ../lib/node_modules/corepack/dist/corepack.js corepack ) -( cd usr/local/bin ; rm -rf npm ) -( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npm-cli.js npm ) -( cd usr/local/bin ; rm -rf npx ) -( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npx-cli.js npx )unraid-ui/src/components/common/dialog/Dialog.vue (1)
35-38: Remove the placeholder comment.
The comment doesn’t add clarity and violates the “no unnecessary comments” guideline.🧹 Proposed cleanup
-// ... (rest of props destructuring or use `props` directly)As per coding guidelines, "Never add comments unless they are needed for clarity of function".
web/src/components/Onboarding/graphql/availableLanguages.query.ts (1)
1-11: Prefer the codegengraphqlhelper for typed documents.Using the project
graphqlhelper keeps this query aligned with generated typings.🔧 Proposed refactor
-import gql from 'graphql-tag'; +import { graphql } from '~/composables/gql.js'; -export const GET_AVAILABLE_LANGUAGES_QUERY = gql` +export const GET_AVAILABLE_LANGUAGES_QUERY = graphql(/* GraphQL */ ` query GetAvailableLanguages { availableLanguages { code name url } } -`; +`);After switching, re-run
pnpm codegento update types.As per coding guidelines, Always use TypeScript imports with
.jsextensions for ESM compatibility; web/**/*: Always runpnpm codegenfor GraphQL code generation in the web directory.api/src/unraid-api/config/onboarding-state.service.ts (1)
31-31: Redundant?? undefinedexpression.The nullish coalescing
?? undefinedis redundant since the expression already evaluates toundefinedwhen the left-hand side is nullish.♻️ Suggested simplification
- return (getters.emhttp().var?.regState as RegistrationState | undefined) ?? undefined; + return getters.emhttp().var?.regState as RegistrationState | undefined;web/src/components/Onboarding/components/LogoCloud.vue (1)
104-115: Row expansion may create excessive duplicates.The while loop duplicates the entire
row.itemsarray each iteration. For a row with 4 items andMIN_ITEMS_PER_ROW = 15, this results in 16 items (4→8→16). Consider slicing to exactly meet the minimum.♻️ More precise expansion
const rows = computed(() => { return originalRows.map((row) => { - let currentItems = [...row.items]; - while (currentItems.length < MIN_ITEMS_PER_ROW) { - currentItems = [...currentItems, ...row.items]; + const baseItems = row.items; + const repeatCount = Math.ceil(MIN_ITEMS_PER_ROW / baseItems.length); + const currentItems = Array.from({ length: repeatCount }, () => baseItems).flat(); - } return { ...row, items: currentItems, }; }); });web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue (2)
299-302: Magic delay for modal opening.The
setTimeout(..., 100)is a timing workaround that may be fragile. Consider usingnextTickor awaiting the mutation's refetch completion before opening the modal.♻️ Suggested improvement
const applyAndOpenPreset = async (preset: Preset) => { activePresetId.value = preset.id; draftJson.value = formattedOverrides(preset.overrides); errorMessage.value = ''; await applyOverrides(); - // Force open modal - setTimeout(() => activationModalStore.setIsHidden(false), 100); + // Open modal after state has settled + await nextTick(); + activationModalStore.setIsHidden(false); };Import
nextTickfrom vue:import { ref, nextTick } from 'vue';
29-53: Consider using generated GraphQL documents.The mutations are defined inline using
parse(). If the project uses GraphQL codegen (per guidelines mentioningpnpm codegen), these should ideally be moved to.graphqlfiles for type safety and consistency.web/__test__/components/Onboarding/OnboardingPartnerLogo.test.ts (1)
26-26: Rename the stub variable to match the Onboarding naming for clarity.Proposed change
-const mockActivationPartnerLogoImg = { +const mockOnboardingPartnerLogoImg = { template: '<div data-testid="partner-logo-img"></div>', props: ['partnerInfo'], };- OnboardingPartnerLogoImg: mockActivationPartnerLogoImg, + OnboardingPartnerLogoImg: mockOnboardingPartnerLogoImg,api/src/unraid-api/graph/resolvers/vars/vars.service.ts (1)
61-66: Consider improving the return type handling.The double cast (
as unknown as Vars) is a code smell. The spread ofcurrentVarsmay include properties not defined in theVarstype. Consider explicitly constructing the return object with only the necessary fields or properly typingcurrentVars.web/src/components/Onboarding/store/upgradeOnboarding.ts (1)
36-36: Consider simplifying the loading wrapper.The
computed(() => onboardingLoading.value)creates an extra computed layer around an already-reactive ref. You could exposeonboardingLoadingdirectly or rename it toloadingduring destructuring.♻️ Optional simplification
const { result: onboardingResult, - loading: onboardingLoading, + loading, refetch, } = useQuery(ONBOARDING_QUERY, {}, { errorPolicy: 'all' }); // ... in return statement: return { - loading: computed(() => onboardingLoading.value), + loading, // ... };web/src/components/Onboarding/components/OnboardingConsole.vue (1)
40-64: Consider accessibility for screen readers.The logs area uses color and icons for status indication. For better accessibility, consider adding
aria-live="polite"to the logs container so screen readers announce new entries.♿ Optional accessibility improvement
<!-- Logs Area --> <div + aria-live="polite" class="scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-transparent flex-1 space-y-1 overflow-y-auto p-4" >web/src/components/Onboarding/graphql/coreSettings.mutations.ts (1)
1-39: File naming convention: consider using.mutation.ts(singular).The coding guidelines specify that GraphQL mutations should be placed in
.mutation.tsfiles (singular). Other files in this PR follow the singular convention (e.g.,installLanguage.mutation.ts). Consider renaming tocoreSettings.mutation.tsfor consistency.The mutation definitions themselves are well-structured and correctly request appropriate return fields.
web/src/store/purchase.ts (1)
34-39: Remove the stale TODO about a type cast.There’s no cast here; the comment adds noise and conflicts with the “no unnecessary comments” guideline.
🧹 Suggested cleanup
- const buildActionPayload = (type: PurchaseActionType) => [ - { - /** - * `@todo` Remove the type cast once the payload type can be more specific. - */ - server: buildServerPayload(), - type, - }, - ]; + const buildActionPayload = (type: PurchaseActionType) => [ + { + server: buildServerPayload(), + type, + }, + ];api/src/unraid-api/graph/resolvers/info/network/network.service.ts (1)
15-31: Avoidas InfoNetworkInterfacecasts; type the mapping instead.Casts can mask missing fields. Prefer explicit typing to let TS enforce shape.
🔧 Suggested refactor
- return sysInfo.map((iface) => { - return { + return sysInfo.map((iface): InfoNetworkInterface => ({ id: `info/network/${iface.iface}`, name: iface.iface, description: iface.ifaceName, // Label macAddress: iface.mac, status: iface.operstate, protocol: iface.ip4 ? (iface.ip6 ? 'ipv4+ipv6' : 'ipv4') : iface.ip6 ? 'ipv6' : 'none', ipAddress: iface.ip4, netmask: iface.ip4subnet, gateway: 'unknown', useDhcp: iface.dhcp, ipv6Address: iface.ip6, ipv6Netmask: iface.ip6subnet, useDhcp6: false, - } as InfoNetworkInterface; - }); + }));- return { + const mapped: InfoNetworkInterface = { id: `info/network/primary`, name: primary.iface, macAddress: primary.mac, ipAddress: primary.ip4, netmask: primary.ip4subnet, useDhcp: primary.dhcp, ipv6Address: primary.ip6, - } as InfoNetworkInterface; + }; + return mapped;Also applies to: 53-61
web/src/components/Onboarding/standalone/WelcomeModal.standalone.vue (1)
13-38: Remove non-essential inline comments.These are self-evident and conflict with the no-comment guideline.
🧹 Suggested cleanup
-// Disable attribute inheritance to prevent modelValue from showing on root element @@ -// Initialize showModal based on conditions @@ -// Template ref for the teleport containerweb/src/components/Onboarding/steps/OnboardingPluginsStep.vue (1)
80-101: Reflect installed plugins in the toggle state.Right now an installed plugin can appear OFF but disabled, which is confusing. Consider auto-selecting installed plugin IDs (or showing a distinct “Installed” state) so the UI reflects reality.
| 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(`Failed to persist onboarding tracker state: ${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.
Silent write failure may cause inconsistent state reporting.
When writeTrackerState fails, it logs the error but doesn't propagate it. The callers (markCompleted/reset) then call syncConfig() and return getState(). Since this.state is only updated on line 180 after a successful write, getState() will return the old state, but the caller won't know the operation failed.
Consider either re-throwing the error or returning a success indicator.
🐛 Proposed fix - propagate failure
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(`Failed to persist onboarding tracker state: ${error}`);
+ throw error;
}
}📝 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.
| 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(`Failed to persist onboarding tracker state: ${error}`); | |
| } | |
| } | |
| 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(`Failed to persist onboarding tracker state: ${error}`); | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@api/src/unraid-api/config/onboarding-tracker.service.ts` around lines 177 -
184, The writeTrackerState method swallows file write errors causing callers
(markCompleted/reset) to proceed with syncConfig() and return an unchanged
getState(); update writeTrackerState to propagate failure so callers can react:
change writeTrackerState to re-throw the caught error (or return a boolean
success) instead of only logging, and update markCompleted and reset to handle
the thrown error (or check the boolean) before calling syncConfig() and
returning getState(); reference the methods writeTrackerState, markCompleted,
reset, syncConfig, getState, and the this.state assignment to locate and
implement the change.
| @Field(() => Boolean, { description: 'Indicates if a partner logo exists' }) | ||
| @IsOptional() | ||
| @IsBoolean() | ||
| hasPartnerLogo?: boolean | null; |
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
🏁 Script executed:
# First, let's read the file and see the context around lines 148-151
head -n 160 api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts | tail -n 30Repository: unraid/api
Length of output: 930
🏁 Script executed:
# Search for hasPartnerLogo usage across the codebase
rg -n "hasPartnerLogo" api/src/unraid-api -g "*.ts" -B 2 -A 2Repository: unraid/api
Length of output: 2635
🏁 Script executed:
# Check if branding is a separate object and how it's assigned
rg -n "branding" api/src/unraid-api/graph/resolvers/customization -g "*.ts" -B 3 -A 3Repository: unraid/api
Length of output: 13264
Add { nullable: true } to @Field() decorator for hasPartnerLogo.
The field is declared non-nullable in GraphQL (@Field(() => Boolean)) but optional in the data type (boolean | null). If BrandingConfig is instantiated without setting hasPartnerLogo, GraphQL will receive undefined for a field it expects to be a non-null Boolean, causing potential serialization errors.
Proposed fix
-@Field(() => Boolean, { description: 'Indicates if a partner logo exists' })
+@Field(() => Boolean, { nullable: true, description: 'Indicates if a partner logo exists' })
`@IsOptional`()
`@IsBoolean`()
hasPartnerLogo?: boolean | null;📝 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.
| @Field(() => Boolean, { description: 'Indicates if a partner logo exists' }) | |
| @IsOptional() | |
| @IsBoolean() | |
| hasPartnerLogo?: boolean | null; | |
| `@Field`(() => Boolean, { nullable: true, description: 'Indicates if a partner logo exists' }) | |
| `@IsOptional`() | |
| `@IsBoolean`() | |
| hasPartnerLogo?: boolean | null; |
🤖 Prompt for AI Agents
In `@api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts`
around lines 148 - 151, The GraphQL field decorator for hasPartnerLogo is
declared non-nullable while the TypeScript type allows null/undefined; update
the `@Field`() on hasPartnerLogo to include { nullable: true } so the GraphQL
schema matches the TypeScript type (e.g., change `@Field`(() => Boolean, {
description: 'Indicates if a partner logo exists' }) to include nullable: true)
ensuring ActivationCodeModel/hasPartnerLogo can be omitted or null without
serialization errors.
| const state = this.onboardingTracker.getState(); | ||
| const currentVersion = this.onboardingTracker.getCurrentVersion() ?? 'unknown'; | ||
| const partnerInfo = await this.onboardingService.getPublicPartnerInfo(); | ||
| const activationData = await this.onboardingService.getActivationData(); | ||
|
|
||
| // Compute the status based on completion state and version | ||
| let status: OnboardingStatus; | ||
| if (!state.completed) { | ||
| status = OnboardingStatus.INCOMPLETE; | ||
| } else if (state.completedAtVersion && state.completedAtVersion !== currentVersion) { | ||
| status = OnboardingStatus.UPGRADE; | ||
| } else { | ||
| status = OnboardingStatus.COMPLETED; |
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.
Avoid treating an unknown currentVersion as an upgrade.
If getCurrentVersion() fails, the 'unknown' sentinel will mark any completed system as UPGRADE. Guard the upgrade check on a real currentVersion so completed systems don't get misclassified.
🔧 Suggested fix
- const currentVersion = this.onboardingTracker.getCurrentVersion() ?? 'unknown';
+ const currentVersion = this.onboardingTracker.getCurrentVersion();
@@
- } else if (state.completedAtVersion && state.completedAtVersion !== currentVersion) {
+ } else if (
+ currentVersion &&
+ state.completedAtVersion &&
+ state.completedAtVersion !== currentVersion
+ ) {
status = OnboardingStatus.UPGRADE;
} else {
status = OnboardingStatus.COMPLETED;
}📝 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 state = this.onboardingTracker.getState(); | |
| const currentVersion = this.onboardingTracker.getCurrentVersion() ?? 'unknown'; | |
| const partnerInfo = await this.onboardingService.getPublicPartnerInfo(); | |
| const activationData = await this.onboardingService.getActivationData(); | |
| // Compute the status based on completion state and version | |
| let status: OnboardingStatus; | |
| if (!state.completed) { | |
| status = OnboardingStatus.INCOMPLETE; | |
| } else if (state.completedAtVersion && state.completedAtVersion !== currentVersion) { | |
| status = OnboardingStatus.UPGRADE; | |
| } else { | |
| status = OnboardingStatus.COMPLETED; | |
| const state = this.onboardingTracker.getState(); | |
| const currentVersion = this.onboardingTracker.getCurrentVersion(); | |
| const partnerInfo = await this.onboardingService.getPublicPartnerInfo(); | |
| const activationData = await this.onboardingService.getActivationData(); | |
| // Compute the status based on completion state and version | |
| let status: OnboardingStatus; | |
| if (!state.completed) { | |
| status = OnboardingStatus.INCOMPLETE; | |
| } else if ( | |
| currentVersion && | |
| state.completedAtVersion && | |
| state.completedAtVersion !== currentVersion | |
| ) { | |
| status = OnboardingStatus.UPGRADE; | |
| } else { | |
| status = OnboardingStatus.COMPLETED; |
🤖 Prompt for AI Agents
In `@api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts`
around lines 66 - 78, The upgrade branch misclassifies completed systems when
getCurrentVersion() returns the 'unknown' sentinel; change the logic in the
resolver around onboardingTracker.getCurrentVersion(), state.completedAtVersion
and the OnboardingStatus assignment so the UPGRADE case only runs if a real
currentVersion is present (e.g., currentVersion is truthy and not the 'unknown'
sentinel) and state.completedAtVersion !== currentVersion; otherwise treat it as
COMPLETED. Update the conditional that sets OnboardingStatus.UPGRADE to include
the extra guard against the 'unknown' sentinel.
| vi.mocked(fs.mkdir).mockResolvedValue(undefined as any); | ||
| vi.mocked(fs.access).mockReset(); | ||
| vi.mocked(fs.readdir).mockReset(); | ||
| vi.mocked(fs.readFile).mockReset(); | ||
| vi.mocked(fs.writeFile).mockReset(); | ||
| vi.mocked(fs.copyFile).mockReset(); | ||
| vi.mocked(fileExists).mockReset(); | ||
| vi.mocked(fs.access).mockResolvedValue(undefined as any); | ||
| vi.mocked(fs.readdir).mockResolvedValue([]); | ||
| vi.mocked(fs.readFile).mockResolvedValue(''); | ||
| vi.mocked(fs.writeFile).mockResolvedValue(undefined as any); | ||
| vi.mocked(fs.copyFile).mockResolvedValue(undefined as any); | ||
| vi.mocked(fileExists).mockResolvedValue(false); |
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
🏁 Script executed:
cat -n api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts | head -50Repository: unraid/api
Length of output: 2582
🏁 Script executed:
rg -n "vi\.mock\(" api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsRepository: unraid/api
Length of output: 272
🏁 Script executed:
rg -n "import.*fs" api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsRepository: unraid/api
Length of output: 90
🏁 Script executed:
sed -n '170,195p' api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsRepository: unraid/api
Length of output: 1513
🏁 Script executed:
sed -n '977,1010p' api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.tsRepository: unraid/api
Length of output: 2035
Add module mock for fs/promises and remove as any casts.
fs/promises is not mocked, causing vi.mocked(fs.*) to fail. Add vi.mock('fs/promises') at the top level alongside the file-exists mock. Remove the as any casts—proper typing is already demonstrated elsewhere in this test file (lines ~1000) and aligns with coding guidelines.
🔧 Suggested fix
import * as fs from 'fs/promises';
@@
vi.mock('@app/core/utils/files/file-exists.js');
+vi.mock('fs/promises');
@@
- vi.mocked(fs.mkdir).mockResolvedValue(undefined as any);
+ vi.mocked(fs.mkdir).mockResolvedValue(undefined);
@@
- vi.mocked(fs.access).mockResolvedValue(undefined as any);
+ vi.mocked(fs.access).mockResolvedValue(undefined);
@@
- vi.mocked(fs.writeFile).mockResolvedValue(undefined as any);
- vi.mocked(fs.copyFile).mockResolvedValue(undefined as any);
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
+ vi.mocked(fs.copyFile).mockResolvedValue(undefined);Also applies to lines 983–1005.
🧰 Tools
🪛 GitHub Actions: CI - Main (API)
[error] 176-176: vi.mocked(...).mockResolvedValue is not a function
🪛 GitHub Check: Test API
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > getActivationData > should return null and log error on readdir failure
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > getActivationData > should return null if no .activationcode file exists
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > getActivationData > should return null if activation dir does not exist
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > onModuleInit > should handle errors during activation setup
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > onModuleInit > should create flag and apply customizations if activation dir exists and flag is missing
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > onModuleInit > should skip customizations when first boot already completed
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > onModuleInit > should skip setup if activation directory does not exist
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > onModuleInit > should log error and rethrow non-ENOENT errors during activation dir access
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > onModuleInit > should log error if dynamix user config path is missing
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
[failure] 176-176: src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts > OnboardingService > should be defined
TypeError: vi.mocked(...).mockResolvedValue is not a function
❯ src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts:176:29
🤖 Prompt for AI Agents
In `@api/src/unraid-api/graph/resolvers/customization/onboarding.service.spec.ts`
around lines 176 - 188, The tests are failing because fs/promises isn't
module-mocked and there are unnecessary "as any" casts; add a top-level
vi.mock('fs/promises') next to the existing vi.mock('file-exists') and then
remove the "as any" casts in the mock setup (replace
vi.mocked(fs.mkdir).mockResolvedValue(undefined as any) etc. with properly typed
mockResolvedValue calls), ensuring you use vi.mocked(...) for fs.mkdir,
fs.access, fs.readdir, fs.readFile, fs.writeFile, fs.copyFile and for fileExists
as shown elsewhere in this test file (see other mocks around lines ~1000) so
typings align.
| const paths = getters.paths(); | ||
|
|
||
| // Construct BrandingConfig with computed logo presence | ||
| const branding = activationData.branding ? { ...activationData.branding } : {}; | ||
| branding.hasPartnerLogo = await fileExists(paths.activation.logo); | ||
| branding.logoUrl = paths.webgui.logo.assetPath; // Using default for now as placeholder | ||
|
|
||
| return { | ||
| hasPartnerLogo: await fileExists(paths.activation.logo), | ||
| partnerName: activationData?.partnerName, | ||
| partnerUrl: activationData?.partnerUrl, | ||
| partnerLogoUrl: paths.webgui.logo.assetPath, | ||
| partner: activationData.partner, | ||
| branding: branding as any, // Type assertion for now to match strict class structure if needed, or instantiate class | ||
| }; |
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
🏁 Script executed:
# First, check if the file exists and read the relevant section
find . -name "onboarding.service.ts" -type fRepository: unraid/api
Length of output: 126
🏁 Script executed:
# Find the activation-code.model.ts file
find . -name "activation-code.model.ts" -o -name "activation-code.model.js" | head -20Repository: unraid/api
Length of output: 129
🏁 Script executed:
# Search for BrandingConfig in the codebase
rg -n "class BrandingConfig|export.*BrandingConfig" --type ts --type jsRepository: unraid/api
Length of output: 3191
🏁 Script executed:
# Check the actual code around lines 125-135 in the onboarding.service.ts file
rg -n "as any" api/src/unraid-api/graph/resolvers/customization/ -A 3 -B 3Repository: unraid/api
Length of output: 50366
🏁 Script executed:
# Read the BrandingConfig class definition
cat -n api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts | sed -n '100,130p'Repository: unraid/api
Length of output: 1146
🏁 Script executed:
# Read the onboarding.service.ts around the problematic area
cat -n api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts | sed -n '115,145p'Repository: unraid/api
Length of output: 1481
🏁 Script executed:
# Check imports in onboarding.service.ts to see what's currently imported from activation-code.model
head -30 api/src/unraid-api/graph/resolvers/customization/onboarding.service.tsRepository: unraid/api
Length of output: 1593
🏁 Script executed:
# Check the method signature and return type of getPublicPartnerInfo
cat -n api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts | sed -n '100,115p'Repository: unraid/api
Length of output: 874
🏁 Script executed:
# Look for the method signature more broadly
rg -n "getPublicPartnerInfo" api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts -A 2 -B 2Repository: unraid/api
Length of output: 270
🏁 Script executed:
# Check what PublicPartnerInfo looks like
cat -n api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts | sed -n '85,110p'Repository: unraid/api
Length of output: 903
🏁 Script executed:
# Find PublicPartnerInfo class definition
rg -n "class PublicPartnerInfo" api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts -A 15Repository: unraid/api
Length of output: 515
🏁 Script executed:
# Check if BrandingConfig has a constructor or if plainToClass/plainToInstance works
rg -n "plainToClass|plainToInstance" api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts -B 2 -A 2Repository: unraid/api
Length of output: 466
🏁 Script executed:
# Check the complete BrandingConfig class to see all fields including hasPartnerLogo and logoUrl
cat -n api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts | sed -n '103,165p'Repository: unraid/api
Length of output: 2345
🏁 Script executed:
# Verify that activationData is properly typed as ActivationCode
rg -n "activationData.*ActivationCode" api/src/unraid-api/graph/resolvers/customization/onboarding.service.tsRepository: unraid/api
Length of output: 205
🏁 Script executed:
# Check the complete method to understand the full context
cat -n api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts | sed -n '114,140p'Repository: unraid/api
Length of output: 1352
Remove as any cast; instantiate BrandingConfig properly instead.
The code spreads activationData.branding into a plain object and mutates it, then casts to any to match the return type. Instead, create a proper BrandingConfig instance to satisfy type safety.
♻️ Suggested fix
import {
ActivationCode,
+ BrandingConfig,
OnboardingState,
PublicPartnerInfo,
} from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js';
- const branding = activationData.branding ? { ...activationData.branding } : {};
+ const branding = activationData.branding
+ ? Object.assign(new BrandingConfig(), activationData.branding)
+ : undefined;
branding.hasPartnerLogo = await fileExists(paths.activation.logo);
- branding.logoUrl = paths.webgui.logo.assetPath; // Using default for now as placeholder
+ branding.logoUrl = paths.webgui.logo.assetPath;
return {
partner: activationData.partner,
- branding: branding as any, // Type assertion for now to match strict class structure if needed, or instantiate class
+ branding,
};📝 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 paths = getters.paths(); | |
| // Construct BrandingConfig with computed logo presence | |
| const branding = activationData.branding ? { ...activationData.branding } : {}; | |
| branding.hasPartnerLogo = await fileExists(paths.activation.logo); | |
| branding.logoUrl = paths.webgui.logo.assetPath; // Using default for now as placeholder | |
| return { | |
| hasPartnerLogo: await fileExists(paths.activation.logo), | |
| partnerName: activationData?.partnerName, | |
| partnerUrl: activationData?.partnerUrl, | |
| partnerLogoUrl: paths.webgui.logo.assetPath, | |
| partner: activationData.partner, | |
| branding: branding as any, // Type assertion for now to match strict class structure if needed, or instantiate class | |
| }; | |
| import { | |
| ActivationCode, | |
| BrandingConfig, | |
| OnboardingState, | |
| PublicPartnerInfo, | |
| } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js'; | |
| const paths = getters.paths(); | |
| // Construct BrandingConfig with computed logo presence | |
| const branding = new BrandingConfig(); | |
| if (activationData.branding) { | |
| Object.assign(branding, activationData.branding); | |
| } | |
| branding.hasPartnerLogo = await fileExists(paths.activation.logo); | |
| branding.logoUrl = paths.webgui.logo.assetPath; | |
| return { | |
| partner: activationData.partner, | |
| branding, | |
| }; |
🤖 Prompt for AI Agents
In `@api/src/unraid-api/graph/resolvers/customization/onboarding.service.ts`
around lines 125 - 135, The code currently spreads activationData.branding into
a plain object and casts it with "branding as any"; replace this with a real
BrandingConfig instance: create a new BrandingConfig(), copy relevant properties
from activationData.branding (or use a constructor/initializer if available),
set hasPartnerLogo using await fileExists(paths.activation.logo) and set logoUrl
to paths.webgui.logo.assetPath, then return branding typed as BrandingConfig (no
"as any"). Update the block around getters.paths(), activationData and the
return object so the returned branding is an actual BrandingConfig instance
rather than a mutated plain object.
| it('should compute isFreshInstall from backend when regState is not ENOKEYFILE', () => { | ||
| vi.mocked(useQuery).mockImplementation((query) => { | ||
| if (query === ACTIVATION_CODE_QUERY) { | ||
| return createCompleteQueryMock( | ||
| { | ||
| customization: { | ||
| onboardingState: { | ||
| registrationState: 'REGISTERED' as RegistrationState, | ||
| isFreshInstall: false, // Backend determines this value | ||
| }, | ||
| }, | ||
| }, | ||
| false | ||
| ); | ||
| } | ||
|
|
||
| return createCompleteQueryMock(null, false); | ||
| }); | ||
|
|
||
| const store = useActivationCodeDataStore(); | ||
|
|
||
| expect(store.isFreshInstall).toBe(false); | ||
| }); |
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
🏁 Script executed:
#!/bin/bash
# Search for RegistrationState enum definition
rg -n 'enum RegistrationState|type RegistrationState' --type=tsRepository: unraid/api
Length of output: 490
🏁 Script executed:
#!/bin/bash
# Search for REGISTERED value within enum definitions
rg -n -B5 -A15 'RegistrationState' --type=ts | grep -A20 'enum\|type.*RegistrationState'Repository: unraid/api
Length of output: 46537
🏁 Script executed:
#!/bin/bash
# Look for GraphQL generated types containing RegistrationState
fd -e ts -e tsx | xargs rg -l 'RegistrationState' | head -5Repository: unraid/api
Length of output: 274
🏁 Script executed:
#!/bin/bash
# Get the full RegistrationState enum from the web graphql file
sed -n '2372,2450p' web/composables/gql/graphql.tsRepository: unraid/api
Length of output: 3827
🏁 Script executed:
#!/bin/bash
# Search for the actual RegistrationState enum definition with all its values
rg -A 40 'export enum RegistrationState' web/composables/gql/graphql.ts | head -50Repository: unraid/api
Length of output: 1152
🏁 Script executed:
#!/bin/bash
# Check the test file to see the context and what registrationState values are being used
cat -n web/__test__/store/activationCodeData.test.ts | sed -n '180,202p'Repository: unraid/api
Length of output: 944
🏁 Script executed:
#!/bin/bash
# Verify what enum is being imported/used in the test file
head -20 web/__test__/store/activationCodeData.test.tsRepository: unraid/api
Length of output: 809
The REGISTERED value does not exist in the RegistrationState enum; use a valid enum value instead.
Line 187 uses 'REGISTERED' as RegistrationState, but REGISTERED is not a member of the enum. The available values are: BASIC, TRIAL, PLUS, PRO, STARTER, UNLEASHED, LIFETIME, and various error states (ENOKEYFILE, EEXPIRED, etc.). Replace the type cast with an actual enum value appropriate for this test scenario.
🤖 Prompt for AI Agents
In `@web/__test__/store/activationCodeData.test.ts` around lines 180 - 202, The
test uses a non-existent enum member 'REGISTERED' for RegistrationState; replace
the cast with a real enum value (e.g., RegistrationState.BASIC or any valid
non-ENOKEYFILE state) in the mocked response for ACTIVATION_CODE_QUERY so the
useQuery mock returns a valid onboardingState.registrationState; ensure the test
still simulates backend-determined isFreshInstall and that
useActivationCodeDataStore().isFreshInstall assertion remains unchanged.
| # Clean and Build | ||
| echo "Building project..." | ||
| pnpm run clean | ||
| pnpm run build |
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.
Fail fast on build errors and run pnpm codegen before build.
If pnpm run clean or pnpm run build fails, the script still proceeds and may deploy stale artifacts. Also, pnpm codegen is required in web/ before building.
🛠️ Proposed fix
echo "Building project..."
-pnpm run clean
-pnpm run build
+pnpm run clean || exit $?
+pnpm run codegen || exit $?
+pnpm run build || exit $?📝 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.
| # Clean and Build | |
| echo "Building project..." | |
| pnpm run clean | |
| pnpm run build | |
| # Clean and Build | |
| echo "Building project..." | |
| pnpm run clean || exit $? | |
| pnpm run codegen || exit $? | |
| pnpm run build || exit $? |
🤖 Prompt for AI Agents
In `@web/scripts/deploy-dev.sh` around lines 35 - 38, The script currently runs
"pnpm run clean" and "pnpm run build" without failing fast and omits required
code generation; update the deploy script to run "pnpm codegen" in web/ before
building and ensure the script fails immediately on any command error (e.g.,
enable "set -e" or check exit codes) so that if "pnpm run clean" or "pnpm run
build" fails the deploy stops and no stale artifacts are used.
| <template> | ||
| <template v-if="partnerInfo?.partner?.url"> | ||
| <a | ||
| v-if="partnerInfo?.partner?.url" | ||
| :href="partnerInfo?.partner?.url" | ||
| class="opacity-100 hover:opacity-75 focus:opacity-75" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| > | ||
| <OnboardingPartnerLogoImg :partner-info="partnerInfo" /> | ||
| </a> | ||
| <OnboardingPartnerLogoImg v-else :partner-info="partnerInfo" /> | ||
| </template> |
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.
Unreachable v-else branch due to redundant conditional logic.
The outer <template v-if> on Line 15 already ensures partnerInfo?.partner?.url is truthy. The inner v-if on Line 17 checks the same condition, making it always true within that scope. Consequently, the v-else on Line 25 can never execute.
Expected behavior appears to be: render link wrapper when URL exists, otherwise render logo directly. The fix should restructure the conditionals:
🐛 Proposed fix
<template>
- <template v-if="partnerInfo?.partner?.url">
- <a
- v-if="partnerInfo?.partner?.url"
- :href="partnerInfo?.partner?.url"
- class="opacity-100 hover:opacity-75 focus:opacity-75"
- target="_blank"
- rel="noopener noreferrer"
- >
- <OnboardingPartnerLogoImg :partner-info="partnerInfo" />
- </a>
- <OnboardingPartnerLogoImg v-else :partner-info="partnerInfo" />
- </template>
+ <a
+ v-if="partnerInfo?.partner?.url"
+ :href="partnerInfo?.partner?.url"
+ class="opacity-100 hover:opacity-75 focus:opacity-75"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <OnboardingPartnerLogoImg :partner-info="partnerInfo" />
+ </a>
+ <OnboardingPartnerLogoImg v-else :partner-info="partnerInfo" />
</template>🤖 Prompt for AI Agents
In `@web/src/components/Onboarding/components/OnboardingPartnerLogo.vue` around
lines 14 - 26, The current template uses a redundant outer <template
v-if="partnerInfo?.partner?.url"> which makes the inner v-else unreachable;
remove the outer conditional and apply a single conditional around the anchor vs
direct image rendering so the component renders an <a> wrapper when
partnerInfo?.partner?.url is present and otherwise renders
<OnboardingPartnerLogoImg> directly—locate the OnboardingPartnerLogo.vue
template and adjust the conditional logic around the <a> element and
<OnboardingPartnerLogoImg> (referencing partnerInfo?.partner?.url and the
OnboardingPartnerLogoImg component) to implement this behavior.
| @@ -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.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and view its content
find . -path "*web/src/components/Onboarding/graphql/activationOnboarding.query.ts" -type f 2>/dev/null | head -20Repository: unraid/api
Length of output: 123
🏁 Script executed:
# Find the gql composable file
find . -path "*composables/gql*" -type f 2>/dev/null | head -20Repository: unraid/api
Length of output: 377
🏁 Script executed:
# Check import patterns in similar .query.ts files to see if .js extension is used
rg "import.*from.*composables" web/src --type ts -A 1 -B 1 | head -50Repository: unraid/api
Length of output: 3011
🏁 Script executed:
# Search for actual imports with .js extensions in TypeScript files
rg "from ['\"].*\.js['\"]" web/src --type ts | head -30Repository: unraid/api
Length of output: 1448
🏁 Script executed:
# Read the file being reviewed
cat -n web/src/components/Onboarding/graphql/activationOnboarding.query.tsRepository: unraid/api
Length of output: 366
🏁 Script executed:
# Check a similar .query.ts file for comparison
cat -n web/src/components/queries/public-oidc-providers.query.ts | head -20Repository: unraid/api
Length of output: 427
🏁 Script executed:
# Check if the import resolves to index.ts or directly to gql.ts
cat -n web/src/composables/gql/index.tsRepository: unraid/api
Length of output: 126
Add .js extension to the graphql composable import for ESM compatibility.
The import should reference the explicit module path: ~/composables/gql/gql.js (consistent with other .query.ts files in the codebase like public-oidc-providers.query.ts).
Proposed change
-import { graphql } from '~/composables/gql';
+import { graphql } from '~/composables/gql/gql.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/gql.js'; |
🤖 Prompt for AI Agents
In `@web/src/components/Onboarding/graphql/activationOnboarding.query.ts` at line
1, The import for the graphql symbol in activationOnboarding.query.ts needs to
use the explicit ESM module entry like other .query.ts files (e.g.,
public-oidc-providers.query.ts); update the import statement so it references
the .js entry used by the project's graphql composable to ensure ESM
compatibility. Locate the top-level import of graphql and change it to match the
explicit .js composable import pattern used elsewhere in the codebase.
| import { computed, onMounted, onUnmounted, ref } from 'vue'; | ||
| import { storeToRefs } from 'pinia'; | ||
|
|
||
| import { Dialog } from '@unraid/ui'; | ||
|
|
||
| import OnboardingPartnerLogo from '~/components/Onboarding/components/OnboardingPartnerLogo.vue'; | ||
| import OnboardingSteps from '~/components/Onboarding/OnboardingSteps.vue'; | ||
| import OnboardingWelcomeStep from '~/components/Onboarding/steps/OnboardingWelcomeStep.vue'; | ||
| import { useWelcomeModalDataStore } from '~/components/Onboarding/store/welcomeModalData'; | ||
| import { useThemeStore } from '~/store/theme'; | ||
|
|
||
| // Disable attribute inheritance to prevent modelValue from showing on root element | ||
|
|
||
| defineOptions({ | ||
| inheritAttrs: false, | ||
| }); | ||
|
|
||
| const { partnerInfo, isFreshInstall } = storeToRefs(useWelcomeModalDataStore()); | ||
|
|
||
| const { fetchTheme } = useThemeStore(); | ||
|
|
||
| (async () => { | ||
| try { | ||
| await fetchTheme(); | ||
| } catch (error) { | ||
| console.error('Error loading theme:', error); | ||
| } | ||
| })(); | ||
|
|
||
| const isLoginPage = computed(() => window.location.pathname.includes('login')); | ||
|
|
||
| // Initialize showModal based on conditions | ||
|
|
||
| const showModal = ref(isLoginPage.value || isFreshInstall.value); | ||
|
|
||
| // Template ref for the teleport container | ||
|
|
||
| const modalContainer = ref<HTMLElement>(); | ||
|
|
||
| const dropdownHide = () => { | ||
| showModal.value = false; | ||
| }; | ||
|
|
||
| const showWelcomeModal = () => { | ||
| showModal.value = true; | ||
| }; | ||
|
|
||
| defineExpose({ | ||
| showWelcomeModal, | ||
| }); | ||
|
|
||
| const hideWelcomeModal = () => { | ||
| showModal.value = false; | ||
| }; |
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.
Fresh-install welcome modal can stay hidden after async data loads.
showModal is initialized before isFreshInstall resolves, so the modal may never open on fresh installs. Consider reacting to isFreshInstall and preventing re-open after dismissal.
✅ Suggested fix
-import { computed, onMounted, onUnmounted, ref } from 'vue';
+import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue';
@@
-const showModal = ref(isLoginPage.value || isFreshInstall.value);
+const dismissed = ref(false);
+const showModal = ref(isLoginPage.value || isFreshInstall.value);
+
+watchEffect(() => {
+ if (dismissed.value) return;
+ if (!showModal.value && (isLoginPage.value || isFreshInstall.value)) {
+ showModal.value = true;
+ }
+});
@@
-const dropdownHide = () => {
- showModal.value = false;
-};
+const dropdownHide = () => {
+ dismissed.value = true;
+ showModal.value = false;
+};
@@
-const hideWelcomeModal = () => {
- showModal.value = false;
-};
+const hideWelcomeModal = () => {
+ dismissed.value = true;
+ showModal.value = false;
+};📝 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 { computed, onMounted, onUnmounted, ref } from 'vue'; | |
| import { storeToRefs } from 'pinia'; | |
| import { Dialog } from '@unraid/ui'; | |
| import OnboardingPartnerLogo from '~/components/Onboarding/components/OnboardingPartnerLogo.vue'; | |
| import OnboardingSteps from '~/components/Onboarding/OnboardingSteps.vue'; | |
| import OnboardingWelcomeStep from '~/components/Onboarding/steps/OnboardingWelcomeStep.vue'; | |
| import { useWelcomeModalDataStore } from '~/components/Onboarding/store/welcomeModalData'; | |
| import { useThemeStore } from '~/store/theme'; | |
| // Disable attribute inheritance to prevent modelValue from showing on root element | |
| defineOptions({ | |
| inheritAttrs: false, | |
| }); | |
| const { partnerInfo, isFreshInstall } = storeToRefs(useWelcomeModalDataStore()); | |
| const { fetchTheme } = useThemeStore(); | |
| (async () => { | |
| try { | |
| await fetchTheme(); | |
| } catch (error) { | |
| console.error('Error loading theme:', error); | |
| } | |
| })(); | |
| const isLoginPage = computed(() => window.location.pathname.includes('login')); | |
| // Initialize showModal based on conditions | |
| const showModal = ref(isLoginPage.value || isFreshInstall.value); | |
| // Template ref for the teleport container | |
| const modalContainer = ref<HTMLElement>(); | |
| const dropdownHide = () => { | |
| showModal.value = false; | |
| }; | |
| const showWelcomeModal = () => { | |
| showModal.value = true; | |
| }; | |
| defineExpose({ | |
| showWelcomeModal, | |
| }); | |
| const hideWelcomeModal = () => { | |
| showModal.value = false; | |
| }; | |
| import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'; | |
| import { storeToRefs } from 'pinia'; | |
| import { Dialog } from '@unraid/ui'; | |
| import OnboardingPartnerLogo from '~/components/Onboarding/components/OnboardingPartnerLogo.vue'; | |
| import OnboardingSteps from '~/components/Onboarding/OnboardingSteps.vue'; | |
| import OnboardingWelcomeStep from '~/components/Onboarding/steps/OnboardingWelcomeStep.vue'; | |
| import { useWelcomeModalDataStore } from '~/components/Onboarding/store/welcomeModalData'; | |
| import { useThemeStore } from '~/store/theme'; | |
| // Disable attribute inheritance to prevent modelValue from showing on root element | |
| defineOptions({ | |
| inheritAttrs: false, | |
| }); | |
| const { partnerInfo, isFreshInstall } = storeToRefs(useWelcomeModalDataStore()); | |
| const { fetchTheme } = useThemeStore(); | |
| (async () => { | |
| try { | |
| await fetchTheme(); | |
| } catch (error) { | |
| console.error('Error loading theme:', error); | |
| } | |
| })(); | |
| const isLoginPage = computed(() => window.location.pathname.includes('login')); | |
| // Initialize showModal based on conditions | |
| const dismissed = ref(false); | |
| const showModal = ref(isLoginPage.value || isFreshInstall.value); | |
| watchEffect(() => { | |
| if (dismissed.value) return; | |
| if (!showModal.value && (isLoginPage.value || isFreshInstall.value)) { | |
| showModal.value = true; | |
| } | |
| }); | |
| // Template ref for the teleport container | |
| const modalContainer = ref<HTMLElement>(); | |
| const dropdownHide = () => { | |
| dismissed.value = true; | |
| showModal.value = false; | |
| }; | |
| const showWelcomeModal = () => { | |
| showModal.value = true; | |
| }; | |
| defineExpose({ | |
| showWelcomeModal, | |
| }); | |
| const hideWelcomeModal = () => { | |
| dismissed.value = true; | |
| showModal.value = false; | |
| }; |
🤖 Prompt for AI Agents
In `@web/src/components/Onboarding/standalone/WelcomeModal.standalone.vue` around
lines 2 - 55, showModal is initialized before isFreshInstall resolves so the
modal can remain closed on fresh installs; change initialization to react to
isFreshInstall (and isLoginPage) by setting showModal when isFreshInstall.value
becomes true (use a watcher/effect on isFreshInstall and isLoginPage) and add a
local dismissed flag that dropdownHide/hideWelcomeModal set to true to prevent
auto re-open; update showWelcomeModal to respect the dismissed flag and
reference the existing showModal, isFreshInstall, isLoginPage,
dropdownHide/hideWelcomeModal, and showWelcomeModal symbols when implementing
the watcher and dismissal logic.
…m time configuration
SystemTimeto manage system time settings, including current time, timezone, NTP status, and NTP servers.systemTimequery to retrieve current system time configuration.updateSystemTimemutation to modify system time settings.This update enhances the API's capability to manage and retrieve system time configurations effectively.
Summary by CodeRabbit
Release Notes
Lines changed: 472 total changes across API and web components.
✏️ Tip: You can customize this high-level summary in your review settings.