Skip to content

Feat/onboarding internal boot#1881

Draft
Ajit-Mehrotra wants to merge 39 commits intomainfrom
feat/onboarding-internal-boot
Draft

Feat/onboarding internal boot#1881
Ajit-Mehrotra wants to merge 39 commits intomainfrom
feat/onboarding-internal-boot

Conversation

@Ajit-Mehrotra
Copy link
Contributor

@Ajit-Mehrotra Ajit-Mehrotra commented Mar 2, 2026

Summary by CodeRabbit

  • New Features

    • New "Configure Boot" onboarding step to create internal boot pools (optional BIOS update & reboot)
    • Disks now surface device IDs, sectors and sector-size information
    • UPS reporting includes nominal and current power metrics
    • Plugin install status gains FAILED state
    • Vars exposes boot eligibility, boot-transfer flag and reserved-names
  • Style

    • Partner-logo flag is now optional
  • Documentation

    • Added guide describing internal boot onboarding differences

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an internal-boot onboarding feature plus supporting schema, backend service, disk metadata mapping, frontend UI/store/composables, GraphQL operations, tests, and docs; also extends Vars, Disk, and UPSPower types and makes BrandingConfig.hasPartnerLogo nullable.

Changes

Cohort / File(s) Summary
GraphQL schema & client
api/generated-schema.graphql, web/src/composables/gql/graphql.ts, web/src/composables/gql/gql.ts
Added CreateInternalBootPoolInput, OnboardingInternalBootResult and createInternalBootPool mutation; added GetInternalBootContext & GetInternalBootStepVisibility documents and graphql overloads; made BrandingConfig.hasPartnerLogo nullable; added Disk/Vars/UPSPower fields.
Backend onboarding service & wiring
api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts, api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts, api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts, api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts, api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts, api/src/unraid-api/graph/resolvers/resolvers.module.ts
New OnboardingInternalBootService implementing createInternalBootPool (emcmd/efibootmgr sequences, validation, output aggregation); provider registered and resolver wired; unit tests added for success, BIOS-update path, validation and failure.
Backend models & types
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts, api/src/core/types/states/var.ts, api/src/unraid-api/graph/resolvers/disks/disks.model.ts, api/src/unraid-api/graph/resolvers/vars/vars.model.ts
GraphQL input/result types for internal boot added with validators; Var type extended with bootEligible/enableBootTransfer/reservedNames; Disk model extended with emhttpDeviceId/sectors/sectorSize.
Backend parsing & disk service
api/src/store/state-parsers/var.ts, api/src/__test__/store/state-parsers/var.test.ts, api/src/__test__/store/modules/emhttp.test.ts, api/src/unraid-api/graph/resolvers/disks/disks.service.ts, api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts
Parser now reads bootEligible and exposes new var fields; disk service reads store.emhttp.devices, normalizes devices and maps emhttpDeviceId, sectors, sectorSize onto Disk results; tests updated/added.
Frontend onboarding store & types
web/src/components/Onboarding/store/onboardingDraft.ts, web/components.d.ts
Added internal boot draft state, types and actions (internalBootSelection, bootMode, internalBootInitialized, internalBootSkipped, internalBootApplySucceeded, setters); registered OnboardingInternalBootStep global component type.
Frontend composable & GraphQL
web/src/components/Onboarding/composables/internalBoot.ts, web/src/components/Onboarding/graphql/createInternalBootPool.mutation.ts, web/src/components/Onboarding/graphql/getInternalBootContext.query.ts, web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts
New composable exports submitInternalBootCreation and submitInternalBootReboot (mutation + CSRF reboot form); added CREATE_INTERNAL_BOOT_POOL mutation and queries for context and step visibility.
Frontend onboarding UI & registry
web/src/components/Onboarding/OnboardingModal.vue, web/src/components/Onboarding/OnboardingSteps.vue, web/src/components/Onboarding/stepRegistry.ts, web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue, web/src/components/Onboarding/steps/OnboardingSummaryStep.vue, web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
Introduced CONFIGURE_BOOT step, registry metadata and modal visibility query; added OnboardingInternalBootStep component with validation/UI; Summary and NextSteps integrated internal-boot apply flow and reboot confirmation.
Frontend tests
web/__test__/components/Onboarding/OnboardingModal.test.ts, web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts, web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts, web/__test__/components/Onboarding/internalBoot.test.ts
Added/updated tests covering step visibility, context, UI flows, submitInternalBootCreation behavior, reboot form submission, success/error paths, and reboot confirmation dialog.
Onboarding diagnostics & utilities
web/src/components/Onboarding/composables/onboardingErrorDiagnostics.ts, web/src/components/Onboarding/components/OnboardingConsole.vue
Added structured onboarding error diagnostics builder and UI support for showing formatted diagnostics in console logs.
Localization & docs
web/src/locales/en.json, docs/onboarding-internal-boot-port-differences.md
Large set of onboarding-related translation keys added; new document describing differences between internal-boot port and web GUI flows.
Misc frontend adjustments
web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue, web/src/components/Onboarding/steps/OnboardingPluginsStep.vue, other onboarding components/tests
Various i18n, presets, plugin detection, and UI refinements to integrate internal-boot flow and translations; updated tests and presets accordingly.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as Onboarding UI
    participant Store as Onboarding Store
    participant Apollo as Apollo Client
    participant Backend as API Server
    participant Emcmd as emcmd/efibootmgr

    User->>Frontend: open onboarding
    Frontend->>Apollo: GetInternalBootStepVisibility
    Apollo->>Backend: query vars.enableBootTransfer
    Backend-->>Apollo: enableBootTransfer
    Apollo-->>Frontend: visibility result
    Frontend->>User: show CONFIGURE_BOOT (if visible)
    User->>Frontend: configure pool & devices
    Frontend->>Store: setInternalBootSelection(...)
    User->>Frontend: apply
    Frontend->>Apollo: CreateInternalBootPool(input)
    Apollo->>Backend: mutation createInternalBootPool
    Backend->>Emcmd: run emcmd / efibootmgr steps
    Emcmd-->>Backend: step outputs
    Backend-->>Apollo: mutation result (ok, code, output)
    Apollo-->>Frontend: result
    Frontend->>Store: setInternalBootApplySucceeded(true)
    Frontend->>User: show success / prompt reboot
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I dug a tiny pool tonight,
Emcmd hummed and LEDs gleamed bright,
Pools and slots in careful line,
EFI entries set just fine.
Hooray — onboarding hops to light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/onboarding internal boot' clearly summarizes the primary change: adding internal boot functionality to the onboarding flow.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/onboarding-internal-boot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Ajit-Mehrotra Ajit-Mehrotra self-assigned this Mar 2, 2026
return null;
}

const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;

Check failure

Code scanning / CodeQL

Inefficient regular expression High

This part of the regular expression may cause exponential backtracking on strings starting with 'a' and containing many repetitions of '-'.

Copilot Autofix

AI about 4 hours ago

In general, to fix this kind of problem, remove ambiguity inside repeated constructs: avoid (...*X)* where ... and X can both match the same characters. A common approach is to restructure the pattern so that the repeated part is a single, unambiguous character class (or a sequence without overlapping alternatives), or to factor repetition differently.

Here, the intent appears to be:

  • The name must start with a lowercase letter: [a-z].
  • Subsequent characters can be letters, digits, ~ . _ -, but the name must end with a letter, _, or -. That is enforced by ([a-z0-9~._-]*[a-z_-])* after the initial letter.

We can express the same constraint without nested ambiguous repetition by:

  1. Allowing an optional middle part of arbitrary valid characters, and
  2. Requiring a final character from the restricted set, guarded so we don’t require it for 1-character names.

Equivalent, unambiguous regex:

/^[a-z](?:[a-z0-9~._-]*[a-z_-])?$/

Explanation:

  • ^[a-z] — first character.
  • (?: ... )? — optional tail, so a single-letter name is allowed.
  • [a-z0-9~._-]* — zero or more allowed characters.
  • [a-z_-] — final character from the stricter set; this no longer directly follows another overlapping token inside the same quantified group, so the boundary is fixed and avoids exponential backtracking.

This changes only the regex literal on line 475 in web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue. No imports or additional definitions are required.

Suggested changeset 1
web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
--- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
+++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
@@ -472,7 +472,7 @@
     return null;
   }
 
-  const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
+  const poolNamePattern = /^[a-z](?:[a-z0-9~._-]*[a-z_-])?$/;
   if (!poolNamePattern.test(normalizedPoolName)) {
     formError.value = t('onboarding.internalBootStep.validation.poolFormat');
     return null;
EOF
@@ -472,7 +472,7 @@
return null;
}

