From 048ffbb091aa5697f82ac35ee96bce053d0e74a9 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 10:34:55 -0400 Subject: [PATCH 01/12] fix: prefer devs.ini for onboarding boot eligibility --- .../OnboardingInternalBootStep.test.ts | 54 +++++++++++---- .../steps/OnboardingInternalBootStep.vue | 67 ++----------------- 2 files changed, 45 insertions(+), 76 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts index bb8d7d8b5b..c9f9af5855 100644 --- a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts @@ -113,28 +113,24 @@ describe('OnboardingInternalBootStep', () => { device: '/dev/sda', size: gib(32), serialNum: 'BOOT-1', - emhttpDeviceId: 'boot-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdb', size: gib(32), serialNum: 'PARITY-1', - emhttpDeviceId: 'parity-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdc', size: gib(32), serialNum: 'ARRAY-1', - emhttpDeviceId: 'array-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdd', size: gib(32), serialNum: 'CACHE-1', - emhttpDeviceId: 'cache-disk', interfaceType: DiskInterfaceType.SATA, }, { @@ -166,10 +162,6 @@ describe('OnboardingInternalBootStep', () => { expect(wrapper.text()).toContain('ARRAY_NOT_STOPPED'); expect(wrapper.text()).toContain('ENABLE_BOOT_TRANSFER_UNKNOWN'); expect(wrapper.text()).toContain('BOOT_ELIGIBLE_UNKNOWN'); - expect(wrapper.text()).toContain('ASSIGNED_TO_BOOT'); - expect(wrapper.text()).toContain('ASSIGNED_TO_PARITY'); - expect(wrapper.text()).toContain('ASSIGNED_TO_ARRAY'); - expect(wrapper.text()).toContain('ASSIGNED_TO_CACHE'); expect(wrapper.text()).toContain('TOO_SMALL'); expect(wrapper.text()).not.toContain('NO_UNASSIGNED_DISKS'); expect(wrapper.find('[data-testid="brand-button"]').attributes('disabled')).toBeDefined(); @@ -317,14 +309,12 @@ describe('OnboardingInternalBootStep', () => { device: '/dev/sda', size: gib(32), serialNum: 'BOOT-1', - emhttpDeviceId: 'boot-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdb', size: gib(32), serialNum: 'PARITY-1', - emhttpDeviceId: 'parity-disk', interfaceType: DiskInterfaceType.SATA, }, ], @@ -436,7 +426,6 @@ describe('OnboardingInternalBootStep', () => { device: '/dev/sda', size: gib(32), serialNum: 'CACHE-1', - emhttpDeviceId: 'cache-disk', interfaceType: DiskInterfaceType.SATA, }, { @@ -474,7 +463,6 @@ describe('OnboardingInternalBootStep', () => { expect(deviceSelect.text()).toContain('USB-1'); expect(deviceSelect.text()).not.toContain('CACHE-1'); expect(deviceSelect.text()).not.toContain('SMALL-1'); - expect(wrapper.text()).not.toContain('ASSIGNED_TO_CACHE'); const biosWarning = wrapper.get('[data-testid="internal-boot-update-bios-warning"]'); const eligibilityPanel = wrapper.get('[data-testid="internal-boot-eligibility-panel"]'); expect( @@ -483,8 +471,48 @@ describe('OnboardingInternalBootStep', () => { ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); await wrapper.get('[data-testid="internal-boot-eligibility-toggle"]').trigger('click'); await flushPromises(); - expect(wrapper.text()).toContain('ASSIGNED_TO_CACHE'); expect(wrapper.text()).toContain('TOO_SMALL'); expect(wrapper.find('[data-testid="brand-button"]').attributes('disabled')).toBeUndefined(); }); + + it('treats disks present in devs.ini as unassigned even when array data still references the device', async () => { + draftStore.bootMode = 'storage'; + contextResult.value = { + array: { + state: ArrayState.STOPPED, + boot: null, + parities: [], + disks: [{ device: '/dev/sda' }], + caches: [], + }, + vars: { + fsState: 'Stopped', + bootEligible: true, + enableBootTransfer: 'yes', + reservedNames: '', + }, + shares: [], + disks: [ + { + device: '/dev/sda', + size: gib(32), + serialNum: 'UNASSIGNED-1', + emhttpDeviceId: 'disk-1', + interfaceType: DiskInterfaceType.SATA, + }, + ], + }; + + const wrapper = mountComponent(); + await flushPromises(); + + expect(wrapper.find('[data-testid="internal-boot-intro-panel"]').exists()).toBe(true); + expect(wrapper.find('[data-testid="internal-boot-eligibility-panel"]').exists()).toBe(false); + const selects = wrapper.findAll('select'); + expect(selects).toHaveLength(3); + expect(selects[1]?.text()).toContain('UNASSIGNED-1'); + expect(wrapper.text()).not.toContain('ASSIGNED_TO_ARRAY'); + expect(wrapper.text()).not.toContain('NO_UNASSIGNED_DISKS'); + expect(wrapper.find('[data-testid="brand-button"]').attributes('disabled')).toBeUndefined(); + }); }); diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue index f7503717c8..e11b1ff145 100644 --- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue @@ -61,23 +61,12 @@ type InternalBootSystemEligibilityCode = | 'ENABLE_BOOT_TRANSFER_UNKNOWN' | 'BOOT_ELIGIBLE_FALSE' | 'BOOT_ELIGIBLE_UNKNOWN'; -type InternalBootDiskEligibilityCode = - | 'ASSIGNED_TO_BOOT' - | 'ASSIGNED_TO_ARRAY' - | 'ASSIGNED_TO_PARITY' - | 'ASSIGNED_TO_CACHE' - | 'TOO_SMALL'; +type InternalBootDiskEligibilityCode = 'TOO_SMALL'; const MIN_BOOT_SIZE_MIB = 4096; const MIN_ELIGIBLE_DEVICE_SIZE_MIB = MIN_BOOT_SIZE_MIB * 2; const DEFAULT_BOOT_SIZE_MIB = 16384; const BOOT_SIZE_PRESETS_MIB = [16384, 32768, 65536, 131072]; -const ASSIGNMENT_ELIGIBILITY_CODES = new Set([ - 'ASSIGNED_TO_BOOT', - 'ASSIGNED_TO_ARRAY', - 'ASSIGNED_TO_PARITY', - 'ASSIGNED_TO_CACHE', -]); const SYSTEM_ELIGIBILITY_MESSAGE_KEYS: Record = { ARRAY_NOT_STOPPED: 'onboarding.internalBootStep.eligibility.codes.ARRAY_NOT_STOPPED', ALREADY_INTERNAL_BOOT: 'onboarding.internalBootStep.eligibility.codes.ALREADY_INTERNAL_BOOT', @@ -90,10 +79,6 @@ const SYSTEM_ELIGIBILITY_MESSAGE_KEYS: Record = { - ASSIGNED_TO_BOOT: 'onboarding.internalBootStep.eligibility.codes.ASSIGNED_TO_BOOT', - ASSIGNED_TO_ARRAY: 'onboarding.internalBootStep.eligibility.codes.ASSIGNED_TO_ARRAY', - ASSIGNED_TO_PARITY: 'onboarding.internalBootStep.eligibility.codes.ASSIGNED_TO_PARITY', - ASSIGNED_TO_CACHE: 'onboarding.internalBootStep.eligibility.codes.ASSIGNED_TO_CACHE', TOO_SMALL: 'onboarding.internalBootStep.eligibility.codes.TOO_SMALL', }; @@ -161,46 +146,6 @@ const bootSizePreset = ref(''); const customBootSizeGb = ref(''); const updateBios = ref(true); -const addDiskEligibilityCode = ( - diskCodesByDevice: Map>, - deviceName: string | null | undefined, - code: InternalBootDiskEligibilityCode -) => { - const normalizedDeviceName = normalizeDeviceName(deviceName); - if (!normalizedDeviceName) { - return; - } - - const existingCodes = diskCodesByDevice.get(normalizedDeviceName); - if (existingCodes) { - existingCodes.add(code); - return; - } - - diskCodesByDevice.set(normalizedDeviceName, new Set([code])); -}; - -const diskEligibilityCodesByDevice = computed(() => { - const data: GetInternalBootContextQuery | null | undefined = contextResult.value; - const codesByDevice = new Map>(); - if (!data) { - return codesByDevice; - } - - addDiskEligibilityCode(codesByDevice, data.array.boot?.device, 'ASSIGNED_TO_BOOT'); - for (const parityDisk of data.array.parities) { - addDiskEligibilityCode(codesByDevice, parityDisk.device, 'ASSIGNED_TO_PARITY'); - } - for (const arrayDisk of data.array.disks) { - addDiskEligibilityCode(codesByDevice, arrayDisk.device, 'ASSIGNED_TO_ARRAY'); - } - for (const cacheDisk of data.array.caches) { - addDiskEligibilityCode(codesByDevice, cacheDisk.device, 'ASSIGNED_TO_CACHE'); - } - - return codesByDevice; -}); - const templateData = computed(() => { const data: GetInternalBootContextQuery | null | undefined = contextResult.value; if (!data) { @@ -208,11 +153,12 @@ const templateData = computed(() => { } const deviceOptions = data.disks + .filter((disk) => typeof disk.emhttpDeviceId === 'string' && disk.emhttpDeviceId.trim().length > 0) .map((disk) => { const device = normalizeDeviceName(disk.device); const sizeBytes = disk.size; const sizeMiB = toSizeMiB(sizeBytes); - const ineligibilityCodes = Array.from(diskEligibilityCodesByDevice.value.get(device) ?? []); + const ineligibilityCodes: InternalBootDiskEligibilityCode[] = []; if (sizeMiB !== null && sizeMiB < MIN_ELIGIBLE_DEVICE_SIZE_MIB) { ineligibilityCodes.push('TOO_SMALL'); @@ -311,11 +257,6 @@ const allDeviceOptions = computed(() => templateData.value?.deviceOptions ?? []) const deviceOptions = computed(() => allDeviceOptions.value.filter((option) => option.ineligibilityCodes.length === 0) ); -const unassignedDeviceOptions = computed(() => - allDeviceOptions.value.filter( - (option) => !option.ineligibilityCodes.some((code) => ASSIGNMENT_ELIGIBILITY_CODES.has(code)) - ) -); const slotOptions = computed(() => templateData.value?.slotOptions ?? [1, 2]); const reservedNames = computed(() => new Set(templateData.value?.reservedNames ?? [])); const shareNames = computed(() => new Set(templateData.value?.shareNames ?? [])); @@ -341,7 +282,7 @@ const systemEligibilityCodes = computed(() if (bootEligibilityState.value === 'unknown') { codes.push('BOOT_ELIGIBLE_UNKNOWN'); } - if (unassignedDeviceOptions.value.length === 0) { + if (allDeviceOptions.value.length === 0) { codes.push('NO_UNASSIGNED_DISKS'); } From faeea7545df90b4ed07f9ae252c9c82492ec7707 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 11:16:12 -0400 Subject: [PATCH 02/12] Use devs.ini as onboarding assignability source --- api/generated-schema.graphql | 8 +- api/src/unraid-api/cli/generated/graphql.ts | 5 +- .../graph/resolvers/disks/disks.model.ts | 8 -- .../resolvers/disks/disks.resolver.spec.ts | 37 ++++++++ .../graph/resolvers/disks/disks.resolver.ts | 9 ++ .../resolvers/disks/disks.service.spec.ts | 42 +++++++-- .../graph/resolvers/disks/disks.service.ts | 52 ++++------- .../OnboardingInternalBootStep.test.ts | 83 +++--------------- .../Onboarding/OnboardingSummaryStep.test.ts | 19 ++-- web/auto-imports.d.ts | 86 +++++++++---------- .../graphql/getInternalBootContext.query.ts | 13 +-- .../steps/OnboardingInternalBootStep.vue | 8 +- .../steps/OnboardingSummaryStep.vue | 7 +- web/src/composables/gql/gql.ts | 6 +- web/src/composables/gql/graphql.ts | 7 +- web/src/composables/gql/index.ts | 2 +- 16 files changed, 181 insertions(+), 211 deletions(-) diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index d6dd77cc91..c1858e6d6d 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -346,11 +346,6 @@ 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 - """The interface type of the disk""" interfaceType: DiskInterfaceType! @@ -3181,6 +3176,7 @@ type Query { info: Info! docker: Docker! disks: [Disk!]! + assignableDisks: [Disk!]! disk(id: PrefixedID!): Disk! rclone: RCloneBackupSettings! logFiles: [LogFile!]! @@ -3569,4 +3565,4 @@ type Subscription { systemMetricsTemperature: TemperatureMetrics upsUpdates: UPSDevice! pluginInstallUpdates(operationId: ID!): PluginInstallEvent! -} \ No newline at end of file +} diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index a12769f4ec..cfb4c34834 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -714,8 +714,6 @@ 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; /** The firmware revision of the disk */ firmwareRevision: Scalars['String']['output']; id: Scalars['PrefixedID']['output']; @@ -2265,6 +2263,7 @@ export type Query = { apiKeyPossibleRoles: Array; apiKeys: Array; array: UnraidArray; + assignableDisks: Array; cloud: Cloud; config: Config; connect: Connect; @@ -3552,4 +3551,4 @@ export const GetSsoUsersDocument = {"kind":"Document","definitions":[{"kind":"Op export const SystemReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SystemReport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"machineId"}},{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"serial"}},{"kind":"Field","name":{"kind":"Name","value":"uuid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraid"}},{"kind":"Field","name":{"kind":"Name","value":"kernel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"packages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"openssl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const ConnectStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dynamicRemoteAccess"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabledType"}},{"kind":"Field","name":{"kind":"Name","value":"runningType"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode; export const ServicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"online"}},{"kind":"Field","name":{"kind":"Name","value":"uptime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode; -export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode; diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.model.ts b/api/src/unraid-api/graph/resolvers/disks/disks.model.ts index ee30bce0e6..0a7b276754 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.model.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.model.ts @@ -123,14 +123,6 @@ 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(() => DiskInterfaceType, { description: 'The interface type of the disk' }) @IsEnum(DiskInterfaceType) interfaceType!: DiskInterfaceType; diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts index a6ad8d5617..4cabb45216 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts @@ -14,6 +14,7 @@ import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.servic // Mock the DisksService const mockDisksService = { getDisks: vi.fn(), + getAssignableDisks: vi.fn(), getTemperature: vi.fn(), }; @@ -88,6 +89,42 @@ describe('DisksResolver', () => { }); }); + describe('assignableDisks', () => { + it('should return assignable disks from the service', async () => { + const mockResult: Disk[] = [ + { + id: 'SERIAL123', + device: '/dev/sda', + type: 'SSD', + name: 'Samsung SSD 860 EVO 1TB', + vendor: 'Samsung', + size: 1000204886016, + bytesPerSector: 512, + totalCylinders: 121601, + totalHeads: 255, + totalSectors: 1953525168, + totalTracks: 31008255, + tracksPerCylinder: 255, + sectorsPerTrack: 63, + firmwareRevision: 'RVT04B6Q', + serialNum: 'SERIAL123', + interfaceType: DiskInterfaceType.SATA, + smartStatus: DiskSmartStatus.OK, + temperature: -1, + partitions: [], + isSpinning: false, + }, + ]; + mockDisksService.getAssignableDisks.mockResolvedValue(mockResult); + + const result = await resolver.assignableDisks(); + + expect(result).toEqual(mockResult); + expect(service.getAssignableDisks).toHaveBeenCalledTimes(1); + expect(service.getAssignableDisks).toHaveBeenCalledWith(); + }); + }); + describe('temperature', () => { it('should call getTemperature with the disk device', async () => { const mockDisk: Disk = { diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts index 8341e11383..2bec50a536 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts @@ -20,6 +20,15 @@ export class DisksResolver { return this.disksService.getDisks(); } + @Query(() => [Disk]) + @UsePermissions({ + action: AuthAction.READ_ANY, + resource: Resource.DISK, + }) + public async assignableDisks() { + return this.disksService.getAssignableDisks(); + } + @Query(() => Disk) @UsePermissions({ action: AuthAction.READ_ANY, diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts b/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts index b9bfb474ad..c01be11195 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts @@ -377,7 +377,6 @@ 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); @@ -395,8 +394,6 @@ describe('DisksService', () => { expect(spinningDisk).toBeDefined(); expect(spinningDisk?.isSpinning).toBe(true); // From state expect(spinningDisk?.interfaceType).toBe(DiskInterfaceType.SATA); - expect(spinningDisk?.emhttpDeviceId).toBe('WD-WCC7K7YL9876'); - // Check spun down disk const spunDownDisk = disks.find((d) => d.id === 'WD-SPUNDOWN123'); expect(spunDownDisk).toBeDefined(); @@ -485,7 +482,6 @@ 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 () => { @@ -531,6 +527,42 @@ describe('DisksService', () => { }); }); + describe('getAssignableDisks', () => { + it('should return only disks present in devs.ini', async () => { + vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: unknown) => { + if (key === 'store.emhttp.disks') { + return mockArrayDisks; + } + if (key === 'store.emhttp.devices') { + return [ + { device: '/dev/sda' }, + { device: '/dev/sdd' }, + ]; + } + return defaultValue; + }); + + const disks = await service.getAssignableDisks(); + + expect(disks.map((disk) => disk.device)).toEqual(['/dev/sda', '/dev/sdd']); + expect(configService.get).toHaveBeenCalledWith('store.emhttp.devices', []); + }); + + it('should return an empty array when devs.ini is empty', async () => { + vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: unknown) => { + if (key === 'store.emhttp.disks') { + return mockArrayDisks; + } + if (key === 'store.emhttp.devices') { + return []; + } + return defaultValue; + }); + + await expect(service.getAssignableDisks()).resolves.toEqual([]); + }); + }); + describe('getInternalBootDevices', () => { it('should return disks that match the Unraid internal boot partition layout', async () => { mockExeca.mockResolvedValue({ @@ -631,7 +663,6 @@ describe('DisksService', () => { sectorsPerTrack: 1, firmwareRevision: '1', serialNum: 'internal', - emhttpDeviceId: 'internal', interfaceType: DiskInterfaceType.PCIE, smartStatus: DiskSmartStatus.OK, partitions: [], @@ -653,7 +684,6 @@ describe('DisksService', () => { sectorsPerTrack: 1, firmwareRevision: '1', serialNum: 'usb', - emhttpDeviceId: 'usb', interfaceType: DiskInterfaceType.USB, smartStatus: DiskSmartStatus.OK, partitions: [], diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.service.ts b/api/src/unraid-api/graph/resolvers/disks/disks.service.ts index 5c7562c674..b0a4bd82b6 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.service.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.service.ts @@ -45,13 +45,7 @@ const SmartDataSchema = z.object({ .optional() .nullable(), }); -interface EmhttpDevice { - id?: string; - device?: string; -} - interface EmhttpDeviceRecord { - id?: unknown; device?: unknown; } @@ -80,10 +74,10 @@ export class DisksService { constructor(private readonly configService: ConfigService) {} - private getEmhttpDevices(): EmhttpDevice[] { + private getEmhttpDeviceNames(): Set { const rawDevicesValue = this.configService.get('store.emhttp.devices', []); const rawDevices = Array.isArray(rawDevicesValue) ? rawDevicesValue : []; - const emhttpDevices: EmhttpDevice[] = []; + const deviceNames = new Set(); for (const raw of rawDevices) { if (!raw || typeof raw !== 'object') { @@ -91,32 +85,16 @@ export class DisksService { } const record = raw as EmhttpDeviceRecord; - const id = typeof record.id === 'string' ? record.id.trim() : ''; const device = typeof record.device === 'string' ? record.device.trim() : ''; - if (!id || !device) { + if (!device) { continue; } - emhttpDevices.push({ - id, - device: normalizeDeviceName(device), - }); - } - - return emhttpDevices; - } - - private findEmhttpDevice( - disk: Systeminformation.DiskLayoutData, - emhttpDevices: EmhttpDevice[] - ): EmhttpDevice | undefined { - const normalizedSystemDevice = normalizeDeviceName(disk.device); - if (!normalizedSystemDevice) { - return undefined; + deviceNames.add(normalizeDeviceName(device)); } - return emhttpDevices.find((emhttpDevice) => emhttpDevice.device === normalizedSystemDevice); + return deviceNames; } public async getTemperature(device: string): Promise { @@ -164,6 +142,17 @@ export class DisksService { return disk; } + public async getAssignableDisks(): Promise { + const assignableDevices = this.getEmhttpDeviceNames(); + + if (assignableDevices.size === 0) { + return []; + } + + const disks = await this.getDisks(); + return disks.filter((disk) => assignableDevices.has(normalizeDeviceName(disk.device))); + } + public async getInternalBootDevices(): Promise { const [disks, deviceNames] = await Promise.all([ this.getDisks(), @@ -291,8 +280,7 @@ export class DisksService { private async parseDisk( disk: Systeminformation.DiskLayoutData, partitionsToParse: Systeminformation.BlockDevicesData[], - arrayDisks: ArrayDisk[], - emhttpDevices: EmhttpDevice[] + arrayDisks: ArrayDisk[] ): Promise> { const partitions = partitionsToParse // Only get partitions from this disk @@ -357,12 +345,9 @@ export class DisksService { } const arrayDisk = arrayDisks.find((d) => d.id.trim() === disk.serialNum.trim()); - const emhttpDevice = this.findEmhttpDevice(disk, emhttpDevices); - return { ...disk, id: disk.serialNum, // Ensure id is set - emhttpDeviceId: emhttpDevice?.id, smartStatus: DiskSmartStatus[disk.smartStatus?.toUpperCase() as keyof typeof DiskSmartStatus] ?? DiskSmartStatus.UNKNOWN, @@ -380,9 +365,8 @@ export class DisksService { devices.filter((device) => device.type === 'part') ); const arrayDisks = this.configService.get('store.emhttp.disks', []); - const emhttpDevices = this.getEmhttpDevices(); const { data } = await batchProcess(await diskLayout(), async (disk) => - this.parseDisk(disk, partitions, arrayDisks, emhttpDevices) + this.parseDisk(disk, partitions, arrayDisks) ); return data; } diff --git a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts index c9f9af5855..1559d41b0a 100644 --- a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts @@ -96,10 +96,7 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STARTED, - boot: { device: '/dev/sda' }, - parities: [{ device: '/dev/sdb' }], - disks: [{ device: '/dev/sdc' }], - caches: [{ name: 'cache', device: '/dev/sdd' }], + caches: [{ name: 'cache' }], }, vars: { fsState: 'Started', @@ -108,7 +105,7 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), @@ -137,14 +134,12 @@ describe('OnboardingInternalBootStep', () => { device: '/dev/sde', size: gib(6), serialNum: 'SMALL-1', - emhttpDeviceId: 'small-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdf', size: gib(32), serialNum: 'USB-1', - emhttpDeviceId: 'usb-disk', interfaceType: DiskInterfaceType.USB, }, ], @@ -185,9 +180,6 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [], caches: [], }, vars: { @@ -197,12 +189,11 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), serialNum: 'WD-TEST-1234', - emhttpDeviceId: 'eligible-disk', interfaceType: DiskInterfaceType.SATA, }, ], @@ -220,9 +211,6 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [], caches: [], }, vars: { @@ -232,12 +220,11 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), serialNum: 'ELIGIBLE-1', - emhttpDeviceId: 'eligible-disk', interfaceType: DiskInterfaceType.SATA, }, ], @@ -258,10 +245,7 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [], - caches: [{ name: 'cache', device: '/dev/sdz' }], + caches: [{ name: 'cache' }], }, vars: { fsState: 'Stopped', @@ -270,12 +254,11 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), serialNum: 'ELIGIBLE-1', - emhttpDeviceId: 'eligible-disk', interfaceType: DiskInterfaceType.SATA, }, ], @@ -292,9 +275,6 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STOPPED, - boot: { device: '/dev/sda' }, - parities: [{ device: '/dev/sdb' }], - disks: [], caches: [], }, vars: { @@ -304,20 +284,7 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ - { - device: '/dev/sda', - size: gib(32), - serialNum: 'BOOT-1', - interfaceType: DiskInterfaceType.SATA, - }, - { - device: '/dev/sdb', - size: gib(32), - serialNum: 'PARITY-1', - interfaceType: DiskInterfaceType.SATA, - }, - ], + assignableDisks: [], }; const wrapper = mountComponent(); @@ -335,9 +302,6 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [], caches: [], }, vars: { @@ -348,12 +312,11 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), serialNum: 'ELIGIBLE-1', - emhttpDeviceId: 'eligible-disk', interfaceType: DiskInterfaceType.SATA, }, ], @@ -373,9 +336,6 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STARTED, - boot: null, - parities: [], - disks: [], caches: [], }, vars: { @@ -385,12 +345,11 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), serialNum: 'ELIGIBLE-1', - emhttpDeviceId: 'eligible-disk', interfaceType: DiskInterfaceType.SATA, }, ], @@ -409,10 +368,7 @@ describe('OnboardingInternalBootStep', () => { contextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [], - caches: [{ name: 'cache', device: '/dev/sda' }], + caches: [{ name: 'cache' }], }, vars: { fsState: 'Stopped', @@ -421,32 +377,23 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ - { - device: '/dev/sda', - size: gib(32), - serialNum: 'CACHE-1', - interfaceType: DiskInterfaceType.SATA, - }, + assignableDisks: [ { device: '/dev/sdb', size: gib(6), serialNum: 'SMALL-1', - emhttpDeviceId: 'small-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdc', size: gib(32), serialNum: 'ELIGIBLE-1', - emhttpDeviceId: 'eligible-disk', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdd', size: gib(32), serialNum: 'USB-1', - emhttpDeviceId: 'usb-disk', interfaceType: DiskInterfaceType.USB, }, ], @@ -475,14 +422,11 @@ describe('OnboardingInternalBootStep', () => { expect(wrapper.find('[data-testid="brand-button"]').attributes('disabled')).toBeUndefined(); }); - it('treats disks present in devs.ini as unassigned even when array data still references the device', async () => { + it('treats disks present in devs.ini as assignable', async () => { draftStore.bootMode = 'storage'; contextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [{ device: '/dev/sda' }], caches: [], }, vars: { @@ -492,12 +436,11 @@ describe('OnboardingInternalBootStep', () => { reservedNames: '', }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: gib(32), serialNum: 'UNASSIGNED-1', - emhttpDeviceId: 'disk-1', interfaceType: DiskInterfaceType.SATA, }, ], diff --git a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts index 893f45cebc..cc3b692d47 100644 --- a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts @@ -315,28 +315,23 @@ describe('OnboardingSummaryStep', () => { internalBootContextResult.value = { array: { state: ArrayState.STOPPED, - boot: null, - parities: [], - disks: [], caches: [], }, vars: { bootEligible: true, }, shares: [], - disks: [ + assignableDisks: [ { device: '/dev/sda', size: 500 * 1024 * 1024 * 1024, serialNum: 'DISK-A', - emhttpDeviceId: 'diskA', interfaceType: DiskInterfaceType.SATA, }, { device: '/dev/sdb', size: 250 * 1024 * 1024 * 1024, serialNum: 'DISK-B', - emhttpDeviceId: 'diskB', interfaceType: DiskInterfaceType.SATA, }, ], @@ -1109,7 +1104,7 @@ describe('OnboardingSummaryStep', () => { draftStore.internalBootSelection = { poolName: 'boot', slotCount: 2, - devices: ['diskA', 'diskB'], + devices: ['sda', 'sdb'], bootSizeMiB: 16384, updateBios: true, }; @@ -1125,7 +1120,7 @@ describe('OnboardingSummaryStep', () => { draftStore.internalBootSelection = { poolName: 'cache', slotCount: 1, - devices: ['diskA'], + devices: ['sda'], bootSizeMiB: 16384, updateBios: true, }; @@ -1153,7 +1148,7 @@ describe('OnboardingSummaryStep', () => { draftStore.internalBootSelection = { poolName: 'cache', slotCount: 2, - devices: ['diskA', 'diskB'], + devices: ['sda', 'sdb'], bootSizeMiB: 16384, updateBios: true, }; @@ -1172,7 +1167,7 @@ describe('OnboardingSummaryStep', () => { expect(submitInternalBootCreationMock).toHaveBeenCalledWith( { poolName: 'cache', - devices: ['diskA', 'diskB'], + devices: ['sda', 'sdb'], bootSizeMiB: 16384, updateBios: true, }, @@ -1190,7 +1185,7 @@ describe('OnboardingSummaryStep', () => { draftStore.internalBootSelection = { poolName: 'cache', slotCount: 1, - devices: ['diskA'], + devices: ['sda'], bootSizeMiB: 16384, updateBios: false, }; @@ -1213,7 +1208,7 @@ describe('OnboardingSummaryStep', () => { draftStore.internalBootSelection = { poolName: 'cache', slotCount: 1, - devices: ['diskA'], + devices: ['sda'], bootSizeMiB: 16384, updateBios: true, }; diff --git a/web/auto-imports.d.ts b/web/auto-imports.d.ts index 81d84147ed..c54c587cd0 100644 --- a/web/auto-imports.d.ts +++ b/web/auto-imports.d.ts @@ -6,57 +6,57 @@ // biome-ignore lint: disable export {} declare global { - const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey'] - const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale'] - const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts'] - const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale'] - const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts'] - const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey'] - const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey'] - const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey'] - const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey'] - const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey'] - const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey'] - const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey'] - const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap'] - const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey'] - const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey'] - const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig'] - const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup'] - const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons'] - const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch'] - const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup'] - const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload'] - const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField'] - const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd'] - const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale'] - const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay'] - const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal'] - const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable'] - const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy'] - const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast'] + const avatarGroupInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey'] + const defineLocale: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale'] + const defineShortcuts: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts'] + const extendLocale: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale'] + const extractShortcuts: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts'] + const fieldGroupInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey'] + const formBusInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey'] + const formFieldInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey'] + const formInputsInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey'] + const formLoadingInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey'] + const formOptionsInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey'] + const inputIdInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey'] + const kbdKeysMap: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap'] + const localeContextInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey'] + const portalTargetInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey'] + const useAppConfig: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig'] + const useAvatarGroup: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup'] + const useComponentIcons: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons'] + const useContentSearch: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch'] + const useFieldGroup: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup'] + const useFileUpload: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload'] + const useFormField: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField'] + const useKbd: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd'] + const useLocale: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale'] + const useOverlay: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay'] + const usePortal: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal'] + const useResizable: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable'] + const useScrollspy: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy'] + const useToast: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast'] } // for type re-export declare global { // @ts-ignore - export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d') + export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d') // @ts-ignore - export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d') + export type { UseComponentIconsProps } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d') // @ts-ignore - export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d') + export type { UseFileUploadOptions } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d') // @ts-ignore - export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d') + export type { KbdKey, KbdKeySpecific } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d') // @ts-ignore - export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d') + export type { OverlayOptions, Overlay } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d') // @ts-ignore - export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d') + export type { UseResizableProps, UseResizableReturn } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d') // @ts-ignore - export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d' - import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d') + export type { Toast } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d' + import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d') } diff --git a/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts b/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts index 14cce60524..85a685b526 100644 --- a/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts +++ b/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts @@ -4,18 +4,8 @@ export const GET_INTERNAL_BOOT_CONTEXT_QUERY = gql` query GetInternalBootContext { array { state - boot { - device - } - parities { - device - } - disks { - device - } caches { name - device } } vars { @@ -28,11 +18,10 @@ export const GET_INTERNAL_BOOT_CONTEXT_QUERY = gql` shares { name } - disks { + assignableDisks { device size serialNum - emhttpDeviceId interfaceType } } diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue index e11b1ff145..8bb816cc27 100644 --- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue @@ -152,8 +152,7 @@ const templateData = computed(() => { return null; } - const deviceOptions = data.disks - .filter((disk) => typeof disk.emhttpDeviceId === 'string' && disk.emhttpDeviceId.trim().length > 0) + const deviceOptions = data.assignableDisks .map((disk) => { const device = normalizeDeviceName(disk.device); const sizeBytes = disk.size; @@ -165,9 +164,8 @@ const templateData = computed(() => { } const serialNum = disk.serialNum?.trim() || ''; - const emhttpDeviceId = disk.emhttpDeviceId?.trim() || ''; - const optionValue = emhttpDeviceId || device; - const displayId = serialNum || emhttpDeviceId || device; + const optionValue = device; + const displayId = serialNum || device; const sizeLabel = formatBytes(sizeBytes); return { value: optionValue, diff --git a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue index 59a02d6ee9..3633ac0d11 100644 --- a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue @@ -188,7 +188,7 @@ const normalizeDeviceName = (value: string | null | undefined): string => { const internalBootDeviceLabelById = computed(() => { const data: GetInternalBootContextQuery | null | undefined = internalBootContextResult.value; - const disks = data?.disks ?? []; + const disks = data?.assignableDisks ?? []; const labels = new Map(); for (const disk of disks) { @@ -198,9 +198,8 @@ const internalBootDeviceLabelById = computed(() => { } const serialNum = disk.serialNum?.trim() || ''; - const emhttpDeviceId = disk.emhttpDeviceId?.trim() || ''; - const optionValue = emhttpDeviceId || device; - const displayId = serialNum || emhttpDeviceId || device; + const optionValue = device; + const displayId = serialNum || device; const sizeBytes = disk.size; const sizeLabel = formatBytes(sizeBytes); const label = diff --git a/web/src/composables/gql/gql.ts b/web/src/composables/gql/gql.ts index 5c7c70f90b..0279bd6fad 100644 --- a/web/src/composables/gql/gql.ts +++ b/web/src/composables/gql/gql.ts @@ -72,7 +72,7 @@ type Documents = { "\n mutation UpdateSshSettings($enabled: Boolean!, $port: Int = 22) {\n updateSshSettings(input: { enabled: $enabled, port: $port }) {\n id\n useSsh\n portssh\n }\n }\n": typeof types.UpdateSshSettingsDocument, "\n mutation CreateInternalBootPool($input: CreateInternalBootPoolInput!) {\n onboarding {\n createInternalBootPool(input: $input) {\n ok\n code\n output\n }\n }\n }\n": typeof types.CreateInternalBootPoolDocument, "\n query GetCoreSettings {\n customization {\n activationCode {\n system {\n serverName\n comment\n }\n }\n }\n vars {\n name\n sysModel\n useSsh\n localTld\n }\n server {\n name\n comment\n }\n display {\n theme\n locale\n }\n systemTime {\n timeZone\n }\n info {\n primaryNetwork {\n ipAddress\n }\n }\n }\n": typeof types.GetCoreSettingsDocument, - "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n disks {\n device\n size\n serialNum\n emhttpDeviceId\n interfaceType\n }\n }\n": typeof types.GetInternalBootContextDocument, + "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n": typeof types.GetInternalBootContextDocument, "\n mutation InstallLanguage($input: InstallPluginInput!) {\n unraidPlugins {\n installLanguage(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": typeof types.InstallLanguageDocument, "\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": typeof types.InstallPluginDocument, "\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n": typeof types.InstalledUnraidPluginsDocument, @@ -155,7 +155,7 @@ const documents: Documents = { "\n mutation UpdateSshSettings($enabled: Boolean!, $port: Int = 22) {\n updateSshSettings(input: { enabled: $enabled, port: $port }) {\n id\n useSsh\n portssh\n }\n }\n": types.UpdateSshSettingsDocument, "\n mutation CreateInternalBootPool($input: CreateInternalBootPoolInput!) {\n onboarding {\n createInternalBootPool(input: $input) {\n ok\n code\n output\n }\n }\n }\n": types.CreateInternalBootPoolDocument, "\n query GetCoreSettings {\n customization {\n activationCode {\n system {\n serverName\n comment\n }\n }\n }\n vars {\n name\n sysModel\n useSsh\n localTld\n }\n server {\n name\n comment\n }\n display {\n theme\n locale\n }\n systemTime {\n timeZone\n }\n info {\n primaryNetwork {\n ipAddress\n }\n }\n }\n": types.GetCoreSettingsDocument, - "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n disks {\n device\n size\n serialNum\n emhttpDeviceId\n interfaceType\n }\n }\n": types.GetInternalBootContextDocument, + "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n": types.GetInternalBootContextDocument, "\n mutation InstallLanguage($input: InstallPluginInput!) {\n unraidPlugins {\n installLanguage(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": types.InstallLanguageDocument, "\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": types.InstallPluginDocument, "\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n": types.InstalledUnraidPluginsDocument, @@ -429,7 +429,7 @@ export function graphql(source: "\n query GetCoreSettings {\n customization /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n disks {\n device\n size\n serialNum\n emhttpDeviceId\n interfaceType\n }\n }\n"): (typeof documents)["\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n disks {\n device\n size\n serialNum\n emhttpDeviceId\n interfaceType\n }\n }\n"]; +export function graphql(source: "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n"): (typeof documents)["\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/src/composables/gql/graphql.ts b/web/src/composables/gql/graphql.ts index 6eda628155..76fcae74de 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -714,8 +714,6 @@ 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; /** The firmware revision of the disk */ firmwareRevision: Scalars['String']['output']; id: Scalars['PrefixedID']['output']; @@ -2265,6 +2263,7 @@ export type Query = { apiKeyPossibleRoles: Array; apiKeys: Array; array: UnraidArray; + assignableDisks: Array; cloud: Cloud; config: Config; connect: Connect; @@ -3895,7 +3894,7 @@ export type GetCoreSettingsQuery = { __typename?: 'Query', customization?: { __t export type GetInternalBootContextQueryVariables = Exact<{ [key: string]: never; }>; -export type GetInternalBootContextQuery = { __typename?: 'Query', array: { __typename?: 'UnraidArray', state: ArrayState, boot?: { __typename?: 'ArrayDisk', device?: string | null } | null, parities: Array<{ __typename?: 'ArrayDisk', device?: string | null }>, disks: Array<{ __typename?: 'ArrayDisk', device?: string | null }>, caches: Array<{ __typename?: 'ArrayDisk', name?: string | null, device?: string | null }> }, vars: { __typename?: 'Vars', fsState?: string | null, bootEligible?: boolean | null, bootedFromFlashWithInternalBootSetup?: boolean | null, enableBootTransfer?: string | null, reservedNames?: string | null }, shares: Array<{ __typename?: 'Share', name?: string | null }>, disks: Array<{ __typename?: 'Disk', device: string, size: number, serialNum: string, emhttpDeviceId?: string | null, interfaceType: DiskInterfaceType }> }; +export type GetInternalBootContextQuery = { __typename?: 'Query', array: { __typename?: 'UnraidArray', state: ArrayState, boot?: { __typename?: 'ArrayDisk', device?: string | null } | null, parities: Array<{ __typename?: 'ArrayDisk', device?: string | null }>, disks: Array<{ __typename?: 'ArrayDisk', device?: string | null }>, caches: Array<{ __typename?: 'ArrayDisk', name?: string | null, device?: string | null }> }, vars: { __typename?: 'Vars', fsState?: string | null, bootEligible?: boolean | null, bootedFromFlashWithInternalBootSetup?: boolean | null, enableBootTransfer?: string | null, reservedNames?: string | null }, shares: Array<{ __typename?: 'Share', name?: string | null }>, assignableDisks: Array<{ __typename?: 'Disk', device: string, size: number, serialNum: string, interfaceType: DiskInterfaceType }> }; export type InstallLanguageMutationVariables = Exact<{ input: InstallPluginInput; @@ -4084,7 +4083,7 @@ export const SetLocaleDocument = {"kind":"Document","definitions":[{"kind":"Oper export const UpdateSshSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSshSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"port"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"22"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSshSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"port"},"value":{"kind":"Variable","name":{"kind":"Name","value":"port"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"useSsh"}},{"kind":"Field","name":{"kind":"Name","value":"portssh"}}]}}]}}]} as unknown as DocumentNode; export const CreateInternalBootPoolDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateInternalBootPool"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateInternalBootPoolInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"onboarding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInternalBootPool"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetCoreSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCoreSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"useSsh"}},{"kind":"Field","name":{"kind":"Name","value":"localTld"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"display"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"theme"}},{"kind":"Field","name":{"kind":"Name","value":"locale"}}]}},{"kind":"Field","name":{"kind":"Name","value":"systemTime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timeZone"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"primaryNetwork"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetInternalBootContextDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInternalBootContext"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"array"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"boot"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"parities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"disks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"caches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"device"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fsState"}},{"kind":"Field","name":{"kind":"Name","value":"bootEligible"}},{"kind":"Field","name":{"kind":"Name","value":"bootedFromFlashWithInternalBootSetup"}},{"kind":"Field","name":{"kind":"Name","value":"enableBootTransfer"}},{"kind":"Field","name":{"kind":"Name","value":"reservedNames"}}]}},{"kind":"Field","name":{"kind":"Name","value":"shares"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"disks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"serialNum"}},{"kind":"Field","name":{"kind":"Name","value":"emhttpDeviceId"}},{"kind":"Field","name":{"kind":"Name","value":"interfaceType"}}]}}]}}]} as unknown as DocumentNode; +export const GetInternalBootContextDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInternalBootContext"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"array"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"boot"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"parities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"disks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"caches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"device"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fsState"}},{"kind":"Field","name":{"kind":"Name","value":"bootEligible"}},{"kind":"Field","name":{"kind":"Name","value":"bootedFromFlashWithInternalBootSetup"}},{"kind":"Field","name":{"kind":"Name","value":"enableBootTransfer"}},{"kind":"Field","name":{"kind":"Name","value":"reservedNames"}}]}},{"kind":"Field","name":{"kind":"Name","value":"shares"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignableDisks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"serialNum"}},{"kind":"Field","name":{"kind":"Name","value":"interfaceType"}}]}}]}}]} as unknown as DocumentNode; export const InstallLanguageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallLanguage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InstallPluginInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installLanguage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode; export const InstallPluginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallPlugin"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InstallPluginInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installPlugin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode; export const InstalledUnraidPluginsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InstalledUnraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installedUnraidPlugins"}}]}}]} as unknown as DocumentNode; diff --git a/web/src/composables/gql/index.ts b/web/src/composables/gql/index.ts index 0ea4a91cf8..f51599168f 100644 --- a/web/src/composables/gql/index.ts +++ b/web/src/composables/gql/index.ts @@ -1,2 +1,2 @@ export * from "./fragment-masking"; -export * from "./gql"; +export * from "./gql"; \ No newline at end of file From 88ea1dbabf2960b4f36e71bd4aae64856e5450d0 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 11:26:00 -0400 Subject: [PATCH 03/12] Fix onboarding generated gql artifacts --- web/auto-imports.d.ts | 86 +++++++++++++++--------------- web/src/composables/gql/gql.ts | 6 +-- web/src/composables/gql/graphql.ts | 4 +- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/web/auto-imports.d.ts b/web/auto-imports.d.ts index c54c587cd0..81d84147ed 100644 --- a/web/auto-imports.d.ts +++ b/web/auto-imports.d.ts @@ -6,57 +6,57 @@ // biome-ignore lint: disable export {} declare global { - const avatarGroupInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey'] - const defineLocale: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale'] - const defineShortcuts: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts'] - const extendLocale: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale'] - const extractShortcuts: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts'] - const fieldGroupInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey'] - const formBusInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey'] - const formFieldInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey'] - const formInputsInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey'] - const formLoadingInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey'] - const formOptionsInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey'] - const inputIdInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey'] - const kbdKeysMap: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap'] - const localeContextInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey'] - const portalTargetInjectionKey: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey'] - const useAppConfig: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig'] - const useAvatarGroup: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup'] - const useComponentIcons: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons'] - const useContentSearch: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch'] - const useFieldGroup: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup'] - const useFileUpload: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload'] - const useFormField: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField'] - const useKbd: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd'] - const useLocale: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale'] - const useOverlay: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay'] - const usePortal: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal'] - const useResizable: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable'] - const useScrollspy: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy'] - const useToast: typeof import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast'] + const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey'] + const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale'] + const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts'] + const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale'] + const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts'] + const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey'] + const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey'] + const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey'] + const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey'] + const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey'] + const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey'] + const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey'] + const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap'] + const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey'] + const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey'] + const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig'] + const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup'] + const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons'] + const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch'] + const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup'] + const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload'] + const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField'] + const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd'] + const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale'] + const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay'] + const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal'] + const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable'] + const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy'] + const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast'] } // for type re-export declare global { // @ts-ignore - export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d') + export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d') // @ts-ignore - export type { UseComponentIconsProps } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d') + export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d') // @ts-ignore - export type { UseFileUploadOptions } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d') + export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d') // @ts-ignore - export type { KbdKey, KbdKeySpecific } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d') + export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d') // @ts-ignore - export type { OverlayOptions, Overlay } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d') + export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d') // @ts-ignore - export type { UseResizableProps, UseResizableReturn } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d') + export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d') // @ts-ignore - export type { Toast } from '../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d' - import('../../../../Users/elibosley/Code/api/node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d') + export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d' + import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d') } diff --git a/web/src/composables/gql/gql.ts b/web/src/composables/gql/gql.ts index 0279bd6fad..29ec345c85 100644 --- a/web/src/composables/gql/gql.ts +++ b/web/src/composables/gql/gql.ts @@ -72,7 +72,7 @@ type Documents = { "\n mutation UpdateSshSettings($enabled: Boolean!, $port: Int = 22) {\n updateSshSettings(input: { enabled: $enabled, port: $port }) {\n id\n useSsh\n portssh\n }\n }\n": typeof types.UpdateSshSettingsDocument, "\n mutation CreateInternalBootPool($input: CreateInternalBootPoolInput!) {\n onboarding {\n createInternalBootPool(input: $input) {\n ok\n code\n output\n }\n }\n }\n": typeof types.CreateInternalBootPoolDocument, "\n query GetCoreSettings {\n customization {\n activationCode {\n system {\n serverName\n comment\n }\n }\n }\n vars {\n name\n sysModel\n useSsh\n localTld\n }\n server {\n name\n comment\n }\n display {\n theme\n locale\n }\n systemTime {\n timeZone\n }\n info {\n primaryNetwork {\n ipAddress\n }\n }\n }\n": typeof types.GetCoreSettingsDocument, - "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n": typeof types.GetInternalBootContextDocument, + "\n query GetInternalBootContext {\n array {\n state\n caches {\n name\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n": typeof types.GetInternalBootContextDocument, "\n mutation InstallLanguage($input: InstallPluginInput!) {\n unraidPlugins {\n installLanguage(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": typeof types.InstallLanguageDocument, "\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": typeof types.InstallPluginDocument, "\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n": typeof types.InstalledUnraidPluginsDocument, @@ -155,7 +155,7 @@ const documents: Documents = { "\n mutation UpdateSshSettings($enabled: Boolean!, $port: Int = 22) {\n updateSshSettings(input: { enabled: $enabled, port: $port }) {\n id\n useSsh\n portssh\n }\n }\n": types.UpdateSshSettingsDocument, "\n mutation CreateInternalBootPool($input: CreateInternalBootPoolInput!) {\n onboarding {\n createInternalBootPool(input: $input) {\n ok\n code\n output\n }\n }\n }\n": types.CreateInternalBootPoolDocument, "\n query GetCoreSettings {\n customization {\n activationCode {\n system {\n serverName\n comment\n }\n }\n }\n vars {\n name\n sysModel\n useSsh\n localTld\n }\n server {\n name\n comment\n }\n display {\n theme\n locale\n }\n systemTime {\n timeZone\n }\n info {\n primaryNetwork {\n ipAddress\n }\n }\n }\n": types.GetCoreSettingsDocument, - "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n": types.GetInternalBootContextDocument, + "\n query GetInternalBootContext {\n array {\n state\n caches {\n name\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n": types.GetInternalBootContextDocument, "\n mutation InstallLanguage($input: InstallPluginInput!) {\n unraidPlugins {\n installLanguage(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": types.InstallLanguageDocument, "\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": types.InstallPluginDocument, "\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n": types.InstalledUnraidPluginsDocument, @@ -429,7 +429,7 @@ export function graphql(source: "\n query GetCoreSettings {\n customization /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n"): (typeof documents)["\n query GetInternalBootContext {\n array {\n state\n boot {\n device\n }\n parities {\n device\n }\n disks {\n device\n }\n caches {\n name\n device\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n"]; +export function graphql(source: "\n query GetInternalBootContext {\n array {\n state\n caches {\n name\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n"): (typeof documents)["\n query GetInternalBootContext {\n array {\n state\n caches {\n name\n }\n }\n vars {\n fsState\n bootEligible\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n reservedNames\n }\n shares {\n name\n }\n assignableDisks {\n device\n size\n serialNum\n interfaceType\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/src/composables/gql/graphql.ts b/web/src/composables/gql/graphql.ts index 76fcae74de..1cd08a94bf 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -3894,7 +3894,7 @@ export type GetCoreSettingsQuery = { __typename?: 'Query', customization?: { __t export type GetInternalBootContextQueryVariables = Exact<{ [key: string]: never; }>; -export type GetInternalBootContextQuery = { __typename?: 'Query', array: { __typename?: 'UnraidArray', state: ArrayState, boot?: { __typename?: 'ArrayDisk', device?: string | null } | null, parities: Array<{ __typename?: 'ArrayDisk', device?: string | null }>, disks: Array<{ __typename?: 'ArrayDisk', device?: string | null }>, caches: Array<{ __typename?: 'ArrayDisk', name?: string | null, device?: string | null }> }, vars: { __typename?: 'Vars', fsState?: string | null, bootEligible?: boolean | null, bootedFromFlashWithInternalBootSetup?: boolean | null, enableBootTransfer?: string | null, reservedNames?: string | null }, shares: Array<{ __typename?: 'Share', name?: string | null }>, assignableDisks: Array<{ __typename?: 'Disk', device: string, size: number, serialNum: string, interfaceType: DiskInterfaceType }> }; +export type GetInternalBootContextQuery = { __typename?: 'Query', array: { __typename?: 'UnraidArray', state: ArrayState, caches: Array<{ __typename?: 'ArrayDisk', name?: string | null }> }, vars: { __typename?: 'Vars', fsState?: string | null, bootEligible?: boolean | null, bootedFromFlashWithInternalBootSetup?: boolean | null, enableBootTransfer?: string | null, reservedNames?: string | null }, shares: Array<{ __typename?: 'Share', name?: string | null }>, assignableDisks: Array<{ __typename?: 'Disk', device: string, size: number, serialNum: string, interfaceType: DiskInterfaceType }> }; export type InstallLanguageMutationVariables = Exact<{ input: InstallPluginInput; @@ -4083,7 +4083,7 @@ export const SetLocaleDocument = {"kind":"Document","definitions":[{"kind":"Oper export const UpdateSshSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSshSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"port"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"22"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSshSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"port"},"value":{"kind":"Variable","name":{"kind":"Name","value":"port"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"useSsh"}},{"kind":"Field","name":{"kind":"Name","value":"portssh"}}]}}]}}]} as unknown as DocumentNode; export const CreateInternalBootPoolDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateInternalBootPool"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateInternalBootPoolInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"onboarding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInternalBootPool"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetCoreSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCoreSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"useSsh"}},{"kind":"Field","name":{"kind":"Name","value":"localTld"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"display"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"theme"}},{"kind":"Field","name":{"kind":"Name","value":"locale"}}]}},{"kind":"Field","name":{"kind":"Name","value":"systemTime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timeZone"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"primaryNetwork"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetInternalBootContextDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInternalBootContext"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"array"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"boot"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"parities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"disks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}}]}},{"kind":"Field","name":{"kind":"Name","value":"caches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"device"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fsState"}},{"kind":"Field","name":{"kind":"Name","value":"bootEligible"}},{"kind":"Field","name":{"kind":"Name","value":"bootedFromFlashWithInternalBootSetup"}},{"kind":"Field","name":{"kind":"Name","value":"enableBootTransfer"}},{"kind":"Field","name":{"kind":"Name","value":"reservedNames"}}]}},{"kind":"Field","name":{"kind":"Name","value":"shares"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignableDisks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"serialNum"}},{"kind":"Field","name":{"kind":"Name","value":"interfaceType"}}]}}]}}]} as unknown as DocumentNode; +export const GetInternalBootContextDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInternalBootContext"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"array"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"caches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fsState"}},{"kind":"Field","name":{"kind":"Name","value":"bootEligible"}},{"kind":"Field","name":{"kind":"Name","value":"bootedFromFlashWithInternalBootSetup"}},{"kind":"Field","name":{"kind":"Name","value":"enableBootTransfer"}},{"kind":"Field","name":{"kind":"Name","value":"reservedNames"}}]}},{"kind":"Field","name":{"kind":"Name","value":"shares"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignableDisks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"device"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"serialNum"}},{"kind":"Field","name":{"kind":"Name","value":"interfaceType"}}]}}]}}]} as unknown as DocumentNode; export const InstallLanguageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallLanguage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InstallPluginInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installLanguage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode; export const InstallPluginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallPlugin"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InstallPluginInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installPlugin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode; export const InstalledUnraidPluginsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InstalledUnraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installedUnraidPlugins"}}]}}]} as unknown as DocumentNode; From 4a7f2a74bd80f7b5ca66badf80c880bce5f87091 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 11:27:16 -0400 Subject: [PATCH 04/12] Fix onboarding disk size unit labels --- .../components/Onboarding/OnboardingInternalBootStep.test.ts | 4 ++-- .../components/Onboarding/OnboardingSummaryStep.test.ts | 4 ++-- .../Onboarding/steps/OnboardingInternalBootStep.vue | 2 +- web/src/components/Onboarding/steps/OnboardingSummaryStep.vue | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts index 1559d41b0a..0bc28137df 100644 --- a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts @@ -202,8 +202,8 @@ describe('OnboardingInternalBootStep', () => { const wrapper = mountComponent(); await flushPromises(); - expect(wrapper.text()).toContain('WD-TEST-1234 - 32.0 GB (sda)'); - expect(wrapper.text()).not.toContain('eligible-disk - 32.0 GB (sda)'); + expect(wrapper.text()).toContain('WD-TEST-1234 - 32.0 GiB (sda)'); + expect(wrapper.text()).not.toContain('eligible-disk - 32.0 GiB (sda)'); }); it('defaults the storage pool name to cache', async () => { diff --git a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts index cc3b692d47..52d7c47d6b 100644 --- a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts @@ -1111,8 +1111,8 @@ describe('OnboardingSummaryStep', () => { const { wrapper } = mountComponent(); - expect(wrapper.text()).toContain('DISK-A - 500 GB (sda)'); - expect(wrapper.text()).toContain('DISK-B - 250 GB (sdb)'); + expect(wrapper.text()).toContain('DISK-A - 500 GiB (sda)'); + expect(wrapper.text()).toContain('DISK-B - 250 GiB (sdb)'); }); it('requires confirmation before applying storage boot drive changes', async () => { diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue index 8bb816cc27..6e51a28fd6 100644 --- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue @@ -87,7 +87,7 @@ const formatBytes = (bytes: number) => { return t('onboarding.internalBootStep.unknownSize'); } - const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { diff --git a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue index 3633ac0d11..883e19bfa2 100644 --- a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue @@ -163,7 +163,7 @@ const formatBytes = (bytes: number) => { return t('onboarding.internalBootStep.unknownSize'); } - const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { From 64ce9e6ce76a83fd964935754aae0107ba97888b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 11:29:38 -0400 Subject: [PATCH 05/12] Use shared convert formatting for onboarding disk sizes --- .../Onboarding/OnboardingInternalBootStep.test.ts | 4 ++-- .../Onboarding/OnboardingSummaryStep.test.ts | 4 ++-- .../steps/OnboardingInternalBootStep.vue | 14 ++++---------- .../Onboarding/steps/OnboardingSummaryStep.vue | 14 ++++---------- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts index 0bc28137df..6d86d4ee47 100644 --- a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts @@ -202,8 +202,8 @@ describe('OnboardingInternalBootStep', () => { const wrapper = mountComponent(); await flushPromises(); - expect(wrapper.text()).toContain('WD-TEST-1234 - 32.0 GiB (sda)'); - expect(wrapper.text()).not.toContain('eligible-disk - 32.0 GiB (sda)'); + expect(wrapper.text()).toContain('WD-TEST-1234 - 34.4 GB (sda)'); + expect(wrapper.text()).not.toContain('eligible-disk - 34.4 GB (sda)'); }); it('defaults the storage pool name to cache', async () => { diff --git a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts index 52d7c47d6b..05a3a4e5e8 100644 --- a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts @@ -1111,8 +1111,8 @@ describe('OnboardingSummaryStep', () => { const { wrapper } = mountComponent(); - expect(wrapper.text()).toContain('DISK-A - 500 GiB (sda)'); - expect(wrapper.text()).toContain('DISK-B - 250 GiB (sdb)'); + expect(wrapper.text()).toContain('DISK-A - 537 GB (sda)'); + expect(wrapper.text()).toContain('DISK-B - 268 GB (sdb)'); }); it('requires confirmation before applying storage boot drive changes', async () => { diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue index 6e51a28fd6..730df2b00c 100644 --- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue @@ -8,6 +8,7 @@ import { ChevronDownIcon, ChevronRightIcon, ExclamationTriangleIcon } from '@her import { BrandButton } from '@unraid/ui'; import { useOnboardingDraftStore } from '@/components/Onboarding/store/onboardingDraft'; import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'; +import { convert } from 'convert'; import type { OnboardingBootMode, @@ -87,16 +88,9 @@ const formatBytes = (bytes: number) => { return t('onboarding.internalBootStep.unknownSize'); } - const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; - let value = bytes; - let unitIndex = 0; - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex += 1; - } - - const precision = value >= 100 || unitIndex === 0 ? 0 : 1; - return `${value.toFixed(precision)} ${units[unitIndex]}`; + const converted = convert(bytes, 'B').to('best', 'metric'); + const precision = converted.quantity >= 100 || converted.unit === 'B' ? 0 : 1; + return `${converted.quantity.toFixed(precision)} ${converted.unit}`; }; const toSizeMiB = (bytes: number): number | null => { diff --git a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue index 883e19bfa2..fdb2c65426 100644 --- a/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingSummaryStep.vue @@ -44,6 +44,7 @@ import { UPDATE_SYSTEM_TIME_MUTATION } from '@/components/Onboarding/graphql/upd import { useOnboardingModalStore } from '@/components/Onboarding/store/onboardingModalVisibility'; import { useOnboardingStore } from '@/components/Onboarding/store/onboardingStatus'; import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'; +import { convert } from 'convert'; import type { LogEntry } from '@/components/Onboarding/components/OnboardingConsole.vue'; import type { OnboardingErrorDiagnostics } from '@/components/Onboarding/composables/onboardingErrorDiagnostics'; @@ -163,16 +164,9 @@ const formatBytes = (bytes: number) => { return t('onboarding.internalBootStep.unknownSize'); } - const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; - let value = bytes; - let unitIndex = 0; - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex += 1; - } - - const precision = value >= 100 || unitIndex === 0 ? 0 : 1; - return `${value.toFixed(precision)} ${units[unitIndex]}`; + const converted = convert(bytes, 'B').to('best', 'metric'); + const precision = converted.quantity >= 100 || converted.unit === 'B' ? 0 : 1; + return `${converted.quantity.toFixed(precision)} ${converted.unit}`; }; const normalizeDeviceName = (value: string | null | undefined): string => { From c633fb747b681253349494b0f1170e4096410e9a Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 11:33:46 -0400 Subject: [PATCH 06/12] Prefer devs.ini serials for assignable disks --- .../resolvers/disks/disks.service.spec.ts | 5 ++-- .../graph/resolvers/disks/disks.service.ts | 27 ++++++++++++++----- web/src/composables/gql/index.ts | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts b/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts index c01be11195..80209d04ce 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts @@ -535,8 +535,8 @@ describe('DisksService', () => { } if (key === 'store.emhttp.devices') { return [ - { device: '/dev/sda' }, - { device: '/dev/sdd' }, + { id: 'DEVS-SERIAL-SDA', device: '/dev/sda' }, + { id: 'DEVS-SERIAL-SDD', device: '/dev/sdd' }, ]; } return defaultValue; @@ -545,6 +545,7 @@ describe('DisksService', () => { const disks = await service.getAssignableDisks(); expect(disks.map((disk) => disk.device)).toEqual(['/dev/sda', '/dev/sdd']); + expect(disks.map((disk) => disk.serialNum)).toEqual(['DEVS-SERIAL-SDA', 'DEVS-SERIAL-SDD']); expect(configService.get).toHaveBeenCalledWith('store.emhttp.devices', []); }); diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.service.ts b/api/src/unraid-api/graph/resolvers/disks/disks.service.ts index b0a4bd82b6..69df3a3ac2 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.service.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.service.ts @@ -46,6 +46,7 @@ const SmartDataSchema = z.object({ .nullable(), }); interface EmhttpDeviceRecord { + id?: unknown; device?: unknown; } @@ -74,10 +75,10 @@ export class DisksService { constructor(private readonly configService: ConfigService) {} - private getEmhttpDeviceNames(): Set { + private getEmhttpDeviceMap(): Map { const rawDevicesValue = this.configService.get('store.emhttp.devices', []); const rawDevices = Array.isArray(rawDevicesValue) ? rawDevicesValue : []; - const deviceNames = new Set(); + const devices = new Map(); for (const raw of rawDevices) { if (!raw || typeof raw !== 'object') { @@ -85,16 +86,17 @@ export class DisksService { } const record = raw as EmhttpDeviceRecord; + const id = typeof record.id === 'string' ? record.id.trim() : ''; const device = typeof record.device === 'string' ? record.device.trim() : ''; if (!device) { continue; } - deviceNames.add(normalizeDeviceName(device)); + devices.set(normalizeDeviceName(device), id); } - return deviceNames; + return devices; } public async getTemperature(device: string): Promise { @@ -143,14 +145,27 @@ export class DisksService { } public async getAssignableDisks(): Promise { - const assignableDevices = this.getEmhttpDeviceNames(); + const assignableDevices = this.getEmhttpDeviceMap(); if (assignableDevices.size === 0) { return []; } const disks = await this.getDisks(); - return disks.filter((disk) => assignableDevices.has(normalizeDeviceName(disk.device))); + return disks + .filter((disk) => assignableDevices.has(normalizeDeviceName(disk.device))) + .map((disk) => { + const emhttpId = assignableDevices.get(normalizeDeviceName(disk.device))?.trim(); + if (!emhttpId) { + return disk; + } + + return { + ...disk, + id: emhttpId, + serialNum: emhttpId, + }; + }); } public async getInternalBootDevices(): Promise { diff --git a/web/src/composables/gql/index.ts b/web/src/composables/gql/index.ts index f51599168f..0ea4a91cf8 100644 --- a/web/src/composables/gql/index.ts +++ b/web/src/composables/gql/index.ts @@ -1,2 +1,2 @@ export * from "./fragment-masking"; -export * from "./gql"; \ No newline at end of file +export * from "./gql"; From 1782a71696aa0ce6acf3a55f01c0c2e1160fe260 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 17 Mar 2026 12:24:49 -0400 Subject: [PATCH 07/12] Unify onboarding internal boot context refresh --- api/generated-schema.graphql | 15 ++ .../resolvers/mutation/mutation.model.ts | 10 +- .../onboarding-internal-boot.service.spec.ts | 114 ++++++++- .../onboarding-internal-boot.service.ts | 100 +++++++- .../resolvers/onboarding/onboarding.model.ts | 30 +++ .../onboarding/onboarding.mutation.spec.ts | 31 ++- .../onboarding/onboarding.mutation.ts | 12 + .../onboarding/onboarding.query.spec.ts | 37 +++ .../resolvers/onboarding/onboarding.query.ts | 23 ++ .../graph/resolvers/resolvers.module.ts | 2 + .../OnboardingInternalBootStep.test.ts | 220 ++++++++---------- .../Onboarding/OnboardingSummaryStep.test.ts | 42 ++-- .../graphql/getInternalBootContext.query.ts | 27 +-- .../refreshInternalBootContext.mutation.ts | 23 ++ .../steps/OnboardingInternalBootStep.vue | 71 ++++-- .../steps/OnboardingSummaryStep.vue | 2 +- web/src/composables/gql/gql.ts | 12 +- web/src/composables/gql/graphql.ts | 25 +- web/src/locales/en.json | 1 + 19 files changed, 602 insertions(+), 195 deletions(-) create mode 100644 api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.ts create mode 100644 web/src/components/Onboarding/graphql/refreshInternalBootContext.mutation.ts diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index c1858e6d6d..a60cff9547 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1075,6 +1075,17 @@ type OnboardingInternalBootResult { output: String! } +type OnboardingInternalBootContext { + arrayStopped: Boolean! + bootEligible: Boolean + bootedFromFlashWithInternalBootSetup: Boolean! + enableBootTransfer: String + reservedNames: [String!]! + shareNames: [String!]! + poolNames: [String!]! + assignableDisks: [Disk!]! +} + type RCloneDrive { """Provider name""" name: String! @@ -1389,6 +1400,9 @@ type OnboardingMutations { """Create and configure internal boot pool via emcmd operations""" createInternalBootPool(input: CreateInternalBootPoolInput!): OnboardingInternalBootResult! + + """Refresh the internal boot onboarding context from the latest emhttp state""" + refreshInternalBootContext: OnboardingInternalBootContext! } """Onboarding override input for testing""" @@ -3175,6 +3189,7 @@ type Query { publicTheme: Theme! info: Info! docker: Docker! + internalBootContext: OnboardingInternalBootContext! disks: [Disk!]! assignableDisks: [Disk!]! disk(id: PrefixedID!): Disk! diff --git a/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts b/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts index ee26ee8cad..35229b3ca8 100644 --- a/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts +++ b/api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts @@ -2,7 +2,10 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { Onboarding } from '@app/unraid-api/graph/resolvers/customization/activation-code.model.js'; import { Theme } from '@app/unraid-api/graph/resolvers/customization/theme.model.js'; -import { OnboardingInternalBootResult } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.model.js'; +import { + OnboardingInternalBootContext, + OnboardingInternalBootResult, +} from '@app/unraid-api/graph/resolvers/onboarding/onboarding.model.js'; import { RCloneRemote } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; import { PluginInstallOperation } from '@app/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.model.js'; @@ -83,6 +86,11 @@ export class OnboardingMutations { description: 'Create and configure internal boot pool via emcmd operations', }) createInternalBootPool!: OnboardingInternalBootResult; + + @Field(() => OnboardingInternalBootContext, { + description: 'Refresh the internal boot onboarding context from the latest emhttp state', + }) + refreshInternalBootContext!: OnboardingInternalBootContext; } @ObjectType({ diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts index 4b834e55a2..1179c4c64e 100644 --- a/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts @@ -4,8 +4,10 @@ import { execa } from 'execa'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { emcmd } from '@app/core/utils/clients/emcmd.js'; +import { getShares } from '@app/core/utils/shares/get-shares.js'; import { getters } from '@app/store/index.js'; import { loadStateFileSync } from '@app/store/services/state-file-loader.js'; +import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; import { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; import { OnboardingInternalBootService } from '@app/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.js'; @@ -27,27 +29,136 @@ vi.mock('@app/store/services/state-file-loader.js', () => ({ loadStateFileSync: vi.fn(), })); +vi.mock('@app/core/utils/shares/get-shares.js', () => ({ + getShares: vi.fn(), +})); + describe('OnboardingInternalBootService', () => { const internalBootStateService = { getBootedFromFlashWithInternalBootSetup: vi.fn(), invalidateCachedInternalBootDeviceState: vi.fn(), }; + const disksService = { + getAssignableDisks: vi.fn(), + } satisfies Pick; beforeEach(() => { vi.clearAllMocks(); internalBootStateService.getBootedFromFlashWithInternalBootSetup.mockResolvedValue(false); internalBootStateService.invalidateCachedInternalBootDeviceState.mockResolvedValue(undefined); + disksService.getAssignableDisks.mockResolvedValue([]); vi.mocked(getters.emhttp).mockReturnValue({ + var: {}, devices: [], disks: [], } as unknown as ReturnType); + vi.mocked(getShares).mockImplementation(((type?: string) => { + if (type === 'users' || type === 'disks') { + return []; + } + + return { + users: [], + disks: [], + }; + }) as unknown as typeof getShares); }); const createService = () => new OnboardingInternalBootService( - internalBootStateService as unknown as InternalBootStateService + internalBootStateService as unknown as InternalBootStateService, + disksService as unknown as DisksService ); + it('builds internal boot context from the latest emhttp state', async () => { + const assignableDisks = [ + { + id: 'disk-1', + device: '/dev/sda', + serialNum: 'SERIAL-1', + size: 1, + interfaceType: 'SATA', + }, + ]; + disksService.getAssignableDisks.mockResolvedValue(assignableDisks); + vi.mocked(getters.emhttp).mockReturnValue({ + var: { + mdState: 'STOPPED', + bootEligible: true, + enableBootTransfer: 'yes', + reservedNames: 'flash,cache', + }, + devices: [{ id: 'disk-1', device: 'sda' }], + disks: [ + { type: 'CACHE', name: 'cache' }, + { type: 'CACHE', name: 'nvme' }, + { type: 'DATA', name: 'disk1' }, + ], + } as unknown as ReturnType); + vi.mocked(getShares).mockImplementation(((scope?: string) => { + if (scope === 'users') { + return [{ name: 'media' }]; + } + if (scope === 'disks') { + return [{ name: 'diskshare' }]; + } + + return { + users: [{ name: 'media' }], + disks: [{ name: 'diskshare' }], + }; + }) as unknown as typeof getShares); + + const service = createService(); + const result = await service.getInternalBootContext(); + + expect(result).toEqual({ + arrayStopped: true, + bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: ['flash', 'cache'], + shareNames: ['media', 'diskshare'], + poolNames: ['cache', 'nvme'], + assignableDisks, + }); + expect(vi.mocked(loadStateFileSync)).not.toHaveBeenCalled(); + }); + + it('refreshes internal boot context from disk and invalidates cached device state', async () => { + const assignableDisks = [ + { + id: 'disk-1', + device: '/dev/sda', + serialNum: 'SERIAL-1', + size: 1, + interfaceType: 'SATA', + }, + ]; + disksService.getAssignableDisks.mockResolvedValue(assignableDisks); + vi.mocked(getters.emhttp).mockReturnValue({ + var: { + mdState: 'STOPPED', + bootEligible: true, + enableBootTransfer: 'yes', + reservedNames: '', + }, + devices: [{ id: 'disk-1', device: 'sda' }], + disks: [{ type: 'CACHE', name: 'cache' }], + } as unknown as ReturnType); + + const service = createService(); + const result = await service.refreshInternalBootContext(); + + expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes( + 1 + ); + expect(vi.mocked(loadStateFileSync)).toHaveBeenNthCalledWith(1, 'var'); + expect(vi.mocked(loadStateFileSync)).toHaveBeenNthCalledWith(2, 'devs'); + expect(vi.mocked(loadStateFileSync)).toHaveBeenNthCalledWith(3, 'disks'); + expect(result.assignableDisks).toEqual(assignableDisks); + }); + it('runs the internal boot emcmd sequence and returns success', async () => { vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited>); const service = createService(); @@ -100,6 +211,7 @@ describe('OnboardingInternalBootService', () => { it('runs efibootmgr update flow when updateBios is requested', async () => { vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited>); vi.mocked(getters.emhttp).mockReturnValue({ + var: { mdState: 'STOPPED' }, devices: [{ id: 'disk-1', device: 'sdb' }], disks: [{ type: 'FLASH', device: 'sda' }], } as unknown as ReturnType); diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts index 3746571d0e..ad7c573221 100644 --- a/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts @@ -4,14 +4,17 @@ import { execa } from 'execa'; import type { CreateInternalBootPoolInput, + OnboardingInternalBootContext, OnboardingInternalBootResult, } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.model.js'; import { emcmd } from '@app/core/utils/clients/emcmd.js'; import { withTimeout } from '@app/core/utils/misc/with-timeout.js'; +import { getShares } from '@app/core/utils/shares/get-shares.js'; import { getters } from '@app/store/index.js'; import { loadStateFileSync } from '@app/store/services/state-file-loader.js'; import { StateFileKey } from '@app/store/types.js'; -import { ArrayDiskType } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { ArrayDiskType, ArrayState } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; import { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; const INTERNAL_BOOT_COMMAND_TIMEOUT_MS = 180000; @@ -35,7 +38,10 @@ const isEmhttpDeviceRecord = (value: unknown): value is EmhttpDeviceRecord => { export class OnboardingInternalBootService { private readonly logger = new Logger(OnboardingInternalBootService.name); - constructor(private readonly internalBootStateService: InternalBootStateService) {} + constructor( + private readonly internalBootStateService: InternalBootStateService, + private readonly disksService: DisksService + ) {} private async isBootedFromFlashWithInternalBootSetup(): Promise { return this.internalBootStateService.getBootedFromFlashWithInternalBootSetup(); @@ -122,18 +128,100 @@ export class OnboardingInternalBootService { } } - private ensureEmhttpBootContext(): void { + private loadEmhttpBootContext(forceRefresh = false): void { const emhttpState = getters.emhttp(); + const hasVar = + typeof emhttpState.var === 'object' && + emhttpState.var !== null && + Object.keys(emhttpState.var).length > 0; const hasDevices = Array.isArray(emhttpState.devices) && emhttpState.devices.length > 0; const hasDisks = Array.isArray(emhttpState.disks) && emhttpState.disks.length > 0; - if (!hasDevices) { + if (forceRefresh || !hasVar) { + loadStateFileSync(StateFileKey.var); + } + if (forceRefresh || !hasDevices) { loadStateFileSync(StateFileKey.devs); } - if (!hasDisks) { + if (forceRefresh || !hasDisks) { loadStateFileSync(StateFileKey.disks); } } + private splitCsvValues(value: string | null | undefined): string[] { + if (typeof value !== 'string') { + return []; + } + + return value + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + } + + private getPoolNamesFromEmhttpState(): string[] { + const emhttpState = getters.emhttp(); + const names = new Set(); + + for (const disk of emhttpState.disks ?? []) { + const type = typeof disk.type === 'string' ? disk.type : ''; + const name = typeof disk.name === 'string' ? disk.name.trim() : ''; + if (type === ArrayDiskType.CACHE && name.length > 0) { + names.add(name); + } + } + + return Array.from(names); + } + + private getShareNames(): string[] { + const shareNames = new Set(); + for (const share of [...getShares('users'), ...getShares('disks')]) { + const name = typeof share.name === 'string' ? share.name.trim() : ''; + if (name.length > 0) { + shareNames.add(name); + } + } + return Array.from(shareNames); + } + + public async getInternalBootContext(): Promise { + this.loadEmhttpBootContext(); + + const vars = getters.emhttp().var ?? {}; + let bootedFromFlashWithInternalBootSetup = false; + + try { + bootedFromFlashWithInternalBootSetup = + await this.internalBootStateService.getBootedFromFlashWithInternalBootSetup(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn(`Failed to resolve internal boot context boot state: ${message}`); + } + + return { + arrayStopped: vars.mdState === ArrayState.STOPPED || vars.fsState === 'Stopped', + bootEligible: + typeof vars.bootEligible === 'boolean' + ? vars.bootEligible + : vars.bootEligible === null + ? null + : undefined, + bootedFromFlashWithInternalBootSetup, + enableBootTransfer: + typeof vars.enableBootTransfer === 'string' ? vars.enableBootTransfer : null, + reservedNames: this.splitCsvValues(vars.reservedNames), + shareNames: this.getShareNames(), + poolNames: this.getPoolNamesFromEmhttpState(), + assignableDisks: await this.disksService.getAssignableDisks(), + }; + } + + public async refreshInternalBootContext(): Promise { + this.loadEmhttpBootContext(true); + await this.internalBootStateService.invalidateCachedInternalBootDeviceState(); + return this.getInternalBootContext(); + } + private getDeviceMapFromEmhttpState(): Map { const emhttpState = getters.emhttp(); const rawDevices = Array.isArray(emhttpState.devices) ? emhttpState.devices : []; @@ -315,7 +403,7 @@ export class OnboardingInternalBootService { output: string[] ): Promise<{ hadFailures: boolean }> { let hadFailures = false; - this.ensureEmhttpBootContext(); + this.loadEmhttpBootContext(); const devsById = this.getDeviceMapFromEmhttpState(); const existingEntries = await this.runEfiBootMgr([], output); diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts index f16f5534a8..c212298e71 100644 --- a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.model.ts @@ -16,6 +16,7 @@ import { ValidateNested, } from 'class-validator'; +import { Disk } from '@app/unraid-api/graph/resolvers/disks/disks.model.js'; import { RegistrationState } from '@app/unraid-api/graph/resolvers/registration/registration.model.js'; @InputType({ @@ -326,3 +327,32 @@ export class OnboardingInternalBootResult { @Field(() => String) output!: string; } + +@ObjectType({ + description: 'Current onboarding context for configuring internal boot', +}) +export class OnboardingInternalBootContext { + @Field(() => Boolean) + arrayStopped!: boolean; + + @Field(() => Boolean, { nullable: true }) + bootEligible?: boolean | null; + + @Field(() => Boolean) + bootedFromFlashWithInternalBootSetup!: boolean; + + @Field(() => String, { nullable: true }) + enableBootTransfer?: string | null; + + @Field(() => [String]) + reservedNames!: string[]; + + @Field(() => [String]) + shareNames!: string[]; + + @Field(() => [String]) + poolNames!: string[]; + + @Field(() => [Disk]) + assignableDisks!: Disk[]; +} diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts index a1476d97cd..0f4e5a0c9b 100644 --- a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.spec.ts @@ -29,7 +29,11 @@ describe('OnboardingMutationsResolver', () => { const onboardingInternalBootService = { createInternalBootPool: vi.fn(), - } satisfies Pick; + refreshInternalBootContext: vi.fn(), + } satisfies Pick< + OnboardingInternalBootService, + 'createInternalBootPool' | 'refreshInternalBootContext' + >; const defaultOnboardingResponse = { status: OnboardingStatus.INCOMPLETE, @@ -161,4 +165,29 @@ describe('OnboardingMutationsResolver', () => { }); expect(onboardingInternalBootService.createInternalBootPool).toHaveBeenCalledWith(input); }); + + it('delegates refreshInternalBootContext to onboarding internal boot service', async () => { + onboardingInternalBootService.refreshInternalBootContext.mockResolvedValue({ + arrayStopped: true, + bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: [], + shareNames: [], + poolNames: [], + assignableDisks: [], + }); + + await expect(resolver.refreshInternalBootContext()).resolves.toEqual({ + arrayStopped: true, + bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: [], + shareNames: [], + poolNames: [], + assignableDisks: [], + }); + expect(onboardingInternalBootService.refreshInternalBootContext).toHaveBeenCalledWith(); + }); }); diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts index 1011f65b2e..4a6804d0e3 100644 --- a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.mutation.ts @@ -11,6 +11,7 @@ import { OnboardingMutations } from '@app/unraid-api/graph/resolvers/mutation/mu import { OnboardingInternalBootService } from '@app/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.js'; import { CreateInternalBootPoolInput, + OnboardingInternalBootContext, OnboardingInternalBootResult, OnboardingOverrideInput, } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.model.js'; @@ -91,4 +92,15 @@ export class OnboardingMutationsResolver { ): Promise { return this.onboardingInternalBootService.createInternalBootPool(input); } + + @ResolveField(() => OnboardingInternalBootContext, { + description: 'Refresh onboarding internal boot context from the latest emhttp state', + }) + @UsePermissions({ + action: AuthAction.UPDATE_ANY, + resource: Resource.WELCOME, + }) + async refreshInternalBootContext(): Promise { + return this.onboardingInternalBootService.refreshInternalBootContext(); + } } diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.spec.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.spec.ts new file mode 100644 index 0000000000..dbf18cb3f0 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { OnboardingInternalBootService } from '@app/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.js'; +import { OnboardingQueryResolver } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.query.js'; + +describe('OnboardingQueryResolver', () => { + it('delegates internalBootContext to the onboarding internal boot service', async () => { + const onboardingInternalBootService = { + getInternalBootContext: vi.fn().mockResolvedValue({ + arrayStopped: true, + bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: [], + shareNames: [], + poolNames: [], + assignableDisks: [], + }), + } satisfies Pick; + + const resolver = new OnboardingQueryResolver( + onboardingInternalBootService as unknown as OnboardingInternalBootService + ); + + await expect(resolver.internalBootContext()).resolves.toEqual({ + arrayStopped: true, + bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: [], + shareNames: [], + poolNames: [], + assignableDisks: [], + }); + expect(onboardingInternalBootService.getInternalBootContext).toHaveBeenCalledWith(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.ts b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.ts new file mode 100644 index 0000000000..f7f591f361 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/onboarding/onboarding.query.ts @@ -0,0 +1,23 @@ +import { Query, Resolver } from '@nestjs/graphql'; + +import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; +import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; + +import { OnboardingInternalBootService } from '@app/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.js'; +import { OnboardingInternalBootContext } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.model.js'; + +@Resolver(() => OnboardingInternalBootContext) +export class OnboardingQueryResolver { + constructor(private readonly onboardingInternalBootService: OnboardingInternalBootService) {} + + @Query(() => OnboardingInternalBootContext, { + description: 'Get the latest onboarding context for configuring internal boot', + }) + @UsePermissions({ + action: AuthAction.READ_ANY, + resource: Resource.WELCOME, + }) + async internalBootContext(): Promise { + return this.onboardingInternalBootService.getInternalBootContext(); + } +} diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts index 7001406539..f912fea200 100644 --- a/api/src/unraid-api/graph/resolvers/resolvers.module.ts +++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts @@ -22,6 +22,7 @@ import { NotificationsModule } from '@app/unraid-api/graph/resolvers/notificatio import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js'; import { OnboardingInternalBootService } from '@app/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.js'; import { OnboardingMutationsResolver } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.mutation.js'; +import { OnboardingQueryResolver } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.query.js'; import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js'; import { OwnerResolver } from '@app/unraid-api/graph/resolvers/owner/owner.resolver.js'; import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js'; @@ -77,6 +78,7 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js'; OwnerResolver, OnboardingMutationsResolver, OnboardingInternalBootService, + OnboardingQueryResolver, RegistrationResolver, RootMutationsResolver, ServerResolver, diff --git a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts index 6d86d4ee47..faa0243455 100644 --- a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts @@ -1,15 +1,12 @@ import { flushPromises, mount } from '@vue/test-utils'; +import { REFRESH_INTERNAL_BOOT_CONTEXT_MUTATION } from '@/components/Onboarding/graphql/refreshInternalBootContext.mutation'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { GetInternalBootContextQuery } from '~/composables/gql/graphql'; import OnboardingInternalBootStep from '~/components/Onboarding/steps/OnboardingInternalBootStep.vue'; -import { - ArrayState, - DiskInterfaceType, - GetInternalBootContextDocument, -} from '~/composables/gql/graphql'; +import { DiskInterfaceType, GetInternalBootContextDocument } from '~/composables/gql/graphql'; import { createTestI18n } from '../../utils/i18n'; type MockInternalBootSelection = { @@ -20,7 +17,16 @@ type MockInternalBootSelection = { updateBios: boolean; }; -const { draftStore, contextResult, contextLoading, contextError, useQueryMock } = vi.hoisted(() => { +const { + draftStore, + contextResult, + contextLoading, + contextError, + refetchContextMock, + refreshContextMutationMock, + useQueryMock, + useMutationMock, +} = vi.hoisted(() => { const store = { bootMode: 'usb' as 'usb' | 'storage', internalBootSelection: null as MockInternalBootSelection | null, @@ -41,7 +47,10 @@ const { draftStore, contextResult, contextLoading, contextError, useQueryMock } contextResult: { value: null as GetInternalBootContextQuery | null, __v_isRef: true }, contextLoading: { value: false, __v_isRef: true }, contextError: { value: null as unknown, __v_isRef: true }, + refetchContextMock: vi.fn().mockResolvedValue(undefined), + refreshContextMutationMock: vi.fn().mockResolvedValue(undefined), useQueryMock: vi.fn(), + useMutationMock: vi.fn(), }; }); @@ -55,6 +64,7 @@ vi.mock('@unraid/ui', () => ({ })); vi.mock('@vue/apollo-composable', () => ({ + useMutation: useMutationMock, useQuery: useQueryMock, })); @@ -62,14 +72,43 @@ useQueryMock.mockImplementation(() => ({ result: contextResult, loading: contextLoading, error: contextError, + refetch: refetchContextMock, })); +useMutationMock.mockImplementation((document: unknown) => { + if (document === REFRESH_INTERNAL_BOOT_CONTEXT_MUTATION) { + return { + mutate: refreshContextMutationMock, + }; + } + + return { + mutate: vi.fn(), + }; +}); + vi.mock('@/components/Onboarding/store/onboardingDraft', () => ({ useOnboardingDraftStore: () => draftStore, })); const gib = (value: number) => value * 1024 * 1024 * 1024; +const buildContext = ( + overrides: Partial = {} +): GetInternalBootContextQuery => ({ + internalBootContext: { + arrayStopped: true, + bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: [], + shareNames: [], + poolNames: [], + assignableDisks: [], + ...overrides, + }, +}); + const mountComponent = () => mount(OnboardingInternalBootStep, { props: { @@ -93,18 +132,11 @@ describe('OnboardingInternalBootStep', () => { it('renders all available server and disk eligibility codes when storage boot is blocked', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STARTED, - caches: [{ name: 'cache' }], - }, - vars: { - fsState: 'Started', - bootEligible: null, - enableBootTransfer: 'maybe', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ + arrayStopped: false, + bootEligible: null, + enableBootTransfer: 'maybe', + poolNames: ['cache'], assignableDisks: [ { device: '/dev/sda', @@ -143,7 +175,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.USB, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -177,18 +209,7 @@ describe('OnboardingInternalBootStep', () => { it('shows drive serials in the selectable device labels', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [], - }, - vars: { - fsState: 'Stopped', - bootEligible: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ assignableDisks: [ { device: '/dev/sda', @@ -197,7 +218,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.SATA, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -208,18 +229,7 @@ describe('OnboardingInternalBootStep', () => { it('defaults the storage pool name to cache', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [], - }, - vars: { - fsState: 'Stopped', - bootEligible: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ assignableDisks: [ { device: '/dev/sda', @@ -228,7 +238,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.SATA, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -242,18 +252,8 @@ describe('OnboardingInternalBootStep', () => { it('leaves the pool name blank when cache already exists', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [{ name: 'cache' }], - }, - vars: { - fsState: 'Stopped', - bootEligible: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ + poolNames: ['cache'], assignableDisks: [ { device: '/dev/sda', @@ -262,7 +262,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.SATA, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -272,20 +272,11 @@ describe('OnboardingInternalBootStep', () => { it('shows explicit disabled and empty-disk codes when the system reports them', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [], - }, - vars: { - fsState: 'Stopped', - bootEligible: false, - enableBootTransfer: 'no', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ + bootEligible: false, + enableBootTransfer: 'no', assignableDisks: [], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -299,19 +290,8 @@ describe('OnboardingInternalBootStep', () => { it('blocks configuration when internal boot is already configured while the system is still booted from flash', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [], - }, - vars: { - fsState: 'Stopped', - bootEligible: true, - bootedFromFlashWithInternalBootSetup: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ + bootedFromFlashWithInternalBootSetup: true, assignableDisks: [ { device: '/dev/sda', @@ -320,7 +300,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.SATA, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -333,18 +313,8 @@ describe('OnboardingInternalBootStep', () => { it('keeps the blocked headline focused on server state when eligible disks exist', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STARTED, - caches: [], - }, - vars: { - fsState: 'Started', - bootEligible: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ + arrayStopped: false, assignableDisks: [ { device: '/dev/sda', @@ -353,7 +323,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.SATA, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -365,18 +335,8 @@ describe('OnboardingInternalBootStep', () => { it('shows disk-level ineligibility while keeping the form available for eligible disks', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [{ name: 'cache' }], - }, - vars: { - fsState: 'Stopped', - bootEligible: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ + poolNames: ['cache'], assignableDisks: [ { device: '/dev/sdb', @@ -397,7 +357,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.USB, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -424,18 +384,7 @@ describe('OnboardingInternalBootStep', () => { it('treats disks present in devs.ini as assignable', async () => { draftStore.bootMode = 'storage'; - contextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [], - }, - vars: { - fsState: 'Stopped', - bootEligible: true, - enableBootTransfer: 'yes', - reservedNames: '', - }, - shares: [], + contextResult.value = buildContext({ assignableDisks: [ { device: '/dev/sda', @@ -444,7 +393,7 @@ describe('OnboardingInternalBootStep', () => { interfaceType: DiskInterfaceType.SATA, }, ], - }; + }); const wrapper = mountComponent(); await flushPromises(); @@ -458,4 +407,27 @@ describe('OnboardingInternalBootStep', () => { expect(wrapper.text()).not.toContain('NO_UNASSIGNED_DISKS'); expect(wrapper.find('[data-testid="brand-button"]').attributes('disabled')).toBeUndefined(); }); + + it('refreshes internal boot context on demand', async () => { + draftStore.bootMode = 'storage'; + contextResult.value = buildContext({ + assignableDisks: [ + { + device: '/dev/sda', + size: gib(32), + serialNum: 'UNASSIGNED-1', + interfaceType: DiskInterfaceType.SATA, + }, + ], + }); + + const wrapper = mountComponent(); + await flushPromises(); + + await wrapper.get('[data-testid="internal-boot-refresh-button"]').trigger('click'); + await flushPromises(); + + expect(refreshContextMutationMock).toHaveBeenCalledTimes(1); + expect(refetchContextMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts index 05a3a4e5e8..2f8b8e250d 100644 --- a/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts @@ -17,7 +17,6 @@ import type { GetInternalBootContextQuery } from '~/composables/gql/graphql'; import OnboardingSummaryStep from '~/components/Onboarding/steps/OnboardingSummaryStep.vue'; import { - ArrayState, DiskInterfaceType, GetInternalBootContextDocument, PluginInstallStatus, @@ -313,28 +312,29 @@ describe('OnboardingSummaryStep', () => { info: { primaryNetwork: { ipAddress: '192.168.1.2' } }, }; internalBootContextResult.value = { - array: { - state: ArrayState.STOPPED, - caches: [], - }, - vars: { + internalBootContext: { + arrayStopped: true, bootEligible: true, + bootedFromFlashWithInternalBootSetup: false, + enableBootTransfer: 'yes', + reservedNames: [], + shareNames: [], + poolNames: [], + assignableDisks: [ + { + device: '/dev/sda', + size: 500 * 1024 * 1024 * 1024, + serialNum: 'DISK-A', + interfaceType: DiskInterfaceType.SATA, + }, + { + device: '/dev/sdb', + size: 250 * 1024 * 1024 * 1024, + serialNum: 'DISK-B', + interfaceType: DiskInterfaceType.SATA, + }, + ], }, - shares: [], - assignableDisks: [ - { - device: '/dev/sda', - size: 500 * 1024 * 1024 * 1024, - serialNum: 'DISK-A', - interfaceType: DiskInterfaceType.SATA, - }, - { - device: '/dev/sdb', - size: 250 * 1024 * 1024 * 1024, - serialNum: 'DISK-B', - interfaceType: DiskInterfaceType.SATA, - }, - ], }; installedPluginsResult.value = { installedUnraidPlugins: [] }; availableLanguagesResult.value = { diff --git a/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts b/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts index 85a685b526..06e8d9dcfb 100644 --- a/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts +++ b/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts @@ -2,27 +2,20 @@ import gql from 'graphql-tag'; export const GET_INTERNAL_BOOT_CONTEXT_QUERY = gql` query GetInternalBootContext { - array { - state - caches { - name - } - } - vars { - fsState + internalBootContext { + arrayStopped bootEligible bootedFromFlashWithInternalBootSetup enableBootTransfer reservedNames - } - shares { - name - } - assignableDisks { - device - size - serialNum - interfaceType + shareNames + poolNames + assignableDisks { + device + size + serialNum + interfaceType + } } } `; diff --git a/web/src/components/Onboarding/graphql/refreshInternalBootContext.mutation.ts b/web/src/components/Onboarding/graphql/refreshInternalBootContext.mutation.ts new file mode 100644 index 0000000000..232f22cbf8 --- /dev/null +++ b/web/src/components/Onboarding/graphql/refreshInternalBootContext.mutation.ts @@ -0,0 +1,23 @@ +import { graphql } from '~/composables/gql'; + +export const REFRESH_INTERNAL_BOOT_CONTEXT_MUTATION = graphql(/* GraphQL */ ` + mutation RefreshInternalBootContext { + onboarding { + refreshInternalBootContext { + arrayStopped + bootEligible + bootedFromFlashWithInternalBootSetup + enableBootTransfer + reservedNames + shareNames + poolNames + assignableDisks { + device + size + serialNum + interfaceType + } + } + } + } +`); diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue index 730df2b00c..6eaae90d0a 100644 --- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue @@ -1,11 +1,17 @@ @@ -744,6 +766,19 @@ const primaryButtonText = computed(() => t('onboarding.internalBootStep.actions. {{ loadStatusMessage }} +
+ +
+