Skip to content

feat(api): expose internal boot devices in array GraphQL#1894

Merged
Ajit-Mehrotra merged 6 commits intomainfrom
feat/internal-boot-api
Mar 10, 2026
Merged

feat(api): expose internal boot devices in array GraphQL#1894
Ajit-Mehrotra merged 6 commits intomainfrom
feat/internal-boot-api

Conversation

@Ajit-Mehrotra
Copy link
Contributor

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

Summary

Update the array boot API to understand Unraid internal boot devices using the same runtime signal as the webgui, and expose the full boot device set to clients.

This change adds support for type="Boot" entries from disks.ini, introduces BOOT as a public ArrayDiskType, and adds array.bootDevices so mirrored/redundant internal boot members can be represented explicitly.

Why

Unraid now supports booting from storage devices instead of only USB flash devices.

Before this PR:

  • the API only treated FLASH as boot
  • array.boot was a single disk chosen from FLASH
  • internal boot members from disks.ini were not represented correctly
  • clients had no way to inspect multiple boot devices in mirrored internal boot setups

That meant the API no longer matched the webgui/runtime behavior for internal boot.

What Changed

Boot detection and selection

  • Preserve type="Boot" entries from disks.ini in the slots parser.
  • Expose those entries as ArrayDiskType.BOOT.
  • Build array.bootDevices from:
    • all BOOT entries for internal boot
    • otherwise, the mounted /boot FLASH entry for legacy USB boot
  • Select array.boot from bootDevices using the active/runtime member:
    • prefer the entry mounted at /boot
    • otherwise fall back to the first present boot device using the existing selection order

GraphQL contract updates

  • Add BOOT to ArrayDiskType
  • Add array.bootDevices: [ArrayDisk!]!
  • Mark array.boot as deprecated but keep it supported
  • Update generated GraphQL types/schema artifacts in both api and web

Parser/runtime details

  • Keep fsStatus and fsMountpoint on parsed disk objects as internal helper fields so boot selection can distinguish:
    • the active /boot member in mirrored internal boot setups
    • the mounted USB boot device in legacy flash boot setups

Validation and tests

  • Add parser coverage for type="Boot" slots
  • Add array boot selection tests for:
    • internal boot preferred over flash
    • mirrored boot selection preferring the /boot mounted member
    • fallback behavior when only flash boot is present
    • ignoring non-mounted flash rows for legacy USB boot detection

Behavior After This PR

  • Internal boot systems expose boot members as type: BOOT
  • array.bootDevices returns the full boot set
  • array.boot remains available and returns the selected active boot disk
  • Legacy USB boot still works, using the mounted /boot flash entry
  • Mirrored internal boot setups can now be represented without conflating boot devices with cache devices

Summary by CodeRabbit

  • New Features

    • Added support for detecting and enumerating multiple boot devices, enabling better identification of all available boot options on the system.
    • Boot disk selection now prioritizes internal boot devices over legacy USB boot entries with improved fallback logic.
  • Tests

    • Added comprehensive test coverage for boot device detection and selection scenarios.

- Purpose: align array boot detection with Unraid/webgui by honoring \ entries whose type is \, and expose that distinction in GraphQL.
- Before: the API parser collapsed internal boot semantics into existing disk categories, so \ could miss internal boot devices and clients could not distinguish boot-pool entries from flash or cache disks.
- Problem: this diverged from the source-of-truth in emhttp/webgui, caused incorrect \ results for internal boot systems, and hid the boot role from generated client types.
- Change: add \ to \, preserve \ from \ in the slots parser, and select \ using the same boot-disk precedence as webgui with legacy flash fallback.
- How: update the array schema and generated GraphQL artifacts, carry non-enumerated fs status needed for boot selection, add parser and array tests, and keep cache/disk grouping separate from boot-only entries.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d8ce118a-2c72-414c-afcf-5ceb2779ab76

📥 Commits

Reviewing files that changed from the base of the PR and between fb3b50a and bce81a6.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (4)
  • api/generated-schema.graphql
  • api/src/store/state-parsers/slots.ts
  • api/src/unraid-api/graph/resolvers/array/array.model.ts
  • web/src/composables/gql/graphql.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/store/state-parsers/slots.ts

Walkthrough

A new BOOT disk type is introduced to the Unraid API's disk type system, along with boot device detection and selection logic. The changes add infrastructure to parse boot disks from the configuration, enumerate all detected boot devices, and select the active boot disk through a multi-step fallback algorithm, exposing both as GraphQL fields.

Changes