const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
const poolNamePattern = /^[a-z](?:[a-z0-9~._-]*[a-z_-])?$/;
if (!poolNamePattern.test(normalizedPoolName)) {
formError.value = t('onboarding.internalBootStep.validation.poolFormat');
return null;
Copilot is powered by AI and may make mistakes. Always verify output.
@codecov
Copy link

codecov bot commented Mar 2, 2026

Codecov Report

❌ Patch coverage is 54.16843% with 1083 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.17%. Comparing base (7956987) to head (121f5d8).

Files with missing lines Patch % Lines
...ts/Onboarding/steps/OnboardingInternalBootStep.vue 0.14% 686 Missing ⚠️
...ponents/Onboarding/steps/OnboardingSummaryStep.vue 84.12% 93 Missing and 4 partials ⚠️
...src/components/Onboarding/store/onboardingDraft.ts 0.00% 92 Missing ⚠️
...ing/standalone/OnboardingAdminPanel.standalone.vue 0.00% 48 Missing ⚠️
...ers/onboarding/onboarding-internal-boot.service.ts 85.86% 41 Missing ⚠️
...boarding/composables/onboardingErrorDiagnostics.ts 79.09% 35 Missing and 2 partials ⚠️
...onents/Onboarding/components/OnboardingConsole.vue 0.00% 26 Missing ⚠️
.../unraid-api/graph/resolvers/disks/disks.service.ts 75.80% 15 Missing ⚠️
...rc/unraid-api/graph/resolvers/disks/disks.model.ts 25.00% 9 Missing ⚠️
web/src/components/Onboarding/stepRegistry.ts 0.00% 7 Missing ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1881      +/-   ##
==========================================
+ Coverage   48.64%   49.17%   +0.52%     
==========================================
  Files        1013     1020       +7     
  Lines       67635    69623    +1988     
  Branches     6944     7221     +277     
==========================================
+ Hits        32902    34237    +1335     
- Misses      34612    35259     +647     
- Partials      121      127       +6     

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
web/src/components/Onboarding/OnboardingSteps.vue (1)

10-17: Consider centralizing StepId in a shared type.

Lines [10-17] still duplicate step IDs in this component. Exporting a single StepId type from a shared onboarding module would reduce drift risk as steps evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/OnboardingSteps.vue` around lines 10 - 17, This
file defines a local StepId union type that duplicates onboarding step IDs;
remove the local StepId declaration in OnboardingSteps.vue and import a single
exported StepId from a shared onboarding types/module (e.g., export StepId from
a new/shared onboarding types file), then update all references in this
component (and any other components) to use the imported StepId type; ensure the
shared module exports the same union values and update any usages of
'OVERVIEW'|'CONFIGURE_SETTINGS' etc. to the centralized StepId to prevent drift.
web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue (2)

83-90: Consider adding error handling for the reboot submission.

submitInternalBootReboot() is called without awaiting or handling potential errors. If the reboot request fails, the user has no feedback.

♻️ Suggested approach
-const handlePrimaryAction = () => {
+const handlePrimaryAction = async () => {
   if (showRebootButton.value) {
-    submitInternalBootReboot();
-    return;
+    try {
+      await submitInternalBootReboot();
+    } catch (error) {
+      console.error('Reboot request failed:', error);
+    }
+    return;
   }

   props.onComplete();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue` around lines
83 - 90, handlePrimaryAction calls submitInternalBootReboot() without awaiting
or handling failures, so add async error handling: make handlePrimaryAction
async, await submitInternalBootReboot(), wrap the call in try/catch, show user
feedback on error (e.g., set a local error state or call an existing
notification method) and only call props.onComplete() on success; also consider
disabling the primary button or showing a loading state while awaiting
submitInternalBootReboot() to prevent duplicate submissions.

52-54: Hardcoded "Reboot" string should use i18n.

The primaryButtonText uses a hardcoded 'Reboot' string while other text uses t() for translation. This creates an inconsistency for localized builds.

♻️ Suggested fix
 const primaryButtonText = computed(() =>
-  showRebootButton.value ? 'Reboot' : t('onboarding.nextSteps.continueToDashboard')
+  showRebootButton.value ? t('common.reboot', 'Reboot') : t('onboarding.nextSteps.continueToDashboard')
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue` around lines
52 - 54, primaryButtonText uses a hardcoded 'Reboot' instead of the i18n helper;
update the computed to call t() for the reboot label (e.g.,
t('onboarding.nextSteps.reboot')) when showRebootButton.value is true, and add
the corresponding translation key/value to your locale files; reference
primaryButtonText and showRebootButton to locate and change the conditional, and
ensure existing usages of t('onboarding.nextSteps.continueToDashboard') remain
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/Onboarding/composables/internalBoot.ts`:
- Around line 46-56: The code is unsafely casting JSON.parse results to
MkbootpoolResponsePayload and coercing parsed.ok with Boolean(), bypassing
runtime validation and mis-handling string "false"; instead parse into unknown
(no "as" cast), implement a runtime type guard that checks required fields and
their types (e.g., verify the parsed object has the expected properties for
MkbootpoolResponsePayload), apply that guard both after JSON.parse(trimmed) and
after JSON.parse(trimmed.slice(firstBrace, lastBrace + 1)), and for parsed.ok
normalize it explicitly (if typeof ok === 'boolean' use it, else if typeof ok
=== 'string' accept only 'true'/'false' and convert accordingly), returning null
if validation fails; remove all "as MkbootpoolResponsePayload" casts and replace
them with validated, typed returns using the new type guard.
- Around line 85-119: The code processes the fetch result without checking HTTP
success and uses unsafe casts; update the logic to require response.ok when
deciding success (e.g., return ok: response.ok && parsed?.ok === true instead of
Boolean(parsed.ok)) and modify the parsing flow to remove "as
MkbootpoolResponsePayload" casts by moving JSON.parse/validation into a proper
type-guard or validator inside parseMkbootpoolPayload (ensure
parseMkbootpoolPayload returns a typed union or null after validating required
fields like ok, code, output) so you no longer need to cast parsed values before
using them.

In `@web/src/components/Onboarding/stepRegistry.ts`:
- Around line 38-42: INTERNAL_BOOT in the stepRegistry uses hardcoded English
strings for titleKey and descriptionKey; replace them with i18n keys (e.g., set
titleKey to a key like 'onboarding.internalBoot.title' and descriptionKey to
'onboarding.internalBoot.description') to match the project's localization
pattern used by other steps, and add the corresponding entries to the locale
resource files so the keys resolve at runtime.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue`:
- Around line 454-458: The regex assigned to poolNamePattern (used to validate
normalizedPoolName) contains nested quantifiers which can lead to ReDoS; replace
the single complex pattern with a safer two-step validation: first use a simple
allowed-character pattern (e.g., ensure it starts with a lowercase letter and
only contains allowed chars) and then separately check structural rules (e.g.,
does not end or start with a digit, disallow consecutive special characters, or
other constraints). Update the validation logic where poolNamePattern is tested
and set formError.value accordingly so the tests use the new checks instead of
the vulnerable nested-quantifier regex.

In `@web/src/composables/gql/index.ts`:
- Around line 1-2: The re-exports in web/src/composables/gql/index.ts use
extensionless relative paths which breaks ESM conventions; update the two export
statements (export * from './fragment-masking' and export * from './gql') to
include the .js extension (export * from './fragment-masking.js' and export *
from './gql.js') so they match the project's ESM style and other modules like
web/src/utils/authorizationLink.ts.

---

Nitpick comments:
In `@web/src/components/Onboarding/OnboardingSteps.vue`:
- Around line 10-17: This file defines a local StepId union type that duplicates
onboarding step IDs; remove the local StepId declaration in OnboardingSteps.vue
and import a single exported StepId from a shared onboarding types/module (e.g.,
export StepId from a new/shared onboarding types file), then update all
references in this component (and any other components) to use the imported
StepId type; ensure the shared module exports the same union values and update
any usages of 'OVERVIEW'|'CONFIGURE_SETTINGS' etc. to the centralized StepId to
prevent drift.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue`:
- Around line 83-90: handlePrimaryAction calls submitInternalBootReboot()
without awaiting or handling failures, so add async error handling: make
handlePrimaryAction async, await submitInternalBootReboot(), wrap the call in
try/catch, show user feedback on error (e.g., set a local error state or call an
existing notification method) and only call props.onComplete() on success; also
consider disabling the primary button or showing a loading state while awaiting
submitInternalBootReboot() to prevent duplicate submissions.
- Around line 52-54: primaryButtonText uses a hardcoded 'Reboot' instead of the
i18n helper; update the computed to call t() for the reboot label (e.g.,
t('onboarding.nextSteps.reboot')) when showRebootButton.value is true, and add
the corresponding translation key/value to your locale files; reference
primaryButtonText and showRebootButton to locate and change the conditional, and
ensure existing usages of t('onboarding.nextSteps.continueToDashboard') remain
unchanged.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aaa0372 and 4bf525b.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (27)
  • api/generated-schema.graphql
  • api/src/__test__/store/modules/emhttp.test.ts
  • api/src/__test__/store/state-parsers/var.test.ts
  • api/src/core/types/states/var.ts
  • api/src/store/state-parsers/var.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.model.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.service.ts
  • api/src/unraid-api/graph/resolvers/vars/vars.model.ts
  • docs/onboarding-internal-boot-port-differences.md
  • web/__test__/components/Onboarding/OnboardingModal.test.ts
  • web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
  • web/__test__/components/Onboarding/internalBoot.test.ts
  • web/components.d.ts
  • web/src/components/Onboarding/OnboardingModal.vue
  • web/src/components/Onboarding/OnboardingSteps.vue
  • web/src/components/Onboarding/composables/internalBoot.ts
  • web/src/components/Onboarding/graphql/getInternalBootContext.query.ts
  • web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts
  • web/src/components/Onboarding/stepRegistry.ts
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
  • web/src/components/Onboarding/store/onboardingDraft.ts
  • web/src/composables/gql/gql.ts
  • web/src/composables/gql/graphql.ts
  • web/src/composables/gql/index.ts

Comment on lines +454 to +458
const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
if (!poolNamePattern.test(normalizedPoolName)) {
formError.value = 'Use only lowercase with no special characters or leading/trailing digits.';
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential ReDoS vulnerability in pool name regex.

The pattern ([a-z0-9~._-]*[a-z_-])* can cause exponential backtracking on inputs like "a" + "-".repeat(30). The nested quantifiers create overlapping match possibilities.

🔒 Suggested fix with a safer pattern
-  const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
+  // Validates: starts with lowercase letter, contains allowed chars, doesn't end with digit
+  const poolNamePattern = /^[a-z][a-z0-9~._-]*[a-z_-]$|^[a-z]$/;
   if (!poolNamePattern.test(normalizedPoolName)) {

Alternatively, if the intent is to disallow consecutive special characters or enforce specific structure, consider validating in multiple steps:

// Check allowed characters first, then validate structure
const hasValidChars = /^[a-z][a-z0-9~._-]*$/.test(normalizedPoolName);
const endsWithDigit = /\d$/.test(normalizedPoolName);
if (!hasValidChars || endsWithDigit) {
  formError.value = '...';
  return null;
}
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 454-454: Inefficient regular expression
This part of the regular expression may cause exponential backtracking on strings starting with 'a' and containing many repetitions of '-'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue` around
lines 454 - 458, The regex assigned to poolNamePattern (used to validate
normalizedPoolName) contains nested quantifiers which can lead to ReDoS; replace
the single complex pattern with a safer two-step validation: first use a simple
allowed-character pattern (e.g., ensure it starts with a lowercase letter and
only contains allowed chars) and then separately check structural rules (e.g.,
does not end or start with a digit, disallow consecutive special characters, or
other constraints). Update the validation logic where poolNamePattern is tested
and set formError.value accordingly so the tests use the new checks instead of
the vulnerable nested-quantifier regex.

Comment on lines +1 to +2
export * from './fragment-masking';
export * from './gql';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file in question
find . -name "index.ts" -path "*/composables/gql/*" -type f

Repository: unraid/api

Length of output: 119


🏁 Script executed:

# Also check for the referenced files
find . -path "*/composables/gql/*" -type f

Repository: unraid/api

Length of output: 377


🏁 Script executed:

# Search for similar re-export patterns in the web directory
rg "export \* from" web/ --type ts -A 0 -B 0 | head -30

Repository: unraid/api

Length of output: 372


🏁 Script executed:

cat -n web/src/composables/gql/index.ts

Repository: unraid/api

Length of output: 126


🏁 Script executed:

# Check other re-export patterns in web/src to understand consistency
rg "export \* from" web/src --type ts -B 0 -A 0 | sort | uniq -c

Repository: unraid/api

Length of output: 279


🏁 Script executed:

# Check if there's any build or transpilation config that might affect this
find . -name "tsconfig.json" -o -name "nuxt.config.ts" -o -name "vite.config.ts" | head -5 | xargs cat 2>/dev/null

Repository: unraid/api

Length of output: 11785


Add .js extensions to re-exports for ESM compatibility.

Lines 1-2 use extensionless relative paths, which violates the project's ESM convention. Other files in the codebase (e.g., web/src/utils/authorizationLink.ts) consistently use .js extensions in re-exports.

Suggested fix
-export * from './fragment-masking';
-export * from './gql';
+export * from './fragment-masking.js';
+export * from './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.

Suggested change
export * from './fragment-masking';
export * from './gql';
export * from './fragment-masking.js';
export * from './gql.js';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/composables/gql/index.ts` around lines 1 - 2, The re-exports in
web/src/composables/gql/index.ts use extensionless relative paths which breaks
ESM conventions; update the two export statements (export * from
'./fragment-masking' and export * from './gql') to include the .js extension
(export * from './fragment-masking.js' and export * from './gql.js') so they
match the project's ESM style and other modules like
web/src/utils/authorizationLink.ts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

♻️ Duplicate comments (2)
web/src/components/Onboarding/stepRegistry.ts (1)

38-41: ⚠️ Potential issue | 🟡 Minor

Use i18n keys instead of hardcoded metadata strings.

titleKey and descriptionKey for CONFIGURE_BOOT are literal English text, while this registry otherwise stores translation keys. This will bypass localization for this step.

Suggested change
   CONFIGURE_BOOT: {
-    titleKey: 'Setup Boot',
-    descriptionKey: 'Choose USB or storage drive boot',
+    titleKey: 'onboarding.internalBoot.title',
+    descriptionKey: 'onboarding.internalBoot.description',
     icon: 'i-heroicons-circle-stack',
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/stepRegistry.ts` around lines 38 - 41, The
CONFIGURE_BOOT entry in the stepRegistry uses literal English strings for
titleKey and descriptionKey instead of translation keys, breaking localization;
update the CONFIGURE_BOOT object in stepRegistry.ts to use the same i18n key
naming convention as the other entries (replace the hardcoded 'Setup Boot' and
'Choose USB or storage drive boot' with the corresponding translation keys used
elsewhere, e.g., onboarding.* or the project's existing key pattern) so titleKey
and descriptionKey reference resource keys rather than plain text.
web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue (1)

472-475: ⚠️ Potential issue | 🟠 Major

Replace the pool-name regex to avoid catastrophic backtracking.

The current pattern has nested quantifiers and can be exploited with pathological input.

Suggested safer validation
-  const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
-  if (!poolNamePattern.test(normalizedPoolName)) {
+  const startsWithLetter = /^[a-z]/.test(normalizedPoolName);
+  const allowedChars = /^[a-z0-9~._-]+$/.test(normalizedPoolName);
+  const noTrailingDigit = !/[0-9]$/.test(normalizedPoolName);
+  if (!startsWithLetter || !allowedChars || !noTrailingDigit) {
     formError.value = 'Use only lowercase with no special characters or leading/trailing digits.';
     return null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue` around
lines 472 - 475, The current poolNamePattern uses nested quantifiers causing
catastrophic backtracking; replace it with a linear-time-safe pattern and update
the validation to use that pattern. Specifically, change poolNamePattern to a
non-backtracking-safe regex such as /^[a-z](?:[a-z0-9._~-]*[a-z_-])?$/ and keep
using poolNamePattern.test(normalizedPoolName) in the same check; ensure
formError.value remains set on failure (reference poolNamePattern,
normalizedPoolName, formError) and adjust the error text only if needed to match
the new allowed characters.
🧹 Nitpick comments (3)
api/generated-schema.graphql (1)

1386-1388: Make reboot deterministic in the mutation contract.

reboot: Boolean (Line 1468) introduces null/omitted ambiguity for a control flag. Prefer a non-null field with default (reboot: Boolean! = false) to simplify resolver and client logic.

Suggested schema tweak
 input CreateInternalBootPoolInput {
   poolName: String!
   devices: [String!]!
   bootSizeMiB: Int!
   updateBios: Boolean!
-  reboot: Boolean
+  reboot: Boolean! = false
 }

Also applies to: 1462-1469

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/generated-schema.graphql` around lines 1386 - 1388, The mutation input
CreateInternalBootPoolInput makes the control flag reboot nullable (reboot:
Boolean), causing ambiguity; change the input field to a non-null Boolean with a
default (reboot: Boolean! = false) in the GraphQL schema and regenerate types so
resolvers (handlers that read reboot) and any client code assume a definite
boolean value; update any resolver logic in functions referencing
CreateInternalBootPoolInput.reboot to rely on a boolean (no null checks) and
ensure any schema generation pipeline is re-run so TypeScript/clients get the
new non-null default.
web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts (1)

80-99: Make this test less brittle by avoiding exact long-copy assertions.

The behavior is what matters here (dialog opens, confirm action triggers reboot), not the exact wording.

As per coding guidelines: "Test what the code does, not implementation details like exact error message wording."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts` around
lines 80 - 99, The test is brittle because it asserts an exact long copy string;
instead, after setting draftStore.internalBootApplySucceeded = true and mounting
via mountComponent(), trigger the brand-button
(wrapper.find('[data-testid="brand-button"]')) and assert the reboot dialog
opened by checking for a shorter, stable indicator like wrapper.text()
containing 'Confirm Reboot' or the presence of the dialog element, then simulate
the user confirming the dialog (find the dialog confirm button) and assert
submitInternalBootRebootMock was called; keep the existing onComplete assertion
if needed but remove the long-copy expect that checks the full BIOS instruction
string.
web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts (1)

1084-1086: Use a stable error assertion instead of full sentence matching.

This assertion is tightly coupled to message wording and will break on minor copy changes. Prefer checking stable behavior + a durable substring.

Suggested fix
-    expect(wrapper.text()).toContain('Internal boot setup returned an error: mkbootpool failed');
+    expect(wrapper.text()).toContain('Internal boot setup returned an error');

As per coding guidelines: "Test what the code does, not implementation details like exact error message wording."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts` around
lines 1084 - 1086, The test is asserting a full error sentence which is brittle;
change the assertions in OnboardingSummaryStep.test to verify stable behavior
instead: keep the existing
expect(setInternalBootApplySucceededMock).not.toHaveBeenCalledWith(true) and
replace expect(wrapper.text()).toContain('Internal boot setup returned an error:
mkbootpool failed') with a durable check such as expecting wrapper.text()
toContain a short stable substring like 'mkbootpool' or 'Internal boot' (or
assert the presence of the warning element/role that shows error state), and
keep the 'Setup Applied with Warnings' check if that behavior must remain;
reference setInternalBootApplySucceededMock and wrapper.text() to locate and
modify the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/generated-schema.graphql`:
- Line 964: BrandingConfig.hasPartnerLogo was changed from non-nullable to
nullable causing a breaking change; restore it to Boolean! (revert the GraphQL
field definition for BrandingConfig.hasPartnerLogo back to non-nullable) or, if
you must introduce nullability, instead add a new field (e.g.,
hasPartnerLogoNullable) as Boolean, mark the old BrandingConfig.hasPartnerLogo
as deprecated with a clear reason, update any input/usage and regenerate client
types/schema artifacts (codegen) so downstream strict clients remain compatible.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts`:
- Around line 288-292: The devices field currently uses `@IsString`({ each: true
}) which allows empty strings; update validation on the devices property (the
devices!: string[] field in onboarding.model.ts) to reject empty device IDs by
adding a per-item non-empty validator such as `@IsNotEmpty`({ each: true }) or
`@MinLength`(1, { each: true }) alongside the existing `@IsString`, keeping the
`@ArrayMinSize`(1) and `@ArrayMaxSize`(4) decorators so empty strings fail at
schema-validation time rather than later in command execution.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts`:
- Around line 51-53: The test currently weakens typing by passing
onboardingInternalBootService as any; instead construct a real instance of the
OnboardingInternalBootService (the same concrete class used in production) and
pass that instance into the resolver setup alongside onboardingService, then
replace the any-cast with vi.spyOn(onboardingInternalBootService, "methodName")
to stub the specific method you need in the test; locate usages in
onboarding.mutation.spec.ts where onboardingInternalBootService is supplied to
the resolver and replace the cast with a real instance + vi.spyOn to stub
behavior while preserving strong types.

In `@web/__test__/components/Onboarding/internalBoot.test.ts`:
- Around line 95-98: The test currently asserts the exact error output string
for the "result" object; update the assertion to avoid exact wording churn by
only asserting that result.ok is false and that result.output contains the
canonical portion of the message (e.g., "Internal boot setup request failed") or
matches a simple regex, rather than equality with the full fallback sentence;
update the expectation around the result variable in internalBoot.test.ts
accordingly.

In `@web/src/components/Onboarding/composables/internalBoot.ts`:
- Around line 35-63: The mutate call to apolloClient (useApolloClient().client)
can throw and currently lets the function reject instead of returning an
InternalBootSubmitResult; wrap the apolloClient.mutate(...) (the
CREATE_INTERNAL_BOOT_POOL_MUTATION call) in a try/catch and on error return a
structured failure object matching InternalBootSubmitResult (e.g., { ok: false,
code: undefined, output: `Internal boot setup request failed: ${error?.message
|| String(error)}` }) rather than rethrowing; keep existing behavior for empty
data (result check) and preserve variables/fetchPolicy.

In `@web/src/components/Onboarding/OnboardingModal.vue`:
- Around line 101-114: hideInternalBootStep currently only returns true when the
fetched setting equals "no", so a missing/errored internalBootVisibilityResult
will let CONFIGURE_BOOT show; change hideInternalBootStep to default to true
unless the setting explicitly equals "yes" (i.e., treat undefined/error as
hidden). Update the computed that reads
internalBootVisibilityResult.value?.vars?.enableBootTransfer to safely check
existence, normalize the value, and set hideInternalBootStep to (normalized !==
'yes'); leave visibleHardcodedSteps and the CONFIGURE_BOOT conditional as-is so
the step remains hidden unless enabled and not a partner build.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue`:
- Around line 52-54: The new reboot flow uses hardcoded English strings; replace
them with i18n keys and calls so translations are used: change the
primaryButtonText computed to use t('onboarding.nextSteps.reboot') instead of
'Reboot' (keeping the existing t('onboarding.nextSteps.continueToDashboard')
branch), and update all hardcoded strings in the reboot dialog (the dialog
title, body copy, confirm/cancel button labels found around the block currently
at lines ~363-389) to use t(...) with descriptive keys (e.g.,
onboarding.nextSteps.rebootDialog.title, .message, .confirm, .cancel), then add
corresponding entries to the locale resource files. Ensure you import/use the
existing t function used elsewhere in this component.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 131-133: The hardcoded boot-summary and warning-dialog strings
need to be replaced with i18n keys: update the computed bootModeLabel and all
hardcoded labels/buttons in the template (referencing bootModeLabel, the
boot-summary section titles, and the confirmation dialog/button text shown
around the template ranges you added) to use the i18n translation function
(e.g., useI18n()/t or this.$t) and add corresponding keys (e.g.,
onboarding.bootMode.storage, onboarding.bootMode.usb,
onboarding.bootSummary.title, onboarding.warningDialog.title,
onboarding.warningDialog.message, onboarding.warningDialog.confirm,
onboarding.warningDialog.cancel) to the locale files; ensure you import/use the
component i18n helper if needed and run existing linter/type checks for missing
keys.

In `@web/src/components/Onboarding/store/onboardingDraft.ts`:
- Around line 26-57: In normalizePersistedInternalBootSelection replace the
Boolean(candidate.updateBios) coercion with strict boolean parsing: if
candidate.updateBios is a boolean use it, if it's a string accept 'true'
(case-insensitive) as true and 'false' as false, if it's a number accept 1 as
true and 0 as false, otherwise fall back to false (or the existing default).
Apply the same strict-boolean parsing logic to any other persisted flag
coercions in this module (the other Boolean(...) uses that restore persisted
flags) so legacy string/"false" values don't become true.

---

Duplicate comments:
In `@web/src/components/Onboarding/stepRegistry.ts`:
- Around line 38-41: The CONFIGURE_BOOT entry in the stepRegistry uses literal
English strings for titleKey and descriptionKey instead of translation keys,
breaking localization; update the CONFIGURE_BOOT object in stepRegistry.ts to
use the same i18n key naming convention as the other entries (replace the
hardcoded 'Setup Boot' and 'Choose USB or storage drive boot' with the
corresponding translation keys used elsewhere, e.g., onboarding.* or the
project's existing key pattern) so titleKey and descriptionKey reference
resource keys rather than plain text.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue`:
- Around line 472-475: The current poolNamePattern uses nested quantifiers
causing catastrophic backtracking; replace it with a linear-time-safe pattern
and update the validation to use that pattern. Specifically, change
poolNamePattern to a non-backtracking-safe regex such as
/^[a-z](?:[a-z0-9._~-]*[a-z_-])?$/ and keep using
poolNamePattern.test(normalizedPoolName) in the same check; ensure
formError.value remains set on failure (reference poolNamePattern,
normalizedPoolName, formError) and adjust the error text only if needed to match
the new allowed characters.

---

Nitpick comments:
In `@api/generated-schema.graphql`:
- Around line 1386-1388: The mutation input CreateInternalBootPoolInput makes
the control flag reboot nullable (reboot: Boolean), causing ambiguity; change
the input field to a non-null Boolean with a default (reboot: Boolean! = false)
in the GraphQL schema and regenerate types so resolvers (handlers that read
reboot) and any client code assume a definite boolean value; update any resolver
logic in functions referencing CreateInternalBootPoolInput.reboot to rely on a
boolean (no null checks) and ensure any schema generation pipeline is re-run so
TypeScript/clients get the new non-null default.

In `@web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts`:
- Around line 80-99: The test is brittle because it asserts an exact long copy
string; instead, after setting draftStore.internalBootApplySucceeded = true and
mounting via mountComponent(), trigger the brand-button
(wrapper.find('[data-testid="brand-button"]')) and assert the reboot dialog
opened by checking for a shorter, stable indicator like wrapper.text()
containing 'Confirm Reboot' or the presence of the dialog element, then simulate
the user confirming the dialog (find the dialog confirm button) and assert
submitInternalBootRebootMock was called; keep the existing onComplete assertion
if needed but remove the long-copy expect that checks the full BIOS instruction
string.

In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts`:
- Around line 1084-1086: The test is asserting a full error sentence which is
brittle; change the assertions in OnboardingSummaryStep.test to verify stable
behavior instead: keep the existing
expect(setInternalBootApplySucceededMock).not.toHaveBeenCalledWith(true) and
replace expect(wrapper.text()).toContain('Internal boot setup returned an error:
mkbootpool failed') with a durable check such as expecting wrapper.text()
toContain a short stable substring like 'mkbootpool' or 'Internal boot' (or
assert the presence of the warning element/role that shows error state), and
keep the 'Setup Applied with Warnings' check if that behavior must remain;
reference setInternalBootApplySucceededMock and wrapper.text() to locate and
modify the assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: acc5ff23-cefe-44d6-8c68-4d7eee63312d

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf525b and e466872.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (23)
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • web/__test__/components/Onboarding/OnboardingModal.test.ts
  • web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
  • web/__test__/components/Onboarding/internalBoot.test.ts
  • web/src/components/Onboarding/OnboardingModal.vue
  • web/src/components/Onboarding/OnboardingSteps.vue
  • web/src/components/Onboarding/composables/internalBoot.ts
  • web/src/components/Onboarding/graphql/createInternalBootPool.mutation.ts
  • web/src/components/Onboarding/stepRegistry.ts
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
  • web/src/components/Onboarding/store/onboardingDraft.ts
  • web/src/composables/gql/gql.ts
  • web/src/composables/gql/graphql.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Onboarding/OnboardingSteps.vue


"""Indicates if a partner logo exists"""
hasPartnerLogo: Boolean!
hasPartnerLogo: Boolean
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and inspect around line 964
fd "generated-schema.graphql"

Repository: unraid/api

Length of output: 82


🏁 Script executed:

# Once we find the file, let's check its content around line 964
head -970 api/generated-schema.graphql | tail -20

Repository: unraid/api

Length of output: 603


🏁 Script executed:

# Check git history to see if this field was changed from Boolean! to Boolean
cd api && git log -p --follow -S "hasPartnerLogo" -- generated-schema.graphql | head -100

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check if this is a generated file and git history
cd api && git log --oneline -n 20 -- generated-schema.graphql

Repository: unraid/api

Length of output: 122


🏁 Script executed:

# Search for Boolean! (non-nullable boolean) fields to see the pattern
rg "Boolean!" api/generated-schema.graphql | head -20

Repository: unraid/api

Length of output: 551


🏁 Script executed:

# Check the git status and file info
cd api && git log -1 --format="%H %s" -- generated-schema.graphql

Repository: unraid/api

Length of output: 154


🏁 Script executed:

# Look for any comments about generation or if this file is in .gitignore
cat .gitignore | grep -i schema || echo "No schema patterns in gitignore"

Repository: unraid/api

Length of output: 85


🏁 Script executed:

# Check the specific change to hasPartnerLogo in the commit
cd api && git show e4668721 -- generated-schema.graphql | grep -B5 -A5 "hasPartnerLogo"

Repository: unraid/api

Length of output: 732


🏁 Script executed:

# Also check if there's a previous version of the field
cd api && git log -p -- generated-schema.graphql | grep -B3 -A3 "hasPartnerLogo" | head -30

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check full git history to see if hasPartnerLogo existed before this commit
cd api && git log --all --oneline -- generated-schema.graphql | head -10

Repository: unraid/api

Length of output: 828


🏁 Script executed:

# Search for any references to hasPartnerLogo in other files
rg "hasPartnerLogo" --type graphql

Repository: unraid/api

Length of output: 85


🏁 Script executed:

# Check the parent commit to see what the schema looked like before
cd api && git show e4668721^:generated-schema.graphql | grep -i "partner" || echo "Field did not exist in previous version"

Repository: unraid/api

Length of output: 150


🏁 Script executed:

# Check the commit before hasPartnerLogo was added
cd api && git show 15bd7477:generated-schema.graphql | grep -i "partner\|hasPartner" || echo "Field did not exist in that commit either"

Repository: unraid/api

Length of output: 300


🏁 Script executed:

# Let's see when BrandingConfig type was first introduced
rg "type BrandingConfig" -B2 -A20 api/generated-schema.graphql

Repository: unraid/api

Length of output: 567


🏁 Script executed:

# Let's verify the exact change in the commit that introduced hasPartnerLogo
cd api && git show e4668721 -- generated-schema.graphql | grep -B10 -A10 "^+.*hasPartnerLogo" | head -30

Repository: unraid/api

Length of output: 878


🏁 Script executed:

# Check if there are any other commits that might have modified hasPartnerLogo after e4668721
cd api && git log --all -p -- generated-schema.graphql | grep -B5 -A5 "hasPartnerLogo" | grep "^-.*hasPartnerLogo\|^+.*hasPartnerLogo"

Repository: unraid/api

Length of output: 218


🏁 Script executed:

# Find the exact commit that changed hasPartnerLogo from Boolean! to Boolean
cd api && git log --all -p -- generated-schema.graphql | grep -B30 "^-.*hasPartnerLogo: Boolean!" | grep "^commit" | head -1

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Get more details on that commit
cd api && git log --all --oneline -- generated-schema.graphql | head -20

Repository: unraid/api

Length of output: 1514


🏁 Script executed:

# Let's search for the specific change more precisely
cd api && git log --all -p -- generated-schema.graphql | awk '/^-.*hasPartnerLogo: Boolean!/{print NR": "$0}' | head -5

Repository: unraid/api

Length of output: 87


🏁 Script executed:

# Get all commits with their changes to hasPartnerLogo
cd api && git log --all --format="%H %s" -p -- generated-schema.graphql | grep -B1 "hasPartnerLogo" | grep -E "^[0-9a-f]{40}|hasPartnerLogo"

Repository: unraid/api

Length of output: 218


🏁 Script executed:

# Find the commit that made the Boolean! -> Boolean change
cd api && git log --all -p -- generated-schema.graphql | grep -B50 "^-  hasPartnerLogo: Boolean!" | grep "^commit" | head -1

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Get the full commit details
cd api && git log --all --format="%H %s %ai" -- generated-schema.graphql | while read hash msg; do
  if git show "$hash" -- generated-schema.graphql | grep -q "^-  hasPartnerLogo: Boolean!"; then
    echo "$hash $msg"
    break
  fi
done

Repository: unraid/api

Length of output: 185


🏁 Script executed:

# More direct approach - find all changes to Boolean! in the file
cd api && git log --all -p -- generated-schema.graphql | grep -B3 "^-  hasPartnerLogo: Boolean!" | grep "^commit\|^Date\|^-  hasPartnerLogo"

Repository: unraid/api

Length of output: 81


🏁 Script executed:

# Verify the complete history of hasPartnerLogo changes
cd api && git log --all -p -- generated-schema.graphql | grep -B2 "hasPartnerLogo" | grep "^commit\|^Date\|hasPartnerLogo" | head -20

Repository: unraid/api

Length of output: 218


🏁 Script executed:

# Check the current state at HEAD
cd api && git show HEAD:generated-schema.graphql | grep -B2 -A2 "hasPartnerLogo"

Repository: unraid/api

Length of output: 233


🏁 Script executed:

# Check the current state at HEAD with correct path
cd api && git show HEAD:api/generated-schema.graphql | grep -B2 -A2 "hasPartnerLogo"

Repository: unraid/api

Length of output: 316


🏁 Script executed:

# Get the full history showing which commit introduced Boolean! and which removed it
cd api && git log --all --oneline -p -- api/generated-schema.graphql | grep -B10 "hasPartnerLogo: Boolean" | grep "^[0-9a-f]\|hasPartnerLogo"

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Verify what the review is pointing at - check if this is a current issue or already resolved
wc -l api/generated-schema.graphql && sed -n '960,970p' api/generated-schema.graphql

Repository: unraid/api

Length of output: 350


🏁 Script executed:

# Let's trace the exact commits that modified hasPartnerLogo
cd api && git log --all --oneline --follow -- api/generated-schema.graphql | head -30

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check the specific diff between commits 15bd7477 and e4668721
cd api && git diff 15bd7477 e4668721 -- api/generated-schema.graphql | grep -B3 -A3 "hasPartnerLogo"

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Find which commit between 15bd7477 and e4668721 made the breaking change
cd api && git log --all --oneline 15bd7477..e4668721 -- api/generated-schema.graphql

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Let's check the broader context - when was hasPartnerLogo first introduced as Boolean!
cd api && git log --all -p -- api/generated-schema.graphql | grep -B20 "^\+.*hasPartnerLogo: Boolean!" | head -40

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check current working directory to see if there are uncommitted changes
cd api && git status | grep -A5 generated-schema.graphql

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Let's look at the two most recent commits and their impact
cd api && git show 15bd7477:api/generated-schema.graphql | grep -B3 -A3 "hasPartnerLogo" > /tmp/15bd7477.txt && \
git show e4668721:api/generated-schema.graphql | grep -B3 -A3 "hasPartnerLogo" > /tmp/e4668721.txt && \
diff -u /tmp/15bd7477.txt /tmp/e4668721.txt || true

Repository: unraid/api

Length of output: 395


🏁 Script executed:

# Search for the original introduction of hasPartnerLogo with Boolean!
cd api && git log --all --reverse -p -- api/generated-schema.graphql | grep -B5 "hasPartnerLogo: Boolean!" | head -30

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check if there's any deprecation directive on hasPartnerLogo in current state
grep -B2 -A2 "hasPartnerLogo" api/generated-schema.graphql | grep -i "deprecated\|deprecation"

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check if there are any migration notes or comments about this change
git log --all --oneline e4668721 | head -1 && git show e4668721 | head -50

Repository: unraid/api

Length of output: 2418


🏁 Script executed:

# Verify no recent changes have fixed this
cd api && git log --oneline -n 5 -- api/generated-schema.graphql

Repository: unraid/api

Length of output: 36


Breaking change: BrandingConfig.hasPartnerLogo changed from non-nullable to nullable.

The field was changed from Boolean! to Boolean in commit e466872, which breaks clients with generated strict types. Either revert to Boolean!, or introduce a new nullable field and deprecate the old one first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/generated-schema.graphql` at line 964, BrandingConfig.hasPartnerLogo was
changed from non-nullable to nullable causing a breaking change; restore it to
Boolean! (revert the GraphQL field definition for BrandingConfig.hasPartnerLogo
back to non-nullable) or, if you must introduce nullability, instead add a new
field (e.g., hasPartnerLogoNullable) as Boolean, mark the old
BrandingConfig.hasPartnerLogo as deprecated with a clear reason, update any
input/usage and regenerate client types/schema artifacts (codegen) so downstream
strict clients remain compatible.

Comment on lines +288 to +292
@Field(() => [String])
@ArrayMinSize(1)
@ArrayMaxSize(4)
@IsString({ each: true })
devices!: string[];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the package.json to see which version of class-validator is used
fd -e package.json | head -5 | xargs grep -l "class-validator"

Repository: unraid/api

Length of output: 38


🏁 Script executed:

# Search for the onboarding model file and check the full context
cat -n api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts | sed -n '280,300p'

Repository: unraid/api

Length of output: 673


🏁 Script executed:

# Search for usage of the devices field in the codebase
rg "devices" api/src/unraid-api/graph/resolvers/onboarding/ -A 3 -B 3

Repository: unraid/api

Length of output: 8076


🏁 Script executed:

# Check if there's a diskId field mentioned anywhere in the onboarding context
rg "diskId|disk.*Id" api/src/unraid-api/graph/resolvers/onboarding/ -A 2 -B 2

Repository: unraid/api

Length of output: 2904


🏁 Script executed:

# Check class-validator imports to understand what's available
rg "from.*class-validator" api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts

Repository: unraid/api

Length of output: 79


🌐 Web query:

class-validator IsString empty string behavior

💡 Result:

@IsString() in class-validator only checks that the value’s type is a string—so an empty string ("") is still valid and will pass. [1]

If you want to reject empty strings, combine it with a non-empty constraint, e.g. @IsNotEmpty() (fails for "", null, undefined) or use a length constraint like @MinLength(1) / @Length(1, …). [1]

If you’re using @IsOptional(): it skips validation only for null/undefined, not for "", so "" will still be validated (and will still pass @IsString() unless you add @IsNotEmpty() / length rules). [1]

Example

import { IsOptional, IsString, IsNotEmpty } from 'class-validator';

class Dto {
  `@IsOptional`()      // skips only if null/undefined
  `@IsString`()
  `@IsNotEmpty`()      // rejects ""
  name?: string;
}

Sources:
[1] typestack/class-validator README (decorator semantics for @IsOptional(), @IsNotEmpty(), @IsString()) (github.com)

Citations:


🌐 Web query:

class-validator IsNotEmpty each option support

💡 Result:

Yes—@IsNotEmpty() supports the each option via ValidationOptions, so you can validate each element of an array/set/map:

import { IsNotEmpty, ArrayNotEmpty } from 'class-validator';

class Dto {
  `@ArrayNotEmpty`()              // array itself must not be empty
  `@IsNotEmpty`({ each: true })   // each item must be non-empty
  tags: string[];
}

each: true is the standard way to apply a validator to every item in an array (and similarly for Set/Map). [1]

Source(s)
[1] typestack/class-validator README (Validating arrays + ArrayNotEmpty)


Reject empty device strings at schema-validation time.

@IsString({ each: true }) accepts empty strings, so an empty device ID can pass input validation and only fail later during command execution.

🛡️ Suggested validation hardening
 import {
     ArrayMaxSize,
     ArrayMinSize,
+    IsNotEmpty,
     IsBoolean,
     `@Field`(() => [String])
     `@ArrayMinSize`(1)
     `@ArrayMaxSize`(4)
     `@IsString`({ each: true })
+    `@IsNotEmpty`({ each: true })
     devices!: string[];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Field(() => [String])
@ArrayMinSize(1)
@ArrayMaxSize(4)
@IsString({ each: true })
devices!: string[];
import {
ArrayMaxSize,
ArrayMinSize,
IsNotEmpty,
IsBoolean,
// ... other imports
}
Suggested change
@Field(() => [String])
@ArrayMinSize(1)
@ArrayMaxSize(4)
@IsString({ each: true })
devices!: string[];
`@Field`(() => [String])
`@ArrayMinSize`(1)
`@ArrayMaxSize`(4)
`@IsString`({ each: true })
`@IsNotEmpty`({ each: true })
devices!: string[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts` around
lines 288 - 292, The devices field currently uses `@IsString`({ each: true })
which allows empty strings; update validation on the devices property (the
devices!: string[] field in onboarding.model.ts) to reject empty device IDs by
adding a per-item non-empty validator such as `@IsNotEmpty`({ each: true }) or
`@MinLength`(1, { each: true }) alongside the existing `@IsString`, keeping the
`@ArrayMinSize`(1) and `@ArrayMaxSize`(4) decorators so empty strings fail at
schema-validation time rather than later in command execution.

Comment on lines +95 to +98
expect(result).toEqual({
ok: false,
output: 'Internal boot setup request failed: empty API response.',
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid asserting the full fallback error message text.

This assertion is tied to exact wording and will create unnecessary test churn for copy-only updates.

Suggested change
-    expect(result).toEqual({
-      ok: false,
-      output: 'Internal boot setup request failed: empty API response.',
-    });
+    expect(result.ok).toBe(false);
+    expect(result.output).toContain('Internal boot setup request failed');

As per coding guidelines: "Test what the code does, not implementation details like exact error message wording."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).toEqual({
ok: false,
output: 'Internal boot setup request failed: empty API response.',
});
expect(result.ok).toBe(false);
expect(result.output).toContain('Internal boot setup request failed');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/internalBoot.test.ts` around lines 95 -
98, The test currently asserts the exact error output string for the "result"
object; update the assertion to avoid exact wording churn by only asserting that
result.ok is false and that result.output contains the canonical portion of the
message (e.g., "Internal boot setup request failed") or matches a simple regex,
rather than equality with the full fallback sentence; update the expectation
around the result variable in internalBoot.test.ts accordingly.

Comment on lines +35 to +63
const apolloClient = useApolloClient().client;
const { data } = await apolloClient.mutate({
mutation: CREATE_INTERNAL_BOOT_POOL_MUTATION,
variables: {
input: {
poolName: selection.poolName,
devices: selection.devices,
bootSizeMiB: selection.bootSizeMiB,
updateBios: selection.updateBios,
reboot: Boolean(options.reboot),
},
},
fetchPolicy: 'no-cache',
});

const result = data?.onboarding?.createInternalBootPool;
if (!result) {
return {
ok: false,
output: 'Internal boot setup request failed: empty API response.',
};
}

return {
ok: result.ok,
code: result.code ?? undefined,
output: result.output?.trim() || 'No output',
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize mutation exceptions into a structured failure result.

If apolloClient.mutate throws (network/GraphQL), this function rejects instead of returning InternalBootSubmitResult. That breaks the ok-based flow and can short-circuit onboarding apply handling.

Suggested fix
 export const submitInternalBootCreation = async (
   selection: InternalBootSelection,
   options: SubmitInternalBootOptions = {}
 ): Promise<InternalBootSubmitResult> => {
   const apolloClient = useApolloClient().client;
-  const { data } = await apolloClient.mutate({
-    mutation: CREATE_INTERNAL_BOOT_POOL_MUTATION,
-    variables: {
-      input: {
-        poolName: selection.poolName,
-        devices: selection.devices,
-        bootSizeMiB: selection.bootSizeMiB,
-        updateBios: selection.updateBios,
-        reboot: Boolean(options.reboot),
-      },
-    },
-    fetchPolicy: 'no-cache',
-  });
+  try {
+    const { data } = await apolloClient.mutate({
+      mutation: CREATE_INTERNAL_BOOT_POOL_MUTATION,
+      variables: {
+        input: {
+          poolName: selection.poolName,
+          devices: selection.devices,
+          bootSizeMiB: selection.bootSizeMiB,
+          updateBios: selection.updateBios,
+          reboot: Boolean(options.reboot),
+        },
+      },
+      fetchPolicy: 'no-cache',
+    });
 
-  const result = data?.onboarding?.createInternalBootPool;
-  if (!result) {
-    return {
-      ok: false,
-      output: 'Internal boot setup request failed: empty API response.',
-    };
-  }
+    const result = data?.onboarding?.createInternalBootPool;
+    if (!result) {
+      return {
+        ok: false,
+        output: 'Internal boot setup request failed: empty API response.',
+      };
+    }
 
-  return {
-    ok: result.ok,
-    code: result.code ?? undefined,
-    output: result.output?.trim() || 'No output',
-  };
+    return {
+      ok: result.ok,
+      code: result.code ?? undefined,
+      output: result.output?.trim() || 'No output',
+    };
+  } catch (error) {
+    return {
+      ok: false,
+      output:
+        error instanceof Error
+          ? `Internal boot setup request failed: ${error.message}`
+          : 'Internal boot setup request failed.',
+    };
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const apolloClient = useApolloClient().client;
const { data } = await apolloClient.mutate({
mutation: CREATE_INTERNAL_BOOT_POOL_MUTATION,
variables: {
input: {
poolName: selection.poolName,
devices: selection.devices,
bootSizeMiB: selection.bootSizeMiB,
updateBios: selection.updateBios,
reboot: Boolean(options.reboot),
},
},
fetchPolicy: 'no-cache',
});
const result = data?.onboarding?.createInternalBootPool;
if (!result) {
return {
ok: false,
output: 'Internal boot setup request failed: empty API response.',
};
}
return {
ok: result.ok,
code: result.code ?? undefined,
output: result.output?.trim() || 'No output',
};
};
const apolloClient = useApolloClient().client;
try {
const { data } = await apolloClient.mutate({
mutation: CREATE_INTERNAL_BOOT_POOL_MUTATION,
variables: {
input: {
poolName: selection.poolName,
devices: selection.devices,
bootSizeMiB: selection.bootSizeMiB,
updateBios: selection.updateBios,
reboot: Boolean(options.reboot),
},
},
fetchPolicy: 'no-cache',
});
const result = data?.onboarding?.createInternalBootPool;
if (!result) {
return {
ok: false,
output: 'Internal boot setup request failed: empty API response.',
};
}
return {
ok: result.ok,
code: result.code ?? undefined,
output: result.output?.trim() || 'No output',
};
} catch (error) {
return {
ok: false,
output:
error instanceof Error
? `Internal boot setup request failed: ${error.message}`
: 'Internal boot setup request failed.',
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/composables/internalBoot.ts` around lines 35 -
63, The mutate call to apolloClient (useApolloClient().client) can throw and
currently lets the function reject instead of returning an
InternalBootSubmitResult; wrap the apolloClient.mutate(...) (the
CREATE_INTERNAL_BOOT_POOL_MUTATION call) in a try/catch and on error return a
structured failure object matching InternalBootSubmitResult (e.g., { ok: false,
code: undefined, output: `Internal boot setup request failed: ${error?.message
|| String(error)}` }) rather than rethrowing; keep existing behavior for empty
data (result check) and preserve variables/fetchPolicy.

Comment on lines +101 to +114
const hideInternalBootStep = computed(() => {
const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer?.trim().toLowerCase();
return setting === 'no';
});

// Determine which steps to show based on user state
const visibleHardcodedSteps = computed(() =>
HARDCODED_STEPS.filter((step) => showActivationStep.value || step.id !== 'ACTIVATE_LICENSE').filter(
(step) => {
if (step.id !== 'CONFIGURE_BOOT') {
return true;
}
return !isPartnerBuild.value && !hideInternalBootStep.value;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail closed when internal-boot visibility is unknown.

hideInternalBootStep only hides on explicit "no". If the query is missing/errored, CONFIGURE_BOOT is shown by default. For an internal/destructive flow, default should be hidden unless explicitly enabled.

Suggested fix
-const hideInternalBootStep = computed(() => {
-  const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer?.trim().toLowerCase();
-  return setting === 'no';
-});
+const showInternalBootStep = computed(() => {
+  const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer;
+  return typeof setting === 'string' && setting.trim().toLowerCase() === 'yes';
+});
@@
-      return !isPartnerBuild.value && !hideInternalBootStep.value;
+      return !isPartnerBuild.value && showInternalBootStep.value;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hideInternalBootStep = computed(() => {
const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer?.trim().toLowerCase();
return setting === 'no';
});
// Determine which steps to show based on user state
const visibleHardcodedSteps = computed(() =>
HARDCODED_STEPS.filter((step) => showActivationStep.value || step.id !== 'ACTIVATE_LICENSE').filter(
(step) => {
if (step.id !== 'CONFIGURE_BOOT') {
return true;
}
return !isPartnerBuild.value && !hideInternalBootStep.value;
}
const showInternalBootStep = computed(() => {
const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer;
return typeof setting === 'string' && setting.trim().toLowerCase() === 'yes';
});
// Determine which steps to show based on user state
const visibleHardcodedSteps = computed(() =>
HARDCODED_STEPS.filter((step) => showActivationStep.value || step.id !== 'ACTIVATE_LICENSE').filter(
(step) => {
if (step.id !== 'CONFIGURE_BOOT') {
return true;
}
return !isPartnerBuild.value && showInternalBootStep.value;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/OnboardingModal.vue` around lines 101 - 114,
hideInternalBootStep currently only returns true when the fetched setting equals
"no", so a missing/errored internalBootVisibilityResult will let CONFIGURE_BOOT
show; change hideInternalBootStep to default to true unless the setting
explicitly equals "yes" (i.e., treat undefined/error as hidden). Update the
computed that reads internalBootVisibilityResult.value?.vars?.enableBootTransfer
to safely check existence, normalize the value, and set hideInternalBootStep to
(normalized !== 'yes'); leave visibleHardcodedSteps and the CONFIGURE_BOOT
conditional as-is so the step remains hidden unless enabled and not a partner
build.

Comment on lines +26 to +57
const normalizePersistedInternalBootSelection = (
value: unknown
): OnboardingInternalBootSelection | null => {
if (!value || typeof value !== 'object') {
return null;
}

const candidate = value as {
poolName?: unknown;
slotCount?: unknown;
devices?: unknown;
bootSizeMiB?: unknown;
updateBios?: unknown;
};

const poolName = typeof candidate.poolName === 'string' ? candidate.poolName : '';
const parsedSlotCount = Number(candidate.slotCount);
const slotCount = Number.isFinite(parsedSlotCount) ? Math.max(1, Math.min(2, parsedSlotCount)) : 1;
const devices = Array.isArray(candidate.devices)
? candidate.devices.filter((item): item is string => typeof item === 'string')
: [];
const parsedBootSize = Number(candidate.bootSizeMiB);
const bootSizeMiB = Number.isFinite(parsedBootSize) && parsedBootSize > 0 ? parsedBootSize : 16384;

return {
poolName,
slotCount,
devices,
bootSizeMiB,
updateBios: Boolean(candidate.updateBios),
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace Boolean(...) coercion for persisted flags with strict boolean parsing.

Using Boolean(...) will convert "false" to true. That can restore incorrect updateBios/apply/skipped flags from persisted or legacy values.

Suggested fix
+const normalizePersistedBoolean = (value: unknown, fallback = false): boolean => {
+  if (typeof value === 'boolean') return value;
+  if (typeof value === 'string') {
+    const normalized = value.trim().toLowerCase();
+    if (normalized === 'true') return true;
+    if (normalized === 'false') return false;
+  }
+  return fallback;
+};
+
 const normalizePersistedInternalBootSelection = (
   value: unknown
 ): OnboardingInternalBootSelection | null => {
@@
   return {
     poolName,
     slotCount,
     devices,
     bootSizeMiB,
-    updateBios: Boolean(candidate.updateBios),
+    updateBios: normalizePersistedBoolean(candidate.updateBios, false),
   };
 };
@@
-            internalBootInitialized: Boolean(parsed.internalBootInitialized),
+            internalBootInitialized: normalizePersistedBoolean(parsed.internalBootInitialized, false),
             internalBootSkipped:
               parsed.internalBootSkipped !== undefined
-                ? Boolean(parsed.internalBootSkipped)
+                ? normalizePersistedBoolean(parsed.internalBootSkipped, false)
                 : normalizedBootMode === 'usb',
-            internalBootApplySucceeded: Boolean(parsed.internalBootApplySucceeded),
+            internalBootApplySucceeded: normalizePersistedBoolean(
+              parsed.internalBootApplySucceeded,
+              false
+            ),

Also applies to: 215-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/store/onboardingDraft.ts` around lines 26 - 57,
In normalizePersistedInternalBootSelection replace the
Boolean(candidate.updateBios) coercion with strict boolean parsing: if
candidate.updateBios is a boolean use it, if it's a string accept 'true'
(case-insensitive) as true and 'false' as false, if it's a number accept 1 as
true and 0 as false, otherwise fall back to false (or the existing default).
Apply the same strict-boolean parsing logic to any other persisted flag
coercions in this module (the other Boolean(...) uses that restore persisted
flags) so legacy string/"false" values don't become true.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts (1)

124-131: Replace the object cast with a type guard.

rawDevice as EmhttpDeviceRecord can be avoided here; a type guard keeps this path safer and aligns with the repository TS rules.

As per coding guidelines, "Avoid using casting whenever possible, prefer proper typing from the start".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`
around lines 124 - 131, The loop currently casts rawDevice to EmhttpDeviceRecord
(record) which should be replaced by a type guard; implement a type guard
function (e.g., isEmhttpDeviceRecord(value): value is EmhttpDeviceRecord) that
checks required properties and their types (id and device are strings) and use
it in the for loop to narrow rawDevice before accessing id/device, removing the
inline cast; update the loop to continue when the guard fails and then safely
trim record.id and record.device after the guard returns true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`:
- Around line 157-263: The method updateBiosBootEntries currently logs
efibootmgr failures to the output but always returns void, causing callers to
report success even when bios updates failed; change updateBiosBootEntries to
return a boolean (Promise<boolean>) that tracks failures (initialize let failed
= false and set failed = true whenever runEfiBootMgr returns exitCode !== 0),
and at the end return !failed (or throw an Error with joined output if you
prefer). Update all callers (the mutation handler that checks input.updateBios)
to await the new boolean and propagate failure by marking the mutation result as
not ok and appending output when the method returns false. Apply the same
pattern to the other bios-update block referenced (the similar code around the
other runEfiBootMgr calls noted in the comment) so all efibootmgr failures are
surfaced to the mutation result.
- Around line 162-174: The loop is deleting every efibootmgr entry; restrict
deletions to only Unraid-managed labels by checking the parsed label before
calling runEfiBootMgr: use the existing bootLabelMap from parseBootLabelMap
(which maps bootNumber→label) and iterate entries (for (const [bootNumber,
label] of bootLabelMap.entries())), add a guard that label matches the
Unraid-managed pattern (e.g., label.startsWith(UNRAID_LABEL_PREFIX) or use a new
helper isUnraidManagedLabel(label:string):boolean), and only call
this.runEfiBootMgr(['-b', bootNumber, '-B'], output) when the guard passes; add
a UNRAID_LABEL_PREFIX constant or helper method near
onboarding-internal-boot.service.ts to centralize the pattern.

---

Nitpick comments:
In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`:
- Around line 124-131: The loop currently casts rawDevice to EmhttpDeviceRecord
(record) which should be replaced by a type guard; implement a type guard
function (e.g., isEmhttpDeviceRecord(value): value is EmhttpDeviceRecord) that
checks required properties and their types (id and device are strings) and use
it in the for loop to narrow rawDevice before accessing id/device, removing the
inline cast; update the loop to continue when the guard fails and then safely
trim record.id and record.device after the guard returns true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7eb0bba9-358f-4594-85e4-2463c56f2838

📥 Commits

Reviewing files that changed from the base of the PR and between e466872 and 30f5e37.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts

Comment on lines +157 to +263
private async updateBiosBootEntries(devices: string[], output: string[]): Promise<void> {
this.ensureEmhttpBootContext();
const devsById = this.getDeviceMapFromEmhttpState();

const existingEntries = await this.runEfiBootMgr([], output);
const bootLabelMap =
existingEntries.exitCode === 0
? this.parseBootLabelMap(existingEntries.lines)
: new Map<string, string>();

for (const bootNumber of bootLabelMap.values()) {
const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
if (deleteResult.exitCode !== 0) {
output.push(
`efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
);
}
}

for (const bootDevice of devices) {
const bootId = bootDevice;
let device = bootDevice;
if (device === '' || devsById.has(device)) {
const mapped = devsById.get(device);
if (mapped) {
device = mapped;
}
}
if (device === '') {
continue;
}
const devicePath = `/dev/${device}`;
const createResult = await this.runEfiBootMgr(
[
'-c',
'-d',
devicePath,
'-p',
'2',
'-L',
`Unraid Internal Boot - ${bootId}`,
'-l',
EFI_BOOT_PATH,
],
output
);
if (createResult.exitCode !== 0) {
output.push(
`efibootmgr failed for ${this.shellQuote(devicePath)} (rc=${createResult.exitCode})`
);
}
}

const flashDevice = this.getFlashDeviceFromEmhttpState();
if (flashDevice) {
const device = flashDevice.trim();
const devicePath = `/dev/${device}`;
const flashResult = await this.runEfiBootMgr(
['-c', '-d', devicePath, '-p', '1', '-L', 'Unraid Flash'],
output
);
if (flashResult.exitCode !== 0) {
output.push(`efibootmgr failed for flash (rc=${flashResult.exitCode})`);
}
}

const currentEntries = await this.runEfiBootMgr([], output);
if (currentEntries.exitCode !== 0) {
return;
}

const labelMap = this.parseBootLabelMap(currentEntries.lines);
const desiredOrder: string[] = [];

for (const bootId of devices) {
const expectedLabel = `Unraid Internal Boot - ${bootId}`.toLowerCase();
for (const [labelText, bootNumber] of labelMap.entries()) {
if (labelText.toLowerCase().includes(expectedLabel)) {
desiredOrder.push(bootNumber);
break;
}
}
}

for (const [labelText, bootNumber] of labelMap.entries()) {
if (labelText.toLowerCase().includes('unraid flash')) {
desiredOrder.push(bootNumber);
break;
}
}

const uniqueOrder = [...new Set(desiredOrder.filter((entry) => entry.length > 0))];
if (uniqueOrder.length === 0) {
return;
}

const nextBoot = uniqueOrder[0];
const orderArgs = ['-o', uniqueOrder.join(',')];
if (nextBoot) {
orderArgs.push('-n', nextBoot);
}

const orderResult = await this.runEfiBootMgr(orderArgs, output);
if (orderResult.exitCode !== 0) {
output.push(`efibootmgr failed to set boot order (rc=${orderResult.exitCode})`);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate BIOS-update failures to the mutation result.

When input.updateBios is true, failures are logged to output but the method can still return ok: true. This creates false-positive success for a requested operation.

Proposed fix
-    private async updateBiosBootEntries(devices: string[], output: string[]): Promise<void> {
+    private async updateBiosBootEntries(devices: string[], output: string[]): Promise<boolean> {
+        let allSucceeded = true;
         this.ensureEmhttpBootContext();
         const devsById = this.getDeviceMapFromEmhttpState();
@@
             if (deleteResult.exitCode !== 0) {
+                allSucceeded = false;
                 output.push(
                     `efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
                 );
             }
@@
             if (createResult.exitCode !== 0) {
+                allSucceeded = false;
                 output.push(
                     `efibootmgr failed for ${this.shellQuote(devicePath)} (rc=${createResult.exitCode})`
                 );
             }
@@
             if (flashResult.exitCode !== 0) {
+                allSucceeded = false;
                 output.push(`efibootmgr failed for flash (rc=${flashResult.exitCode})`);
             }
@@
         const orderResult = await this.runEfiBootMgr(orderArgs, output);
         if (orderResult.exitCode !== 0) {
+            allSucceeded = false;
             output.push(`efibootmgr failed to set boot order (rc=${orderResult.exitCode})`);
         }
+        return allSucceeded;
     }
@@
             if (input.updateBios) {
                 output.push('Applying BIOS boot entry updates...');
-                await this.updateBiosBootEntries(input.devices, output);
+                const biosUpdated = await this.updateBiosBootEntries(input.devices, output);
+                if (!biosUpdated) {
+                    return {
+                        ok: false,
+                        code: 1,
+                        output: output.join('\n'),
+                    };
+                }
             }

Also applies to: 325-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`
around lines 157 - 263, The method updateBiosBootEntries currently logs
efibootmgr failures to the output but always returns void, causing callers to
report success even when bios updates failed; change updateBiosBootEntries to
return a boolean (Promise<boolean>) that tracks failures (initialize let failed
= false and set failed = true whenever runEfiBootMgr returns exitCode !== 0),
and at the end return !failed (or throw an Error with joined output if you
prefer). Update all callers (the mutation handler that checks input.updateBios)
to await the new boolean and propagate failure by marking the mutation result as
not ok and appending output when the method returns false. Apply the same
pattern to the other bios-update block referenced (the similar code around the
other runEfiBootMgr calls noted in the comment) so all efibootmgr failures are
surfaced to the mutation result.

Comment on lines +162 to +174
const bootLabelMap =
existingEntries.exitCode === 0
? this.parseBootLabelMap(existingEntries.lines)
: new Map<string, string>();

for (const bootNumber of bootLabelMap.values()) {
const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
if (deleteResult.exitCode !== 0) {
output.push(
`efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Scope boot-entry deletion to Unraid-managed labels only.

This loop currently deletes every boot entry returned by efibootmgr, not just entries created by this feature. That can wipe unrelated boot targets.

Proposed fix
-        for (const bootNumber of bootLabelMap.values()) {
-            const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
+        for (const [labelText, bootNumber] of bootLabelMap.entries()) {
+            const normalized = labelText.toLowerCase();
+            const isManagedEntry =
+                normalized.includes('unraid internal boot') || normalized.includes('unraid flash');
+            if (!isManagedEntry) {
+                continue;
+            }
+            const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
             if (deleteResult.exitCode !== 0) {
                 output.push(
                     `efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
                 );
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const bootLabelMap =
existingEntries.exitCode === 0
? this.parseBootLabelMap(existingEntries.lines)
: new Map<string, string>();
for (const bootNumber of bootLabelMap.values()) {
const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
if (deleteResult.exitCode !== 0) {
output.push(
`efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
);
}
}
const bootLabelMap =
existingEntries.exitCode === 0
? this.parseBootLabelMap(existingEntries.lines)
: new Map<string, string>();
for (const [labelText, bootNumber] of bootLabelMap.entries()) {
const normalized = labelText.toLowerCase();
const isManagedEntry =
normalized.includes('unraid internal boot') || normalized.includes('unraid flash');
if (!isManagedEntry) {
continue;
}
const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
if (deleteResult.exitCode !== 0) {
output.push(
`efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`
around lines 162 - 174, The loop is deleting every efibootmgr entry; restrict
deletions to only Unraid-managed labels by checking the parsed label before
calling runEfiBootMgr: use the existing bootLabelMap from parseBootLabelMap
(which maps bootNumber→label) and iterate entries (for (const [bootNumber,
label] of bootLabelMap.entries())), add a guard that label matches the
Unraid-managed pattern (e.g., label.startsWith(UNRAID_LABEL_PREFIX) or use a new
helper isUnraidManagedLabel(label:string):boolean), and only call
this.runEfiBootMgr(['-b', bootNumber, '-B'], output) when the guard passes; add
a UNRAID_LABEL_PREFIX constant or helper method near
onboarding-internal-boot.service.ts to centralize the pattern.

@Ajit-Mehrotra Ajit-Mehrotra force-pushed the feat/onboarding-internal-boot branch from 30f5e37 to 9f917f9 Compare March 5, 2026 00:30
export class CreateInternalBootPoolInput {
@Field(() => String)
@IsString()
@Matches(/^[a-z]([a-z0-9~._-]*[a-z_-])*$/, {

Check failure

Code scanning / CodeQL

Inefficient regular expression High

This part of the regular expression may cause exponential backtracking on strings starting with 'a' and containing many repetitions of '-'.

Copilot Autofix

AI 1 day ago

In general, to fix inefficient regexes that can backtrack exponentially, remove ambiguous nested quantifiers. That usually means avoiding constructs like (A*B)* where A and B can both match the same characters, or rewriting the expression so that repetition is applied only once over a non-ambiguous character class.

Here, the pattern is:

/^[a-z]([a-z0-9~._-]*[a-z_-])*$/

Breaking it down:

  • ^[a-z] – first character must be a lowercase letter.
  • ([a-z0-9~._-]*[a-z_-])* – zero or more repetitions of:
    • [a-z0-9~._-]* – zero or more of letters, digits, ~ . _ -
    • [a-z_-] – then a single letter, underscore, or hyphen.

This construction is equivalent to:

  • Start with a letter.
  • Then have zero or more characters from [a-z0-9~._-], but the string must end with [a-z_-] (letter, underscore, or hyphen).

We can express the same language in a simpler and unambiguous way:

  1. Require all remaining characters to be from the allowed set.
  2. And require that the last character is from [a-z_-].

A straightforward rewrite:

^[a-z][a-z0-9~._-]*[a-z_-]?$

Explanation:

  • ^[a-z] – first char letter (same as before).
  • [a-z0-9~._-]* – zero or more allowed middle characters (same alphabet as before).
  • [a-z_-]? – optionally, force the final character to be a letter/underscore/hyphen.
    • If present, the string ends with [a-z_-] (matching the original requirement).
    • If absent, the name is a single letter (also allowed by the original pattern).

This has only simple, flat quantifiers (* and ?), with no nested ambiguous groups, eliminating the exponential backtracking risk while preserving the intended validation rules.

Concretely, in api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts, update the @Matches decorator on poolName (around line 283) to use the rewritten regex.

No new imports or helper methods are needed; only the regex literal changes.

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

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
--- a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
+++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
@@ -280,7 +280,7 @@
 export class CreateInternalBootPoolInput {
     @Field(() => String)
     @IsString()
-    @Matches(/^[a-z]([a-z0-9~._-]*[a-z_-])*$/, {
+    @Matches(/^[a-z][a-z0-9~._-]*[a-z_-]?$/, {
         message: 'Pool name must match Unraid naming requirements',
     })
     poolName!: string;
EOF
@@ -280,7 +280,7 @@
export class CreateInternalBootPoolInput {
@Field(() => String)
@IsString()
@Matches(/^[a-z]([a-z0-9~._-]*[a-z_-])*$/, {
@Matches(/^[a-z][a-z0-9~._-]*[a-z_-]?$/, {
message: 'Pool name must match Unraid naming requirements',
})
poolName!: string;
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (12)
web/__test__/components/Onboarding/internalBoot.test.ts (1)

95-98: ⚠️ Potential issue | 🟡 Minor

Avoid exact fallback error-string equality in this test.

This assertion is brittle to copy-only changes. Assert behavior (ok: false) and a stable substring/pattern instead.

Suggested change
-    expect(result).toEqual({
-      ok: false,
-      output: 'Internal boot setup request failed: empty API response.',
-    });
+    expect(result.ok).toBe(false);
+    expect(result.output).toContain('Internal boot setup request failed');

As per coding guidelines: "Test what the code does, not implementation details like exact error message wording."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/internalBoot.test.ts` around lines 95 -
98, The test currently asserts exact equality of the error object, which is
brittle; update the assertion to check behavior and a stable substring instead:
assert result.ok is false and that result.output contains a stable phrase (e.g.,
use a substring like "Internal boot" or "API response" via a contains/match
helper) rather than exact string equality; locate the failing assertion in
internalBoot.test.ts (the expect(result).toEqual({...}) block) and replace it
with two assertions that check result.ok === false and that result.output
includes the chosen stable substring.
web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue (1)

52-54: ⚠️ Potential issue | 🟡 Minor

Localize the new reboot CTA and dialog copy.

The reboot button label and dialog strings are still hardcoded English. Please switch these to t(...) keys so this flow remains translatable.

Also applies to: 353-389

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue` around lines
52 - 54, The Reboot CTA and dialog copy are hardcoded English; update the
computed primaryButtonText to use the translation key (e.g.,
t('onboarding.nextSteps.reboot')) instead of 'Reboot' and replace all hardcoded
dialog strings in this component's reboot confirmation dialog (the dialog title,
body/copy, confirm and cancel button labels referenced near the dialog render
block) with appropriate i18n keys such as
onboarding.nextSteps.rebootDialog.title, onboarding.nextSteps.rebootDialog.body,
onboarding.nextSteps.rebootDialog.confirm, and
onboarding.nextSteps.rebootDialog.cancel so the button and dialog become
translatable (use the existing t(...) function and the same
showRebootButton/primaryButtonText logic).
api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts (2)

162-174: ⚠️ Potential issue | 🔴 Critical

Scope boot-entry deletion to Unraid-managed labels only.

This loop deletes every boot entry returned by efibootmgr, not just entries created by this feature. This can wipe unrelated boot targets (Windows, other OS installations).

🔒 Proposed fix
-        for (const bootNumber of bootLabelMap.values()) {
-            const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
+        for (const [labelText, bootNumber] of bootLabelMap.entries()) {
+            const normalized = labelText.toLowerCase();
+            const isManagedEntry =
+                normalized.includes('unraid internal boot') || normalized.includes('unraid flash');
+            if (!isManagedEntry) {
+                continue;
+            }
+            const deleteResult = await this.runEfiBootMgr(['-b', bootNumber, '-B'], output);
             if (deleteResult.exitCode !== 0) {
                 output.push(
                     `efibootmgr failed to delete boot entry ${bootNumber} (rc=${deleteResult.exitCode})`
                 );
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`
around lines 162 - 174, The current loop deletes every boot entry in
bootLabelMap (from parseBootLabelMap) which can remove non-Unraid entries;
restrict deletion to Unraid-managed labels only by filtering bootLabelMap values
before calling runEfiBootMgr. Identify the Unraid label pattern (e.g., a known
prefix or regex) and only iterate over entries matching that pattern (use
bootLabelMap, parseBootLabelMap, bootNumber and runEfiBootMgr references),
leaving unrelated entries untouched and still logging failures for attempted
deletions.

157-263: ⚠️ Potential issue | 🟠 Major

Propagate BIOS-update failures to the mutation result.

When input.updateBios is true, efibootmgr failures are logged to output but the method still allows ok: true to be returned. This creates false-positive success for a requested operation.

🔧 Proposed fix

Change updateBiosBootEntries to return Promise<boolean> tracking success, and check the result:

-    private async updateBiosBootEntries(devices: string[], output: string[]): Promise<void> {
+    private async updateBiosBootEntries(devices: string[], output: string[]): Promise<boolean> {
+        let allSucceeded = true;
         // ... existing code, but set allSucceeded = false on each exitCode !== 0 ...
+        return allSucceeded;
     }

     // In createInternalBootPool:
             if (input.updateBios) {
                 output.push('Applying BIOS boot entry updates...');
-                await this.updateBiosBootEntries(input.devices, output);
+                const biosUpdated = await this.updateBiosBootEntries(input.devices, output);
+                if (!biosUpdated) {
+                    return {
+                        ok: false,
+                        code: 1,
+                        output: output.join('\n'),
+                    };
+                }
             }

Also applies to: 325-328

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`
around lines 157 - 263, The updateBiosBootEntries function currently returns
Promise<void> and logs efibootmgr failures to output but doesn't signal failure
to callers; change updateBiosBootEntries to return Promise<boolean> and track a
local success flag that flips to false whenever any call to runEfiBootMgr
returns exitCode !== 0 (including deletes, creates for each device, flash
create, and setting order), then return that boolean; update callers (the
mutation handler that checks input.updateBios) to call updateBiosBootEntries and
propagate a failed boolean into the mutation result (set ok=false and include
output) so BIOS-update failures are not treated as successful. Ensure references
to runEfiBootMgr, parseBootLabelMap, getFlashDeviceFromEmhttpState and
EFI_BOOT_PATH are preserved while making this change.
web/src/components/Onboarding/steps/OnboardingSummaryStep.vue (2)

1196-1236: ⚠️ Potential issue | 🟡 Minor

Localize the boot drive warning dialog strings.

The dialog title ("Confirm Drive Wipe"), description ("You've selected drives:", "Are you sure you want to continue?"), and button labels ("Cancel", "Continue") are hardcoded and should use i18n keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
1196 - 1236, Replace all hardcoded strings inside the boot drive warning Dialog
with i18n keys: use the component's i18n function (e.g. this.$t or the setup t()
from useI18n) for the title, the two descriptive lines and both button labels.
Update the Dialog block that renders when showBootDriveWarningDialog is true
(and the span labels for selectedBootDevices) to call
t('onboarding.confirmDriveWipe'), t('onboarding.selectedDrives'),
t('onboarding.confirmContinue'), t('common.cancel') and t('common.continue') (or
your project’s chosen keys), leaving handlers handleBootDriveWarningCancel and
handleBootDriveWarningConfirm unchanged. Ensure interpolation or pluralization
is applied if needed for the selected drives label.

1121-1171: ⚠️ Potential issue | 🟡 Minor

Localize the boot configuration UI strings.

The boot summary labels ("Boot Method", "Pool", "Slots", "Boot Reserved", "Update BIOS", "Devices") and values ("Storage Drive(s)", "USB/Flash Drive", "Whole drive", "Yes", "No") are hardcoded English strings. These should use the t() i18n function for consistency with the rest of the onboarding flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
1121 - 1171, Replace hardcoded boot UI strings with i18n calls: wrap all visible
labels ("Boot Method", "Pool", "Slots", "Boot Reserved", "Update BIOS",
"Devices") in t() and ensure displayed values use translations as well (e.g.,
map bootModeLabel to a translation key or compute a translatedBootMode in the
same scope and use t(translatedKey) in the template). Update any boolean/text
outputs rendered from internalBootSummary (internalBootSummary.updateBios ?
'Yes' : 'No') to use t('yes')/t('no') or a small helper like
formatYesNo(updateBios). Also ensure selectedBootDevices' device.label values
are fed through t() or resolved to translated labels where appropriate. Touch
the template around bootModeLabel, internalBootSummary, and selectedBootDevices
to call t() or use translated/computed properties.
web/src/components/Onboarding/store/onboardingDraft.ts (1)

55-55: ⚠️ Potential issue | 🟠 Major

Replace Boolean(...) coercion for persisted flags with strict boolean parsing.

Using Boolean(...) converts the string "false" to true, which can restore incorrect updateBios/initialized/skipped/succeeded flags from persisted or legacy values.

🔧 Suggested fix

Add a helper and use it for all boolean coercions:

const normalizePersistedBoolean = (value: unknown, fallback = false): boolean => {
  if (typeof value === 'boolean') return value;
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase();
    if (normalized === 'true') return true;
    if (normalized === 'false') return false;
  }
  return fallback;
};

Then replace:

-    updateBios: Boolean(candidate.updateBios),
+    updateBios: normalizePersistedBoolean(candidate.updateBios, false),

And similarly for internalBootInitialized, internalBootSkipped, and internalBootApplySucceeded in the deserializer.

Also applies to: 215-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/store/onboardingDraft.ts` at line 55, Replace
the loose Boolean(...) coercion used for persisted flags (e.g., updateBios in
onboardingDraft deserializer and the related internalBootInitialized,
internalBootSkipped, internalBootApplySucceeded flags) with a strict parser: add
a helper function (e.g., normalizePersistedBoolean(value, fallback = false))
that returns true only for boolean true or the string "true"
(case/whitespace-insensitive), false only for boolean false or the string
"false", and fallback otherwise, then call this helper where the code currently
uses Boolean(candidate.updateBios) and the other flagged fields to ensure
persisted strings like "false" are parsed correctly.
web/src/components/Onboarding/OnboardingModal.vue (1)

101-104: ⚠️ Potential issue | 🟠 Major

Fail closed when internal-boot visibility is unknown.

hideInternalBootStep only hides on explicit "no". If the query fails or returns undefined, CONFIGURE_BOOT is shown by default. For an internal/destructive flow, the safer default is hidden unless explicitly enabled.

🔧 Suggested fix
-const hideInternalBootStep = computed(() => {
-  const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer?.trim().toLowerCase();
-  return setting === 'no';
-});
+const showInternalBootStep = computed(() => {
+  const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer;
+  return typeof setting === 'string' && setting.trim().toLowerCase() === 'yes';
+});

// Then update the filter:
-      return !isPartnerBuild.value && !hideInternalBootStep.value;
+      return !isPartnerBuild.value && showInternalBootStep.value;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/OnboardingModal.vue` around lines 101 - 104,
The current computed hideInternalBootStep only hides when setting === 'no',
which shows CONFIGURE_BOOT by default when the query is missing; change the
logic so the boot step is hidden unless the server explicitly returns "yes". In
the computed hideInternalBootStep (which reads
internalBootVisibilityResult.value?.vars?.enableBootTransfer), normalize the
string (trim().toLowerCase()) and return true (hide) unless the normalized value
=== 'yes' — i.e., treat undefined/error/other values as hidden so CONFIGURE_BOOT
is not shown unless explicitly enabled.
web/src/components/Onboarding/composables/internalBoot.ts (1)

35-62: ⚠️ Potential issue | 🟠 Major

Wrap mutation call in try/catch to normalize exceptions.

If apolloClient.mutate throws (network error, GraphQL error), this function rejects instead of returning an InternalBootSubmitResult. This breaks the ok-based flow and can cause unhandled promise rejections in callers like OnboardingSummaryStep.

🔧 Suggested fix
 export const submitInternalBootCreation = async (
   selection: InternalBootSelection,
   options: SubmitInternalBootOptions = {}
 ): Promise<InternalBootSubmitResult> => {
   const apolloClient = useApolloClient().client;
+  try {
     const { data } = await apolloClient.mutate({
       mutation: CREATE_INTERNAL_BOOT_POOL_MUTATION,
       variables: {
         input: {
           poolName: selection.poolName,
           devices: selection.devices,
           bootSizeMiB: selection.bootSizeMiB,
           updateBios: selection.updateBios,
           reboot: Boolean(options.reboot),
         },
       },
       fetchPolicy: 'no-cache',
     });
 
     const result = data?.onboarding?.createInternalBootPool;
     if (!result) {
       return {
         ok: false,
         output: 'Internal boot setup request failed: empty API response.',
       };
     }
 
     return {
       ok: result.ok,
       code: result.code ?? undefined,
       output: result.output?.trim() || 'No output',
     };
+  } catch (error) {
+    return {
+      ok: false,
+      output:
+        error instanceof Error
+          ? `Internal boot setup request failed: ${error.message}`
+          : 'Internal boot setup request failed.',
+    };
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/composables/internalBoot.ts` around lines 35 -
62, The apolloClient.mutate call (using CREATE_INTERNAL_BOOT_POOL_MUTATION) can
throw and currently bubbles up; wrap the mutate call (and subsequent access of
data?.onboarding?.createInternalBootPool) in a try/catch and return a normalized
InternalBootSubmitResult on error (e.g., { ok: false, output: error message or
error.toString(), code: undefined }) instead of letting the exception propagate;
ensure callers always receive an object with ok boolean, code possibly
undefined, and output string.
api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts (1)

288-292: ⚠️ Potential issue | 🟡 Minor

Reject empty device strings at schema-validation time.

@IsString({ each: true }) accepts empty strings, so an empty device ID can pass input validation and only fail later during command execution.

🛡️ Suggested validation hardening
     `@Field`(() => [String])
     `@ArrayMinSize`(1)
     `@ArrayMaxSize`(4)
     `@IsString`({ each: true })
+    `@IsNotEmpty`({ each: true })
     devices!: string[];

Add IsNotEmpty to the imports from class-validator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts` around
lines 288 - 292, The devices array field currently uses `@IsString`({ each: true
}) which allows empty strings; update the validation by importing and adding
IsNotEmpty from class-validator and applying `@IsNotEmpty`({ each: true }) (or
`@IsNotEmpty` on the items) alongside `@IsString` on the devices property in the
onboarding.model.ts so empty device IDs are rejected at schema-validation time.
web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue (1)

472-476: ⚠️ Potential issue | 🟠 Major

Potential ReDoS vulnerability in pool name regex.

Same nested-quantifier pattern as in the backend model. CodeQL flagged this as inefficient with exponential backtracking risk.

🔒 Suggested safer pattern
-  const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
+  // Validates: starts with lowercase letter, contains allowed chars, doesn't end with digit
+  const poolNamePattern = /^[a-z][a-z0-9~._-]*[a-z_-]$|^[a-z]$/;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue` around
lines 472 - 476, The regex assigned to poolNamePattern has a nested-quantifier
that can cause ReDoS; replace it with a safer equivalent without nested
quantifiers. Update the validation to use a non-backtracking-safe pattern such
as one that explicitly allows either a single letter or a letter followed by
middle chars and a non-digit trailing char (e.g., replace poolNamePattern with a
pattern like /^(?:[a-z]|[a-z][a-z0-9~._-]*[a-z_-])$/), keep using
normalizedPoolName.test(...) and set formError.value the same way if it fails.
api/generated-schema.graphql (1)

964-964: ⚠️ Potential issue | 🟠 Major

hasPartnerLogo nullability change is a breaking API contract.

Changing BrandingConfig.hasPartnerLogo from non-nullable to nullable can break downstream strict GraphQL-generated types. If nullability is required, introduce a new nullable field and deprecate the old one first.

🛠️ Backward-compatible direct fix (if nullability is not required)
-  hasPartnerLogo: Boolean
+  hasPartnerLogo: Boolean!
#!/bin/bash
set -euo pipefail

# 1) Confirm current schema nullability
rg -n 'hasPartnerLogo:\s*Boolean!?$' api/generated-schema.graphql -n -C2

# 2) Find client query/usage surface for this field
rg -n 'hasPartnerLogo' web/src -g '*.ts' -g '*.vue' -g '*.graphql' -C2

# 3) Check whether callers already defensively handle nullable values
rg -nP 'hasPartnerLogo\s*(\?\?|\|\||&&|===|!==)' web/src -g '*.ts' -g '*.vue' -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/generated-schema.graphql` at line 964, The schema change made
hasPartnerLogo nullable and that breaks the public API; revert or make a
backward-compatible change: restore BrandingConfig.hasPartnerLogo to its
original non-nullable type (hasPartnerLogo: Boolean!) OR instead add a new
nullable field (e.g., hasPartnerLogoNullable: Boolean) and mark the existing
hasPartnerLogo as deprecated with a `@deprecated`(reason: "...") so clients are
not broken; update generated-schema.graphql accordingly and run the grep checks
mentioned in the review to find and update any callers if you choose to rename
the field.
🧹 Nitpick comments (3)
web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue (1)

74-76: Consider extracting shared constants.

DEFAULT_BOOT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB are duplicated in OnboardingSummaryStep.vue. Consider extracting to a shared constants file if these values need to stay in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue` around
lines 74 - 76, DEFAULT_BOOT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB are duplicated
across components; extract them into a single shared constants module (e.g.,
export named constants BOOT_DEFAULT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB from a
new file) and update both OnboardingInternalBootStep (references to
DEFAULT_BOOT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB) and OnboardingSummaryStep to
import those named constants instead of declaring them locally so the values
remain centralized and in sync.
web/src/components/Onboarding/steps/OnboardingSummaryStep.vue (1)

151-195: Consider extracting shared formatting utilities.

formatBytes, normalizeDeviceName, and deriveDeviceSizeBytes are duplicated from OnboardingInternalBootStep.vue. Consider extracting to a shared utility file to reduce duplication and ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
151 - 195, Duplicate formatting helpers (formatBytes, normalizeDeviceName,
deriveDeviceSizeBytes) should be moved into a shared utility module: create a
new file exporting typed functions formatBytes, normalizeDeviceName, and
deriveDeviceSizeBytes, replace the local implementations in
OnboardingSummaryStep.vue and OnboardingInternalBootStep.vue with imports from
that module, update import paths and remove the duplicate functions from both
components, and ensure exported types/signatures match the existing usages so
callers compile without changes.
web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts (1)

1067-1087: Avoid coupling this case to exact backend error copy.

This assertion is brittle to wording-only changes. Prefer asserting warning classification and a stable prefix/behavior instead of the full interpolated message.

♻️ Suggested test hardening
-    expect(wrapper.text()).toContain('Internal boot setup returned an error: mkbootpool failed');
+    expect(wrapper.text()).toContain('Internal boot setup returned an error');
     expect(wrapper.text()).toContain('Setup Applied with Warnings');

As per coding guidelines: "Test what the code does, not implementation details like exact error message wording."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts` around
lines 1067 - 1087, The test currently asserts the exact backend error string
from submitInternalBootCreationMock which is brittle; update the assertion to
check only the stable warning classification or prefix instead of the full
interpolated error. Specifically, in the test using mountComponent(),
clickApply(), submitInternalBootCreationMock and
setInternalBootApplySucceededMock, replace
expect(wrapper.text()).toContain('Internal boot setup returned an error:
mkbootpool failed') with a check for the stable prefix or semantic indicator
(e.g., expect(wrapper.text()).toContain('Internal boot setup returned an error')
or asserting the presence of the "Setup Applied with Warnings" text or a warning
CSS/ARIA selector), so the test no longer depends on the exact backend message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/unraid-api/graph/resolvers/disks/disks.service.ts`:
- Around line 57-63: The method getEmhttpDevices assumes store.emhttp.devices is
an array and iterates rawDevices directly; guard the config shape by checking
Array.isArray(rawDevices) (or otherwise validate its type) before the for-loop
and treat non-array values as an empty list so the loop cannot throw; update
getEmhttpDevices to only iterate when rawDevices is an array and
document/convert invalid shapes to [] so EmhttpDevice processing remains safe.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts`:
- Around line 283-286: The `@Matches` regex on the poolName property (in
onboarding.model.ts) uses nested quantifiers causing a potential ReDoS; replace
the current pattern used in the Matches decorator on poolName with a flattened,
non-nested equivalent that enforces the same rules (starts with a lowercase
letter, contains only allowed characters a-z0-9~._- and does not allow trailing
digits, ensuring the last character is a letter, underscore or hyphen) and
update any related unit/validation tests to validate the same positive and
negative examples referenced by the original logic; keep the decorator and
property name (poolName) unchanged so callers remain compatible.

---

Duplicate comments:
In `@api/generated-schema.graphql`:
- Line 964: The schema change made hasPartnerLogo nullable and that breaks the
public API; revert or make a backward-compatible change: restore
BrandingConfig.hasPartnerLogo to its original non-nullable type (hasPartnerLogo:
Boolean!) OR instead add a new nullable field (e.g., hasPartnerLogoNullable:
Boolean) and mark the existing hasPartnerLogo as deprecated with a
`@deprecated`(reason: "...") so clients are not broken; update
generated-schema.graphql accordingly and run the grep checks mentioned in the
review to find and update any callers if you choose to rename the field.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`:
- Around line 162-174: The current loop deletes every boot entry in bootLabelMap
(from parseBootLabelMap) which can remove non-Unraid entries; restrict deletion
to Unraid-managed labels only by filtering bootLabelMap values before calling
runEfiBootMgr. Identify the Unraid label pattern (e.g., a known prefix or regex)
and only iterate over entries matching that pattern (use bootLabelMap,
parseBootLabelMap, bootNumber and runEfiBootMgr references), leaving unrelated
entries untouched and still logging failures for attempted deletions.
- Around line 157-263: The updateBiosBootEntries function currently returns
Promise<void> and logs efibootmgr failures to output but doesn't signal failure
to callers; change updateBiosBootEntries to return Promise<boolean> and track a
local success flag that flips to false whenever any call to runEfiBootMgr
returns exitCode !== 0 (including deletes, creates for each device, flash
create, and setting order), then return that boolean; update callers (the
mutation handler that checks input.updateBios) to call updateBiosBootEntries and
propagate a failed boolean into the mutation result (set ok=false and include
output) so BIOS-update failures are not treated as successful. Ensure references
to runEfiBootMgr, parseBootLabelMap, getFlashDeviceFromEmhttpState and
EFI_BOOT_PATH are preserved while making this change.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts`:
- Around line 288-292: The devices array field currently uses `@IsString`({ each:
true }) which allows empty strings; update the validation by importing and
adding IsNotEmpty from class-validator and applying `@IsNotEmpty`({ each: true })
(or `@IsNotEmpty` on the items) alongside `@IsString` on the devices property in the
onboarding.model.ts so empty device IDs are rejected at schema-validation time.

In `@web/__test__/components/Onboarding/internalBoot.test.ts`:
- Around line 95-98: The test currently asserts exact equality of the error
object, which is brittle; update the assertion to check behavior and a stable
substring instead: assert result.ok is false and that result.output contains a
stable phrase (e.g., use a substring like "Internal boot" or "API response" via
a contains/match helper) rather than exact string equality; locate the failing
assertion in internalBoot.test.ts (the expect(result).toEqual({...}) block) and
replace it with two assertions that check result.ok === false and that
result.output includes the chosen stable substring.

In `@web/src/components/Onboarding/composables/internalBoot.ts`:
- Around line 35-62: The apolloClient.mutate call (using
CREATE_INTERNAL_BOOT_POOL_MUTATION) can throw and currently bubbles up; wrap the
mutate call (and subsequent access of data?.onboarding?.createInternalBootPool)
in a try/catch and return a normalized InternalBootSubmitResult on error (e.g.,
{ ok: false, output: error message or error.toString(), code: undefined })
instead of letting the exception propagate; ensure callers always receive an
object with ok boolean, code possibly undefined, and output string.

In `@web/src/components/Onboarding/OnboardingModal.vue`:
- Around line 101-104: The current computed hideInternalBootStep only hides when
setting === 'no', which shows CONFIGURE_BOOT by default when the query is
missing; change the logic so the boot step is hidden unless the server
explicitly returns "yes". In the computed hideInternalBootStep (which reads
internalBootVisibilityResult.value?.vars?.enableBootTransfer), normalize the
string (trim().toLowerCase()) and return true (hide) unless the normalized value
=== 'yes' — i.e., treat undefined/error/other values as hidden so CONFIGURE_BOOT
is not shown unless explicitly enabled.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue`:
- Around line 472-476: The regex assigned to poolNamePattern has a
nested-quantifier that can cause ReDoS; replace it with a safer equivalent
without nested quantifiers. Update the validation to use a non-backtracking-safe
pattern such as one that explicitly allows either a single letter or a letter
followed by middle chars and a non-digit trailing char (e.g., replace
poolNamePattern with a pattern like /^(?:[a-z]|[a-z][a-z0-9~._-]*[a-z_-])$/),
keep using normalizedPoolName.test(...) and set formError.value the same way if
it fails.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue`:
- Around line 52-54: The Reboot CTA and dialog copy are hardcoded English;
update the computed primaryButtonText to use the translation key (e.g.,
t('onboarding.nextSteps.reboot')) instead of 'Reboot' and replace all hardcoded
dialog strings in this component's reboot confirmation dialog (the dialog title,
body/copy, confirm and cancel button labels referenced near the dialog render
block) with appropriate i18n keys such as
onboarding.nextSteps.rebootDialog.title, onboarding.nextSteps.rebootDialog.body,
onboarding.nextSteps.rebootDialog.confirm, and
onboarding.nextSteps.rebootDialog.cancel so the button and dialog become
translatable (use the existing t(...) function and the same
showRebootButton/primaryButtonText logic).

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 1196-1236: Replace all hardcoded strings inside the boot drive
warning Dialog with i18n keys: use the component's i18n function (e.g. this.$t
or the setup t() from useI18n) for the title, the two descriptive lines and both
button labels. Update the Dialog block that renders when
showBootDriveWarningDialog is true (and the span labels for selectedBootDevices)
to call t('onboarding.confirmDriveWipe'), t('onboarding.selectedDrives'),
t('onboarding.confirmContinue'), t('common.cancel') and t('common.continue') (or
your project’s chosen keys), leaving handlers handleBootDriveWarningCancel and
handleBootDriveWarningConfirm unchanged. Ensure interpolation or pluralization
is applied if needed for the selected drives label.
- Around line 1121-1171: Replace hardcoded boot UI strings with i18n calls: wrap
all visible labels ("Boot Method", "Pool", "Slots", "Boot Reserved", "Update
BIOS", "Devices") in t() and ensure displayed values use translations as well
(e.g., map bootModeLabel to a translation key or compute a translatedBootMode in
the same scope and use t(translatedKey) in the template). Update any
boolean/text outputs rendered from internalBootSummary
(internalBootSummary.updateBios ? 'Yes' : 'No') to use t('yes')/t('no') or a
small helper like formatYesNo(updateBios). Also ensure selectedBootDevices'
device.label values are fed through t() or resolved to translated labels where
appropriate. Touch the template around bootModeLabel, internalBootSummary, and
selectedBootDevices to call t() or use translated/computed properties.

In `@web/src/components/Onboarding/store/onboardingDraft.ts`:
- Line 55: Replace the loose Boolean(...) coercion used for persisted flags
(e.g., updateBios in onboardingDraft deserializer and the related
internalBootInitialized, internalBootSkipped, internalBootApplySucceeded flags)
with a strict parser: add a helper function (e.g.,
normalizePersistedBoolean(value, fallback = false)) that returns true only for
boolean true or the string "true" (case/whitespace-insensitive), false only for
boolean false or the string "false", and fallback otherwise, then call this
helper where the code currently uses Boolean(candidate.updateBios) and the other
flagged fields to ensure persisted strings like "false" are parsed correctly.

---

Nitpick comments:
In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts`:
- Around line 1067-1087: The test currently asserts the exact backend error
string from submitInternalBootCreationMock which is brittle; update the
assertion to check only the stable warning classification or prefix instead of
the full interpolated error. Specifically, in the test using mountComponent(),
clickApply(), submitInternalBootCreationMock and
setInternalBootApplySucceededMock, replace
expect(wrapper.text()).toContain('Internal boot setup returned an error:
mkbootpool failed') with a check for the stable prefix or semantic indicator
(e.g., expect(wrapper.text()).toContain('Internal boot setup returned an error')
or asserting the presence of the "Setup Applied with Warnings" text or a warning
CSS/ARIA selector), so the test no longer depends on the exact backend message.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue`:
- Around line 74-76: DEFAULT_BOOT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB are
duplicated across components; extract them into a single shared constants module
(e.g., export named constants BOOT_DEFAULT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB
from a new file) and update both OnboardingInternalBootStep (references to
DEFAULT_BOOT_SIZE_MIB and BOOT_SIZE_PRESETS_MIB) and OnboardingSummaryStep to
import those named constants instead of declaring them locally so the values
remain centralized and in sync.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 151-195: Duplicate formatting helpers (formatBytes,
normalizeDeviceName, deriveDeviceSizeBytes) should be moved into a shared
utility module: create a new file exporting typed functions formatBytes,
normalizeDeviceName, and deriveDeviceSizeBytes, replace the local
implementations in OnboardingSummaryStep.vue and OnboardingInternalBootStep.vue
with imports from that module, update import paths and remove the duplicate
functions from both components, and ensure exported types/signatures match the
existing usages so callers compile without changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a4e6bc52-7c1e-4b2c-8cca-1c9771c13a53

📥 Commits

Reviewing files that changed from the base of the PR and between 30f5e37 and 9f917f9.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (35)
  • api/generated-schema.graphql
  • api/src/__test__/store/modules/emhttp.test.ts
  • api/src/__test__/store/state-parsers/var.test.ts
  • api/src/core/types/states/var.ts
  • api/src/store/state-parsers/var.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.model.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/vars/vars.model.ts
  • docs/onboarding-internal-boot-port-differences.md
  • web/__test__/components/Onboarding/OnboardingModal.test.ts
  • web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
  • web/__test__/components/Onboarding/internalBoot.test.ts
  • web/components.d.ts
  • web/src/components/Onboarding/OnboardingModal.vue
  • web/src/components/Onboarding/OnboardingSteps.vue
  • web/src/components/Onboarding/composables/internalBoot.ts
  • web/src/components/Onboarding/graphql/createInternalBootPool.mutation.ts
  • web/src/components/Onboarding/graphql/getInternalBootContext.query.ts
  • web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts
  • web/src/components/Onboarding/stepRegistry.ts
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
  • web/src/components/Onboarding/store/onboardingDraft.ts
  • web/src/composables/gql/gql.ts
  • web/src/composables/gql/graphql.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • web/src/components/Onboarding/graphql/createInternalBootPool.mutation.ts
  • api/src/unraid-api/graph/resolvers/vars/vars.model.ts
  • api/src/unraid-api/graph/resolvers/disks/disks.model.ts
  • api/src/core/types/states/var.ts
  • web/test/components/Onboarding/OnboardingModal.test.ts
  • web/src/components/Onboarding/stepRegistry.ts
  • api/src/test/store/modules/emhttp.test.ts
  • web/components.d.ts
  • web/test/components/Onboarding/OnboardingNextStepsStep.test.ts
  • web/src/components/Onboarding/graphql/getInternalBootContext.query.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
  • web/src/components/Onboarding/OnboardingSteps.vue
  • api/src/store/state-parsers/var.ts

Comment on lines +57 to +63
private getEmhttpDevices(): EmhttpDevice[] {
const rawDevices = this.configService.get<unknown[]>('store.emhttp.devices', []);
const emhttpDevices: EmhttpDevice[] = [];

for (const raw of rawDevices) {
if (!raw || typeof raw !== 'object') {
continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard config shape before iterating devices.

store.emhttp.devices is assumed iterable. If config parsing returns a non-array object, this loop will throw at runtime.

Suggested fix
-    private getEmhttpDevices(): EmhttpDevice[] {
-        const rawDevices = this.configService.get<unknown[]>('store.emhttp.devices', []);
+    private getEmhttpDevices(): EmhttpDevice[] {
+        const rawDevicesValue = this.configService.get<unknown>('store.emhttp.devices', []);
+        const rawDevices = Array.isArray(rawDevicesValue) ? rawDevicesValue : [];
         const emhttpDevices: EmhttpDevice[] = [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/disks/disks.service.ts` around lines 57 -
63, The method getEmhttpDevices assumes store.emhttp.devices is an array and
iterates rawDevices directly; guard the config shape by checking
Array.isArray(rawDevices) (or otherwise validate its type) before the for-loop
and treat non-array values as an empty list so the loop cannot throw; update
getEmhttpDevices to only iterate when rawDevices is an array and
document/convert invalid shapes to [] so EmhttpDevice processing remains safe.

Comment on lines +283 to +286
@Matches(/^[a-z]([a-z0-9~._-]*[a-z_-])*$/, {
message: 'Pool name must match Unraid naming requirements',
})
poolName!: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential ReDoS vulnerability in pool name regex.

The pattern ([a-z0-9~._-]*[a-z_-])* contains nested quantifiers that can cause exponential backtracking on adversarial inputs like "a" + "-".repeat(30). CodeQL flagged this as inefficient.

🔒 Suggested safer pattern
-    `@Matches`(/^[a-z]([a-z0-9~._-]*[a-z_-])*$/, {
+    `@Matches`(/^[a-z][a-z0-9~._-]*[a-z_-]$|^[a-z]$/, {
         message: 'Pool name must match Unraid naming requirements',
     })

This flattened pattern validates the same constraint (starts with lowercase letter, ends with letter or underscore/hyphen, no trailing digit) without nested quantifiers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Matches(/^[a-z]([a-z0-9~._-]*[a-z_-])*$/, {
message: 'Pool name must match Unraid naming requirements',
})
poolName!: string;
`@Matches`(/^[a-z][a-z0-9~._-]*[a-z_-]$|^[a-z]$/, {
message: 'Pool name must match Unraid naming requirements',
})
poolName!: string;
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 283-283: Inefficient regular expression
This part of the regular expression may cause exponential backtracking on strings starting with 'a' and containing many repetitions of '-'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts` around
lines 283 - 286, The `@Matches` regex on the poolName property (in
onboarding.model.ts) uses nested quantifiers causing a potential ReDoS; replace
the current pattern used in the Matches decorator on poolName with a flattened,
non-nested equivalent that enforces the same rules (starts with a lowercase
letter, contains only allowed characters a-z0-9~._- and does not allow trailing
digits, ensuring the last character is a letter, underscore or hyphen) and
update any related unit/validation tests to validate the same positive and
negative examples referenced by the original logic; keep the decorator and
property name (poolName) unchanged so callers remain compatible.

- Purpose: introduce a dedicated onboarding step for internal boot setup using existing webgui behavior and endpoints.

- Before: onboarding had no internal boot step, so users had to leave onboarding and configure it elsewhere.

- Problem: internal boot setup was disconnected from first-run setup and could not be intentionally skipped/recorded in onboarding state.

- Change: added an INTERNAL_BOOT step after core settings, hidden for partner builds, with skip support and persisted draft state.

- Implementation: added internal boot template parser/submission helper, new step UI with validation (slots/devices/size/update BIOS), and modal/step registry wiring.
- Purpose: execute internal boot configuration during onboarding confirm/apply and surface reboot follow-up.

- Before: summary only applied core settings/plugins/ssh; internal boot selections were not applied.

- Problem: users could choose internal boot but onboarding would not run mkbootpool or reflect reboot needs.

- Change: summary now calls mkbootpool endpoint without reboot, logs command output, and treats failures as warnings in best-effort flow.

- Implementation: added internal boot summary card, included internal boot in apply-change detection, and switched final CTA to reboot when internal boot apply succeeds.
- Purpose: validate onboarding step routing and summary apply behavior after internal boot integration.

- Before: tests assumed old step indexes and had no assertions for internal boot visibility or mkbootpool execution.

- Problem: regressions in partner-step visibility and internal boot apply flow would be hard to detect.

- Change: updated modal tests for INTERNAL_BOOT presence/hiding and adjusted activation-step index expectations.

- Implementation: extended summary tests to mock internal boot submission, verify no-reboot payload, success flag behavior, and warning path on failures.
- Purpose: resolve remaining type-check/test/lint failures after internal-boot onboarding integration.

- Before: web type-check failed on an unused loop variable, onboarding modal test used an outdated step index, and prettier check failed on touched onboarding files.

- Problem: CI gates for type-check/test/lint were not green for the onboarding changes.

- Change: adjusted internal-boot device loop indexing, corrected activation-step test index for ENOKEYFILE1, and applied prettier formatting updates.

- Implementation: updated onboarding step/template/test files and refreshed generated component typings for new onboarding step registration.
- Purpose: use existing GraphQL array state in onboarding to decide whether internal boot setup can run.
- Before: onboarding inferred array stoppage from  template presence, which could misreport when the page response was unexpected (redirect/empty shell).
- Problem: users could see the 'array must be stopped' message even when  because the HTML parse path failed independently of true array state.
- Change: added  query and wired  to require  for configurability.
- Behavior now: array-state gating comes from GraphQL; legacy  parsing is retained only for webgui-specific internal-boot data (eligibility button/device options/presets/defaults).
- UX: status messaging now distinguishes array-not-stopped, array-state lookup failure, and inability to load webgui internal-boot template data.
- How it works:  drives , while template fetch/parse still initializes boot-pool form options.
- Validation: ran
> unraid-monorepo@4.29.2 codegen /Users/ajitmehrotra/Projects/onboarding-internal-boot
> pnpm -r codegen

Scope: 8 of 9 workspace projects
web codegen$ graphql-codegen --config codegen.ts -r dotenv/config
web codegen: ❯ Parse Configuration
web codegen: ✔ Parse Configuration
web codegen: ❯ Generate outputs
web codegen: ❯ Generate to src/composables/gql/
web codegen: ❯ Load GraphQL schemas
web codegen: ✔ Load GraphQL schemas
web codegen: ❯ Load GraphQL documents
web codegen: ✔ Load GraphQL documents
web codegen: ❯ Generate
web codegen: ✔ Generate
web codegen: ✔ Generate to src/composables/gql/
web codegen: ✔ Generate outputs
web codegen: Done
packages/unraid-api-plugin-connect codegen$ MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.ts
packages/unraid-api-plugin-connect codegen: [STARTED] Parse Configuration
packages/unraid-api-plugin-connect codegen: [COMPLETED] Parse Configuration
packages/unraid-api-plugin-connect codegen: [STARTED] Generate outputs
packages/unraid-api-plugin-connect codegen: [STARTED] Generate to src/graphql/generated/client/
packages/unraid-api-plugin-connect codegen: [STARTED] Load GraphQL schemas
packages/unraid-api-plugin-connect codegen: [COMPLETED] Load GraphQL schemas
packages/unraid-api-plugin-connect codegen: [STARTED] Load GraphQL documents
packages/unraid-api-plugin-connect codegen: [COMPLETED] Load GraphQL documents
packages/unraid-api-plugin-connect codegen: [STARTED] Generate
packages/unraid-api-plugin-connect codegen: [COMPLETED] Generate
packages/unraid-api-plugin-connect codegen: [COMPLETED] Generate to src/graphql/generated/client/
packages/unraid-api-plugin-connect codegen: [COMPLETED] Generate outputs
packages/unraid-api-plugin-connect codegen: Done
api codegen$ graphql-codegen --config codegen.ts
api codegen: [STARTED] Parse Configuration
api codegen: [COMPLETED] Parse Configuration
api codegen: [STARTED] Generate outputs
api codegen: [STARTED] Generate to src/unraid-api/cli/generated/
api codegen: [STARTED] Load GraphQL schemas
api codegen: [COMPLETED] Load GraphQL schemas
api codegen: [STARTED] Load GraphQL documents
api codegen: [COMPLETED] Load GraphQL documents
api codegen: [STARTED] Generate
api codegen: [COMPLETED] Generate
api codegen: [COMPLETED] Generate to src/unraid-api/cli/generated/
api codegen: [COMPLETED] Generate outputs
api codegen: Done,
> unraid-monorepo@4.29.2 type-check /Users/ajitmehrotra/Projects/onboarding-internal-boot
> pnpm -r type-check

Scope: 8 of 9 workspace projects
unraid-ui type-check$ vue-tsc --noEmit
web type-check$ vue-tsc --noEmit
unraid-ui type-check: Done
web type-check: Done
api type-check$ tsc --noEmit
api type-check: Done,
> unraid-monorepo@4.29.2 test /Users/ajitmehrotra/Projects/onboarding-internal-boot
> pnpm -r test

Scope: 8 of 9 workspace projects
packages/unraid-api-plugin-health test$ echo "Error: no test specified" && exit 0
packages/unraid-shared test$ vitest run
plugin test$ vitest && pnpm run test:extractor && pnpm run test:shell-detection
unraid-ui test$ vitest
packages/unraid-api-plugin-health test: Error: no test specified
packages/unraid-api-plugin-health test: Done
web test$ vitest run
unraid-ui test:  RUN  v3.2.4 /Users/ajitmehrotra/Projects/onboarding-internal-boot/unraid-ui
packages/unraid-shared test:  RUN  v3.2.4 /Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared
web test:  RUN  v3.2.4 /Users/ajitmehrotra/Projects/onboarding-internal-boot/web
plugin test:  RUN  v3.2.4 /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test
packages/unraid-shared test: Server listening on Unix socket: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/test-ws-unix-1772218335390.sock
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect to Unix socket using ws+unix:// protocol
packages/unraid-shared test: Connecting to: ws+unix:///var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/test-ws-unix-1772218335390.sock:/
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect to Unix socket using ws+unix:// protocol
packages/unraid-shared test: Server: New WebSocket connection on path: /
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect to Unix socket using ws+unix:// protocol
packages/unraid-shared test: Client: Connected successfully!
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect to Unix socket using ws+unix:// protocol
packages/unraid-shared test: Server received: Test message
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect with /graphql path like SocketConfigService
packages/unraid-shared test: Testing SocketConfigService format: ws+unix:///var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/test-ws-unix-1772218335390.sock:/graphql
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect with /graphql path like SocketConfigService
packages/unraid-shared test: Server: New WebSocket connection on path: /graphql
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should connect with /graphql path like SocketConfigService
packages/unraid-shared test: Client: Connected to /graphql path
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Server: Client disconnected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Server: New WebSocket connection on path: /client-0
packages/unraid-shared test:  ✓ src/util/permissions.spec.ts (21 tests) 9ms
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Client 0 connected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Server: Client disconnected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Server: New WebSocket connection on path: /client-1
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Client 1 connected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Server: New WebSocket connection on path: /client-2
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should work with multiple concurrent connections
packages/unraid-shared test: Client 2 connected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should verify the exact implementation used in BaseInternalClientService
packages/unraid-shared test: Server: Client disconnected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should verify the exact implementation used in BaseInternalClientService
packages/unraid-shared test: Server: Client disconnected
packages/unraid-shared test: stdout | src/services/ws-unix-socket-test.spec.ts > WebSocket Unix Socket - Actual Connection Test > should verify the exact implementation used in BaseInternalClientService
packages/unraid-shared test: Server: Client disconnected
packages/unraid-shared test:  ✓ src/util/__tests__/key-order.test.ts (2 tests) 79ms
packages/unraid-shared test:  ✓ src/util/__tests__/permissions.test.ts (25 tests) 13ms
packages/unraid-shared test:  ✓ src/util/__tests__/processing.test.ts (18 tests) 231ms
packages/unraid-shared test:  ✓ src/services/ws-unix-socket-test.spec.ts (5 tests) 465ms
packages/unraid-shared test:    ✓ WebSocket Unix Socket - Actual Connection Test > should verify the exact implementation used in BaseInternalClientService  336ms
packages/unraid-shared test: stderr | src/util/permissions-scopes.spec.ts > convertScopesToPermissions > should handle invalid scope formats gracefully
packages/unraid-shared test: Invalid scope format (missing colon): INVALID_SCOPE
packages/unraid-shared test: Invalid scope format: :action
packages/unraid-shared test: Invalid scope format: RESOURCE:
packages/unraid-shared test: Unknown resource in scope: UNKNOWN:read:any
packages/unraid-shared test:  ✓ src/util/permissions-scopes.spec.ts (5 tests) 12ms
unraid-ui test:  ✓ src/lib/utils.test.ts (8 tests) 8ms
packages/unraid-shared test:  ✓ src/util/__tests__/create-ttl-memoized-loader.test.ts (4 tests) 2ms
packages/unraid-shared test:  ✓ src/services/internal-graphql-client-usage.spec.ts (17 tests) 22ms
packages/unraid-shared test:  ✓ src/use-permissions.directive.spec.ts (24 tests) 4ms
plugin test:  ✓ builder/utils/manifest-validator.test.ts (16 tests) 144ms
plugin test: stderr | builder/__tests__/cli/setup-txz-environment.spec.ts > setupTxzEnvironment > should parse and return provided environment arguments
plugin test: skipValidation is true, skipping validation
plugin test:  ✓ builder/__tests__/cli/setup-txz-environment.spec.ts (4 tests) 5ms
packages/unraid-shared test:  ✓ src/util/__tests__/config-definition.test.ts (7 tests) 9ms
unraid-ui test: stderr | src/composables/useTeleport.test.ts > useTeleport > should return teleportTarget ref with correct value
unraid-ui test: [Vue warn]: onMounted is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
unraid-ui test:  ✓ src/composables/useTeleport.test.ts (6 tests) 127ms
packages/unraid-shared test:  ✓ src/services/socket-config.service.spec.ts (31 tests) 7ms
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > validatePluginEnv > validates required fields
plugin test: Reading txz file from: ./test.txz
plugin test: Mock readFile called with: ./test.txz undefined
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > validatePluginEnv > handles tag option in non-CI mode
plugin test: Reading txz file from: ./test.txz
plugin test: Mock readFile called with: ./test.txz undefined
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > validatePluginEnv > reads release notes when release-notes-path is provided
plugin test: Mock readFile called with: valid-release-notes.txt utf8
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > validatePluginEnv > reads release notes when release-notes-path is provided
plugin test: Reading txz file from: ./test.txz
plugin test: Mock readFile called with: ./test.txz undefined
plugin test: stderr | builder/__tests__/cli/setup-plugin-environment.spec.ts > validatePluginEnv > handles tag option in non-CI mode
plugin test: Tag is set, will generate a TAGGED build
plugin test: stdout | builder/utils/changelog.test.ts > getStagingChangelogFromGit > should generate changelog header with version
plugin test: Non-fatal error: Failed to get changelog from git: Error: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test:     at catchProcessError (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:42:13)
plugin test:     at processTicksAndRejections (node:internal/process/task_queues:103:5)
plugin test:     at outputStream (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:83:17)
plugin test:     at ConventionalGitClient.getTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/GitClient.ts:152:16)
plugin test:     at ConventionalGitClient.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/ConventionalGitClient.ts:100:16)
plugin test:     at ConventionalChangelog.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:240:22)
plugin test:     at ConventionalChangelog.write (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:591:24)
plugin test:     at getStagingChangelogFromGit (/Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.ts:154:22)
plugin test:     at /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.test.ts:18:20
plugin test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:20
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > sets up environment from CLI arguments
plugin test: Plugin version: 2026.02.27.1852
plugin test: Options: {
plugin test:   ci: false,
plugin test:   apiVersion: '4.29.2\n' +
plugin test:     '+fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git\n',
plugin test:   baseUrl: 'https://example.com',
plugin test:   pluginVersion: '2024.05.05.1232',
plugin test:   txzPath: './test.txz'
plugin test: }
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > sets up environment from CLI arguments
plugin test: Reading txz file from: ./test.txz
plugin test: Mock readFile called with: ./test.txz undefined
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > sets up environment from CLI arguments
plugin test: Plugin environment setup successfully: {
plugin test:   ci: false,
plugin test:   apiVersion: '4.29.2\n' +
plugin test:     '+fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git\n',
plugin test:   baseUrl: 'https://example.com',
plugin test:   tag: '',
plugin test:   buildNumber: 1,
plugin test:   txzPath: './test.txz',
plugin test:   pluginVersion: '2024.05.05.1232',
plugin test:   releaseNotes: 'FAST_TEST_CHANGELOG',
plugin test:   txzSha256: '6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72'
plugin test: }
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: stdout | builder/utils/changelog.test.ts > getStagingChangelogFromGit > should generate changelog with tag parameter
plugin test: Non-fatal error: Failed to get changelog from git: Error: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test:     at catchProcessError (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:42:13)
plugin test:     at outputStream (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:83:17)
plugin test:     at ConventionalGitClient.getTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/GitClient.ts:152:16)
plugin test:     at ConventionalGitClient.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/ConventionalGitClient.ts:100:16)
plugin test:     at ConventionalChangelog.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:240:22)
plugin test:     at ConventionalChangelog.write (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:591:24)
plugin test:     at getStagingChangelogFromGit (/Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.ts:154:22)
plugin test:     at /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.test.ts:33:20
plugin test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:20
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > throws when required options are missing
plugin test: Plugin version: 2026.02.27.1852
plugin test: Options: {
plugin test:   ci: false,
plugin test:   apiVersion: '4.29.2\n' +
plugin test:     '+fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git\n',
plugin test:   baseUrl: 'http://localhost:5858',
plugin test:   pluginVersion: '2026.02.27.1852'
plugin test: }
plugin test: TXZ path not found at: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/deploy/dynamix.unraid.net-4.29.2
plugin test: +fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: -x86_64-1.txz
plugin test: Attempting to find TXZ using apiVersion: 4.29.2
plugin test: +fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: Could not find TXZ at: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/deploy/dynamix.unraid.net-4.29.2
plugin test: +fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: -x86_64-1.txz
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > handles optional CLI arguments
plugin test: Plugin version: 2026.02.27.1852
plugin test: Options: {
plugin test:   ci: true,
plugin test:   apiVersion: '4.29.2\n' +
plugin test:     '+fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git\n',
plugin test:   baseUrl: 'https://example.com',
plugin test:   pluginVersion: '2024.05.05.1232',
plugin test:   txzPath: './test.txz',
plugin test:   tag: 'PR1203'
plugin test: }
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > handles optional CLI arguments
plugin test: Reading txz file from: ./test.txz
plugin test: Mock readFile called with: ./test.txz undefined
plugin test: stdout | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > handles optional CLI arguments
plugin test: Plugin environment setup successfully: {
plugin test:   ci: true,
plugin test:   apiVersion: '4.29.2\n' +
plugin test:     '+fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git\n',
plugin test:   baseUrl: 'https://example.com',
plugin test:   tag: 'PR1203',
plugin test:   buildNumber: 1,
plugin test:   txzPath: './test.txz',
plugin test:   pluginVersion: '2024.05.05.1232',
plugin test:   releaseNotes: 'FAST_TEST_CHANGELOG',
plugin test:   txzSha256: '6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72'
plugin test: }
plugin test: stderr | builder/__tests__/cli/setup-plugin-environment.spec.ts > setupPluginEnv > handles optional CLI arguments
plugin test: Tag is set, will generate a TAGGED build
plugin test:  ✓ builder/__tests__/cli/setup-plugin-environment.spec.ts (8 tests) 277ms
plugin test: stdout | builder/utils/changelog.test.ts > getStagingChangelogFromGit > should handle error gracefully and return tag
plugin test: Non-fatal error: Failed to get changelog from git: Error: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test:     at catchProcessError (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:42:13)
plugin test:     at processTicksAndRejections (node:internal/process/task_queues:103:5)
plugin test:     at outputStream (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:83:17)
plugin test:     at ConventionalGitClient.getTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/GitClient.ts:152:16)
plugin test:     at ConventionalGitClient.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/ConventionalGitClient.ts:100:16)
plugin test:     at ConventionalChangelog.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:240:22)
plugin test:     at ConventionalChangelog.write (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:591:24)
plugin test:     at getStagingChangelogFromGit (/Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.ts:154:22)
plugin test:     at /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.test.ts:64:20
plugin test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:20
plugin test: stdout | builder/utils/changelog.test.ts > getStagingChangelogFromGit > should use conventional-changelog v7 API correctly
plugin test: Non-fatal error: Failed to get changelog from git: Error: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test:     at catchProcessError (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:42:13)
plugin test:     at processTicksAndRejections (node:internal/process/task_queues:103:5)
plugin test:     at outputStream (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:83:17)
plugin test:     at ConventionalGitClient.getTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/GitClient.ts:152:16)
plugin test:     at ConventionalGitClient.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/ConventionalGitClient.ts:100:16)
plugin test:     at ConventionalChangelog.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:240:22)
plugin test:     at ConventionalChangelog.write (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:591:24)
plugin test:     at getStagingChangelogFromGit (/Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.ts:154:22)
plugin test:     at /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.test.ts:81:7
plugin test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:20
packages/unraid-shared test:  ✓ src/jsonforms/__tests__/settings.test.ts (3 tests) 3ms
unraid-ui test:  ✓ src/components/common/button/Button.test.ts (4 tests) 23ms
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336665/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:159:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336665/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336677/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
unraid-ui test:  Test Files  3 passed (3)
unraid-ui test:       Tests  18 passed (18)
unraid-ui test:    Start at  13:52:14
unraid-ui test:    Duration  1.86s (transform 605ms, setup 0ms, collect 2.04s, tests 159ms, environment 1.67s, prepare 230ms)
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336685/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336698/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:174:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM    WARN [ConfigFileHandler:test-config.json] Migration failed. Using defaults.
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM    WARN [ConfigFileHandler:test-config.json] Error: Migration failed
packages/unraid-shared test:     at TestConfigDefinition.migrateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:109:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:65:54�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:174:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Invalid version: must be >= 1
packages/unraid-shared test:     at TestConfigFilePersister.validate �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/__tests__/config-file.test.ts:80:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:93:34�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.loadOrMigrateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:223:20�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.onModuleInit �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:159:5�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/__tests__/config-file.test.ts:178:5
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336685/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Invalid maxRetries: must be >= 0
packages/unraid-shared test:     at TestConfigDefinition.validate �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:94:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:57:36�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:204:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336701/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336721/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
packages/unraid-shared test:     at JSON.parse (<anonymous>)
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:92:25�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:218:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336726/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
plugin test: stdout | builder/utils/changelog.test.ts > getStagingChangelogFromGit > should validate changelog structure
plugin test: Non-fatal error: Failed to get changelog from git: Error: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test:     at catchProcessError (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:42:13)
plugin test:     at outputStream (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:83:17)
plugin test:     at ConventionalGitClient.getTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/GitClient.ts:152:16)
plugin test:     at ConventionalGitClient.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/ConventionalGitClient.ts:100:16)
plugin test:     at ConventionalChangelog.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:240:22)
plugin test:     at ConventionalChangelog.write (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:591:24)
plugin test:     at getStagingChangelogFromGit (/Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.ts:154:22)
plugin test:     at /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.test.ts:95:20
plugin test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336737/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:279:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM    WARN [ConfigFileHandler:test-config.json] Migration failed. Using defaults.
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM    WARN [ConfigFileHandler:test-config.json] Error: Special validation failure: migration + defaults combo invalid
packages/unraid-shared test:     at SpecialMigrationDefinition.validate �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:266:19�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:66:52�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:279:22
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336745/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336754/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336754/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.loadOrMigrateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:223:20�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.onModuleInit �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:159:5�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/__tests__/config-file.test.ts:295:5
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336754/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336757/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336754/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   ERROR [ConfigFileHandler:test-config.json] Error: Validation failed
packages/unraid-shared test:     at TestConfigDefinition.validate �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:78:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.writeConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:105:38�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:376:41
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:155:11
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:26
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:1897:20
packages/unraid-shared test:     at new Promise (<anonymous>)
packages/unraid-shared test:     at runWithTimeout (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:1863:10)
packages/unraid-shared test:     at runTest (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:1574:12)
packages/unraid-shared test:     at runSuite (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:1729:8)
packages/unraid-shared test: Cannot write invalid config
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
unraid-ui test: Done
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336770/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM    WARN [ConfigFilePersister:test-config.json] Cannot persist undefined config
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336772/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336777/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.updateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:146:29�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:406:23
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336777/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336777/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   ERROR [ConfigFileHandler:test-config.json] Failed to update config
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   ERROR [ConfigFileHandler:test-config.json] Error: Invalid version: must be >= 1
packages/unraid-shared test:     at TestConfigDefinition.validate �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:85:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.updateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:147:47�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:423:23
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /invalid/readonly/path/test-config.json
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   ERROR [ConfigFileHandler:test-config.json] [Error: ENOENT: no such file or directory, open '/invalid/readonly/path/test-config.json.tmp-2218336795ee3d5e'] {
packages/unraid-shared test:   errno: �[33m-2�[39m,
packages/unraid-shared test:   code: �[32m'ENOENT'�[39m,
packages/unraid-shared test:   syscall: �[32m'open'�[39m,
packages/unraid-shared test:   path: �[32m'/invalid/readonly/path/test-config.json.tmp-2218336795ee3d5e'�[39m
packages/unraid-shared test: }
packages/unraid-shared test: Error writing config to '/invalid/readonly/path/test-config.json'
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336802/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/__tests__/config-file-handler.test.ts:453:20
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336802/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
plugin test: stdout | builder/utils/changelog.test.ts > getStagingChangelogFromGit > should include actual commits when using gitRawCommitsOpts with tag
plugin test: Non-fatal error: Failed to get changelog from git: Error: fatal: invalid gitfile format: /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/.git
plugin test:     at catchProcessError (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:42:13)
plugin test:     at processTicksAndRejections (node:internal/process/task_queues:103:5)
plugin test:     at outputStream (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@simple-libs+child-process-utils@1.0.1/node_modules/@simple-libs/child-process-utils/src/index.ts:83:17)
plugin test:     at ConventionalGitClient.getTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/GitClient.ts:152:16)
plugin test:     at ConventionalGitClient.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@conventional-changelog+git-client@2.5.1_conventional-commits-filter@5.0.0_conventional-commits-parser@6.2.0/node_modules/@conventional-changelog/git-client/src/ConventionalGitClient.ts:100:16)
plugin test:     at ConventionalChangelog.getSemverTags (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:240:22)
plugin test:     at ConventionalChangelog.write (file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/conventional-changelog@7.1.1_conventional-commits-filter@5.0.0/node_modules/conventional-changelog/src/ConventionalChangelog.ts:591:24)
plugin test:     at getStagingChangelogFromGit (/Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.ts:154:22)
plugin test:     at /Users/ajitmehrotra/Projects/onboarding-internal-boot/plugin/builder/utils/changelog.test.ts:115:20
plugin test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/.pnpm/@vitest+runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336823/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44435  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-handler-test-1772218336802/test-config.json
packages/unraid-shared test:  ✓ src/util/__tests__/config-file-handler.test.ts (17 tests) 183ms
web test:  ✓ __test__/components/ThemeSwitcher.test.ts (10 tests) 76ms
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336904/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336904/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.loadOrMigrateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:223:20�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.onModuleInit �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:159:5�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/__tests__/config-file.test.ts:391:5
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Writing config to /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336904/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Config is unchanged, skipping write
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFilePersister:test-config.json] Config path: /var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336914/test-config.json
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error: Config file does not exist at '/var/folders/95/f6tc2xdd2t703__z22w4lxzh0000gn/T/config-test-1772218336914/test-config.json'
packages/unraid-shared test:     at ConfigFileHandler.readConfigFile �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:89:13�[90m)�[39m
packages/unraid-shared test:     at ConfigFileHandler.loadConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/util/config-file-handler.ts:56:26�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.loadOrMigrateConfig �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:223:20�[90m)�[39m
packages/unraid-shared test:     at TestConfigFilePersister.onModuleInit �[90m(/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/config-file.ts:159:5�[90m)�[39m
packages/unraid-shared test:     at �[90m/Users/ajitmehrotra/Projects/onboarding-internal-boot/packages/unraid-shared/�[39msrc/services/__tests__/config-file.test.ts:399:5
packages/unraid-shared test:     at file:///Users/ajitmehrotra/Projects/onboarding-internal-boot/node_modules/�[4m.pnpm�[24m/@vitest+runner@3.2.4/node_modules/�[4m@vitest/runner�[24m/dist/chunk-hooks.js:752:20
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM   DEBUG [ConfigFileHandler:test-config.json] Error loading config. Attempting to migrate...
packages/unraid-shared test: [Nest] 44433  - 02/27/2026, 1:52:16 PM VERBOSE [ConfigFileHandler:test-config.json] Existing config unreadable, proceeding with write
packages/unraid-shared test: …
Purpose: remove fragile dependency on /Main/PoolDevices HTML parsing for internal-boot onboarding setup data.

Before: onboarding fetched and parsed the legacy PoolDevices page to infer array availability, eligibility, and candidate devices.

Problem: in this app context the PoolDevices page can be unavailable/redirected, causing parsing to fail and blocking setup even when server state is valid.

Change: introduced GetInternalBootContext GraphQL query (array, vars.fsState, disks) and derived onboarding options directly from API state.

Behavior now: array-stopped checks come from GraphQL state, candidate devices come from unassigned non-USB disks, and pool/boot-size defaults are computed locally.

Simplification: removed template HTML parsing logic from internalBoot composable; it now focuses on mkbootpool submission and reboot post only.

Compatibility: mkbootpool.php and Boot.php invocation paths remain unchanged for apply/reboot behavior.

Validation: ran pnpm codegen, pnpm type-check, pnpm test, and pnpm lint in web successfully.
Purpose: provide onboarding with the same core boot-setup eligibility/device identity data used by webgui state files, without adding new HTTP endpoints.

Before: GraphQL exposed array/fs state and system disk data, but not vars.bootEligible or devs.ini-backed disk assignment identifiers/sectors.

Problem: onboarding could not reliably mirror webgui bootable-pool gating/device identity because mkbootpool expects emhttp disk IDs and webgui eligibility depends on bootEligible.

Change: added bootEligible to var parsing/core type/GraphQL Vars model and surfaced emhttp devs metadata on existing Disk type (emhttpDeviceId, sectors, sectorSize).

How it works: DisksService now reads store.emhttp.devices, matches entries by normalized device name, and projects emhttp assignment metadata alongside existing disk data.

Tests: updated var parser snapshot and extended disks service tests to assert emhttp metadata mapping and config access.

Validation: pnpm --filter ./api type-check, pnpm --filter ./api lint, and targeted vitest for var parser + disks service passed.
Purpose: switch internal-boot onboarding inputs to API-provided boot eligibility and emhttp-backed disk identity metadata.

Before: onboarding inferred eligibility and device IDs from non-webgui sources and local heuristics, which could drift from mkbootpool expectations.

Problem: when PoolDevices rendering is unavailable, onboarding still needs deterministic data for gating and for args[] disk IDs accepted by mkbootpool.

Change: internal boot context query now requests vars.bootEligible and disk emhttp metadata; step maps selectable devices using emhttpDeviceId plus sector data and uses bootEligible for eligibility messaging.

Behavior: array stopped gating uses array/vars state, eligibility uses vars.bootEligible, and boot-size math uses sectors*sectorSize (fallback to disk size).

Compatibility: mkbootpool.php submission path is unchanged; selected device values now align with emhttp disk IDs expected by cmdAssignDisk flow.

Validation: pnpm codegen, pnpm --filter @unraid/web type-check, pnpm --filter @unraid/web test, and pnpm --filter @unraid/web lint passed.
- Purpose: provide a dedicated onboarding query that mirrors webgui internal-boot context sourcing from emhttp state.

- Before: onboarding fetched mixed GraphQL fields (array/vars/disks) and derived boot device context from system disk data.

- Problem: migration parity was weaker because validation inputs and selectable boot devices were not sourced the same way as webgui.

- Change: added  GraphQL query with device options from  (devs.ini), plus pool/share/reserved-name context and boot defaults.

- Change: added resolver/service/model wiring and schema artifacts, including deterministic device sorting aligned to CacheDevices.page behavior.

- Change: added service unit tests and updated emhttp state snapshot expectations for .

- How it works: service reads emhttp var/disks/shares/devices, computes boot-pool defaults and constraints, and returns a single query payload consumed by onboarding.
- Purpose: port internal-boot onboarding UI logic to consume the dedicated API context with webgui-aligned inputs.

- Before: the step mixed array/vars/disks queries and inferred devices/constraints client-side from generic disk metadata.

- Problem: validation and selectable-device behavior could diverge from CacheDevices.page during migration.

- Change: switched query consumption to onboardingInternalBoot context and regenerated typed GraphQL documents.

- Change: added pool-name collision validation against reserved names, user shares, and existing pools to match webgui checks.

- Change: removed HTML-parsing/system-disk derivation paths; device options now come directly from API-provided devs.ini context.

- How it works: the step initializes defaults, slot limits, preset bounds, and validation rules from one query payload and persists the same draft selection flow.
- Purpose: revert the custom onboarding-specific internal boot GraphQL surface and return to existing API surfaces only.
- Before: Query included  with custom resolver/service/types and schema entries generated from that path.
- Problem: this diverged from the migration goal of reusing existing API data and increased maintenance/behavior drift risk.
- Change: removed  resolver/service/model files, detached providers from , and removed schema/codegen artifacts for that field.
- How it works now: API exposes no new onboarding internal-boot query; consumers must use existing /// GraphQL fields.
- Purpose: align onboarding internal-boot setup with existing API queries instead of a custom onboarding query.
- Before: onboarding step read a dedicated onboardingInternalBoot GraphQL field.
- Problem: extra query surface was unnecessary for this migration and made parity/debugging harder.
- Change: rewired onboarding internal-boot context query to existing fields (array, vars, shares, disks), updated step logic to derive options from those fields, and regenerated web GraphQL artifacts.
- How it works: onboarding now checks array state and boot eligibility from existing data, builds device options from disks, and keeps summary/apply behavior on the existing mkbootpool POST flow.
- Tracking: added docs/onboarding-internal-boot-port-differences.md to keep an explicit list of current non-1:1 differences versus webgui.
- Purpose: surface the same var.ini signals webgui uses so onboarding can match visibility and validation behavior without new endpoints.
- Before: GraphQL vars did not expose enableBootTransfer or reservedNames, so web had to infer/hardcode parts of webgui logic.
- Problem: onboarding could not reliably hide the step when already booting internally and could not run reserved-name validation parity.
- Change: added enableBootTransfer and reservedNames to Vars GraphQL model, state types/parser typing, generated schema, and API generated GraphQL types.
- How it works: clients can now read vars.enableBootTransfer and vars.reservedNames directly from the existing vars query.
- Purpose: match webgui behavior for internal-boot onboarding visibility and pool-name validation.
- Before: internal-boot step still appeared when already booting internally, and validation did not include reservedNames parity checks.
- Problem: users on internal boot saw an unnecessary step and validation could diverge from CacheDevices.page constraints.
- Change: added lightweight step-visibility query in onboarding modal using vars.enableBootTransfer, expanded internal-boot context query to include vars.reservedNames/enableBootTransfer, and aligned validation ordering/checks with webgui (reserved names, shares, pools, regex).
- How it works: step is filtered out when enableBootTransfer is no; onboarding validates pool names against the same var/share/pool sources and ordering used by webgui logic.
- Tracking: updated the internal-boot port differences document to remove resolved parity gaps and keep remaining deltas explicit.
- Purpose: make onboarding internal-boot submit the same device identifiers used by webgui.
- Before: dropdown option value preferred GraphQL disk.id, which includes server-prefixed IDs and did not match webgui mkbootpool args.
- Problem: mkbootpool requests could fail or behave unexpectedly because device args were not in emhttp devs.ini id format.
- Change: updated internal boot device option mapping to use emhttpDeviceId first, with device name fallback.
- How it works: submitted args[] now mirror webgui option values, improving compatibility with mkbootpool.php and downstream scripts.
- Purpose: make onboarding internal-boot apply robust when mkbootpool.php returns non-ideal payloads.
- Before: onboarding expected clean JSON and surfaced a generic failure when the response body was empty or noisy.
- Problem: users could see unhelpful failures even with HTTP 200, making server-side debugging difficult.
- Change: added tolerant response parsing (strict JSON first, then JSON substring fallback) and  parity with webgui ajax behavior.
- Change: when response body is empty, onboarding now attempts  and reports detailed HTTP diagnostics as fallback.
- Result: failures now return actionable output instead of opaque request-failed messages.
- Purpose: keep onboarding internal-boot context aligned with fields actually consumed by the UI.
- Before: the context query fetched extra disk fields that were not used by the onboarding step.
- Problem: unused fields increased payload and made emhttp-derived metadata origin less explicit.
- Change: removed unused disk fields from GetInternalBootContext and aliased emhttp-backed metadata to explicit names.
- Change: updated the internal-boot step to consume emhttpSectors and emhttpSectorSize while preserving emhttpDeviceId selection behavior.
- Change: regenerated GraphQL client artifacts for the updated query shape.
- Result: smaller query surface, clearer field provenance, and no behavior change in selection/size calculations.
- Purpose: make onboarding internal-boot requests match webgui PHP post expectations.
- Before: onboarding posted args to mkbootpool.php without csrf_token and relied only on same-origin cookies.
- Problem: on systems enforcing CSRF checks, mkbootpool.php can return an empty HTTP 200 response instead of JSON output.
- Change: append csrf_token from global csrf_token into form payload and also send x-csrf-token header when available.
- Change: remove HTTP fetch fallback to /boot/config/internal_boot/output.log because that filesystem path is not generally web-served.
- Result: onboarding requests now follow existing webgui request conventions and avoid noisy 404 fallback probes.
- Purpose: align onboarding internal-boot logging with the rest of onboarding and make reboot submit path reliable.
- Before: successful internal-boot apply logged full script output into onboarding console, and reboot submit used a minimal form post that could land on Boot.php without executing expected reboot flow.
- Problem: extra success log noise reduced signal in onboarding logs, and reboot flow could appear inert in some contexts.
- Change: removed verbose success output logging from summary step; success now logs the same concise status style as other onboarding actions.
- Change: reboot submit now posts directly to /plugins/dynamix/include/Boot.php, targets _top, and includes csrf_token when available.
- Change: tightened empty mkbootpool-response message to concise guidance.
- Change: added composable tests for mkbootpool payload csrf inclusion and reboot form construction.
- Result: cleaner onboarding logs and a reboot action that follows webgui/php expectations more robustly.
- Purpose: introduce an explicit onboarding boot mode choice so users can select USB boot or storage-drive boot before internal boot setup.
- Before: onboarding always presented internal-boot configuration as the main path and relied on skip behavior rather than an explicit boot mode.
- Problem: users could not clearly choose USB-first flow, and internal-boot controls were always visible regardless of boot preference.
- Change: added persisted bootMode state (usb/storage) to the onboarding draft store with legacy-safe normalization.
- Change: renamed step metadata/title to "Configure Boot" to match the new UX intent.
- Change: updated the step UI to show two radio options, default to USB flow, and only render internal-boot form controls when storage boot is selected.
- Behavior: Continue now advances directly for USB mode and validates/submits internal-boot config only for storage mode.
- Purpose: add explicit user confirmation for destructive boot actions and make boot choices visible in onboarding summary.
- Before: summary only showed internal boot details when present, confirm-and-apply executed immediately, and reboot action had no warning dialog.
- Problem: users could trigger disk-destructive storage boot setup without an explicit confirmation and had no final-step reminder about flash-device handling during reboot.
- Change: summary now always shows a full-width Boot Configuration section under Plugins, including selected boot method and storage details when applicable.
- Change: confirm-and-apply now opens a warning dialog listing selected boot drives before proceeding with storage boot setup.
- Change: next-steps reboot action now opens a warning dialog and requires an "I understand" confirmation before issuing reboot.
- Change: improved summary field layout to wrap long values/device names for responsive behavior.
- Purpose: protect the new boot-mode UX and confirmation dialog behavior with focused onboarding tests.
- Before: tests did not cover the summary wipe-confirmation gate or the next-steps reboot warning dialog.
- Problem: destructive-action safeguards and reboot confirmation behavior could regress without detection.
- Change: updated summary tests to include bootMode state, auto-confirm storage wipe in shared apply helper, and explicit coverage for the cancel path.
- Change: added assertions that Boot Configuration is always rendered for USB mode.
- Change: added a new next-steps test suite that verifies reboot warning dialog flow and "I understand" confirmation behavior.
- Purpose: align internal step identifiers with the new Configure Boot UX naming.
- Before: onboarding used INTERNAL_BOOT as the underlying step id while UI labels showed Configure Boot.
- Problem: mismatch between step id and user-facing language made flow metadata and tests less clear.
- Change: renamed step id references from INTERNAL_BOOT to CONFIGURE_BOOT in step registry, modal flow wiring, and stepper typings.
- Change: updated onboarding modal tests to mock the renamed step id.
- Behavior: functional flow is unchanged; this is an identifier rename for consistency.
- Purpose: remove the temporary non-interactive state on the Setup Boot step when arriving from Configure Settings.
- Before: radio inputs and primary action were disabled while internal-boot context was loading, causing a visible lag where boot choice could not be changed.
- Problem: users saw the step render with delayed interactivity and inconsistent perceived selection state.
- Change: boot-mode radios now lock only when the step itself is saving, not while internal-boot options are loading.
- Change: primary action is now enabled for USB mode immediately and only waits on loading when storage boot setup is selected.
- Change: boot-mode normalization now applies immediately via a typed helper to keep selection stable during draft hydration.
- Change: renamed user-facing step title from "Configure Boot" to "Setup Boot".
- Purpose: align Setup Boot form controls with Unraid primary accent styling.
- Before: boot mode radios and the update BIOS checkbox used browser-default accent colors.
- Problem: controls looked visually inconsistent with the rest of the onboarding UI.
- Change: applied accent-primary to both boot mode radios and the update BIOS checkbox.
- Behavior: no functional changes; only input accent color styling.
- Purpose: expose a first-class onboarding mutation to create an internal boot pool via emcmd.

- Before: onboarding web logic called mkbootpool directly from the browser path, with no typed API contract.

- Problem: direct script calls bypassed GraphQL permissions/validation and made backend orchestration hard to test.

- Change: added CreateInternalBootPool input/output models, resolver field wiring, service implementation, and vitest coverage for happy/error/validation paths.

- How it works: resolver delegates to OnboardingInternalBootService, which runs ordered emcmd steps with timeout handling and structured output/result codes; API GraphQL schema/types were regenerated to publish the new mutation contract.
- Purpose: move onboarding internal boot submission onto the typed GraphQL API contract.

- Before: the composable posted URL-encoded args directly to mkbootpool.php and parsed raw text responses.

- Problem: browser-side script coupling made the flow brittle, harder to type-check, and inconsistent with GraphQL-first onboarding paths.

- Change: added createInternalBootPool mutation document, updated composable to call Apollo mutate, and rewrote tests around mutation payload/response behavior including empty-response handling.

- How it works: submitInternalBootCreation now sends CreateInternalBootPoolInput, consumes onboarding.createInternalBootPool, and uses regenerated web GraphQL documents/types for compile-time safety.
- Purpose: Improve boot-drive onboarding clarity for both US and EU audiences and simplify destructive-action confirmation.

- Before: Boot step copy was long, USB wording was inconsistent, summary boot method repeated full action labels, devices were plain text, and wipe confirmation used dense paragraph text.

- Problem: The step was harder to skim, terminology was less globally clear, and high-risk confirmation content was difficult to parse quickly.

- Change: Updated boot-step copy and radio labels to use USB/Flash wording, changed default internal boot pool fallback from cache to boot, shortened summary boot method values, rendered selected devices as right-aligned badges, and simplified the wipe confirmation dialog title/content with drive badges.

- How: Edited OnboardingInternalBootStep and OnboardingSummaryStep templates/computed values and aligned summary tests to new labels/dialog text.
- Purpose: Improve onboarding clarity around storage boot selection and reboot instructions while keeping summary details easy to scan.

- Before: Summary device badges showed only raw IDs, reboot confirmation used "I understand" and lacked BIOS guidance, and setup-boot BIOS messaging was a static inline note.

- Problem: Users could miss key hardware context (device size/name mapping) and important post-config boot-order guidance, especially during destructive and reboot actions.

- Change: Added device-size/name labels in boot summary and wipe confirmation badges, updated reboot dialog confirmation label to "I Understand", added BIOS boot-order blockquote in Next Steps, and replaced setup-boot BIOS helper text with a conditional warning blockquote shown when BIOS update is selected.

- How: Wired Summary step to internal-boot context query for disk metadata formatting, updated Next Steps and Setup Boot templates, and aligned onboarding tests to cover the new copy and rendered details.
- Purpose: make updateBios in onboarding internal boot perform the same EFI boot entry work as mkbootpool.php.

- Before: createInternalBootPool accepted updateBios but only logged a note and skipped BIOS/EFI changes.

- Problem: onboarding behavior diverged from WebGUI flow, so internal boot entries and boot order were not updated when requested.

- Change: added efibootmgr execution after successful pool creation to delete stale entries, create internal-boot entries, recreate Unraid Flash entry, and set boot order/next boot.

- How: use existing API emhttp state via getters.emhttp() for id->device and flash-device resolution, with loadStateFileSync(StateFileKey.devs/disks) fallback when state is not preloaded.

- Tests: expanded onboarding internal boot service spec to cover the updateBios efibootmgr sequence and retained existing emcmd-only, duplicate-device, and failure-path coverage.
- Purpose: improve Confirm & Apply troubleshooting without cluttering normal onboarding logs.

- Before: failures were mostly logged as short strings and several paths dropped request/response context entirely.

- Problem: support could not reliably diagnose network, GraphQL, timeout, or TLS issues from copied onboarding logs.

- Change: attach structured diagnostics to error log entries only, and render them in a collapsible Technical details block.

- How: added a reusable diagnostics builder that extracts request/response/status/error metadata, classifies error type, redacts sensitive keys, truncates oversized payloads, and is wired across onboarding apply failure paths (mutations, plugin/language install flows, refetches, and fatal catch).
- Purpose: Prevent fresh-install onboarding modal from reappearing after onboarding is already completed at the current version.

- Before: The fresh-install branch only checked registration state (ENOKEYFILE) and session modal flags, so completed systems could still auto-open onboarding.

- Problem: Users could see onboarding again even when onboarding-tracker indicated completion, especially across hostname/session contexts.

- After: Fresh-install auto-open now also requires onboarding to be incomplete, while force-open and temporary bypass testing controls remain intact.

- Implementation: activationCodeModal now reads onboarding completed state from upgradeOnboarding store and gates isVisible with !completed for the default fresh-install path.
- Purpose: Align reboot confirmation messaging with intended priority of instructions in the final onboarding step.

- Before: The descriptive paragraph warned about not removing the flash drive, while the blockquote contained BIOS boot-order guidance.

- Problem: The visual emphasis and supporting text hierarchy were reversed from the intended UX content order.

- After: The description now contains BIOS boot-order guidance and the highlighted blockquote now contains the flash-drive warning.

- Implementation: Swapped the two existing strings in the confirm reboot popup without changing layout, styles, or behavior.
- Purpose: Prevent onboarding from offering/reinstalling Tailscale when the preview plugin variant is already installed.

- Before: Install detection only matched canonical plugin filenames from the configured plugin URL, so tailscale-preview.plg was not recognized as satisfying tailscale.

- Problem: Users with tailscale-preview.plg could still be prompted to install Tailscale and see inconsistent installed status in onboarding.

- After: Tailscale install detection treats both tailscale.plg and tailscale-preview.plg as installed across plugin selection and summary/apply flows.

- Implementation: Added alias-aware filename matching helpers and metadata in OnboardingPluginsStep and OnboardingSummaryStep, and replaced single-filename checks with alias-set matching.
- Purpose: make onboarding copy consistently translatable by routing user-facing text through locale keys.
- Before: onboarding had mixed hardcoded strings and translation lookups across steps, dialogs, console labels, and runtime status/result messaging.
- Problem: mixed patterns caused gaps in the translation system and made copy updates and localization coverage inconsistent.
- Change: replaced onboarding literals with / lookups, added structured  key groups (including internal boot, summary, modal, console, stepper, plugin labels, and missing license keys), and updated step metadata to use translation keys.
- How it works: components now resolve display text from , including dynamic/interpolated strings for validation, logs, warnings, and confirmations, while preserving existing English behavior.
- Purpose of this change: stabilize onboarding modal gating tests and keep onboarding fixtures consistent across related checks.

- Behavior before: modal gating tests could fail when onboarding completion refs were missing, and related fixtures drifted across test/debug paths.

- Why that was a problem: failures were noisy and made onboarding validation less reliable in CI and local verification.

- What this change accomplishes: ensures modal gating tests run deterministically and aligns dependent onboarding fixture payloads/expectations.

- How it works: adds the missing onboarding store mock ref for gating tests and updates linked onboarding fixture data used by related tests and override flows.
- Purpose: stabilize onboarding GraphQL operations in HTTPS sessions on Firefox by avoiding unnecessary identity writes and deferring identity mutation timing.
- Before: onboarding could run UpdateServerIdentity early, and server identity writes executed emcmd even when name/comment/sysModel were unchanged.
- Why this was a problem: identity writes can trigger Unraid web identity/certificate regeneration or nginx cert reload paths; Firefox then sees a different presented certificate for the same IP, invalidates prior trust state, and blocks subsequent /graphql requests until the user re-accepts the new cert.
- What this change accomplishes: prevents avoidable cert rotation events and prevents mid-flow TLS breakage while language/plugin mutations are running.
- How it works:
  - Add a no-op guard in ServerService.updateServerIdentity to skip emcmd when identity fields are unchanged.
  - Reuse a shared response builder for both no-op and updated paths.
  - Add unit tests covering unchanged identity/sysModel no-op behavior.
  - Move onboarding identity apply to the end of summary execution so plugin/language/finalization calls complete before any potential cert-affecting identity change.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue (1)

78-93: ⚠️ Potential issue | 🟠 Major

Keep hasPartnerLogo in BrandingConfigPayload (nullable) to avoid type drift.

hasPartnerLogo was removed here, but it is still part of the API schema (nullable) and is still consumed in this component at Line 689. This creates inconsistent local typing and can hide null/unknown states.

🔧 Proposed fix
 type BrandingConfigPayload = {
   bannerImage?: string;
   caseModelImage?: string;
   partnerLogoLightUrl?: string;
   partnerLogoDarkUrl?: string;
+  hasPartnerLogo?: boolean | null;
   onboardingTitle?: string;
   onboardingSubtitle?: string;
   onboardingTitleFreshInstall?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue`
around lines 78 - 93, The BrandingConfigPayload type was stripped of the
nullable hasPartnerLogo field, causing type drift with the API and runtime use
in OnboardingAdminPanel.standalone.vue (the component still reads
hasPartnerLogo); restore hasPartnerLogo?: boolean | null to
BrandingConfigPayload so the local type matches the API and preserves nullable
semantics, update any references to treat it as possibly null/undefined (e.g.,
null checks in the component where hasPartnerLogo is used) to avoid unsafe
access.
♻️ Duplicate comments (2)
web/src/components/Onboarding/OnboardingModal.vue (1)

101-114: ⚠️ Potential issue | 🟠 Major

Fail closed for CONFIGURE_BOOT visibility (show only on explicit enable).

Current logic is fail-open: unknown/missing visibility data still displays the internal-boot step.

✅ Safer visibility logic
-const hideInternalBootStep = computed(() => {
-  const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer?.trim().toLowerCase();
-  return setting === 'no';
-});
+const showInternalBootStep = computed(() => {
+  const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer;
+  return typeof setting === 'string' && setting.trim().toLowerCase() === 'yes';
+});
@@
-      return !isPartnerBuild.value && !hideInternalBootStep.value;
+      return !isPartnerBuild.value && showInternalBootStep.value;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/OnboardingModal.vue` around lines 101 - 114,
The current visibility logic for the CONFIGURE_BOOT step is fail-open because
hideInternalBootStep checks for setting === 'no' and any unknown/missing value
falls through to showing the step; change it to only show CONFIGURE_BOOT when
the visibility setting is explicitly enabled. Update the computed logic around
hideInternalBootStep / visibleHardcodedSteps (referencing
internalBootVisibilityResult, enableBootTransfer, isPartnerBuild, and
HARDCODED_STEPS) so you parse and normalize the setting once and require setting
=== 'yes' (or an explicit enabled constant) before returning true for
CONFIGURE_BOOT; otherwise treat it as hidden. Ensure the CONFIGURE_BOOT branch
in visibleHardcodedSteps uses this new explicit-enable check and still respects
isPartnerBuild.value.
web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue (1)

475-479: ⚠️ Potential issue | 🟠 Major

Replace the pool-name regex with a non-backtracking validation.

The current pattern is backtracking-prone and still leaves this input-validation path exposed to ReDoS under crafted input.

🔒 Safer validation pattern
-  const poolNamePattern = /^[a-z]([a-z0-9~._-]*[a-z_-])*$/;
-  if (!poolNamePattern.test(normalizedPoolName)) {
+  const hasAllowedCharacters = /^[a-z][a-z0-9~._-]*$/.test(normalizedPoolName);
+  const endsWithAllowedTerminal = /[a-z_-]$/.test(normalizedPoolName);
+  if (!hasAllowedCharacters || !endsWithAllowedTerminal) {
     formError.value = t('onboarding.internalBootStep.validation.poolFormat');
     return null;
   }
#!/bin/bash
# Verification: this should return no matches after applying the fix.
rg -n "poolNamePattern\\s*=\\s*/\\^\\[a-z\\]\\(\\[a-z0-9~\\._-\\]\\*\\[a-z_-\\]\\)\\*\\$/" web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue` around
lines 475 - 479, The current poolNamePattern variable contains a
backtracking-prone regex; replace it with a non-backtracking / atomic pattern
that enforces "starts with a lowercase letter, then allowed chars [a-z0-9~._-],
and ends (if any additional chars) with a letter, underscore or hyphen" to
prevent ReDoS. Update the poolNamePattern constant in
OnboardingInternalBootStep.vue accordingly (leave the surrounding validation
logic using normalizedPoolName, formError.value and t(...) intact) so the same
validation semantics are preserved but using a safe, non-nested-quantifier
regex.
🧹 Nitpick comments (8)
api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts (1)

146-156: Prefer typed mock composition over double-casting.

Line 156 uses as unknown as ...; this can be avoided by deriving from the existing typed mocked value.

♻️ Suggested refactor
-        vi.mocked(getters.emhttp).mockReturnValue({
-            var: {
-                name: 'Tower',
-                fsState: 'Stopped',
-                regGuid: 'GUID-123',
-                port: '80',
-                comment: 'Tower comment',
-                sysModel: 'Model X200',
-            },
-            networks: [{ ipaddr: ['192.168.1.10'] }],
-        } as unknown as ReturnType<typeof getters.emhttp>);
+        const current = vi.mocked(getters.emhttp)();
+        vi.mocked(getters.emhttp).mockReturnValue({
+            ...current,
+            var: {
+                ...current.var,
+                name: 'Tower',
+                fsState: 'Stopped',
+                regGuid: 'GUID-123',
+                port: '80',
+                comment: 'Tower comment',
+                sysModel: 'Model X200',
+            },
+            networks: [{ ipaddr: ['192.168.1.10'] }],
+        });

As per coding guidelines **/*.{ts,tsx}: "Avoid using casting whenever possible, prefer proper typing from the start."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts` around
lines 146 - 156, The test currently forces a type with "as unknown as
ReturnType<typeof getters.emhttp>"—replace that double-cast by constructing a
properly typed mock object first: declare a const (e.g., emhttpMock) typed as
ReturnType<typeof getters.emhttp> and populate its fields (var, networks, etc.),
then pass that const into vi.mocked(getters.emhttp).mockReturnValue(emhttpMock);
update the mocked value creation near the vi.mocked(getters.emhttp) call and
remove the unnecessary casts.
web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue (2)

600-600: Use localized theme label in alt-text interpolation (optional).

You currently interpolate the theme code (white, black, etc.). Passing the selected theme’s localized label would improve a11y text quality.

Proposed refinement
+const selectedThemeLabel = computed(
+  () => themeItems.value.find((item) => item.value === selectedTheme.value)?.label ?? selectedTheme.value
+);
-:alt="t('onboarding.coreSettings.themePreviewAlt', { theme: selectedTheme })"
+:alt="t('onboarding.coreSettings.themePreviewAlt', { theme: selectedThemeLabel })"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue` at line
600, The alt text currently passes the theme code into the translation key
(t('onboarding.coreSettings.themePreviewAlt', { theme: selectedTheme })); change
it to pass the localized theme label instead—create or use a computed/method
(e.g., selectedThemeLabel or getThemeLabel(selectedTheme)) that maps
selectedTheme to its i18n label and call
t('onboarding.coreSettings.themePreviewAlt', { theme: selectedThemeLabel }) so
the interpolated value is the human-readable, localized theme name.

40-41: Consider using a canonical default hostname instead of a localized string for serverName.

While the current English value 'Tower' is valid and form validation will catch invalid entries, mixing localization with hostname constraints creates unnecessary friction. If a locale translator adds a value that fails validation (e.g., too long, non-ASCII characters), users in that locale will be blocked at form submission until they manually edit the field. The validation rules are strict (max 15 chars, alphanumeric/dot/dash only, no trailing dots/dashes) and will reject many reasonable translations. Recommend using a canonical, validation-safe default (e.g., 'unraid' or 'server') and localizing only the placeholder text and labels instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue` around
lines 40 - 41, Replace the localized default value used for serverName in
OnboardingCoreSettingsStep.vue with a validation-safe canonical hostname (e.g.,
'server' or 'unraid') instead of t('onboarding.coreSettings.defaultServerName');
keep all user-facing localization for the input placeholder/label only (use the
existing translation keys for the placeholder), and ensure the chosen default
conforms to the form's hostname rules (max 15 chars, only alphanumeric/dot/dash,
no trailing dots/dashes) so locale translators cannot introduce invalid
defaults.
web/__test__/store/activationCodeModal.test.ts (1)

91-94: Add one assertion for the completed gating path.

The new mock is wired, but there’s no explicit test proving completed = true hides the modal. Add a focused case to lock this behavior.

Proposed test addition
+  it('is not visible when onboarding is completed', () => {
+    mockIsFreshInstall.value = true;
+    mockIsHidden.value = null;
+    mockCallbackData.value = null;
+    mockCompleted.value = true;
+
+    expect(store.isVisible).toBe(false);
+  });

As per coding guidelines, **/__test__/store/**/*.ts: Test Pinia action side effects and state changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/store/activationCodeModal.test.ts` around lines 91 - 94, Add a
focused test that verifies the "completed" gating path hides the modal: in
web/__test__/store/activationCodeModal.test.ts mock useOnboardingStore to return
completed: true (as you already set up) and add an assertion that the
ActivationCodeModal (or the component/selector under test) is not rendered or is
hidden when completed is true; locate the mock by the symbol useOnboardingStore
and assert the expected DOM or component state change (e.g., modal absence or
visibility flag) to lock the behavior.
web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue (1)

95-98: Consider adding error handling for the reboot submission.

submitInternalBootReboot() is called without awaiting or handling potential errors. If the reboot command fails, the user receives no feedback.

🛡️ Suggested improvement
-const handleConfirmReboot = () => {
+const handleConfirmReboot = async () => {
   showRebootWarningDialog.value = false;
-  submitInternalBootReboot();
+  try {
+    await submitInternalBootReboot();
+  } catch (e) {
+    console.error('Reboot submission failed:', e);
+    // Consider showing user feedback if reboot fails
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue` around lines
95 - 98, The reboot confirmation currently calls submitInternalBootReboot()
without awaiting or handling errors in handleConfirmReboot, so failures give no
feedback; update handleConfirmReboot to await submitInternalBootReboot() (or
handle its returned Promise), catch any errors, and show appropriate UI feedback
(e.g., set showRebootWarningDialog back to true or display an error
toast/message) while preserving successful-path behavior; reference
handleConfirmReboot, showRebootWarningDialog, and submitInternalBootReboot when
making the change.
web/src/components/Onboarding/composables/onboardingErrorDiagnostics.ts (1)

29-33: Consider extending the sensitive key pattern.

The current pattern is comprehensive but could miss additional sensitive patterns like bearer, jwt, or credential.

🔒 Optional enhancement
-const SENSITIVE_KEY_PATTERN = /(authorization|cookie|csrf|password|secret|token|api[_-]?key|session)/i;
+const SENSITIVE_KEY_PATTERN = /(authorization|cookie|csrf|password|secret|token|api[_-]?key|session|bearer|jwt|credential)/i;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/composables/onboardingErrorDiagnostics.ts`
around lines 29 - 33, Update the SENSITIVE_KEY_PATTERN constant to include
additional sensitive token/key names such as "bearer", "jwt", and "credential":
modify the regex used in SENSITIVE_KEY_PATTERN so it also matches those terms
(case-insensitively) alongside the existing alternatives, ensuring any code
paths that rely on SENSITIVE_KEY_PATTERN (e.g., sanitization or diagnostics in
onboardingErrorDiagnostics.ts) will now detect these additional sensitive keys.
web/src/components/Onboarding/steps/OnboardingSummaryStep.vue (1)

159-203: Extract internal-boot formatting helpers to a shared utility.

formatBytes, normalizeDeviceName, and deriveDeviceSizeBytes are duplicated with web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue (Lines 77-128). Consolidating avoids behavior drift between step and summary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
159 - 203, Extract the duplicated helpers formatBytes, normalizeDeviceName, and
deriveDeviceSizeBytes into a new shared utility module (e.g., export these
functions from a new file) and replace the inline definitions in
OnboardingSummaryStep.vue and OnboardingInternalBootStep.vue with imports from
that module; ensure the exported function names and signatures match exactly
(formatBytes(bytes: number), normalizeDeviceName(value: string | null |
undefined): string, deriveDeviceSizeBytes(sectors, sectorSize, fallbackSize))
and update both components to import and use those named exports so behavior
remains consistent.
web/src/components/Onboarding/steps/OnboardingPluginsStep.vue (1)

108-112: Remove speculative inline comments before merge.

These comments read as open design questions rather than functional clarification. They should be resolved in code/PR discussion and removed from the component.

♻️ Suggested cleanup
-  // Auto-select uninstalled recommended plugins, but maybe respect user choice?
-  // Current logic: If installed, add to selection?
-  // If installed, we probably can't "install" it again easily, but we can verify it.
-  // We'll leave them selected if they are installed, or disable them?
...
-  // Clear selection? Or just move on?
-  // User clicked "Skip", so we probably shouldn't install anything.
As per coding guidelines "Never add comments unless they are needed for clarity of function".

Also applies to: 137-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingPluginsStep.vue` around lines
108 - 112, Remove the speculative inline comments in OnboardingPluginsStep.vue
(the comment block around lines 108–112 and the ones around 137–138) that pose
open design questions; instead delete them from the file so only purposeful,
clarifying comments remain. Search the component for any remaining
TODO/question-style comments and remove or replace them with a clear code-level
note or resolved decision documented in the PR, making sure not to change
behavior in methods like the plugin selection logic in this component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/unraid-api/graph/resolvers/servers/server.service.ts`:
- Line 42: The localurl assignment may produce a trailing colon when port is
empty; update the logic in the server.service.ts resolver where localurl is
built (referencing localurl, lanip, port) to only append `:${port}` when port is
non-empty/defined and lanip exists—i.e., if lanip is falsy return '', if lanip
truthy and port truthy return `http://${lanip}:${port}`, otherwise return
`http://${lanip}`—so no stray colon is emitted.

In `@web/__test__/store/activationCodeModal.test.ts`:
- Line 12: Update the import to use the ESM-compatible file extension: change
the import of the useOnboardingStore from the module named upgradeOnboarding to
include the .js extension (i.e., import { useOnboardingStore } from
'~/components/Onboarding/store/upgradeOnboarding.js'), ensuring the test file
references upgradeOnboarding.js for ESM/TypeScript resolution.

In
`@web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue`:
- Around line 79-80: The activation GraphQL query in
web/src/components/Onboarding/graphql/activationCode.query.ts does not select
the newly added bannerImage and caseModelImage fields, so wire them into the
query's selection set (the activation/activationCode response fields) to match
the payload/presets you added; also update any related TypeScript types or
generated schemas used by that file so the refetch/store will include
bannerImage and caseModelImage.

---

Outside diff comments:
In
`@web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue`:
- Around line 78-93: The BrandingConfigPayload type was stripped of the nullable
hasPartnerLogo field, causing type drift with the API and runtime use in
OnboardingAdminPanel.standalone.vue (the component still reads hasPartnerLogo);
restore hasPartnerLogo?: boolean | null to BrandingConfigPayload so the local
type matches the API and preserves nullable semantics, update any references to
treat it as possibly null/undefined (e.g., null checks in the component where
hasPartnerLogo is used) to avoid unsafe access.

---

Duplicate comments:
In `@web/src/components/Onboarding/OnboardingModal.vue`:
- Around line 101-114: The current visibility logic for the CONFIGURE_BOOT step
is fail-open because hideInternalBootStep checks for setting === 'no' and any
unknown/missing value falls through to showing the step; change it to only show
CONFIGURE_BOOT when the visibility setting is explicitly enabled. Update the
computed logic around hideInternalBootStep / visibleHardcodedSteps (referencing
internalBootVisibilityResult, enableBootTransfer, isPartnerBuild, and
HARDCODED_STEPS) so you parse and normalize the setting once and require setting
=== 'yes' (or an explicit enabled constant) before returning true for
CONFIGURE_BOOT; otherwise treat it as hidden. Ensure the CONFIGURE_BOOT branch
in visibleHardcodedSteps uses this new explicit-enable check and still respects
isPartnerBuild.value.

In `@web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue`:
- Around line 475-479: The current poolNamePattern variable contains a
backtracking-prone regex; replace it with a non-backtracking / atomic pattern
that enforces "starts with a lowercase letter, then allowed chars [a-z0-9~._-],
and ends (if any additional chars) with a letter, underscore or hyphen" to
prevent ReDoS. Update the poolNamePattern constant in
OnboardingInternalBootStep.vue accordingly (leave the surrounding validation
logic using normalizedPoolName, formError.value and t(...) intact) so the same
validation semantics are preserved but using a safe, non-nested-quantifier
regex.

---

Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts`:
- Around line 146-156: The test currently forces a type with "as unknown as
ReturnType<typeof getters.emhttp>"—replace that double-cast by constructing a
properly typed mock object first: declare a const (e.g., emhttpMock) typed as
ReturnType<typeof getters.emhttp> and populate its fields (var, networks, etc.),
then pass that const into vi.mocked(getters.emhttp).mockReturnValue(emhttpMock);
update the mocked value creation near the vi.mocked(getters.emhttp) call and
remove the unnecessary casts.

In `@web/__test__/store/activationCodeModal.test.ts`:
- Around line 91-94: Add a focused test that verifies the "completed" gating
path hides the modal: in web/__test__/store/activationCodeModal.test.ts mock
useOnboardingStore to return completed: true (as you already set up) and add an
assertion that the ActivationCodeModal (or the component/selector under test) is
not rendered or is hidden when completed is true; locate the mock by the symbol
useOnboardingStore and assert the expected DOM or component state change (e.g.,
modal absence or visibility flag) to lock the behavior.

In `@web/src/components/Onboarding/composables/onboardingErrorDiagnostics.ts`:
- Around line 29-33: Update the SENSITIVE_KEY_PATTERN constant to include
additional sensitive token/key names such as "bearer", "jwt", and "credential":
modify the regex used in SENSITIVE_KEY_PATTERN so it also matches those terms
(case-insensitively) alongside the existing alternatives, ensuring any code
paths that rely on SENSITIVE_KEY_PATTERN (e.g., sanitization or diagnostics in
onboardingErrorDiagnostics.ts) will now detect these additional sensitive keys.

In `@web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue`:
- Line 600: The alt text currently passes the theme code into the translation
key (t('onboarding.coreSettings.themePreviewAlt', { theme: selectedTheme }));
change it to pass the localized theme label instead—create or use a
computed/method (e.g., selectedThemeLabel or getThemeLabel(selectedTheme)) that
maps selectedTheme to its i18n label and call
t('onboarding.coreSettings.themePreviewAlt', { theme: selectedThemeLabel }) so
the interpolated value is the human-readable, localized theme name.
- Around line 40-41: Replace the localized default value used for serverName in
OnboardingCoreSettingsStep.vue with a validation-safe canonical hostname (e.g.,
'server' or 'unraid') instead of t('onboarding.coreSettings.defaultServerName');
keep all user-facing localization for the input placeholder/label only (use the
existing translation keys for the placeholder), and ensure the chosen default
conforms to the form's hostname rules (max 15 chars, only alphanumeric/dot/dash,
no trailing dots/dashes) so locale translators cannot introduce invalid
defaults.

In `@web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue`:
- Around line 95-98: The reboot confirmation currently calls
submitInternalBootReboot() without awaiting or handling errors in
handleConfirmReboot, so failures give no feedback; update handleConfirmReboot to
await submitInternalBootReboot() (or handle its returned Promise), catch any
errors, and show appropriate UI feedback (e.g., set showRebootWarningDialog back
to true or display an error toast/message) while preserving successful-path
behavior; reference handleConfirmReboot, showRebootWarningDialog, and
submitInternalBootReboot when making the change.

In `@web/src/components/Onboarding/steps/OnboardingPluginsStep.vue`:
- Around line 108-112: Remove the speculative inline comments in
OnboardingPluginsStep.vue (the comment block around lines 108–112 and the ones
around 137–138) that pose open design questions; instead delete them from the
file so only purposeful, clarifying comments remain. Search the component for
any remaining TODO/question-style comments and remove or replace them with a
clear code-level note or resolved decision documented in the PR, making sure not
to change behavior in methods like the plugin selection logic in this component.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 159-203: Extract the duplicated helpers formatBytes,
normalizeDeviceName, and deriveDeviceSizeBytes into a new shared utility module
(e.g., export these functions from a new file) and replace the inline
definitions in OnboardingSummaryStep.vue and OnboardingInternalBootStep.vue with
imports from that module; ensure the exported function names and signatures
match exactly (formatBytes(bytes: number), normalizeDeviceName(value: string |
null | undefined): string, deriveDeviceSizeBytes(sectors, sectorSize,
fallbackSize)) and update both components to import and use those named exports
so behavior remains consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 41503729-2e28-4e0b-86c8-287bcb23ea6c

📥 Commits

Reviewing files that changed from the base of the PR and between 9f917f9 and dd6bf10.

📒 Files selected for processing (20)
  • api/src/unraid-api/graph/resolvers/servers/server.service.spec.ts
  • api/src/unraid-api/graph/resolvers/servers/server.service.ts
  • web/__test__/components/Onboarding/OnboardingCoreSettingsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingOverviewStep.test.ts
  • web/__test__/store/activationCodeModal.test.ts
  • web/src/components/Onboarding/OnboardingModal.vue
  • web/src/components/Onboarding/OnboardingSteps.vue
  • web/src/components/Onboarding/components/OnboardingConsole.vue
  • web/src/components/Onboarding/composables/onboardingErrorDiagnostics.ts
  • web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue
  • web/src/components/Onboarding/stepRegistry.ts
  • web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingLicenseStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/src/components/Onboarding/steps/OnboardingOverviewStep.vue
  • web/src/components/Onboarding/steps/OnboardingPluginsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
  • web/src/components/Onboarding/store/activationCodeModal.ts
  • web/src/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/Onboarding/stepRegistry.ts

status: ServerStatus.ONLINE,
wanip: '',
lanip,
localurl: lanip ? `http://${lanip}:${port}` : '',
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard local URL formatting when port is missing.

Line 42 can return http://<lanip>: if port is empty.

🐛 Suggested fix
-            localurl: lanip ? `http://${lanip}:${port}` : '',
+            localurl: lanip ? `http://${lanip}${port ? `:${port}` : ''}` : '',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
localurl: lanip ? `http://${lanip}:${port}` : '',
localurl: lanip ? `http://${lanip}${port ? `:${port}` : ''}` : '',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/servers/server.service.ts` at line 42, The
localurl assignment may produce a trailing colon when port is empty; update the
logic in the server.service.ts resolver where localurl is built (referencing
localurl, lanip, port) to only append `:${port}` when port is non-empty/defined
and lanip exists—i.e., if lanip is falsy return '', if lanip truthy and port
truthy return `http://${lanip}:${port}`, otherwise return `http://${lanip}`—so
no stray colon is emitted.


import { useActivationCodeDataStore } from '~/components/Onboarding/store/activationCodeData';
import { useActivationCodeModalStore } from '~/components/Onboarding/store/activationCodeModal';
import { useOnboardingStore } from '~/components/Onboarding/store/upgradeOnboarding';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n web/__test__/store/activationCodeModal.test.ts | head -20

Repository: unraid/api

Length of output: 1108


🏁 Script executed:

# Check if other imports in the same file use .js extensions
grep -n "from ['\"]~/" web/__test__/store/activationCodeModal.test.ts

# Check other test files in the same directory to see the pattern
find web/__test__/store -name "*.test.ts" -exec grep -h "from ['\"]~/" {} \; | head -20

# Check store files themselves to see what imports they use
grep -n "from ['\"]~/" web/components/Onboarding/store/upgradeOnboarding.ts 2>/dev/null | head -10
grep -n "from ['\"]~/" web/store/server.ts 2>/dev/null | head -10

Repository: unraid/api

Length of output: 2353


🏁 Script executed:

# Check if .js versions of these imports actually exist as compiled files
ls -la web/components/Onboarding/store/ | grep -E "(upgradeOnboarding|activationCodeData|activationCodeModal)"

# Check if these are TypeScript source files or if there's a dist/build directory
ls -la web/store/ | grep -E "(server|callbackActions)" | head -5

# Look for build or dist output
find . -maxdepth 2 -type d -name "dist" -o -name ".nuxt" -o -name "build"

# Check if nuxt.config or similar build config specifies .js extensions
cat -n nuxt.config.* 2>/dev/null | head -20

Repository: unraid/api

Length of output: 191


🏁 Script executed:

# Find the actual directory structure
find . -type f -name "activationCodeModal.test.ts"
find . -type f -name "upgradeOnboarding.ts"
find . -type f -name "server.ts" | grep store

# Look at the repository structure
git ls-files | grep -E "(store|Onboarding)" | head -30

Repository: unraid/api

Length of output: 1633


🏁 Script executed:

# Check the actual import paths and see what the ~ alias resolves to
cat nuxt.config.ts 2>/dev/null | grep -A 5 "alias"
cat nuxt.config.js 2>/dev/null | grep -A 5 "alias"
cat vite.config.ts 2>/dev/null | grep -A 10 "alias"

# Look at a few actual store files to see if they use .js extensions in their imports
head -20 web/src/store/server.ts
head -20 web/src/components/Onboarding/store/upgradeOnboarding.ts

# Check if there are any other test files with the .js extension pattern
grep -r "from ['\"]~.*\.js" web/__test__/ 2>/dev/null | head -10

Repository: unraid/api

Length of output: 2140


🏁 Script executed:

# Check all imports in the test file we're reviewing
cat -n web/__test__/store/activationCodeModal.test.ts | grep "from ['\"]"

# Check another test file in the same directory to see the pattern
ls -la web/__test__/store/*.test.ts
head -30 web/__test__/store/server.test.ts 2>/dev/null | grep "from ['\"]"
head -30 web/__test__/store/account.test.ts 2>/dev/null | grep "from ['\"]"

Repository: unraid/api

Length of output: 3424


🏁 Script executed:

# Check multiple test files to see the pattern
head -30 web/__test__/store/account.test.ts | grep "from ['\"]"
head -30 web/__test__/store/server.test.ts | grep "from ['\"]"
head -30 web/__test__/store/dropdown.test.ts | grep "from ['\"]"

# Check if there's a tsconfig or build config that handles this
cat tsconfig.json 2>/dev/null | head -40
cat web/tsconfig.json 2>/dev/null | head -40

Repository: unraid/api

Length of output: 2060


🏁 Script executed:

# Check if the one file that uses .js extensions is recent or was there from the start
git log --oneline -1 web/__test__/composables/useAuthorizationLink.test.ts

# See if there's a pattern of recent changes adding .js extensions
git log --oneline --all -- '*.test.ts' | head -20

# Check the vite/nuxt config to see if it explicitly requires .js extensions for ESM
grep -A 10 "ssr\|esm\|module" nuxt.config.ts 2>/dev/null

Repository: unraid/api

Length of output: 1641


🏁 Script executed:

# Check the most recent store test files to see the import pattern they follow
git show dd6bf102:web/__test__/store/activationCodeModal.test.ts | head -20 2>/dev/null
git show dd6bf102:web/__test__/store/account.test.ts | head -20 2>/dev/null

# Check if the .js extension requirement is documented in any config or comments
grep -r "\.js" .cursor/rules/ 2>/dev/null
grep -r "ESM\|esm" AGENTS.md 2>/dev/null

# Check the actual imports in a non-test store file
cat web/src/store/server.ts | grep -E "^import.*from.*['\"]~" | head -15

Repository: unraid/api

Length of output: 2741


Add .js extension to the store import on line 12 for ESM compatibility.

import { useOnboardingStore } from '~/components/Onboarding/store/upgradeOnboarding.js';

The repository's coding guidelines require TypeScript imports to use .js extensions for ESM compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/store/activationCodeModal.test.ts` at line 12, Update the import
to use the ESM-compatible file extension: change the import of the
useOnboardingStore from the module named upgradeOnboarding to include the .js
extension (i.e., import { useOnboardingStore } from
'~/components/Onboarding/store/upgradeOnboarding.js'), ensuring the test file
references upgradeOnboarding.js for ESM/TypeScript resolution.

Comment on lines +79 to +80
bannerImage?: string;
caseModelImage?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wire new branding fields into the activation query.

You added bannerImage and caseModelImage to the payload/presets, but web/src/components/Onboarding/graphql/activationCode.query.ts (Line 19-Line 38 in provided snippet) does not request them. After refetch, store-backed UI/data paths won’t receive these fields.

🔧 Proposed fix (outside this file)
 branding {
   header
   headermetacolor
   background
   showBannerGradient
   theme
+  bannerImage
+  caseModelImage
   partnerLogoLightUrl
   partnerLogoDarkUrl
   hasPartnerLogo
   onboardingTitle
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/standalone/OnboardingAdminPanel.standalone.vue`
around lines 79 - 80, The activation GraphQL query in
web/src/components/Onboarding/graphql/activationCode.query.ts does not select
the newly added bannerImage and caseModelImage fields, so wire them into the
query's selection set (the activation/activationCode response fields) to match
the payload/presets you added; also update any related TypeScript types or
generated schemas used by that file so the refetch/store will include
bannerImage and caseModelImage.

- Purpose: correct misleading onboarding completion sequencing in the summary log and improve server description presentation.
- Before: the console could show "Finalizing setup" and "Setup complete" before the late identity mutation ran, which looked like completion happened early.
- Why that was a problem: users interpreted onboarding as finished while a remaining identity operation was still pending.
- What this change accomplishes: setup completion is now logged only after the identity step is attempted, and server description is displayed in a read-only textbox-style block on the summary card.
- How it works:
  - Move finalization logging to the end phase, immediately before the deferred identity apply.
  - Emit "Setup complete" only after the identity apply attempt when onboarding completion was marked.
  - Add a computed  fallback and render it in a non-editable input-like container.
- Purpose: ensure OnboardingSummaryStep shows the correct boot method in summary state.

- Before: boot mode defaulted to USB unless draft boot selections were already set in store state.

- Problem: internal-boot flows with persisted context could render a USB label even when a storage boot device exists.

- Change: selectedBootMode now falls back to internal boot context array.boot.device to detect storage mode.

- Implementation: keep existing store checks first, then inspect context device string and treat non-empty values as storage; also align server description styling with accented summary field visuals.
@Ajit-Mehrotra Ajit-Mehrotra force-pushed the feat/onboarding-internal-boot branch from dd6bf10 to 121f5d8 Compare March 6, 2026 00:11
@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1881/dynamix.unraid.net.plg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant