Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
33e218a
feat(onboarding): add internal boot step to flow
Ajit-Mehrotra Feb 27, 2026
1ceb91a
feat(onboarding): apply internal boot setup from summary
Ajit-Mehrotra Feb 27, 2026
f3b1084
test(onboarding): cover internal boot step and apply path
Ajit-Mehrotra Feb 27, 2026
3b24591
fix(onboarding): make onboarding checks pass cleanly
Ajit-Mehrotra Feb 27, 2026
bbb8fa7
fix(onboarding): gate internal boot step by array state
Ajit-Mehrotra Feb 27, 2026
73bfd11
refactor(onboarding): replace pool page parsing with graphql context
Ajit-Mehrotra Feb 27, 2026
d2c44d6
feat(api): expose boot eligibility and emhttp disk metadata
Ajit-Mehrotra Feb 27, 2026
10b1c41
refactor(onboarding): consume boot setup context from graphql
Ajit-Mehrotra Feb 27, 2026
12fecc4
feat(api): add onboarding internal boot context query
Ajit-Mehrotra Feb 27, 2026
953ef91
refactor(web): migrate internal boot step to onboarding query
Ajit-Mehrotra Feb 27, 2026
f1cadef
refactor(api): remove onboarding internal boot context resolver
Ajit-Mehrotra Feb 27, 2026
e853cde
refactor(web): use existing graphql context for internal boot
Ajit-Mehrotra Feb 27, 2026
bdccaca
feat(api): expose internal-boot vars for onboarding parity
Ajit-Mehrotra Feb 27, 2026
9e1cf0f
feat(web): hide internal-boot step on internal installs
Ajit-Mehrotra Feb 27, 2026
7922857
fix(web): send emhttp device ids to mkbootpool
Ajit-Mehrotra Feb 27, 2026
d4408f8
fix(web-onboarding): harden mkbootpool response handling
Ajit-Mehrotra Feb 27, 2026
dec0957
refactor(web-onboarding): trim internal boot disk payload
Ajit-Mehrotra Feb 27, 2026
f962f8a
fix(web-onboarding): include csrf token in mkbootpool post
Ajit-Mehrotra Feb 28, 2026
859e925
fix(web-onboarding): streamline internal boot logs and reboot submit
Ajit-Mehrotra Feb 28, 2026
0c7fd8d
feat(onboarding): add boot mode selection to configure boot step
Ajit-Mehrotra Mar 3, 2026
e9c16ac
feat(onboarding): add boot safety dialogs and summary boot section
Ajit-Mehrotra Mar 3, 2026
860e134
test(onboarding): cover boot warnings and new next-steps reboot flow
Ajit-Mehrotra Mar 3, 2026
c04fbfb
refactor(onboarding): rename step id to configure boot
Ajit-Mehrotra Mar 3, 2026
4997396
fix(onboarding): unlock setup boot radios during context loading
Ajit-Mehrotra Mar 3, 2026
d9fca18
style(onboarding): use primary accent for boot inputs
Ajit-Mehrotra Mar 3, 2026
03798a2
feat(api): add internal boot onboarding mutation flow
Ajit-Mehrotra Mar 4, 2026
e35417c
feat(web): switch internal boot setup to GraphQL mutation
Ajit-Mehrotra Mar 4, 2026
0579cf0
feat(onboarding): clarify boot selection and summary UX
Ajit-Mehrotra Mar 4, 2026
01e5122
feat(onboarding): refine boot messaging and reboot guidance
Ajit-Mehrotra Mar 4, 2026
ec41b9b
feat(onboarding): implement BIOS update flow for internal boot
Ajit-Mehrotra Mar 4, 2026
0000d3e
feat(onboarding): add sanitized error diagnostics to apply logs
Ajit-Mehrotra Mar 5, 2026
2244d2d
fix(onboarding): gate fresh-install modal on completion
Ajit-Mehrotra Mar 5, 2026
8600918
fix(onboarding): swap reboot confirm warning copy
Ajit-Mehrotra Mar 5, 2026
c08c9fc
fix(onboarding): detect tailscale preview as installed
Ajit-Mehrotra Mar 5, 2026
b67b5a8
feat(onboarding): move onboarding UI copy to i18n keys
Ajit-Mehrotra Mar 5, 2026
9fc123b
fix(onboarding): stabilize modal gating test fixtures
Ajit-Mehrotra Mar 5, 2026
881e104
fix(onboarding): prevent TLS churn during onboarding apply
Ajit-Mehrotra Mar 5, 2026
4658449
fix(onboarding): align completion logs with identity apply
Ajit-Mehrotra Mar 5, 2026
121f5d8
fix(web): infer boot mode from internal context
Ajit-Mehrotra Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,17 @@ type Disk implements Node {
"""The serial number of the disk"""
serialNum: String!

"""
Device identifier from emhttp devs.ini used by disk assignment commands
"""
emhttpDeviceId: String

"""Sector count from emhttp devs.ini for this device"""
sectors: Float

"""Sector size in bytes from emhttp devs.ini for this device"""
sectorSize: Float

"""The interface type of the disk"""
interfaceType: DiskInterfaceType!

Expand Down Expand Up @@ -580,6 +591,9 @@ type Vars implements Node {
cacheNumDevices: Int
cacheSbNumDisks: Int
fsState: String
bootEligible: Boolean
enableBootTransfer: String
reservedNames: String

"""Human friendly string of array events happening"""
fsProgress: String
Expand Down Expand Up @@ -1050,6 +1064,13 @@ type Customization {
availableLanguages: [Language!]
}

"""Result of attempting internal boot pool setup"""
type OnboardingInternalBootResult {
ok: Boolean!
code: Int
output: String!
}

type RCloneDrive {
"""Provider name"""
name: String!
Expand Down Expand Up @@ -1361,6 +1382,9 @@ type OnboardingMutations {

"""Clear onboarding override state and reload from disk"""
clearOnboardingOverride: Onboarding!

"""Create and configure internal boot pool via emcmd operations"""
createInternalBootPool(input: CreateInternalBootPoolInput!): OnboardingInternalBootResult!
}

"""Onboarding override input for testing"""
Expand Down Expand Up @@ -1435,6 +1459,15 @@ input PartnerInfoOverrideInput {
branding: BrandingConfigInput
}

"""Input for creating an internal boot pool during onboarding"""
input CreateInternalBootPoolInput {
poolName: String!
devices: [String!]!
bootSizeMiB: Int!
updateBios: Boolean!
reboot: Boolean
}

"""Unraid plugin management mutations"""
type UnraidPluginsMutations {
"""Install an Unraid plugin and track installation progress"""
Expand Down
1 change: 1 addition & 0 deletions api/src/__test__/store/modules/emhttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
expect(varState).toMatchInlineSnapshot(`
{
"bindMgt": false,
"bootEligible": false,
"cacheNumDevices": NaN,
"cacheSbNumDisks": NaN,
"comment": "Dev Server",
Expand Down
1 change: 1 addition & 0 deletions api/src/__test__/store/state-parsers/var.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ test('Returns parsed state file', async () => {
expect(parse(stateFile)).toMatchInlineSnapshot(`
{
"bindMgt": false,
"bootEligible": false,
"cacheNumDevices": NaN,
"cacheSbNumDisks": NaN,
"comment": "Dev Server",
Expand Down
6 changes: 6 additions & 0 deletions api/src/core/types/states/var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export type Var = {
fsProgress: string;
/** Current state of the array. */
fsState: string;
/** Whether this system is eligible to create a bootable pool. */
bootEligible: boolean;
/** Whether boot transfer from flash to internal is enabled ("yes" means currently booted from flash). */
enableBootTransfer?: string;
/** Comma-separated list of reserved names from var.ini. */
reservedNames?: string;
fsUnmountableMask: string;
fuseDirectio: string;
fuseDirectioDefault: string;
Expand Down
4 changes: 4 additions & 0 deletions api/src/store/state-parsers/var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export type VarIni = {
fsNumUnmountable: string;
fsProgress: string;
fsState: string;
bootEligible: string;
enableBootTransfer?: string;
reservedNames?: string;
fsUnmountableMask: string;
fuseDirectio: string;
fuseDirectioDefault: string;
Expand Down Expand Up @@ -195,6 +198,7 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
fsCopyPrcnt: toNumber(iniFile.fsCopyPrcnt),
fsNumMounted: toNumber(iniFile.fsNumMounted),
fsNumUnmountable: toNumber(iniFile.fsNumUnmountable),
bootEligible: iniBooleanToJsBoolean(iniFile.bootEligible, false),
hideDotFiles: iniBooleanToJsBoolean(iniFile.hideDotFiles, false),
localMaster: iniBooleanToJsBoolean(iniFile.localMaster, false),
maxArraysz: toNumber(iniFile.maxArraysz),
Expand Down
34 changes: 34 additions & 0 deletions api/src/unraid-api/cli/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,15 @@ export type CreateApiKeyInput = {
roles?: InputMaybe<Array<Role>>;
};

/** Input for creating an internal boot pool during onboarding */
export type CreateInternalBootPoolInput = {
bootSizeMiB: Scalars['Int']['input'];
devices: Array<Scalars['String']['input']>;
poolName: Scalars['String']['input'];
reboot?: InputMaybe<Scalars['Boolean']['input']>;
updateBios: Scalars['Boolean']['input'];
};

export type CreateRCloneRemoteInput = {
name: Scalars['String']['input'];
parameters: Scalars['JSON']['input'];
Expand Down Expand Up @@ -701,6 +710,8 @@ export type Disk = Node & {
bytesPerSector: Scalars['Float']['output'];
/** The device path of the disk (e.g. /dev/sdb) */
device: Scalars['String']['output'];
/** Device identifier from emhttp devs.ini used by disk assignment commands */
emhttpDeviceId?: Maybe<Scalars['String']['output']>;
/** The firmware revision of the disk */
firmwareRevision: Scalars['String']['output'];
id: Scalars['PrefixedID']['output'];
Expand All @@ -712,6 +723,10 @@ export type Disk = Node & {
name: Scalars['String']['output'];
/** The partitions on the disk */
partitions: Array<DiskPartition>;
/** Sector size in bytes from emhttp devs.ini for this device */
sectorSize?: Maybe<Scalars['Float']['output']>;
/** Sector count from emhttp devs.ini for this device */
sectors?: Maybe<Scalars['Float']['output']>;
/** The number of sectors per track */
sectorsPerTrack: Scalars['Float']['output'];
/** The serial number of the disk */
Expand Down Expand Up @@ -1963,20 +1978,36 @@ export type Onboarding = {
status: OnboardingStatus;
};

/** Result of attempting internal boot pool setup */
export type OnboardingInternalBootResult = {
__typename?: 'OnboardingInternalBootResult';
code?: Maybe<Scalars['Int']['output']>;
ok: Scalars['Boolean']['output'];
output: Scalars['String']['output'];
};

/** Onboarding related mutations */
export type OnboardingMutations = {
__typename?: 'OnboardingMutations';
/** Clear onboarding override state and reload from disk */
clearOnboardingOverride: Onboarding;
/** Mark onboarding as completed */
completeOnboarding: Onboarding;
/** Create and configure internal boot pool via emcmd operations */
createInternalBootPool: OnboardingInternalBootResult;
/** Reset onboarding progress (for testing) */
resetOnboarding: Onboarding;
/** Override onboarding state for testing (in-memory only) */
setOnboardingOverride: Onboarding;
};


/** Onboarding related mutations */
export type OnboardingMutationsCreateInternalBootPoolArgs = {
input: CreateInternalBootPoolInput;
};


/** Onboarding related mutations */
export type OnboardingMutationsSetOnboardingOverrideArgs = {
input: OnboardingOverrideInput;
Expand Down Expand Up @@ -3175,6 +3206,7 @@ export type UserAccount = Node & {
export type Vars = Node & {
__typename?: 'Vars';
bindMgt?: Maybe<Scalars['Boolean']['output']>;
bootEligible?: Maybe<Scalars['Boolean']['output']>;
cacheNumDevices?: Maybe<Scalars['Int']['output']>;
cacheSbNumDisks?: Maybe<Scalars['Int']['output']>;
comment?: Maybe<Scalars['String']['output']>;
Expand All @@ -3187,6 +3219,7 @@ export type Vars = Node & {
domain?: Maybe<Scalars['String']['output']>;
domainLogin?: Maybe<Scalars['String']['output']>;
domainShort?: Maybe<Scalars['String']['output']>;
enableBootTransfer?: Maybe<Scalars['String']['output']>;
enableFruit?: Maybe<Scalars['String']['output']>;
flashGuid?: Maybe<Scalars['String']['output']>;
flashProduct?: Maybe<Scalars['String']['output']>;
Expand Down Expand Up @@ -3274,6 +3307,7 @@ export type Vars = Node & {
/** Registration owner */
regTo?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<RegistrationType>;
reservedNames?: Maybe<Scalars['String']['output']>;
safeMode?: Maybe<Scalars['Boolean']['output']>;
sbClean?: Maybe<Scalars['Boolean']['output']>;
sbEvents?: Maybe<Scalars['Int']['output']>;
Expand Down
24 changes: 24 additions & 0 deletions api/src/unraid-api/graph/resolvers/disks/disks.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ export class Disk extends Node {
@IsString()
serialNum!: string;

@Field(() => String, {
nullable: true,
description: 'Device identifier from emhttp devs.ini used by disk assignment commands',
})
@IsOptional()
@IsString()
emhttpDeviceId?: string;

@Field(() => Number, {
nullable: true,
description: 'Sector count from emhttp devs.ini for this device',
})
@IsOptional()
@IsNumber()
sectors?: number;

@Field(() => Number, {
nullable: true,
description: 'Sector size in bytes from emhttp devs.ini for this device',
})
@IsOptional()
@IsNumber()
sectorSize?: number;

@Field(() => DiskInterfaceType, { description: 'The interface type of the disk' })
@IsEnum(DiskInterfaceType)
interfaceType!: DiskInterfaceType;
Expand Down
35 changes: 35 additions & 0 deletions api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,27 @@ describe('DisksService', () => {
},
];

const mockEmhttpDevices = [
{
id: 'S4ENNF0N123456',
device: 'sda',
sectors: 1000215216,
sector_size: 512,
},
{
id: 'WD-WCC7K7YL9876',
device: 'sdb',
sectors: 7814037168,
sector_size: 512,
},
{
id: 'WD-SPUNDOWN123',
device: 'sdd',
sectors: 7814037168,
sector_size: 512,
},
];

const mockDiskLayoutData: Systeminformation.DiskLayoutData[] = [
{
device: '/dev/sda',
Expand Down Expand Up @@ -309,6 +330,9 @@ describe('DisksService', () => {
if (key === 'store.emhttp.disks') {
return mockArrayDisks;
}
if (key === 'store.emhttp.devices') {
return mockEmhttpDevices;
}
return defaultValue;
}),
};
Expand Down Expand Up @@ -353,6 +377,7 @@ describe('DisksService', () => {
expect(mockDiskLayout).toHaveBeenCalledTimes(1);
expect(mockBlockDevices).toHaveBeenCalledTimes(1);
expect(configService.get).toHaveBeenCalledWith('store.emhttp.disks', []);
expect(configService.get).toHaveBeenCalledWith('store.emhttp.devices', []);
expect(mockBatchProcess).toHaveBeenCalledTimes(1);

expect(disks).toHaveLength(mockDiskLayoutData.length);
Expand All @@ -370,6 +395,9 @@ describe('DisksService', () => {
expect(spinningDisk).toBeDefined();
expect(spinningDisk?.isSpinning).toBe(true); // From state
expect(spinningDisk?.interfaceType).toBe(DiskInterfaceType.SATA);
expect(spinningDisk?.emhttpDeviceId).toBe('WD-WCC7K7YL9876');
expect(spinningDisk?.sectors).toBe(7814037168);
expect(spinningDisk?.sectorSize).toBe(512);

// Check spun down disk
const spunDownDisk = disks.find((d) => d.id === 'WD-SPUNDOWN123');
Expand All @@ -389,6 +417,9 @@ describe('DisksService', () => {
if (key === 'store.emhttp.disks') {
return [];
}
if (key === 'store.emhttp.devices') {
return [];
}
return defaultValue;
});

Expand All @@ -413,6 +444,9 @@ describe('DisksService', () => {
if (key === 'store.emhttp.disks') {
return disksWithSpaces;
}
if (key === 'store.emhttp.devices') {
return mockEmhttpDevices;
}
return defaultValue;
});

Expand Down Expand Up @@ -453,6 +487,7 @@ describe('DisksService', () => {

// Verify we're accessing the state through ConfigService
expect(configService.get).toHaveBeenCalledWith('store.emhttp.disks', []);
expect(configService.get).toHaveBeenCalledWith('store.emhttp.devices', []);
});

it('should handle empty disk layout or block devices', async () => {
Expand Down
Loading
Loading