diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index b373873c11..9c82b25b98 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -548,6 +548,7 @@ type Vars implements Node { flashProduct: String flashVendor: String tpmGuid: String + usbGuid: String regCheck: String regFile: String regGuid: String diff --git a/api/src/core/types/states/var.ts b/api/src/core/types/states/var.ts index 7b7dc7eaaf..246343e506 100644 --- a/api/src/core/types/states/var.ts +++ b/api/src/core/types/states/var.ts @@ -33,6 +33,7 @@ export type Var = { flashProduct: string; flashVendor: string; tpmGuid?: string; + usbGuid?: string; /** Current progress of the {@link ?content=mover | mover}. */ fsCopyPrcnt: number; fsNumMounted: number; diff --git a/api/src/store/services/__test__/state-file-loader.test.ts b/api/src/store/services/__test__/state-file-loader.test.ts index 288f2df04f..ddbafc7e2b 100644 --- a/api/src/store/services/__test__/state-file-loader.test.ts +++ b/api/src/store/services/__test__/state-file-loader.test.ts @@ -13,7 +13,7 @@ const VAR_FIXTURE = readFileSync(new URL('../../../../dev/states/var.ini', impor const writeVarFixture = (dir: string, safeMode: 'yes' | 'no') => { const content = VAR_FIXTURE.replace(/safeMode="(yes|no)"/, `safeMode="${safeMode}"`).replace( /flashGUID="([^"]+)"/, - 'flashGUID="$1"\ntpmGUID="01-V35H8S0L1QHK1SBG1XHXJNH7"' + 'flashGUID="$1"\ntpmGUID="01-V35H8S0L1QHK1SBG1XHXJNH7"\nusbGUID="058F-6387-0000-0000F1F1E1C6"' ); writeFileSync(join(dir, `${StateFileKey.var}.ini`), content); }; @@ -47,6 +47,7 @@ describe('loadStateFileSync', () => { expect(result?.safeMode).toBe(true); expect(result?.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(result?.usbGuid).toBe('058F-6387-0000-0000F1F1E1C6'); expect(dispatchSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'emhttp/updateEmhttpState', @@ -55,6 +56,7 @@ describe('loadStateFileSync', () => { state: expect.objectContaining({ safeMode: true, tpmGuid: '01-V35H8S0L1QHK1SBG1XHXJNH7', + usbGuid: '058F-6387-0000-0000F1F1E1C6', }), }, }) diff --git a/api/src/store/state-parsers/var.ts b/api/src/store/state-parsers/var.ts index e0446a1220..6132943b6b 100644 --- a/api/src/store/state-parsers/var.ts +++ b/api/src/store/state-parsers/var.ts @@ -40,6 +40,7 @@ export type VarIni = { flashProduct: string; flashVendor: string; tpmGuid?: string; + usbGuid?: string; fsCopyPrcnt: string; fsNumMounted: string; fsNumUnmountable: string; diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index 5119a999ad..688be77e9c 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -3479,6 +3479,7 @@ export type Vars = Node & { sysModel?: Maybe; timeZone?: Maybe; tpmGuid?: Maybe; + usbGuid?: Maybe; /** Should a NTP server be used for time sync? */ useNtp?: Maybe; useSsh?: Maybe; 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 d249043341..d150656587 100644 --- a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts +++ b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts @@ -314,6 +314,9 @@ export class Vars extends Node { @Field({ nullable: true }) tpmGuid?: string; + @Field({ nullable: true }) + usbGuid?: string; + @Field({ nullable: true }) regCheck?: string; diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php index 53050ebab7..94fa6c8b5d 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php @@ -288,7 +288,10 @@ public function getServerState() "flashProduct" => $this->var['flashProduct'], "flashVendor" => $this->var['flashVendor'], "flashBackupActivated" => $this->flashBackupActivated, + "flashGuid" => $this->var['flashGUID'] ?? '', "guid" => $this->var['flashGUID'], + "tpmGuid" => $this->var['tpmGUID'] ?? '', + "usbGuid" => $this->var['usbGUID'] ?? '', "hasRemoteApikey" => $this->hasRemoteApikey, "internalPort" => _var($_SERVER, 'SERVER_PORT'), "keyfile" => $this->keyfileBase64UrlSafe, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a158aa8b9..ef0057ccdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1154,8 +1154,8 @@ importers: specifier: 3.0.7 version: 3.0.7(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.3)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31) '@unraid/shared-callbacks': - specifier: 3.1.0 - version: 3.1.0 + specifier: 3.3.0 + version: 3.3.0 '@unraid/ui': specifier: link:../unraid-ui version: link:../unraid-ui @@ -4973,8 +4973,8 @@ packages: cpu: [x64, arm64] os: [linux, darwin] - '@unraid/shared-callbacks@3.1.0': - resolution: {integrity: sha512-Zvz9nlvLSjbTstplvarbH2rwZrsl4ns7djpDmqPi4xH35PvYeK9ut1A/Hkd2Rbl/+nvAMF77d5ZzZkSTb4IoSg==} + '@unraid/shared-callbacks@3.3.0': + resolution: {integrity: sha512-yA207fLusDK/4XNfLuikl5H/AslXsalglHBcAP8HzvdZckmeVYBlsOG1fTbwowex+3lyWMSGBTDEQ4a1KxOcbA==} '@unraid/tailwind-rem-to-rem@2.0.0': resolution: {integrity: sha512-zccpQx5fvEBkAB0JkRwwtyRrT9l26LsjkozLy44LGv0NdZGaxgscniIqJRM+OQj5pSpsWDzExebAtUKdE98Flg==} @@ -16269,7 +16269,7 @@ snapshots: - encoding - supports-color - '@unraid/shared-callbacks@3.1.0': + '@unraid/shared-callbacks@3.3.0': dependencies: crypto-js: 4.2.0 diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts index 788542a161..d32ff3ec5c 100644 --- a/web/__test__/components/Registration.test.ts +++ b/web/__test__/components/Registration.test.ts @@ -118,6 +118,7 @@ const initialServerState = { dateTimeFormat: { date: 'MMM D, YYYY', time: 'h:mm A' }, deviceCount: 0, flashGuid: '', + usbGuid: '', guid: '', keyfile: '', mdState: '', @@ -342,6 +343,7 @@ describe('Registration.standalone.vue', () => { serverStore.state = 'PRO'; serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.usbGuid = '058F-6387-0000-0000F1F1E1C6'; serverStore.tpmGuid = '01-V35H8S0L1QHK1SBG1XHXJNH7'; serverStore.keyfile = 'keyfile-present'; @@ -351,9 +353,42 @@ describe('Registration.standalone.vue', () => { expect(moveButton.exists()).toBe(true); expect(moveButton.attributes('disabled')).toBeUndefined(); + expect(findItemByLabel(t('registration.flashGuid'))?.props('text')).toBe( + '058F-6387-0000-0000F1F1E1C6' + ); expect(findItemByLabel(t('TPM GUID'))?.props('text')).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); }); + it('shows both licensing GUIDs when no key is installed', async () => { + serverStore.state = 'ENOKEYFILE'; + serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.usbGuid = '058F-6387-0000-0000F1F1E1C6'; + serverStore.tpmGuid = '01-V35H8S0L1QHK1SBG1XHXJNH7'; + + await wrapper.vm.$nextTick(); + + expect(findItemByLabel(t('registration.flashGuid'))?.props('text')).toBe( + '058F-6387-0000-0000F1F1E1C6' + ); + expect(findItemByLabel(t('TPM GUID'))?.props('text')).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(wrapper.find('[data-testid="move-license-to-tpm"]').exists()).toBe(false); + }); + + it('does not label the selected TPM GUID as a USB GUID', async () => { + serverStore.guid = '01-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.flashGuid = '01-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.tpmGuid = '01-V35H8S0L1QHK1SBG1XHXJNH7'; + serverStore.usbGuid = ''; + + await wrapper.vm.$nextTick(); + + expect(findItemByLabel(t('registration.flashGuid'))).toBeUndefined(); + expect(findItemByLabel(t('registration.deviceGuid'))?.props('text')).toBe( + '01-V35H8S0L1QHK1SBG1XHXJNH7' + ); + }); + it('shows Move License to TPM when flashGuid is missing but the active GUID is still a flash GUID', async () => { serverStore.state = 'PRO'; serverStore.guid = '058F-6387-0000-0000F1F1E1C6'; diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index 9d1127c73c..1c8e15a70f 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -188,6 +188,8 @@ const getStore = () => { description: store.description, deviceCount: store.deviceCount, expireTime: store.expireTime, + flashGuid: store.flashGuid || undefined, + usbGuid: store.usbGuid || undefined, flashProduct: store.flashProduct, flashVendor: store.flashVendor, guid: store.guid, @@ -203,6 +205,7 @@ const getStore = () => { regTy: store.regTy, regUpdatesExpired: store.regUpdatesExpired, state: store.state, + tpmGuid: store.tpmGuid || undefined, wanFQDN: store.wanFQDN, }; @@ -218,6 +221,8 @@ const getStore = () => { deviceCount: store.deviceCount, description: store.description, expireTime: store.expireTime, + flashGuid: store.flashGuid || undefined, + usbGuid: store.usbGuid || undefined, flashProduct: store.flashProduct, flashVendor: store.flashVendor, guid: store.guid, @@ -233,6 +238,7 @@ const getStore = () => { regTy: store.regTy, regUpdatesExpired: store.regUpdatesExpired, state: store.state, + tpmGuid: store.tpmGuid || undefined, wanFQDN: store.wanFQDN, }; @@ -292,6 +298,14 @@ const getStore = () => { return store; }; +const getActualStore = () => + useServerStore( + createTestingPinia({ + createSpy: vi.fn, + stubActions: false, + }) + ); + // Mock dependent stores vi.mock('~/store/account', () => ({ useAccountStore: vi.fn(() => ({ @@ -651,6 +665,7 @@ describe('useServerStore', () => { deviceCount: 6, description: 'Test Server', expireTime: 123, + flashGuid: 'flash-guid-1', flashProduct: 'TestFlash', flashVendor: 'TestVendor', guid: '123456', @@ -675,6 +690,7 @@ describe('useServerStore', () => { expect(payload.description).toBe('Test Server'); expect(payload.deviceCount).toBe(6); expect(payload.expireTime).toBe(123); + expect(payload.flashGuid).toBe('flash-guid-1'); expect(payload.flashProduct).toBe('TestFlash'); expect(payload.flashVendor).toBe('TestVendor'); expect(payload.guid).toBe('123456'); @@ -767,6 +783,8 @@ describe('useServerStore', () => { regGuid: 'reg-guid-1', regTy: 'Plus', state: 'PLUS' as ServerState, + tpmGuid: '01-TPM-GUID-1', + usbGuid: 'usb-guid-1', wanFQDN: 'test.myunraid.net', }); @@ -777,6 +795,7 @@ describe('useServerStore', () => { expect(payload.deviceCount).toBe(6); expect(payload.description).toBe('Test Server'); expect(payload.expireTime).toBe(123); + expect(payload.flashGuid).toBe('flash-guid-1'); expect(payload.flashProduct).toBe('TestFlash'); expect(payload.flashVendor).toBe('TestVendor'); expect(payload.guid).toBe('123456'); @@ -792,9 +811,80 @@ describe('useServerStore', () => { expect(payload.regTy).toBe('Plus'); expect(payload.regUpdatesExpired).toBe(true); expect(payload.state).toBe('PLUS'); + expect(payload.tpmGuid).toBe('01-TPM-GUID-1'); + expect(payload.usbGuid).toBe('usb-guid-1'); expect(payload.wanFQDN).toBe('test.myunraid.net'); }); + it('should include physical identities in the actual callback payloads', () => { + const store = getActualStore(); + + store.setServer({ + flashGuid: '01-V35H8S0L1QHK1SBG1XHXJNH7', + guid: '01-V35H8S0L1QHK1SBG1XHXJNH7', + tpmGuid: '01-V35H8S0L1QHK1SBG1XHXJNH7', + usbGuid: '058F-6387-0000-0000F1F1E1C6', + } as Server); + + expect(store.serverPurchasePayload.flashGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverPurchasePayload.guid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverPurchasePayload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverPurchasePayload.usbGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(store.serverAccountPayload.flashGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverAccountPayload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverAccountPayload.usbGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(store.serverReplacePayload.flashGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverReplacePayload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(store.serverReplacePayload.usbGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + }); + + it('should preserve physical identities without inventing a selected GUID for an unregistered server', () => { + const store = getActualStore(); + + store.setServer({ + flashGuid: '', + guid: '', + regGuid: '', + state: 'ENOKEYFILE', + tpmGuid: '01-V35H8S0L1QHK1SBG1XHXJNH7', + usbGuid: '058F-6387-0000-0000F1F1E1C6', + } as Server); + + for (const payload of [ + store.serverPurchasePayload, + store.serverAccountPayload, + store.serverReplacePayload, + ]) { + expect(payload.flashGuid).toBeUndefined(); + expect(payload.tpmGuid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); + expect(payload.usbGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + } + expect(store.serverPurchasePayload.guid).toBe(''); + expect(store.serverAccountPayload.guid).toBe(''); + expect(store.serverReplacePayload.guid).toBeUndefined(); + }); + + it('should keep the legacy selected GUID when physical identity fields are absent', () => { + const store = getActualStore(); + + store.setServer({ + flashGuid: '058F-6387-0000-0000F1F1E1C6', + guid: '058F-6387-0000-0000F1F1E1C6', + state: 'PRO', + } as Server); + + for (const payload of [ + store.serverPurchasePayload, + store.serverAccountPayload, + store.serverReplacePayload, + ]) { + expect(payload.flashGuid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(payload.guid).toBe('058F-6387-0000-0000F1F1E1C6'); + expect(payload.usbGuid).toBeUndefined(); + expect(payload.tpmGuid).toBeUndefined(); + } + }); + it('should create serverReplacePayload with TPM guid when available on flash boot', () => { const store = getStore(); diff --git a/web/package.json b/web/package.json index d957dfb691..9f3d5c8b4e 100644 --- a/web/package.json +++ b/web/package.json @@ -117,7 +117,7 @@ "@nuxt/ui": "4.8.2", "@tanstack/vue-table": "8.21.3", "@tiptap/y-tiptap": "3.0.7", - "@unraid/shared-callbacks": "3.1.0", + "@unraid/shared-callbacks": "3.3.0", "@unraid/ui": "link:../unraid-ui", "@vue/apollo-composable": "4.2.2", "@vueuse/components": "13.8.0", diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue index d68331d4d2..a62438aec5 100644 --- a/web/src/components/Registration.standalone.vue +++ b/web/src/components/Registration.standalone.vue @@ -46,6 +46,7 @@ const { hasDistinctTpmGuid, dateTimeFormat, deviceCount, + usbGuid, flashProduct, flashVendor, guid, @@ -133,18 +134,24 @@ const showTpmTransferButton = computed((): boolean => ); const disableTpmTransferButton = computed((): boolean => showTrialExpiration.value); -// Organize items into three sections -const bootDeviceItems = computed((): RegistrationItemProps[] => { - return [ - ...(guid.value +const licensingGuidItems = computed((): RegistrationItemProps[] => { + const items: RegistrationItemProps[] = [ + ...(usbGuid.value ? [ { - label: t('registration.deviceGuid'), - text: guid.value, + label: t('registration.flashGuid'), + text: usbGuid.value, }, ] - : []), - ...(showTpmTransferButton.value && tpmGuid.value + : guid.value + ? [ + { + label: t('registration.deviceGuid'), + text: guid.value, + }, + ] + : []), + ...(tpmGuid.value && (usbGuid.value || tpmGuid.value !== guid.value) ? [ { label: t('registration.tpmGuid'), @@ -152,6 +159,15 @@ const bootDeviceItems = computed((): RegistrationItemProps[] => { }, ] : []), + ]; + + return items; +}); + +// Organize items into three sections +const bootDeviceItems = computed((): RegistrationItemProps[] => { + return [ + ...licensingGuidItems.value, ...(bootDeviceType.value ? [ { diff --git a/web/src/composables/gql/gql.ts b/web/src/composables/gql/gql.ts index 6ec86bda9a..e7599e4fb7 100644 --- a/web/src/composables/gql/gql.ts +++ b/web/src/composables/gql/gql.ts @@ -99,7 +99,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 bootedFromFlashWithInternalBootSetup\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 usbGuid\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 = { @@ -188,7 +188,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 bootedFromFlashWithInternalBootSetup\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 usbGuid\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, }; @@ -549,7 +549,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 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"]; +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 usbGuid\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 usbGuid\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 e9be3b367b..9aa1806af9 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -313,8 +313,6 @@ export type ArrayMutations = { clearArrayDiskStatistics: Scalars['Boolean']['output']; /** Mount a disk in the array */ mountArrayDisk: ArrayDisk; - /** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */ - removeDiskFromArray: UnraidArray; /** Set array state */ setState: UnraidArray; /** Unmount a disk from the array */ @@ -337,11 +335,6 @@ export type ArrayMutationsMountArrayDiskArgs = { }; -export type ArrayMutationsRemoveDiskFromArrayArgs = { - input: ArrayDiskInput; -}; - - export type ArrayMutationsSetStateArgs = { input: ArrayStateInput; }; @@ -3486,6 +3479,7 @@ export type Vars = Node & { sysModel?: Maybe; timeZone?: Maybe; tpmGuid?: Maybe; + usbGuid?: Maybe; /** Should a NTP server be used for time sync? */ useNtp?: Maybe; useSsh?: Maybe; @@ -4171,7 +4165,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', 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 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, usbGuid?: string | null, mdState?: string | null, regGen?: string | null, regState?: RegistrationState | null, configError?: ConfigErrorState | null, configValid?: boolean | null } }; export type GetThemeQueryVariables = Exact<{ [key: string]: never; }>; @@ -4263,5 +4257,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":"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 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":"usbGuid"}},{"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/locales/en.json b/web/src/locales/en.json index 0214592618..255f45f758 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -692,6 +692,7 @@ "registration.bootDeviceType.internalBootMulti": "Internal Boot (Multi-device)", "registration.bootDeviceType.tpm": "TPM", "registration.deviceGuid": "Device GUID", + "registration.flashGuid": "USB Flash GUID", "registration.flashProduct": "Flash Product", "registration.flashVendor": "Flash Vendor", "registration.general.goToToolsRegistrationToLearn": "Go to Tools > Registration to Learn More", diff --git a/web/src/store/server.fragment.ts b/web/src/store/server.fragment.ts index efe897c393..0af489f7af 100644 --- a/web/src/store/server.fragment.ts +++ b/web/src/store/server.fragment.ts @@ -91,6 +91,7 @@ export const SERVER_STATE_QUERY = graphql(/* GraphQL */ ` bootedFromFlashWithInternalBootSetup flashGuid tpmGuid + usbGuid mdState regGen regState diff --git a/web/src/store/server.ts b/web/src/store/server.ts index 5bde8e4b53..2b834510d3 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -150,6 +150,7 @@ export const useServerStore = defineStore('server', () => { const state = ref(); const theme = ref(); const tpmGuid = ref(''); + const usbGuid = ref(''); watch(theme, (newVal) => { if (newVal) { themeStore.setTheme(newVal); @@ -202,6 +203,8 @@ export const useServerStore = defineStore('server', () => { flashGuid: flashGuid.value, flashProduct: flashProduct.value, flashVendor: flashVendor.value, + // Preserve the selected legacy GUID, including an empty no-key value. + // Account resolves usbGuid/tpmGuid separately; neither is an implicit fallback. guid: guid.value, bootedFromFlashWithInternalBootSetup: bootedFromFlashWithInternalBootSetup.value, inIframe: inIframe.value, @@ -225,6 +228,7 @@ export const useServerStore = defineStore('server', () => { state: state.value, theme: theme.value, tpmGuid: tpmGuid.value, + usbGuid: usbGuid.value, uptime: uptime.value, username: username.value, wanFQDN: wanFQDN.value, @@ -253,6 +257,7 @@ export const useServerStore = defineStore('server', () => { description: description.value, deviceCount: deviceCount.value, expireTime: expireTime.value, + flashGuid: flashGuid.value || undefined, flashProduct: flashProduct.value, flashVendor: flashVendor.value, guid: guid.value, @@ -268,6 +273,8 @@ export const useServerStore = defineStore('server', () => { regTy: regTy.value, regUpdatesExpired: regUpdatesExpired.value, state: state.value, + tpmGuid: tpmGuid.value || undefined, + usbGuid: usbGuid.value || undefined, wanFQDN: wanFQDN.value, ...overrides, }; @@ -1145,6 +1152,9 @@ export const useServerStore = defineStore('server', () => { if (typeof data?.tpmGuid !== 'undefined') { tpmGuid.value = data.tpmGuid; } + if (typeof data?.usbGuid !== 'undefined') { + usbGuid.value = data.usbGuid; + } if (typeof data?.updateOsIgnoredReleases !== 'undefined') { updateOsIgnoredReleases.value = data.updateOsIgnoredReleases; } @@ -1206,6 +1216,7 @@ export const useServerStore = defineStore('server', () => { regTy: data.registration?.type ?? undefined, state: data.registration?.state ?? data.vars?.regState ?? undefined, tpmGuid: data.vars?.tpmGuid ?? undefined, + usbGuid: data.vars?.usbGuid ?? '', config: data.config ? { id: 'config', ...data.config } : { @@ -1462,6 +1473,7 @@ export const useServerStore = defineStore('server', () => { state, theme, tpmGuid, + usbGuid, updateOsIgnoredReleases, updateOsNotificationsEnabled, updateOsResponse, diff --git a/web/types/server.ts b/web/types/server.ts index e64b0f0e84..b7bb8751f1 100644 --- a/web/types/server.ts +++ b/web/types/server.ts @@ -121,6 +121,7 @@ export interface Server { state?: ServerState; theme?: Theme | undefined; tpmGuid?: string; + usbGuid?: string; updateOsIgnoredReleases?: string[]; updateOsNotificationsEnabled?: boolean; updateOsResponse?: ServerUpdateOsResponse;