Cohort / File(s) Summary
GraphQL Schema & Type Definitions
api/generated-schema.graphql, api/src/unraid-api/graph/resolvers/array/array.model.ts, web/src/composables/gql/graphql.ts
Added BOOT enum value to ArrayDiskType; updated ArrayDisk.type field description to differentiate Boot and Data types; introduced new UnraidArray.bootDevices field to expose all detected boot devices alongside existing boot field; updated boot field description.
Boot Disk Selection Implementation
api/src/core/modules/array/get-array-data.ts
Introduced boot device detection and selection logic via helpers (getInternalBootDevices, getUsbBootDevices, getBootDevices, selectBootDisk) implementing multi-step boot selection; refactored to use bootDevices array and filtered disksWithDevice subset; exposed new bootDevices field in UnraidArray return.
State Parsing & Disk Slot Handling
api/src/store/state-parsers/slots.ts
Extended SlotType to include 'Boot' variant; refined SlotStatus to use ArrayDiskStatus keys; relaxed SlotFsStatus to string; added resolveSlotId helper for ID derivation; updated parse filter logic to accept Boot slots; added non-enumerable fsStatus and fsMountpoint properties via Object.defineProperties; added fsMountpoint optional field to IniSlot.
Test Coverage
api/src/__test__/core/modules/array/get-array-data.test.ts, api/src/__test__/store/state-parsers/slots.test.ts, api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
Added comprehensive test suite for boot disk selection logic with scenarios covering multiple boot devices, fallback selection, and device enumeration; exposed IniSlot type in slots test; added BOOT disk test case; updated mock data with bootDevices field.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hark! A BOOT type hops into the light,
With boot devices gathered left and right,
Through fallback logic we select the best,
Each disk parsed true, each line is blessed,
The schema grows with selection's might! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: exposing internal boot devices in the array GraphQL API, which aligns with the PR's core objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/internal-boot-api

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link
Member

@elibosley elibosley left a comment

Choose a reason for hiding this comment

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

This looks safe - just make sure you test for a case where there is no internal boot disk

@Ajit-Mehrotra Ajit-Mehrotra self-assigned this Mar 9, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
api/src/store/state-parsers/slots.ts (1)

100-106: Non-enumerable fsStatus is intentionally internal-only but carries subtle risks.

Defining fsStatus as non-enumerable means:

  • Direct access (disk.fsStatus) works ✓
  • The selectBootDisk function can read it ✓
  • Object spread ({...disk}) loses the property
  • JSON.stringify omits it
  • Object.keys() excludes it

This appears intentional (internal boot selection without GraphQL exposure), but consider adding a brief code comment explaining why non-enumerable was chosen, to prevent future confusion when someone attempts to spread or serialize disk objects.

