diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 6f35a56d8e..66bcc8231c 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -596,6 +596,7 @@ type Vars implements Node { fsState: String bootEligible: Boolean enableBootTransfer: String + bootedFromFlashWithInternalBootSetup: Boolean reservedNames: String """Human friendly string of array events happening""" diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.module.ts b/api/src/unraid-api/graph/resolvers/disks/disks.module.ts index 69521c9e2f..1619ff2f61 100644 --- a/api/src/unraid-api/graph/resolvers/disks/disks.module.ts +++ b/api/src/unraid-api/graph/resolvers/disks/disks.module.ts @@ -4,11 +4,12 @@ import { ArrayModule } from '@app/unraid-api/graph/resolvers/array/array.module. import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js'; import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; import { InternalBootNotificationService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-notification.service.js'; +import { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; import { NotificationsModule } from '@app/unraid-api/graph/resolvers/notifications/notifications.module.js'; @Module({ imports: [ArrayModule, NotificationsModule], - providers: [DisksResolver, DisksService, InternalBootNotificationService], - exports: [DisksResolver, DisksService], + providers: [DisksResolver, DisksService, InternalBootNotificationService, InternalBootStateService], + exports: [DisksResolver, DisksService, InternalBootStateService], }) export class DisksModule {} diff --git a/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.spec.ts b/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.spec.ts index 739d0bd534..728d13b683 100644 --- a/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.spec.ts @@ -6,8 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ArrayDiskType } from '@app/unraid-api/graph/resolvers/array/array.model.js'; import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; -import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; import { InternalBootNotificationService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-notification.service.js'; +import { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; import { NotificationImportance } from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; @@ -15,8 +15,8 @@ const mockArrayService = { getArrayData: vi.fn(), }; -const mockDisksService = { - getInternalBootDevices: vi.fn(), +const mockInternalBootStateService = { + getBootedFromFlashWithInternalBootSetupForBootDisk: vi.fn(), }; const mockNotificationsService = { @@ -37,8 +37,8 @@ describe('InternalBootNotificationService', () => { useValue: mockArrayService, }, { - provide: DisksService, - useValue: mockDisksService, + provide: InternalBootStateService, + useValue: mockInternalBootStateService, }, { provide: NotificationsService, @@ -62,7 +62,9 @@ describe('InternalBootNotificationService', () => { device: 'sda', }, }); - mockDisksService.getInternalBootDevices.mockResolvedValue([{ device: '/dev/nvme0n1' }]); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockResolvedValue( + true + ); mockNotificationsService.notifyIfUnique.mockResolvedValue(null); await service.onApplicationBootstrap(); @@ -84,7 +86,9 @@ describe('InternalBootNotificationService', () => { device: 'nvme0n1', }, }); - mockDisksService.getInternalBootDevices.mockResolvedValue([{ device: '/dev/nvme0n1' }]); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockResolvedValue( + true + ); await service.onApplicationBootstrap(); @@ -99,7 +103,9 @@ describe('InternalBootNotificationService', () => { device: 'sda', }, }); - mockDisksService.getInternalBootDevices.mockResolvedValue([]); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockResolvedValue( + false + ); await service.onApplicationBootstrap(); @@ -120,7 +126,9 @@ describe('InternalBootNotificationService', () => { device: 'sda', }, }); - mockDisksService.getInternalBootDevices.mockResolvedValue([{ device: '/dev/nvme0n1' }]); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockResolvedValue( + true + ); mockNotificationsService.notifyIfUnique.mockResolvedValue(null); const bootstrapPromise = service.onApplicationBootstrap(); @@ -139,7 +147,9 @@ describe('InternalBootNotificationService', () => { mockArrayService.getArrayData.mockResolvedValue({ boot: undefined, }); - mockDisksService.getInternalBootDevices.mockResolvedValue([{ device: '/dev/nvme0n1' }]); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockResolvedValue( + true + ); const bootstrapPromise = service.onApplicationBootstrap(); @@ -163,7 +173,9 @@ describe('InternalBootNotificationService', () => { device: 'sda', }, }); - mockDisksService.getInternalBootDevices.mockRejectedValue(new Error('lsblk failed')); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockRejectedValue( + new Error('lsblk failed') + ); await expect(service.onApplicationBootstrap()).resolves.toBeUndefined(); @@ -183,7 +195,9 @@ describe('InternalBootNotificationService', () => { device: 'sda', }, }); - mockDisksService.getInternalBootDevices.mockResolvedValue([{ device: '/dev/nvme0n1' }]); + mockInternalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk.mockResolvedValue( + true + ); mockNotificationsService.notifyIfUnique.mockRejectedValue(new Error('notify failed')); await expect(service.onApplicationBootstrap()).resolves.toBeUndefined(); diff --git a/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.ts b/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.ts index dfab58b4e8..c96b234243 100644 --- a/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.ts +++ b/api/src/unraid-api/graph/resolvers/disks/internal-boot-notification.service.ts @@ -3,7 +3,7 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import type { NotificationData } from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { ArrayDiskType } from '@app/unraid-api/graph/resolvers/array/array.model.js'; import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.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 { NotificationImportance } from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; @@ -16,7 +16,7 @@ export class InternalBootNotificationService implements OnApplicationBootstrap { constructor( private readonly arrayService: ArrayService, - private readonly disksService: DisksService, + private readonly internalBootStateService: InternalBootStateService, private readonly notificationsService: NotificationsService ) {} @@ -39,8 +39,11 @@ export class InternalBootNotificationService implements OnApplicationBootstrap { } try { - const internalBootDevices = await this.disksService.getInternalBootDevices(); - if (internalBootDevices.length === 0) { + const bootedFromFlashWithInternalBootSetup = + await this.internalBootStateService.getBootedFromFlashWithInternalBootSetupForBootDisk( + bootDisk + ); + if (!bootedFromFlashWithInternalBootSetup) { this.logger.debug( `Skipping internal boot notification: no internal boot candidates found for ${bootDisk.device ?? bootDisk.id}` ); diff --git a/api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts b/api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts new file mode 100644 index 0000000000..039a5141b5 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; + +import type { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { ArrayDiskType } from '@app/unraid-api/graph/resolvers/array/array.model.js'; +import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js'; +import { DisksService } from '@app/unraid-api/graph/resolvers/disks/disks.service.js'; + +@Injectable() +export class InternalBootStateService { + constructor( + private readonly arrayService: ArrayService, + private readonly disksService: DisksService + ) {} + + public async getBootedFromFlashWithInternalBootSetup(): Promise { + const array = await this.arrayService.getArrayData(); + return this.getBootedFromFlashWithInternalBootSetupForBootDisk(array.boot); + } + + public async getBootedFromFlashWithInternalBootSetupForBootDisk( + bootDisk: Pick | null | undefined + ): Promise { + if (!bootDisk || bootDisk.type !== ArrayDiskType.FLASH) { + return false; + } + + const internalBootDevices = await this.disksService.getInternalBootDevices(); + return internalBootDevices.length > 0; + } +} 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 a57700b50a..316ccf73ad 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,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { emcmd } from '@app/core/utils/clients/emcmd.js'; import { getters } from '@app/store/index.js'; import { loadStateFileSync } from '@app/store/services/state-file-loader.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'; vi.mock('@app/core/utils/clients/emcmd.js', () => ({ @@ -25,17 +26,27 @@ vi.mock('@app/store/services/state-file-loader.js', () => ({ })); describe('OnboardingInternalBootService', () => { + const internalBootStateService = { + getBootedFromFlashWithInternalBootSetup: vi.fn(), + }; + beforeEach(() => { vi.clearAllMocks(); + internalBootStateService.getBootedFromFlashWithInternalBootSetup.mockResolvedValue(false); vi.mocked(getters.emhttp).mockReturnValue({ devices: [], disks: [], } as unknown as ReturnType); }); + const createService = () => + new OnboardingInternalBootService( + internalBootStateService as unknown as InternalBootStateService + ); + it('runs the internal boot emcmd sequence and returns success', async () => { vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited>); - const service = new OnboardingInternalBootService(); + const service = createService(); const result = await service.createInternalBootPool({ poolName: 'cache', @@ -116,7 +127,7 @@ describe('OnboardingInternalBootService', () => { stderr: '', exitCode: 0, } as Awaited>); - const service = new OnboardingInternalBootService(); + const service = createService(); const result = await service.createInternalBootPool({ poolName: 'cache', @@ -183,7 +194,7 @@ describe('OnboardingInternalBootService', () => { stderr: '', exitCode: 1, } as Awaited>); - const service = new OnboardingInternalBootService(); + const service = createService(); const result = await service.createInternalBootPool({ poolName: 'cache', @@ -201,7 +212,7 @@ describe('OnboardingInternalBootService', () => { }); it('returns validation error for duplicate devices', async () => { - const service = new OnboardingInternalBootService(); + const service = createService(); const result = await service.createInternalBootPool({ poolName: 'cache', @@ -218,9 +229,48 @@ describe('OnboardingInternalBootService', () => { expect(vi.mocked(emcmd)).not.toHaveBeenCalled(); }); + it('returns validation error when internal boot is already configured while booted from flash', async () => { + internalBootStateService.getBootedFromFlashWithInternalBootSetup.mockResolvedValue(true); + const service = createService(); + + const result = await service.createInternalBootPool({ + poolName: 'cache', + devices: ['disk-1'], + bootSizeMiB: 16384, + updateBios: false, + }); + + expect(result).toMatchObject({ + ok: false, + code: 3, + }); + expect(result.output).toContain('internal boot is already configured'); + expect(vi.mocked(emcmd)).not.toHaveBeenCalled(); + }); + + it('returns failure output when the internal boot state lookup throws', async () => { + internalBootStateService.getBootedFromFlashWithInternalBootSetup.mockRejectedValue( + new Error('state lookup failed') + ); + const service = createService(); + + const result = await service.createInternalBootPool({ + poolName: 'cache', + devices: ['disk-1'], + bootSizeMiB: 16384, + updateBios: false, + }); + + expect(result.ok).toBe(false); + expect(result.code).toBe(1); + expect(result.output).toContain('mkbootpool: command failed or timed out'); + expect(result.output).toContain('state lookup failed'); + expect(vi.mocked(emcmd)).not.toHaveBeenCalled(); + }); + it('returns failure output when emcmd command throws', async () => { vi.mocked(emcmd).mockRejectedValue(new Error('socket failure')); - const service = new OnboardingInternalBootService(); + const service = createService(); const result = await service.createInternalBootPool({ poolName: 'cache', 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 622a735f9a..d3ef1bd74b 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 @@ -12,6 +12,7 @@ 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 { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; const INTERNAL_BOOT_COMMAND_TIMEOUT_MS = 180000; const EFI_BOOT_PATH = '\\EFI\\BOOT\\BOOTX64.EFI'; @@ -32,6 +33,12 @@ const isEmhttpDeviceRecord = (value: unknown): value is EmhttpDeviceRecord => { @Injectable() export class OnboardingInternalBootService { + constructor(private readonly internalBootStateService: InternalBootStateService) {} + + private async isBootedFromFlashWithInternalBootSetup(): Promise { + return this.internalBootStateService.getBootedFromFlashWithInternalBootSetup(); + } + private async runStep( commandText: string, command: Record, @@ -342,6 +349,14 @@ export class OnboardingInternalBootService { } try { + if (await this.isBootedFromFlashWithInternalBootSetup()) { + return { + ok: false, + code: 3, + output: 'mkbootpool: internal boot is already configured while the system is still booted from flash', + }; + } + await this.runStep( 'debug=cmdCreatePool,cmdAssignDisk,cmdMakeBootable', { debug: 'cmdCreatePool,cmdAssignDisk,cmdMakeBootable' }, diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts index 15ec2b727d..d249043341 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts @@ -437,6 +437,9 @@ export class Vars extends Node { @Field({ nullable: true }) enableBootTransfer?: string; + @Field({ nullable: true }) + bootedFromFlashWithInternalBootSetup?: boolean; + @Field({ nullable: true }) reservedNames?: string; diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts index cfb0f385ab..83e40a128e 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts @@ -1,15 +1,35 @@ import type { TestingModule } from '@nestjs/testing'; +import { Logger } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getters } from '@app/store/index.js'; +import { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; import { VarsResolver } from '@app/unraid-api/graph/resolvers/vars/vars.resolver.js'; import { VarsService } from '@app/unraid-api/graph/resolvers/vars/vars.service.js'; +vi.mock('@app/store/index.js', () => ({ + getters: { + emhttp: vi.fn(), + }, +})); + describe('VarsResolver', () => { let resolver: VarsResolver; + const internalBootStateService = { + getBootedFromFlashWithInternalBootSetup: vi.fn(), + }; beforeEach(async () => { + vi.clearAllMocks(); + vi.mocked(getters.emhttp).mockReturnValue({ + var: { + enableBootTransfer: 'yes', + }, + } as unknown as ReturnType); + internalBootStateService.getBootedFromFlashWithInternalBootSetup.mockResolvedValue(false); + const module: TestingModule = await Test.createTestingModule({ providers: [ VarsResolver, @@ -17,6 +37,10 @@ describe('VarsResolver', () => { provide: VarsService, useValue: {}, }, + { + provide: InternalBootStateService, + useValue: internalBootStateService, + }, ], }).compile(); @@ -26,4 +50,22 @@ describe('VarsResolver', () => { it('should be defined', () => { expect(resolver).toBeDefined(); }); + + it('returns vars with a safe null boot state when the shared lookup throws', async () => { + const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); + internalBootStateService.getBootedFromFlashWithInternalBootSetup.mockRejectedValue( + new Error('lookup failed') + ); + + const result = await resolver.vars(); + + expect(result).toMatchObject({ + id: 'vars', + enableBootTransfer: 'yes', + bootedFromFlashWithInternalBootSetup: null, + }); + expect(warnSpy).toHaveBeenCalledWith( + 'Failed to resolve bootedFromFlashWithInternalBootSetup in vars(): lookup failed' + ); + }); }); diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts index 3ea90995e8..f17b70a6cd 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts @@ -1,15 +1,22 @@ +import { Logger } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; import { getters } from '@app/store/index.js'; +import { InternalBootStateService } from '@app/unraid-api/graph/resolvers/disks/internal-boot-state.service.js'; import { UpdateSshInput, Vars } from '@app/unraid-api/graph/resolvers/vars/vars.model.js'; import { VarsService } from '@app/unraid-api/graph/resolvers/vars/vars.service.js'; @Resolver(() => Vars) export class VarsResolver { - constructor(private readonly varsService: VarsService) {} + private readonly logger = new Logger(VarsResolver.name); + + constructor( + private readonly varsService: VarsService, + private readonly internalBootStateService: InternalBootStateService + ) {} @Query(() => Vars) @UsePermissions({ @@ -17,9 +24,21 @@ export class VarsResolver { resource: Resource.VARS, }) public async vars() { + let bootedFromFlashWithInternalBootSetup: boolean | null = null; + try { + bootedFromFlashWithInternalBootSetup = + await this.internalBootStateService.getBootedFromFlashWithInternalBootSetup(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn( + `Failed to resolve bootedFromFlashWithInternalBootSetup in vars(): ${message}` + ); + } + return { id: 'vars', ...(getters.emhttp().var ?? {}), + bootedFromFlashWithInternalBootSetup, }; } diff --git a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts index 1005019235..bb8d7d8b5b 100644 --- a/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts +++ b/web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts @@ -336,11 +336,48 @@ describe('OnboardingInternalBootStep', () => { await wrapper.get('[data-testid="internal-boot-eligibility-toggle"]').trigger('click'); await flushPromises(); expect(wrapper.text()).toContain('ENABLE_BOOT_TRANSFER_DISABLED'); - expect(wrapper.text()).toContain('ALREADY_INTERNAL_BOOT'); expect(wrapper.text()).toContain('BOOT_ELIGIBLE_FALSE'); expect(wrapper.text()).toContain('NO_UNASSIGNED_DISKS'); }); + 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, + boot: null, + parities: [], + disks: [], + caches: [], + }, + vars: { + fsState: 'Stopped', + bootEligible: true, + bootedFromFlashWithInternalBootSetup: true, + enableBootTransfer: 'yes', + reservedNames: '', + }, + shares: [], + disks: [ + { + device: '/dev/sda', + size: gib(32), + serialNum: 'ELIGIBLE-1', + emhttpDeviceId: 'eligible-disk', + interfaceType: DiskInterfaceType.SATA, + }, + ], + }; + + const wrapper = mountComponent(); + await flushPromises(); + + await wrapper.get('[data-testid="internal-boot-eligibility-toggle"]').trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('ALREADY_INTERNAL_BOOT'); + expect(wrapper.find('[data-testid="brand-button"]').attributes('disabled')).toBeDefined(); + }); + it('keeps the blocked headline focused on server state when eligible disks exist', async () => { draftStore.bootMode = 'storage'; contextResult.value = { diff --git a/web/__test__/components/Onboarding/OnboardingModal.test.ts b/web/__test__/components/Onboarding/OnboardingModal.test.ts index 74d824fdd6..8d7ec6a29d 100644 --- a/web/__test__/components/Onboarding/OnboardingModal.test.ts +++ b/web/__test__/components/Onboarding/OnboardingModal.test.ts @@ -8,6 +8,7 @@ import { createTestI18n } from '../../utils/i18n'; type InternalBootVisibilityResult = { value: { vars: { + bootedFromFlashWithInternalBootSetup: boolean | null; enableBootTransfer: string | null; }; }; @@ -29,6 +30,7 @@ const { internalBootVisibilityResult: { value: { vars: { + bootedFromFlashWithInternalBootSetup: false, enableBootTransfer: 'yes', }, }, @@ -188,6 +190,7 @@ describe('OnboardingModal.vue', () => { onboardingDraftStore.internalBootApplySucceeded.value = false; internalBootVisibilityResult.value = { vars: { + bootedFromFlashWithInternalBootSetup: false, enableBootTransfer: 'yes', }, }; @@ -332,6 +335,7 @@ describe('OnboardingModal.vue', () => { it('hides internal boot step when boot transfer state is unknown', () => { internalBootVisibilityResult.value = { vars: { + bootedFromFlashWithInternalBootSetup: null, enableBootTransfer: null, }, }; @@ -354,6 +358,7 @@ describe('OnboardingModal.vue', () => { it('hides internal boot step when already booting internally', () => { internalBootVisibilityResult.value = { vars: { + bootedFromFlashWithInternalBootSetup: false, enableBootTransfer: 'no', }, }; @@ -365,6 +370,21 @@ describe('OnboardingModal.vue', () => { expect(wrapper.find('[data-testid="plugins-step"]').exists()).toBe(true); }); + it('hides internal boot step when still booted from flash but internal boot is already configured', () => { + internalBootVisibilityResult.value = { + vars: { + bootedFromFlashWithInternalBootSetup: true, + enableBootTransfer: 'yes', + }, + }; + onboardingDraftStore.currentStepIndex.value = 2; + + const wrapper = mountComponent(); + + expect(wrapper.find('[data-testid="internal-boot-step"]').exists()).toBe(false); + expect(wrapper.find('[data-testid="plugins-step"]').exists()).toBe(true); + }); + it('opens exit confirmation when close button is clicked', async () => { const wrapper = mountComponent(); diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index 90a8177bc5..93be118662 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -400,6 +400,16 @@ describe('useServerStore', () => { expect(store.state).toBe('PRO'); }); + it('should allow clearing the reported internal boot setup state back to the fallback value', () => { + const store = getStore(); + + store.setServer({ bootedFromFlashWithInternalBootSetup: true }); + expect(store.bootedFromFlashWithInternalBootSetup).toBe(true); + + store.setServer({ bootedFromFlashWithInternalBootSetup: undefined }); + expect(store.bootedFromFlashWithInternalBootSetup).toBe(false); + }); + it('should compute regDevs correctly based on regTy', () => { const store = getStore(); diff --git a/web/src/components/Onboarding/OnboardingModal.vue b/web/src/components/Onboarding/OnboardingModal.vue index 5b4a881213..bc450672a0 100644 --- a/web/src/components/Onboarding/OnboardingModal.vue +++ b/web/src/components/Onboarding/OnboardingModal.vue @@ -82,7 +82,13 @@ const { result: internalBootVisibilityResult } = useQuery(GetInternalBootStepVis const showInternalBootStep = computed(() => { const setting = internalBootVisibilityResult.value?.vars?.enableBootTransfer; - return typeof setting === 'string' && setting.trim().toLowerCase() === 'yes'; + const bootedFromFlashWithInternalBootSetup = + internalBootVisibilityResult.value?.vars?.bootedFromFlashWithInternalBootSetup === true; + return ( + typeof setting === 'string' && + setting.trim().toLowerCase() === 'yes' && + !bootedFromFlashWithInternalBootSetup + ); }); // Determine which steps to show based on user state diff --git a/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts b/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts index 874f427d49..14cce60524 100644 --- a/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts +++ b/web/src/components/Onboarding/graphql/getInternalBootContext.query.ts @@ -21,6 +21,7 @@ export const GET_INTERNAL_BOOT_CONTEXT_QUERY = gql` vars { fsState bootEligible + bootedFromFlashWithInternalBootSetup enableBootTransfer reservedNames } diff --git a/web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts b/web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts index b826ebed99..1c2727f558 100644 --- a/web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts +++ b/web/src/components/Onboarding/graphql/getInternalBootStepVisibility.query.ts @@ -3,6 +3,7 @@ import gql from 'graphql-tag'; export const GET_INTERNAL_BOOT_STEP_VISIBILITY_QUERY = gql` query GetInternalBootStepVisibility { vars { + bootedFromFlashWithInternalBootSetup enableBootTransfer } } diff --git a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue index 7eadd998da..f7503717c8 100644 --- a/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue +++ b/web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue @@ -288,6 +288,9 @@ const internalBootTransferState = computed(() => { } return 'unknown'; }); +const bootedFromFlashWithInternalBootSetup = computed( + () => contextResult.value?.vars?.bootedFromFlashWithInternalBootSetup === true +); const bootEligibilityState = computed(() => { const eligibility = contextResult.value?.vars?.bootEligible; if (eligibility === true) { @@ -323,8 +326,11 @@ const systemEligibilityCodes = computed(() if (!isArrayStopped.value) { codes.push('ARRAY_NOT_STOPPED'); } + if (bootedFromFlashWithInternalBootSetup.value) { + codes.push('ALREADY_INTERNAL_BOOT'); + } if (internalBootTransferState.value === 'disabled') { - codes.push('ENABLE_BOOT_TRANSFER_DISABLED', 'ALREADY_INTERNAL_BOOT'); + codes.push('ENABLE_BOOT_TRANSFER_DISABLED'); } if (internalBootTransferState.value === 'unknown') { codes.push('ENABLE_BOOT_TRANSFER_UNKNOWN'); @@ -353,6 +359,7 @@ const diskEligibilityIssues = computed(() => const canConfigure = computed( () => internalBootTransferState.value === 'enabled' && + !bootedFromFlashWithInternalBootSetup.value && isArrayStopped.value && bootEligibilityState.value === 'eligible' && deviceOptions.value.length > 0 diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue index 2796fc95e6..e5ab57f4c6 100644 --- a/web/src/components/Registration.standalone.vue +++ b/web/src/components/Registration.standalone.vue @@ -46,6 +46,7 @@ const { activationCode } = storeToRefs(useActivationCodeDataStore()); const { bootDeviceType, + bootedFromFlashWithInternalBootSetup, dateTimeFormat, deviceCount, flashProduct, @@ -136,13 +137,7 @@ const showPartnerActivationCode = computed(() => { }); const showTpmTransferButton = computed((): boolean => Boolean( - (keyInstalled.value || showTrialExpiration.value) && - bootDeviceType.value === 'flash' && - // The active GUID tells us whether we're still booted from flash, even if - // flashGuid is missing or has already fallen back to the TPM value. - guid.value && - tpmGuid.value && - guid.value !== tpmGuid.value + (keyInstalled.value || showTrialExpiration.value) && bootedFromFlashWithInternalBootSetup.value ) ); const disableTpmTransferButton = computed((): boolean => showTrialExpiration.value); diff --git a/web/src/composables/gql/gql.ts b/web/src/composables/gql/gql.ts index beafb3feb3..933f271f91 100644 --- a/web/src/composables/gql/gql.ts +++ b/web/src/composables/gql/gql.ts @@ -74,8 +74,8 @@ 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 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 GetInternalBootStepVisibility {\n vars {\n enableBootTransfer\n }\n }\n": typeof types.GetInternalBootStepVisibilityDocument, + "\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 GetInternalBootStepVisibility {\n vars {\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n }\n }\n": typeof types.GetInternalBootStepVisibilityDocument, "\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, @@ -95,7 +95,7 @@ type Documents = { "\n query IsSSOEnabled {\n isSSOEnabled\n }\n": typeof types.IsSsoEnabledDocument, "\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": typeof types.PartialCloudFragmentDoc, "\n query cloudState {\n cloud {\n ...PartialCloud\n }\n }\n": typeof types.CloudStateDocument, - "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n": typeof types.ServerStateDocument, + "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n bootedFromFlashWithInternalBootSetup\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n": typeof types.ServerStateDocument, "\n query getTheme {\n publicTheme {\n name\n showBannerImage\n showBannerGradient\n headerBackgroundColor\n showHeaderDescription\n headerPrimaryTextColor\n headerSecondaryTextColor\n }\n }\n": typeof types.GetThemeDocument, }; const documents: Documents = { @@ -159,8 +159,8 @@ 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 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 GetInternalBootStepVisibility {\n vars {\n enableBootTransfer\n }\n }\n": types.GetInternalBootStepVisibilityDocument, + "\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 GetInternalBootStepVisibility {\n vars {\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n }\n }\n": types.GetInternalBootStepVisibilityDocument, "\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, @@ -180,7 +180,7 @@ const documents: Documents = { "\n query IsSSOEnabled {\n isSSOEnabled\n }\n": types.IsSsoEnabledDocument, "\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc, "\n query cloudState {\n cloud {\n ...PartialCloud\n }\n }\n": types.CloudStateDocument, - "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n": types.ServerStateDocument, + "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n bootedFromFlashWithInternalBootSetup\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n": types.ServerStateDocument, "\n query getTheme {\n publicTheme {\n name\n showBannerImage\n showBannerGradient\n headerBackgroundColor\n showHeaderDescription\n headerPrimaryTextColor\n headerSecondaryTextColor\n }\n }\n": types.GetThemeDocument, }; @@ -441,11 +441,11 @@ 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 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 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 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"]; /** * 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 GetInternalBootStepVisibility {\n vars {\n enableBootTransfer\n }\n }\n"): (typeof documents)["\n query GetInternalBootStepVisibility {\n vars {\n enableBootTransfer\n }\n }\n"]; +export function graphql(source: "\n query GetInternalBootStepVisibility {\n vars {\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n }\n }\n"): (typeof documents)["\n query GetInternalBootStepVisibility {\n vars {\n bootedFromFlashWithInternalBootSetup\n enableBootTransfer\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -525,7 +525,7 @@ export function graphql(source: "\n query cloudState {\n cloud {\n ...P /** * 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 serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n"): (typeof documents)["\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n"]; +export function graphql(source: "\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n bootedFromFlashWithInternalBootSetup\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\n }\n }\n"): (typeof documents)["\n query serverState {\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n type\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n bootedFromFlashWithInternalBootSetup\n flashGuid\n tpmGuid\n mdState\n regGen\n regState\n configError\n configValid\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 d00f44c1fb..b424688a9a 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -3209,6 +3209,7 @@ export type Vars = Node & { __typename?: 'Vars'; bindMgt?: Maybe; bootEligible?: Maybe; + bootedFromFlashWithInternalBootSetup?: Maybe; cacheNumDevices?: Maybe; cacheSbNumDisks?: Maybe; comment?: Maybe; @@ -3904,12 +3905,12 @@ 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, 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 }>, disks: Array<{ __typename?: 'Disk', device: string, size: number, serialNum: string, emhttpDeviceId?: string | null, interfaceType: DiskInterfaceType }> }; export type GetInternalBootStepVisibilityQueryVariables = Exact<{ [key: string]: never; }>; -export type GetInternalBootStepVisibilityQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', enableBootTransfer?: string | null } }; +export type GetInternalBootStepVisibilityQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', bootedFromFlashWithInternalBootSetup?: boolean | null, enableBootTransfer?: string | null } }; export type InstallLanguageMutationVariables = Exact<{ input: InstallPluginInput; @@ -4027,7 +4028,7 @@ export type CloudStateQuery = { __typename?: 'Query', cloud: ( export type ServerStateQueryVariables = Exact<{ [key: string]: never; }>; -export type ServerStateQuery = { __typename?: 'Query', config: { __typename?: 'Config', error?: string | null, valid?: boolean | null }, info: { __typename?: 'Info', os: { __typename?: 'InfoOs', hostname?: string | null } }, owner: { __typename?: 'Owner', avatar: string, username: string }, registration?: { __typename?: 'Registration', type?: RegistrationType | null, state?: RegistrationState | null, expiration?: string | null, updateExpiration?: string | null, keyFile?: { __typename?: 'KeyFile', contents?: string | null } | null } | null, vars: { __typename?: 'Vars', flashGuid?: string | null, tpmGuid?: string | null, mdState?: string | null, regGen?: string | null, regState?: RegistrationState | null, configError?: ConfigErrorState | null, configValid?: boolean | null } }; +export type ServerStateQuery = { __typename?: 'Query', config: { __typename?: 'Config', error?: string | null, valid?: boolean | null }, info: { __typename?: 'Info', os: { __typename?: 'InfoOs', hostname?: string | null } }, owner: { __typename?: 'Owner', avatar: string, username: string }, registration?: { __typename?: 'Registration', type?: RegistrationType | null, state?: RegistrationState | null, expiration?: string | null, updateExpiration?: string | null, keyFile?: { __typename?: 'KeyFile', contents?: string | null } | null } | null, vars: { __typename?: 'Vars', bootedFromFlashWithInternalBootSetup?: boolean | null, flashGuid?: string | null, tpmGuid?: string | null, mdState?: string | null, regGen?: string | null, regState?: RegistrationState | null, configError?: ConfigErrorState | null, configValid?: boolean | null } }; export type GetThemeQueryVariables = Exact<{ [key: string]: never; }>; @@ -4095,8 +4096,8 @@ 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":"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 GetInternalBootStepVisibilityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInternalBootStepVisibility"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enableBootTransfer"}}]}}]}}]} 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 GetInternalBootStepVisibilityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInternalBootStepVisibility"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bootedFromFlashWithInternalBootSetup"}},{"kind":"Field","name":{"kind":"Name","value":"enableBootTransfer"}}]}}]}}]} 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; @@ -4115,5 +4116,5 @@ export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":" export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode; export const IsSsoEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IsSSOEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isSSOEnabled"}}]}}]} as unknown as DocumentNode; export const CloudStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"cloudState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; -export const ServerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"flashGuid"}},{"kind":"Field","name":{"kind":"Name","value":"tpmGuid"}},{"kind":"Field","name":{"kind":"Name","value":"mdState"}},{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}}]} as unknown as DocumentNode; +export const ServerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bootedFromFlashWithInternalBootSetup"}},{"kind":"Field","name":{"kind":"Name","value":"flashGuid"}},{"kind":"Field","name":{"kind":"Name","value":"tpmGuid"}},{"kind":"Field","name":{"kind":"Name","value":"mdState"}},{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}}]} as unknown as DocumentNode; export const GetThemeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getTheme"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicTheme"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerImage"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"headerBackgroundColor"}},{"kind":"Field","name":{"kind":"Name","value":"showHeaderDescription"}},{"kind":"Field","name":{"kind":"Name","value":"headerPrimaryTextColor"}},{"kind":"Field","name":{"kind":"Name","value":"headerSecondaryTextColor"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/web/src/store/server.fragment.ts b/web/src/store/server.fragment.ts index dd8933f3f0..efe897c393 100644 --- a/web/src/store/server.fragment.ts +++ b/web/src/store/server.fragment.ts @@ -88,6 +88,7 @@ export const SERVER_STATE_QUERY = graphql(/* GraphQL */ ` updateExpiration } vars { + bootedFromFlashWithInternalBootSetup flashGuid tpmGuid mdState diff --git a/web/src/store/server.ts b/web/src/store/server.ts index b6d97b0f8a..1df696e7c5 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -92,6 +92,7 @@ export const useServerStore = defineStore('server', () => { const flashProduct = ref(''); const flashVendor = ref(''); const guid = ref(''); + const reportedBootedFromFlashWithInternalBootSetup = ref(undefined); const bootDeviceType = computed( (): 'flash' | 'internalBoot' | 'internalBootMulti' | 'tpm' | undefined => { if (!guid.value) return undefined; @@ -139,6 +140,9 @@ export const useServerStore = defineStore('server', () => { bootDeviceType.value === 'flash' && Boolean(guid.value && tpmGuid.value && guid.value !== tpmGuid.value) ); + const bootedFromFlashWithInternalBootSetup = computed( + () => reportedBootedFromFlashWithInternalBootSetup.value ?? hasDistinctTpmGuid.value + ); const replaceFlashGuid = computed(() => { if (hasDistinctTpmGuid.value) { return tpmGuid.value; @@ -203,6 +207,7 @@ export const useServerStore = defineStore('server', () => { flashProduct: flashProduct.value, flashVendor: flashVendor.value, guid: guid.value, + bootedFromFlashWithInternalBootSetup: bootedFromFlashWithInternalBootSetup.value, inIframe: inIframe.value, keyfile: keyfile.value, lanIp: lanIp.value, @@ -1064,6 +1069,9 @@ export const useServerStore = defineStore('server', () => { if (typeof data?.guid !== 'undefined') { guid.value = data.guid; } + if (data && 'bootedFromFlashWithInternalBootSetup' in data) { + reportedBootedFromFlashWithInternalBootSetup.value = data.bootedFromFlashWithInternalBootSetup; + } if (typeof data?.keyfile !== 'undefined') { keyfile.value = data.keyfile; } @@ -1175,6 +1183,7 @@ export const useServerStore = defineStore('server', () => { data.registration && data.registration.keyFile && data.registration.keyFile.contents ? data.registration.keyFile.contents : undefined, + bootedFromFlashWithInternalBootSetup: data.vars?.bootedFromFlashWithInternalBootSetup ?? undefined, flashGuid: data.vars?.flashGuid ?? undefined, mdState: data.vars?.mdState ?? undefined, regGen: data.vars && data.vars.regGen ? parseInt(data.vars.regGen) : undefined, @@ -1410,6 +1419,7 @@ export const useServerStore = defineStore('server', () => { flashVendor, guid, bootDeviceType, + bootedFromFlashWithInternalBootSetup, keyfile, inIframe, locale, diff --git a/web/types/server.ts b/web/types/server.ts index 99b5403d2b..e64b0f0e84 100644 --- a/web/types/server.ts +++ b/web/types/server.ts @@ -95,6 +95,7 @@ export interface Server { flashProduct?: string; flashVendor?: string; guid?: string; + bootedFromFlashWithInternalBootSetup?: boolean; inIframe?: boolean; keyfile?: string; lanIp?: string;