📝 Suggested documentation
+            // fsStatus is non-enumerable to keep it internal for boot selection
+            // without exposing it through GraphQL or object serialization
             Object.defineProperties(result, {
                 fsStatus: {
                     value: slot.fsStatus ?? null,
                     enumerable: false,
                     writable: true,
                 },
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/store/state-parsers/slots.ts` around lines 100 - 106, The fsStatus
property is defined as non-enumerable via Object.defineProperties(result, {
fsStatus: { ... } }) which is intentional for internal-only use (readable by
selectBootDisk and direct access) but will be omitted by object spread,
JSON.stringify and Object.keys; add a brief inline comment immediately above the
Object.defineProperties call (referencing fsStatus, selectBootDisk and result)
that states why fsStatus is non-enumerable, that it must remain internal-only,
and warns about spread/serialization omissions to prevent future confusion.
🤖 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/array/array.model.ts`:
- Around line 132-133: The property fsStatus is declared without a GraphQL
decorator so it won't be exposed; add the `@Field` decorator to the fsStatus
property (e.g. `@Field`(() => String, { nullable: true })) and ensure Field is
imported from `@nestjs/graphql`, placing the decorator directly above the fsStatus
declaration in the same class (the property named fsStatus in array.model.ts) so
it becomes part of the generated GraphQL schema.

---

Nitpick comments:
In `@api/src/store/state-parsers/slots.ts`:
- Around line 100-106: The fsStatus property is defined as non-enumerable via
Object.defineProperties(result, { fsStatus: { ... } }) which is intentional for
internal-only use (readable by selectBootDisk and direct access) but will be
omitted by object spread, JSON.stringify and Object.keys; add a brief inline
comment immediately above the Object.defineProperties call (referencing
fsStatus, selectBootDisk and result) that states why fsStatus is non-enumerable,
that it must remain internal-only, and warns about spread/serialization
omissions to prevent future confusion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e0799012-0036-4d5f-b7dc-8306c9a47bdc

📥 Commits

Reviewing files that changed from the base of the PR and between 4b231ad and a5af026.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (7)
  • api/generated-schema.graphql
  • api/src/__test__/core/modules/array/get-array-data.test.ts
  • api/src/__test__/store/state-parsers/slots.test.ts
  • api/src/core/modules/array/get-array-data.ts
  • api/src/store/state-parsers/slots.ts
  • api/src/unraid-api/graph/resolvers/array/array.model.ts
  • web/src/composables/gql/graphql.ts

@codecov
Copy link

codecov bot commented Mar 9, 2026

Codecov Report

❌ Patch coverage is 81.70732% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.83%. Comparing base (4b231ad) to head (bce81a6).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...rc/unraid-api/graph/resolvers/array/array.model.ts 18.18% 9 Missing ⚠️
api/src/core/modules/array/get-array-data.ts 88.37% 5 Missing ⚠️
api/src/store/state-parsers/slots.ts 96.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1894      +/-   ##
==========================================
+ Coverage   49.73%   49.83%   +0.09%     
==========================================
  Files        1021     1021              
  Lines       69966    70037      +71     
  Branches     7383     7421      +38     
==========================================
+ Hits        34799    34901     +102     
+ Misses      35044    35013      -31     
  Partials      123      123              

☔ 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.

- Purpose: expose all detected boot devices while keeping a single selected boot entry for compatibility.
- Before: the API only returned one boot disk and could not represent mirrored internal boot members.
- Problem: internal boot can expose multiple type="Boot" entries in disks.ini, but clients had no way to inspect the full boot set.
- Change: add array.bootDevices, deprecate array.boot, and select array.boot from the active /boot-mounted boot member when multiple boot disks exist.
- Details: preserve boot-specific runtime metadata from disks.ini parsing, update array boot selection logic, add tests, and regenerate GraphQL-facing types/schema artifacts.
- Purpose: make root and api type-check runs deterministic in a clean workspace.
- Before: api type-check could run before @unraid/shared had built dist artifacts, which broke workspace package resolution.
- Problem: unresolved @unraid/shared imports cascaded into many false type errors across API models and tests.
- Change: add a pretype-check step that builds @unraid/shared before running tsc in the API package.
- Details: this keeps pnpm type-check green without changing runtime behavior or broadening TypeScript path hacks.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/composables/gql/graphql.ts`:
- Around line 263-264: Docstring for the Disk discriminant is incorrect: it says
“Array” but the exported enum discriminant is DATA. Update the property comment
for "type: ArrayDiskType" (and any related JSDoc) to reflect the actual enum
value, e.g., "Data (DATA) - used to differentiate Boot / Cache / Flash /
Parity", or rename the enum/discriminant if you intended "Array" instead of
"DATA"; ensure the text and the enum member (DATA on ArrayDiskType) are
consistent so client typings match runtime values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 00933214-ba48-44f0-9b79-cac8ccff008b

📥 Commits

Reviewing files that changed from the base of the PR and between a5af026 and fb3b50a.

⛔ Files ignored due to path filters (1)
  • api/src/unraid-api/cli/generated/graphql.ts is excluded by !**/generated/**
📒 Files selected for processing (8)
  • api/generated-schema.graphql
  • api/package.json
  • api/src/__test__/core/modules/array/get-array-data.test.ts
  • api/src/core/modules/array/get-array-data.ts
  • api/src/store/state-parsers/slots.ts
  • api/src/unraid-api/graph/resolvers/array/array.model.ts
  • api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
  • web/src/composables/gql/graphql.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/array/array.model.ts

@Ajit-Mehrotra Ajit-Mehrotra changed the title feat(array): expose internal boot disks as BOOT feat(api): expose internal boot devices in array GraphQL Mar 10, 2026
- Purpose: keep the existing array.boot field as a supported part of the contract.
- Before: array.boot was marked deprecated even though it still represents the active boot disk clients care about.
- Problem: the deprecation suggested clients should stop using array.boot entirely, which did not match the intended API design.
- Change: remove the deprecation and update the field description to state that boot returns the active boot disk.
- Details: update the source GraphQL model and sync the generated schema and client type artifacts in api and web.
@Ajit-Mehrotra Ajit-Mehrotra marked this pull request as ready for review March 10, 2026 16:06
- Purpose: address the remaining CodeRabbit review notes on the internal boot API changes.
- Before: the public disk type docs still said Array even though the enum value is DATA, and the internal-only boot-selection metadata did not explain why it stays off the GraphQL schema.
- Problem: client typings were misleading and the internal fsStatus/fsMountpoint fields looked like accidental omissions.
- Change: update the disk type description to reference Data (DATA) and document that fsStatus/fsMountpoint are runtime-only helpers kept out of GraphQL and object serialization.
- Details: sync the source model, slots parser comment, and generated API/web GraphQL artifacts with the clarified behavior.
@Ajit-Mehrotra
Copy link
Contributor Author

addressing coderabbit, testing briefly, then should be good for another round of PR review

@github-actions
Copy link
Contributor

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

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

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bce81a6c1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

disks.filter((disk) => disk.type === ArrayDiskType.BOOT);

const getUsbBootDevices = (disks: ArrayDisk[]): ArrayDisk[] =>
disks.filter((disk) => disk.type === ArrayDiskType.FLASH && disk.fsMountpoint === '/boot');

Choose a reason for hiding this comment

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

P1 Badge Restore fallback when FLASH rows lack fsMountpoint

Filtering USB boot devices to disk.fsMountpoint === '/boot' drops legacy flash boot detection when disks.ini does not include fsMountpoint (the parser sets it to null in api/src/store/state-parsers/slots.ts), so bootDevices becomes empty and array.boot resolves to undefined even though a FLASH disk is present. This is a regression from the previous behavior (which selected the flash disk by type) and will surface on environments that only report fsStatus for flash entries.

Useful? React with 👍 / 👎.

@Ajit-Mehrotra Ajit-Mehrotra requested a review from pujitm March 10, 2026 16:42
@Ajit-Mehrotra Ajit-Mehrotra merged commit 0736709 into main Mar 10, 2026
13 checks passed
@Ajit-Mehrotra Ajit-Mehrotra deleted the feat/internal-boot-api branch March 10, 2026 17:39
Ajit-Mehrotra added a commit that referenced this pull request Mar 10, 2026
## Summary

Update the array boot API to understand Unraid internal boot devices
using the same runtime signal as the webgui, and expose the full boot
device set to clients.

This change adds support for `type="Boot"` entries from `disks.ini`,
introduces `BOOT` as a public `ArrayDiskType`, and adds
`array.bootDevices` so mirrored/redundant internal boot members can be
represented explicitly.

## Why

Unraid now supports booting from storage devices instead of only USB
flash devices.

Before this PR:
- the API only treated `FLASH` as boot
- `array.boot` was a single disk chosen from `FLASH`
- internal boot members from `disks.ini` were not represented correctly
- clients had no way to inspect multiple boot devices in mirrored
internal boot setups

That meant the API no longer matched the webgui/runtime behavior for
internal boot.

## What Changed

### Boot detection and selection
- Preserve `type="Boot"` entries from `disks.ini` in the slots parser.
- Expose those entries as `ArrayDiskType.BOOT`.
- Build `array.bootDevices` from:
  - all `BOOT` entries for internal boot
  - otherwise, the mounted `/boot` `FLASH` entry for legacy USB boot
- Select `array.boot` from `bootDevices` using the active/runtime
member:
  - prefer the entry mounted at `/boot`
- otherwise fall back to the first present boot device using the
existing selection order

### GraphQL contract updates
- Add `BOOT` to `ArrayDiskType`
- Add `array.bootDevices: [ArrayDisk!]!`
- Mark `array.boot` as deprecated but keep it supported
- Update generated GraphQL types/schema artifacts in both `api` and
`web`

### Parser/runtime details
- Keep `fsStatus` and `fsMountpoint` on parsed disk objects as internal
helper fields so boot selection can distinguish:
  - the active `/boot` member in mirrored internal boot setups
  - the mounted USB boot device in legacy flash boot setups

### Validation and tests
- Add parser coverage for `type="Boot"` slots
- Add array boot selection tests for:
  - internal boot preferred over flash
  - mirrored boot selection preferring the `/boot` mounted member
  - fallback behavior when only flash boot is present
  - ignoring non-mounted flash rows for legacy USB boot detection

## Behavior After This PR

- Internal boot systems expose boot members as `type: BOOT`
- `array.bootDevices` returns the full boot set
- `array.boot` remains available and returns the selected active boot
disk
- Legacy USB boot still works, using the mounted `/boot` flash entry
- Mirrored internal boot setups can now be represented without
conflating boot devices with cache devices


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for detecting and enumerating multiple boot devices,
enabling better identification of all available boot options on the
system.
* Boot disk selection now prioritizes internal boot devices over legacy
USB boot entries with improved fallback logic.

* **Tests**
* Added comprehensive test coverage for boot device detection and
selection scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

3 participants