From 78c4b927b49552fe4227b10742697ac711e5128b Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 09:32:32 -0400
Subject: [PATCH 1/9] Add TPM licensing availability to registration
---
api/dev/states/var.ini | 3 +-
api/generated-schema.graphql | 1 +
api/src/__test__/store/modules/emhttp.test.ts | 1 +
.../__test__/store/state-parsers/var.test.ts | 1 +
api/src/core/types/states/var.ts | 1 +
.../__test__/state-file-loader.test.ts | 11 +++++--
api/src/store/state-parsers/var.ts | 1 +
api/src/unraid-api/cli/generated/graphql.ts | 5 +++-
.../graph/resolvers/vars/vars.model.ts | 3 ++
web/__test__/components/Registration.test.ts | 20 +++++++++++++
web/_webGui/testWebComponents.page | 2 ++
web/src/_data/serverState.ts | 3 ++
.../components/Registration.standalone.vue | 29 +++++++++++++++++++
web/src/composables/gql/gql.ts | 6 ++--
web/src/composables/gql/graphql.ts | 7 +++--
web/src/locales/en.json | 6 ++++
web/src/store/server.fragment.ts | 2 ++
web/src/store/server.ts | 15 ++++++++++
web/types/server.ts | 2 ++
19 files changed, 109 insertions(+), 10 deletions(-)
diff --git a/api/dev/states/var.ini b/api/dev/states/var.ini
index 662903a4c0..c8df905cfb 100644
--- a/api/dev/states/var.ini
+++ b/api/dev/states/var.ini
@@ -91,6 +91,7 @@ configValid="ineligible"
joinStatus="Not joined"
deviceCount="4"
flashGUID="0000-0000-0000-000000000000"
+tpmGUID="03-V35H8S0L1QHK1SBG1XHXJNH7"
flashProduct="DataTraveler_3.0"
flashVendor="KINGSTON"
regCheck=""
@@ -141,4 +142,4 @@ shareSMBCount="1"
shareNFSCount="0"
shareMoverActive="no"
reservedNames="parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31"
-csrf_token="0000000000000000"
\ No newline at end of file
+csrf_token="0000000000000000"
diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql
index 2f51f85b5b..6f35a56d8e 100644
--- a/api/generated-schema.graphql
+++ b/api/generated-schema.graphql
@@ -552,6 +552,7 @@ type Vars implements Node {
flashGuid: String
flashProduct: String
flashVendor: String
+ tpmGuid: String
regCheck: String
regFile: String
regGuid: String
diff --git a/api/src/__test__/store/modules/emhttp.test.ts b/api/src/__test__/store/modules/emhttp.test.ts
index e9e987e859..696f8d35c6 100644
--- a/api/src/__test__/store/modules/emhttp.test.ts
+++ b/api/src/__test__/store/modules/emhttp.test.ts
@@ -1144,6 +1144,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"sysFlashSlots": 1,
"sysModel": "Dell R710",
"timeZone": "Australia/Adelaide",
+ "tpmGuid": "03-V35H8S0L1QHK1SBG1XHXJNH7",
"useNetbios": "yes",
"useNtp": true,
"useSsh": true,
diff --git a/api/src/__test__/store/state-parsers/var.test.ts b/api/src/__test__/store/state-parsers/var.test.ts
index 9b70869387..26042cc934 100644
--- a/api/src/__test__/store/state-parsers/var.test.ts
+++ b/api/src/__test__/store/state-parsers/var.test.ts
@@ -161,6 +161,7 @@ test('Returns parsed state file', async () => {
"sysFlashSlots": 1,
"sysModel": "Dell R710",
"timeZone": "Australia/Adelaide",
+ "tpmGuid": "03-V35H8S0L1QHK1SBG1XHXJNH7",
"useNetbios": "yes",
"useNtp": true,
"useSsh": true,
diff --git a/api/src/core/types/states/var.ts b/api/src/core/types/states/var.ts
index 2245f78c34..7b7dc7eaaf 100644
--- a/api/src/core/types/states/var.ts
+++ b/api/src/core/types/states/var.ts
@@ -32,6 +32,7 @@ export type Var = {
flashGuid: string;
flashProduct: string;
flashVendor: string;
+ tpmGuid?: 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 d699cdcab3..6738014c33 100644
--- a/api/src/store/services/__test__/state-file-loader.test.ts
+++ b/api/src/store/services/__test__/state-file-loader.test.ts
@@ -11,7 +11,10 @@ import { StateFileKey } from '@app/store/types.js';
const VAR_FIXTURE = readFileSync(new URL('../../../../dev/states/var.ini', import.meta.url), 'utf-8');
const writeVarFixture = (dir: string, safeMode: 'yes' | 'no') => {
- const content = VAR_FIXTURE.replace(/safeMode="(yes|no)"/, `safeMode="${safeMode}"`);
+ const content = VAR_FIXTURE.replace(/safeMode="(yes|no)"/, `safeMode="${safeMode}"`).replace(
+ /flashGUID="([^"]+)"/,
+ 'flashGUID="$1"\ntpmGUID="03-V35H8S0L1QHK1SBG1XHXJNH7"'
+ );
writeFileSync(join(dir, `${StateFileKey.var}.ini`), content);
};
@@ -43,12 +46,16 @@ describe('loadStateFileSync', () => {
const result = loadStateFileSync(StateFileKey.var);
expect(result?.safeMode).toBe(true);
+ expect(result?.tpmGuid).toBe('03-V35H8S0L1QHK1SBG1XHXJNH7');
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: 'emhttp/updateEmhttpState',
payload: {
field: StateFileKey.var,
- state: expect.objectContaining({ safeMode: true }),
+ state: expect.objectContaining({
+ safeMode: true,
+ tpmGuid: '03-V35H8S0L1QHK1SBG1XHXJNH7',
+ }),
},
})
);
diff --git a/api/src/store/state-parsers/var.ts b/api/src/store/state-parsers/var.ts
index 140569d71e..e0446a1220 100644
--- a/api/src/store/state-parsers/var.ts
+++ b/api/src/store/state-parsers/var.ts
@@ -39,6 +39,7 @@ export type VarIni = {
flashGuid: string;
flashProduct: string;
flashVendor: string;
+ tpmGuid?: 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 e95c361499..1f03d6b58f 100644
--- a/api/src/unraid-api/cli/generated/graphql.ts
+++ b/api/src/unraid-api/cli/generated/graphql.ts
@@ -414,6 +414,8 @@ export type BrandingConfig = {
background?: Maybe;
/** Banner image source. Supports local path, remote URL, or data URI/base64. */
bannerImage?: Maybe;
+ /** Built-in case model value written to case-model.cfg when no custom override is supplied. */
+ caseModel?: Maybe;
/** Case model image source. Supports local path, remote URL, or data URI/base64. */
caseModelImage?: Maybe;
/** Indicates if a partner logo exists */
@@ -451,6 +453,7 @@ export type BrandingConfig = {
export type BrandingConfigInput = {
background?: InputMaybe;
bannerImage?: InputMaybe;
+ caseModel?: InputMaybe;
caseModelImage?: InputMaybe;
hasPartnerLogo?: InputMaybe;
header?: InputMaybe;
@@ -3548,4 +3551,4 @@ export const GetSsoUsersDocument = {"kind":"Document","definitions":[{"kind":"Op
export const SystemReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SystemReport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"machineId"}},{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"serial"}},{"kind":"Field","name":{"kind":"Name","value":"uuid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraid"}},{"kind":"Field","name":{"kind":"Name","value":"kernel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"packages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"openssl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
export const ConnectStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dynamicRemoteAccess"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabledType"}},{"kind":"Field","name":{"kind":"Name","value":"runningType"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode;
export const ServicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"online"}},{"kind":"Field","name":{"kind":"Name","value":"uptime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode;
-export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode;
+export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
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 1097db4f15..15ec2b727d 100644
--- a/api/src/unraid-api/graph/resolvers/vars/vars.model.ts
+++ b/api/src/unraid-api/graph/resolvers/vars/vars.model.ts
@@ -311,6 +311,9 @@ export class Vars extends Node {
@Field({ nullable: true })
flashVendor?: string;
+ @Field({ nullable: true })
+ tpmGuid?: string;
+
@Field({ nullable: true })
regCheck?: string;
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index d6d363d925..a08e0770ce 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -121,6 +121,7 @@ vi.mock('~/components/UserProfile/UptimeExpire.vue', () => ({
const initialServerState = {
dateTimeFormat: { date: 'MMM D, YYYY', time: 'h:mm A' },
deviceCount: 0,
+ flashGuid: '',
guid: '',
keyfile: '',
regGuid: '',
@@ -133,6 +134,7 @@ const initialServerState = {
state: 'ENOKEYFILE',
stateData: { heading: 'Default Heading', message: 'Default Message' },
stateDataError: false,
+ tpmGuid: '',
tooManyDevices: false,
};
@@ -288,6 +290,24 @@ describe('Registration.standalone.vue', () => {
expect(attachedStorageDevicesItem?.props('text')).toBe('8 out of unlimited devices');
});
+ it('shows TPM transfer guidance when TPM licensing is available', async () => {
+ serverStore.state = 'PRO';
+ serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.keyfile = 'keyfile-present';
+
+ await wrapper.vm.$nextTick();
+
+ const transferNotice = wrapper.find('[data-testid="tpm-transfer-available"]');
+
+ expect(transferNotice.exists()).toBe(true);
+ expect(transferNotice.text()).toContain('TPM licensing is available on this server.');
+ expect(transferNotice.text()).toContain('Stop the array.');
+ expect(transferNotice.text()).toContain('Remove the USB flash boot device.');
+ expect(transferNotice.text()).toContain('Start the array.');
+ });
+
it('adds Activate Trial fallback for ENOKEYFILE partner activation', async () => {
activationCodeStateHolder.current!.value = {
code: 'PARTNER-CODE-123',
diff --git a/web/_webGui/testWebComponents.page b/web/_webGui/testWebComponents.page
index 58b07af40c..3aad585c30 100644
--- a/web/_webGui/testWebComponents.page
+++ b/web/_webGui/testWebComponents.page
@@ -90,6 +90,7 @@ $serverData = [
"flashProduct" => $var['flashProduct'],
"flashVendor" => $var['flashVendor'],
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
+ "flashGuid" => $var['flashGUID'],
"guid" => $var['flashGUID'],
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
"lanIp" => ipaddr(),
@@ -113,6 +114,7 @@ $serverData = [
"registeredTime" => $myservers['remote']['regWizTime'] ?? '',
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
+ "tpmGuid" => $var['tpmGUID'] ?? '',
"textColor" => ($header) ? '#'.$header : '',
"theme" => $display['theme'],
"ts" => time(),
diff --git a/web/src/_data/serverState.ts b/web/src/_data/serverState.ts
index 92f2b20adc..2a9293ace3 100644
--- a/web/src/_data/serverState.ts
+++ b/web/src/_data/serverState.ts
@@ -49,6 +49,7 @@ import type {
const state: ServerState = 'BASIC' as ServerState;
const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from
+const currentTpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server
const keyfileBase64 = '';
@@ -147,6 +148,7 @@ const baseServerState: Server = {
deviceCount: 3,
expireTime,
flashBackupActivated: !!connectPluginInstalled,
+ flashGuid: currentFlashGuid,
flashProduct: 'SanDisk_3.2Gen1',
flashVendor: 'USB',
guid: currentFlashGuid,
@@ -181,6 +183,7 @@ const baseServerState: Server = {
name: 'white',
textColor: '',
},
+ tpmGuid: currentTpmGuid,
// updateOsResponse: {
// version: '6.12.6',
// name: 'Unraid 6.12.6',
diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue
index 5c54af1156..5674511f7e 100644
--- a/web/src/components/Registration.standalone.vue
+++ b/web/src/components/Registration.standalone.vue
@@ -47,6 +47,7 @@ const {
bootDeviceType,
dateTimeFormat,
deviceCount,
+ flashGuid,
flashProduct,
flashVendor,
guid,
@@ -63,6 +64,7 @@ const {
state,
stateData,
stateDataError,
+ tpmGuid,
tooManyDevices,
} = storeToRefs(serverStore);
@@ -132,6 +134,15 @@ const showPartnerActivationCode = computed(() => {
(currentState === 'ENOKEYFILE' || currentState === 'TRIAL' || currentState === 'EEXPIRED')
);
});
+const showTpmTransferInfo = computed((): boolean =>
+ Boolean(
+ keyInstalled.value &&
+ bootDeviceType.value === 'flash' &&
+ flashGuid.value &&
+ tpmGuid.value &&
+ flashGuid.value !== tpmGuid.value
+ )
+);
// Organize items into three sections
const bootDeviceItems = computed((): RegistrationItemProps[] => {
@@ -384,6 +395,24 @@ const actionItems = computed((): RegistrationItemProps[] => {
class="rounded-lg border border-gray-200 p-4 dark:border-gray-700"
>
{{ t('registration.actions') }}
+
+
+ {{ t('registration.tpmTransferAvailable') }}
+
+
+ {{ t('registration.tpmTransferAvailableDescription') }}
+
+
+ - {{ t('registration.tpmTransferAvailableSteps.stopArray') }}
+ - {{ t('registration.tpmTransferAvailableSteps.removeFlash') }}
+ - {{ t('registration.tpmTransferAvailableSteps.transferOnRegistrationPage') }}
+ - {{ t('registration.tpmTransferAvailableSteps.startArray') }}
+
+
;
sysModel?: Maybe;
timeZone?: Maybe;
+ tpmGuid?: Maybe;
/** Should a NTP server be used for time sync? */
useNtp?: Maybe;
useSsh?: Maybe;
@@ -4026,7 +4027,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', 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', flashGuid?: string | null, tpmGuid?: string | null, regGen?: string | null, regState?: RegistrationState | null, configError?: ConfigErrorState | null, configValid?: boolean | null } };
export type GetThemeQueryVariables = Exact<{ [key: string]: never; }>;
@@ -4114,5 +4115,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":"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;
+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":"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 1c6973b4fe..2c1533ec6d 100644
--- a/web/src/locales/en.json
+++ b/web/src/locales/en.json
@@ -672,6 +672,12 @@
"registration.registeredTo": "Registered to",
"registration.activationCode": "Activation Code",
"registration.partnerActivationDetected": "It appears you already have a license associated with this server. You can activate it now for free to unlock all features.",
+ "registration.tpmTransferAvailable": "TPM licensing is available on this server.",
+ "registration.tpmTransferAvailableDescription": "To move this license from your USB flash device to TPM, follow these steps from Tools > Registration.",
+ "registration.tpmTransferAvailableSteps.stopArray": "Stop the array.",
+ "registration.tpmTransferAvailableSteps.removeFlash": "Remove the USB flash boot device.",
+ "registration.tpmTransferAvailableSteps.transferOnRegistrationPage": "On Tools > Registration, initiate a license transfer.",
+ "registration.tpmTransferAvailableSteps.startArray": "Start the array.",
"registration.replaceCheck.checkEligibility": "Check Eligibility",
"registration.transferLicenseToNewDevice": "Transfer License to New Device",
"registration.trialExpiration": "Trial expiration",
diff --git a/web/src/store/server.fragment.ts b/web/src/store/server.fragment.ts
index a384604b82..3613f3d296 100644
--- a/web/src/store/server.fragment.ts
+++ b/web/src/store/server.fragment.ts
@@ -88,6 +88,8 @@ export const SERVER_STATE_QUERY = graphql(/* GraphQL */ `
updateExpiration
}
vars {
+ flashGuid
+ tpmGuid
regGen
regState
configError
diff --git a/web/src/store/server.ts b/web/src/store/server.ts
index e4c74399b2..d39aee868a 100644
--- a/web/src/store/server.ts
+++ b/web/src/store/server.ts
@@ -88,6 +88,7 @@ export const useServerStore = defineStore('server', () => {
const email = ref('');
const expireTime = ref(0);
const flashBackupActivated = ref(false);
+ const flashGuid = ref('');
const flashProduct = ref('');
const flashVendor = ref('');
const guid = ref('');
@@ -136,6 +137,7 @@ export const useServerStore = defineStore('server', () => {
const ssoEnabled = ref(false);
const state = ref();
const theme = ref();
+ const tpmGuid = ref('');
watch(theme, (newVal) => {
if (newVal) {
themeStore.setTheme(newVal);
@@ -185,6 +187,7 @@ export const useServerStore = defineStore('server', () => {
deviceCount: deviceCount.value,
email: email.value,
expireTime: expireTime.value,
+ flashGuid: flashGuid.value,
flashProduct: flashProduct.value,
flashVendor: flashVendor.value,
guid: guid.value,
@@ -207,6 +210,7 @@ export const useServerStore = defineStore('server', () => {
site: site.value,
state: state.value,
theme: theme.value,
+ tpmGuid: tpmGuid.value,
uptime: uptime.value,
username: username.value,
wanFQDN: wanFQDN.value,
@@ -288,6 +292,7 @@ export const useServerStore = defineStore('server', () => {
regTy: regTy.value,
site: site.value,
state: state.value,
+ tpmGuid: tpmGuid.value,
uptime: uptime.value,
username: username.value,
wanFQDN: wanFQDN.value,
@@ -1027,6 +1032,9 @@ export const useServerStore = defineStore('server', () => {
if (typeof data?.flashBackupActivated !== 'undefined') {
flashBackupActivated.value = data.flashBackupActivated;
}
+ if (typeof data?.flashGuid !== 'undefined') {
+ flashGuid.value = data.flashGuid;
+ }
if (typeof data?.flashProduct !== 'undefined') {
flashProduct.value = data.flashProduct;
}
@@ -1087,6 +1095,9 @@ export const useServerStore = defineStore('server', () => {
if (typeof data?.theme !== 'undefined') {
theme.value = data.theme;
}
+ if (typeof data?.tpmGuid !== 'undefined') {
+ tpmGuid.value = data.tpmGuid;
+ }
if (typeof data?.updateOsIgnoredReleases !== 'undefined') {
updateOsIgnoredReleases.value = data.updateOsIgnoredReleases;
}
@@ -1141,9 +1152,11 @@ export const useServerStore = defineStore('server', () => {
data.registration && data.registration.keyFile && data.registration.keyFile.contents
? data.registration.keyFile.contents
: undefined,
+ flashGuid: data.vars?.flashGuid ?? undefined,
regGen: data.vars && data.vars.regGen ? parseInt(data.vars.regGen) : undefined,
regTy: data.registration?.type ?? undefined,
state: data.registration?.state ?? data.vars?.regState ?? undefined,
+ tpmGuid: data.vars?.tpmGuid ?? undefined,
config: data.config
? { id: 'config', ...data.config }
: {
@@ -1368,6 +1381,7 @@ export const useServerStore = defineStore('server', () => {
deviceCount,
expireTime,
flashBackupActivated,
+ flashGuid,
flashProduct,
flashVendor,
guid,
@@ -1395,6 +1409,7 @@ export const useServerStore = defineStore('server', () => {
ssoEnabled,
state,
theme,
+ tpmGuid,
updateOsIgnoredReleases,
updateOsNotificationsEnabled,
updateOsResponse,
diff --git a/web/types/server.ts b/web/types/server.ts
index 8045936f79..5c80e48f9e 100644
--- a/web/types/server.ts
+++ b/web/types/server.ts
@@ -91,6 +91,7 @@ export interface Server {
email?: string;
expireTime?: number;
flashBackupActivated?: boolean;
+ flashGuid?: string;
flashProduct?: string;
flashVendor?: string;
guid?: string;
@@ -117,6 +118,7 @@ export interface Server {
ssoEnabled?: boolean;
state?: ServerState;
theme?: Theme | undefined;
+ tpmGuid?: string;
updateOsIgnoredReleases?: string[];
updateOsNotificationsEnabled?: boolean;
updateOsResponse?: ServerUpdateOsResponse;
From b7e26280c93ee95d82dbd96d55aa58a263371140 Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 09:45:38 -0400
Subject: [PATCH 2/9] Hide TPM transfer notice for trial licenses
---
web/__test__/components/Registration.test.ts | 12 ++++++++++++
web/src/components/Registration.standalone.vue | 3 +++
2 files changed, 15 insertions(+)
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index a08e0770ce..2860ad3ba2 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -308,6 +308,18 @@ describe('Registration.standalone.vue', () => {
expect(transferNotice.text()).toContain('Start the array.');
});
+ it('does not show TPM transfer guidance for trial states', async () => {
+ serverStore.state = 'TRIAL';
+ serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.keyfile = 'keyfile-present';
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('[data-testid="tpm-transfer-available"]').exists()).toBe(false);
+ });
+
it('adds Activate Trial fallback for ENOKEYFILE partner activation', async () => {
activationCodeStateHolder.current!.value = {
code: 'PARTNER-CODE-123',
diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue
index 5674511f7e..4a80ceaec8 100644
--- a/web/src/components/Registration.standalone.vue
+++ b/web/src/components/Registration.standalone.vue
@@ -137,7 +137,10 @@ const showPartnerActivationCode = computed(() => {
const showTpmTransferInfo = computed((): boolean =>
Boolean(
keyInstalled.value &&
+ !showTrialExpiration.value &&
bootDeviceType.value === 'flash' &&
+ // On TPM systems, flashGuid may fall back to the TPM GUID, so only show
+ // this while we're still booted from flash and the GUIDs differ.
flashGuid.value &&
tpmGuid.value &&
flashGuid.value !== tpmGuid.value
From 7abf943f2ba806d6445077eb671d1b950dcfc014 Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 12:47:56 -0400
Subject: [PATCH 3/9] Refine TPM transfer registration guidance
---
web/__test__/components/Registration.test.ts | 23 +++++
.../components/Registration.standalone.vue | 90 +++++++++++++++++--
web/src/locales/en.json | 7 +-
3 files changed, 112 insertions(+), 8 deletions(-)
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index 2860ad3ba2..dc4839cba2 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -305,7 +305,10 @@ describe('Registration.standalone.vue', () => {
expect(transferNotice.text()).toContain('TPM licensing is available on this server.');
expect(transferNotice.text()).toContain('Stop the array.');
expect(transferNotice.text()).toContain('Remove the USB flash boot device.');
+ expect(transferNotice.text()).toContain('Refresh this page.');
+ expect(transferNotice.text()).toContain('Press Replace Key.');
expect(transferNotice.text()).toContain('Start the array.');
+ expect(transferNotice.text()).not.toContain('Tools > Registration');
});
it('does not show TPM transfer guidance for trial states', async () => {
@@ -320,6 +323,26 @@ describe('Registration.standalone.vue', () => {
expect(wrapper.find('[data-testid="tpm-transfer-available"]').exists()).toBe(false);
});
+ it('shows checked TPM transfer steps after switching to TPM boot', async () => {
+ serverStore.state = 'EGUID';
+ serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6';
+
+ await wrapper.vm.$nextTick();
+
+ const transferNotice = wrapper.find('[data-testid="tpm-transfer-ready"]');
+
+ expect(transferNotice.exists()).toBe(true);
+ expect(transferNotice.text()).toContain('Continue your TPM license transfer.');
+ expect(transferNotice.text()).toContain('The first two steps are already complete.');
+ expect(transferNotice.text()).toContain('[x]');
+ expect(transferNotice.text()).toContain('Stop the array.');
+ expect(transferNotice.text()).toContain('Remove the USB flash boot device.');
+ expect(transferNotice.text()).toContain('Press Replace Key.');
+ expect(transferNotice.text()).toContain('Start the array.');
+ });
+
it('adds Activate Trial fallback for ENOKEYFILE partner activation', async () => {
activationCodeStateHolder.current!.value = {
code: 'PARTNER-CODE-123',
diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue
index 4a80ceaec8..aa3d2dbceb 100644
--- a/web/src/components/Registration.standalone.vue
+++ b/web/src/components/Registration.standalone.vue
@@ -146,6 +146,56 @@ const showTpmTransferInfo = computed((): boolean =>
flashGuid.value !== tpmGuid.value
)
);
+const showTpmTransferReadyInfo = computed((): boolean =>
+ Boolean(
+ !showTrialExpiration.value &&
+ state.value === 'EGUID' &&
+ bootDeviceType.value === 'tpm' &&
+ guid.value &&
+ tpmGuid.value &&
+ guid.value === tpmGuid.value
+ )
+);
+const tpmTransferPreparationSteps = computed(() => [
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.stopArray'),
+ },
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.removeFlash'),
+ },
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.refreshPage'),
+ },
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.replaceKey'),
+ },
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.startArray'),
+ },
+]);
+const tpmTransferReadySteps = computed(() => [
+ {
+ completed: true,
+ label: t('registration.tpmTransferAvailableSteps.stopArray'),
+ },
+ {
+ completed: true,
+ label: t('registration.tpmTransferAvailableSteps.removeFlash'),
+ },
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.replaceKey'),
+ },
+ {
+ completed: false,
+ label: t('registration.tpmTransferAvailableSteps.startArray'),
+ },
+]);
// Organize items into three sections
const bootDeviceItems = computed((): RegistrationItemProps[] => {
@@ -409,12 +459,40 @@ const actionItems = computed((): RegistrationItemProps[] => {
{{ t('registration.tpmTransferAvailableDescription') }}
-
- - {{ t('registration.tpmTransferAvailableSteps.stopArray') }}
- - {{ t('registration.tpmTransferAvailableSteps.removeFlash') }}
- - {{ t('registration.tpmTransferAvailableSteps.transferOnRegistrationPage') }}
- - {{ t('registration.tpmTransferAvailableSteps.startArray') }}
-
+
+ -
+ {{ step.completed ? '[x]' : '[ ]' }}
+ {{ step.label }}
+
+
+
+
+
+ {{ t('registration.tpmTransferReady') }}
+
+
+ {{ t('registration.tpmTransferReadyDescription') }}
+
+
+ -
+
+ {{ step.completed ? '[x]' : '[ ]' }}
+
+ {{ step.label }}
+
+
Registration.",
+ "registration.tpmTransferAvailableDescription": "To move this license from your USB flash device to TPM, complete these steps on this page.",
"registration.tpmTransferAvailableSteps.stopArray": "Stop the array.",
"registration.tpmTransferAvailableSteps.removeFlash": "Remove the USB flash boot device.",
- "registration.tpmTransferAvailableSteps.transferOnRegistrationPage": "On Tools > Registration, initiate a license transfer.",
+ "registration.tpmTransferAvailableSteps.refreshPage": "Refresh this page.",
+ "registration.tpmTransferAvailableSteps.replaceKey": "Press Replace Key.",
"registration.tpmTransferAvailableSteps.startArray": "Start the array.",
+ "registration.tpmTransferReady": "Continue your TPM license transfer.",
+ "registration.tpmTransferReadyDescription": "The first two steps are already complete. Press Replace Key to transfer this license to TPM, then start the array.",
"registration.replaceCheck.checkEligibility": "Check Eligibility",
"registration.transferLicenseToNewDevice": "Transfer License to New Device",
"registration.trialExpiration": "Trial expiration",
From c31be46b218b2c4a9817c17f4c4a87b666122b76 Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 13:46:54 -0400
Subject: [PATCH 4/9] Rename registration boot device copy
---
web/__test__/components/Registration.test.ts | 3 +++
web/src/locales/en.json | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index dc4839cba2..b3c724ff3f 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -260,6 +260,9 @@ describe('Registration.standalone.vue', () => {
await wrapper.vm.$nextTick();
+ expect(wrapper.text()).toContain('License Device');
+ expect(wrapper.text()).toContain('License device type');
+
const keyTypeItem = findItemByLabel(t('License key type'));
expect(keyTypeItem).toBeDefined();
diff --git a/web/src/locales/en.json b/web/src/locales/en.json
index 0ffa226c10..f7a9351bce 100644
--- a/web/src/locales/en.json
+++ b/web/src/locales/en.json
@@ -647,8 +647,8 @@
"notifications.sidebar.unreadTab": "Unread",
"registration.actions": "Actions",
"registration.attachedStorageDevices": "Attached Storage Devices",
- "registration.bootDevice": "Boot Device",
- "registration.bootDeviceType": "Boot device type",
+ "registration.bootDevice": "License Device",
+ "registration.bootDeviceType": "License device type",
"registration.bootDeviceType.flash": "USB Flash",
"registration.bootDeviceType.internalBoot": "Internal Boot",
"registration.bootDeviceType.internalBootMulti": "Internal Boot (Multi-device)",
From 1f243d7667208b426c20e32469ae1205bed1f278 Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 14:13:35 -0400
Subject: [PATCH 5/9] Drive TPM transfer step from array state
---
web/__test__/components/Registration.test.ts | 39 +++++++++++++++++++
.../components/Registration.standalone.vue | 13 +++++--
web/src/composables/gql/gql.ts | 6 +--
web/src/composables/gql/graphql.ts | 4 +-
web/src/locales/en.json | 1 +
web/src/store/server.fragment.ts | 1 +
web/src/store/server.ts | 7 ++++
web/types/server.ts | 1 +
8 files changed, 64 insertions(+), 8 deletions(-)
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index b3c724ff3f..5e6bdbdf4f 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -124,6 +124,7 @@ const initialServerState = {
flashGuid: '',
guid: '',
keyfile: '',
+ mdState: '',
regGuid: '',
regTm: '',
regTo: '',
@@ -297,6 +298,7 @@ describe('Registration.standalone.vue', () => {
serverStore.state = 'PRO';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.mdState = 'STOPPED';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.keyfile = 'keyfile-present';
@@ -314,6 +316,23 @@ describe('Registration.standalone.vue', () => {
expect(transferNotice.text()).not.toContain('Tools > Registration');
});
+ it('only checks the stop-array step when the array is stopped', async () => {
+ serverStore.state = 'PRO';
+ serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
+ serverStore.mdState = 'STARTED';
+ serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.keyfile = 'keyfile-present';
+
+ await wrapper.vm.$nextTick();
+
+ const transferNotice = wrapper.find('[data-testid="tpm-transfer-available"]');
+
+ expect(transferNotice.exists()).toBe(true);
+ expect(transferNotice.text()).not.toContain('[x] Stop the array.');
+ expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./);
+ });
+
it('does not show TPM transfer guidance for trial states', async () => {
serverStore.state = 'TRIAL';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
@@ -329,6 +348,7 @@ describe('Registration.standalone.vue', () => {
it('shows checked TPM transfer steps after switching to TPM boot', async () => {
serverStore.state = 'EGUID';
serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.mdState = 'STOPPED';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6';
@@ -346,6 +366,25 @@ describe('Registration.standalone.vue', () => {
expect(transferNotice.text()).toContain('Start the array.');
});
+ it('shows the stop-array step as incomplete in TPM-ready state while the array is running', async () => {
+ serverStore.state = 'EGUID';
+ serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.mdState = 'STARTED';
+ serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
+ serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6';
+
+ await wrapper.vm.$nextTick();
+
+ const transferNotice = wrapper.find('[data-testid="tpm-transfer-ready"]');
+
+ expect(transferNotice.exists()).toBe(true);
+ expect(transferNotice.text()).toContain(
+ 'The USB flash boot device is already removed. Stop the array, then press Replace Key to transfer this license to TPM.'
+ );
+ expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./);
+ expect(transferNotice.text()).toMatch(/\[x\]Remove the USB flash boot device\./);
+ });
+
it('adds Activate Trial fallback for ENOKEYFILE partner activation', async () => {
activationCodeStateHolder.current!.value = {
code: 'PARTNER-CODE-123',
diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue
index aa3d2dbceb..8aa7c6abce 100644
--- a/web/src/components/Registration.standalone.vue
+++ b/web/src/components/Registration.standalone.vue
@@ -54,6 +54,7 @@ const {
keyActions,
keyfile,
computedRegDevs,
+ mdState,
regGuid,
regTm,
regTo,
@@ -156,9 +157,15 @@ const showTpmTransferReadyInfo = computed((): boolean =>
guid.value === tpmGuid.value
)
);
+const isArrayStopped = computed((): boolean => mdState.value === 'STOPPED');
+const tpmTransferReadyDescription = computed(() =>
+ isArrayStopped.value
+ ? t('registration.tpmTransferReadyDescription')
+ : t('registration.tpmTransferReadyDescriptionArrayRunning')
+);
const tpmTransferPreparationSteps = computed(() => [
{
- completed: false,
+ completed: isArrayStopped.value,
label: t('registration.tpmTransferAvailableSteps.stopArray'),
},
{
@@ -180,7 +187,7 @@ const tpmTransferPreparationSteps = computed(() => [
]);
const tpmTransferReadySteps = computed(() => [
{
- completed: true,
+ completed: isArrayStopped.value,
label: t('registration.tpmTransferAvailableSteps.stopArray'),
},
{
@@ -479,7 +486,7 @@ const actionItems = computed((): RegistrationItemProps[] => {
{{ t('registration.tpmTransferReady') }}
- {{ t('registration.tpmTransferReadyDescription') }}
+ {{ tpmTransferReadyDescription }}
- ;
-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, 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', 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; }>;
@@ -4115,5 +4115,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":"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":"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/locales/en.json b/web/src/locales/en.json
index f7a9351bce..367d446da2 100644
--- a/web/src/locales/en.json
+++ b/web/src/locales/en.json
@@ -681,6 +681,7 @@
"registration.tpmTransferAvailableSteps.startArray": "Start the array.",
"registration.tpmTransferReady": "Continue your TPM license transfer.",
"registration.tpmTransferReadyDescription": "The first two steps are already complete. Press Replace Key to transfer this license to TPM, then start the array.",
+ "registration.tpmTransferReadyDescriptionArrayRunning": "The USB flash boot device is already removed. Stop the array, then press Replace Key to transfer this license to TPM.",
"registration.replaceCheck.checkEligibility": "Check Eligibility",
"registration.transferLicenseToNewDevice": "Transfer License to New Device",
"registration.trialExpiration": "Trial expiration",
diff --git a/web/src/store/server.fragment.ts b/web/src/store/server.fragment.ts
index 3613f3d296..dd8933f3f0 100644
--- a/web/src/store/server.fragment.ts
+++ b/web/src/store/server.fragment.ts
@@ -90,6 +90,7 @@ export const SERVER_STATE_QUERY = graphql(/* GraphQL */ `
vars {
flashGuid
tpmGuid
+ mdState
regGen
regState
configError
diff --git a/web/src/store/server.ts b/web/src/store/server.ts
index d39aee868a..42208defa4 100644
--- a/web/src/store/server.ts
+++ b/web/src/store/server.ts
@@ -109,6 +109,7 @@ export const useServerStore = defineStore('server', () => {
const lanIp = ref('');
const license = ref('');
const locale = ref('');
+ const mdState = ref('');
const name = ref('');
const osVersion = ref('');
const osVersionBranch = ref('stable');
@@ -196,6 +197,7 @@ export const useServerStore = defineStore('server', () => {
lanIp: lanIp.value,
license: license.value,
locale: locale.value,
+ mdState: mdState.value,
name: name.value,
osVersion: osVersion.value,
osVersionBranch: osVersionBranch.value,
@@ -1056,6 +1058,9 @@ export const useServerStore = defineStore('server', () => {
if (typeof data?.locale !== 'undefined') {
locale.value = data.locale;
}
+ if (typeof data?.mdState !== 'undefined') {
+ mdState.value = data.mdState;
+ }
if (typeof data?.name !== 'undefined') {
name.value = data.name;
}
@@ -1153,6 +1158,7 @@ export const useServerStore = defineStore('server', () => {
? data.registration.keyFile.contents
: undefined,
flashGuid: data.vars?.flashGuid ?? undefined,
+ mdState: data.vars?.mdState ?? undefined,
regGen: data.vars && data.vars.regGen ? parseInt(data.vars.regGen) : undefined,
regTy: data.registration?.type ?? undefined,
state: data.registration?.state ?? data.vars?.regState ?? undefined,
@@ -1389,6 +1395,7 @@ export const useServerStore = defineStore('server', () => {
keyfile,
inIframe,
locale,
+ mdState,
lanIp,
name,
osVersion,
diff --git a/web/types/server.ts b/web/types/server.ts
index 5c80e48f9e..99b5403d2b 100644
--- a/web/types/server.ts
+++ b/web/types/server.ts
@@ -100,6 +100,7 @@ export interface Server {
lanIp?: string;
license?: string;
locale?: string;
+ mdState?: string;
name?: string;
osVersion?: string;
osVersionBranch?: ServerOsVersionBranch;
From 50a58e847b872a178bcf93ba04391f3d0dd5ac3d Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 14:14:10 -0400
Subject: [PATCH 6/9] Reload after callback feedback close
---
.../components/CallbackFeedback.test.ts | 32 +++++++++++++++++++
.../UserProfile/CallbackFeedback.vue | 17 +++++++---
2 files changed, 45 insertions(+), 4 deletions(-)
diff --git a/web/__test__/components/CallbackFeedback.test.ts b/web/__test__/components/CallbackFeedback.test.ts
index 79d2028b0e..abdb0e8a64 100644
--- a/web/__test__/components/CallbackFeedback.test.ts
+++ b/web/__test__/components/CallbackFeedback.test.ts
@@ -190,6 +190,8 @@ vi.mock('~/store/updateOsActions', () => ({
}));
describe('CallbackFeedback.vue', () => {
+ const originalLocation = window.location;
+
beforeEach(() => {
accountAction.value = undefined;
accountActionHide.value = false;
@@ -220,6 +222,11 @@ describe('CallbackFeedback.vue', () => {
mockSetCallbackStatus.mockClear();
mockInstallOsUpdate.mockClear();
mockSetUpdateOsStatus.mockClear();
+
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: originalLocation,
+ });
});
afterEach(() => {
@@ -302,4 +309,29 @@ describe('CallbackFeedback.vue', () => {
expect(wrapper.find('.modal').attributes('data-error')).toBe('true');
expect(wrapper.find('.modal').attributes('data-success')).toBe('false');
});
+
+ it('reloads the page when the modal is dismissed after a callback action', async () => {
+ const mockReload = vi.fn();
+
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: {
+ ...originalLocation,
+ reload: mockReload,
+ },
+ });
+
+ callbackStatus.value = 'success';
+ keyActionType.value = 'purchase';
+ keyInstallStatus.value = 'success';
+ keyType.value = 'Pro';
+
+ const wrapper = mountComponent();
+
+ wrapper.findComponent({ name: 'Modal' }).vm.$emit('close');
+ await wrapper.vm.$nextTick();
+
+ expect(mockSetCallbackStatus).toHaveBeenCalledWith('ready');
+ expect(mockReload).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/web/src/components/UserProfile/CallbackFeedback.vue b/web/src/components/UserProfile/CallbackFeedback.vue
index ace442e8ac..47d3430866 100644
--- a/web/src/components/UserProfile/CallbackFeedback.vue
+++ b/web/src/components/UserProfile/CallbackFeedback.vue
@@ -110,11 +110,15 @@ const subheading = computed(() => {
});
const closeText = computed(() => t('common.close')); // !connectPluginInstalled.value ? t('userProfile.callbackFeedback.noThanks') :
-const close = () => {
+const close = (options?: { reload?: boolean }) => {
if (callbackStatus.value === 'loading') {
return;
}
- return callbackActionsStore.setCallbackStatus('ready');
+ callbackActionsStore.setCallbackStatus('ready');
+
+ if (options?.reload) {
+ window.location.reload();
+ }
};
const confirmUpdateOs = () => {
@@ -254,7 +258,7 @@ const showPostInstallKeyError = computed(() =>
:error="callbackStatus === 'error'"
:success="callbackStatus === 'success' && updateOsStatus !== 'confirming'"
:show-close-x="callbackStatus !== 'loading'"
- @close="close"
+ @close="close({ reload: true })"
>
-
+
Date: Fri, 13 Mar 2026 14:16:36 -0400
Subject: [PATCH 7/9] Add TPM messaging for trial licenses
---
web/__test__/components/Registration.test.ts | 11 +++++++++-
.../components/Registration.standalone.vue | 21 +++++++++++++++++++
web/src/locales/en.json | 2 ++
3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index 5e6bdbdf4f..2da30d2801 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -333,7 +333,7 @@ describe('Registration.standalone.vue', () => {
expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./);
});
- it('does not show TPM transfer guidance for trial states', async () => {
+ it('shows TPM purchase guidance instead of TPM transfer steps for trial states', async () => {
serverStore.state = 'TRIAL';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
@@ -342,6 +342,15 @@ describe('Registration.standalone.vue', () => {
await wrapper.vm.$nextTick();
+ const trialNotice = wrapper.find('[data-testid="tpm-transfer-trial"]');
+
+ expect(trialNotice.exists()).toBe(true);
+ expect(trialNotice.text()).toContain(
+ 'TPM licensing will be available after you purchase a license.'
+ );
+ expect(trialNotice.text()).toContain(
+ 'Trial licenses cannot be moved to TPM. Once you purchase a license for this server, you will be able to transfer it from your USB flash device to TPM.'
+ );
expect(wrapper.find('[data-testid="tpm-transfer-available"]').exists()).toBe(false);
});
diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue
index 8aa7c6abce..f5f686b1ab 100644
--- a/web/src/components/Registration.standalone.vue
+++ b/web/src/components/Registration.standalone.vue
@@ -147,6 +147,15 @@ const showTpmTransferInfo = computed((): boolean =>
flashGuid.value !== tpmGuid.value
)
);
+const showTpmTransferTrialInfo = computed((): boolean =>
+ Boolean(
+ showTrialExpiration.value &&
+ bootDeviceType.value === 'flash' &&
+ flashGuid.value &&
+ tpmGuid.value &&
+ flashGuid.value !== tpmGuid.value
+ )
+);
const showTpmTransferReadyInfo = computed((): boolean =>
Boolean(
!showTrialExpiration.value &&
@@ -455,6 +464,18 @@ const actionItems = computed((): RegistrationItemProps[] => {
class="rounded-lg border border-gray-200 p-4 dark:border-gray-700"
>
{{ t('registration.actions') }}
+
+
+ {{ t('registration.tpmTransferTrial') }}
+
+
+ {{ t('registration.tpmTransferTrialDescription') }}
+
+
Date: Fri, 13 Mar 2026 14:20:49 -0400
Subject: [PATCH 8/9] Remove connect sign in from registration flow
---
web/__test__/components/Auth.test.ts | 23 +++++++++--------
web/__test__/components/Registration.test.ts | 11 ++++++++
.../components/Registration.standalone.vue | 12 +--------
web/src/store/server.ts | 25 -------------------
4 files changed, 25 insertions(+), 46 deletions(-)
diff --git a/web/__test__/components/Auth.test.ts b/web/__test__/components/Auth.test.ts
index eebfdea084..a1ba3c04d2 100644
--- a/web/__test__/components/Auth.test.ts
+++ b/web/__test__/components/Auth.test.ts
@@ -5,7 +5,7 @@
import { nextTick, ref } from 'vue';
import { mount } from '@vue/test-utils';
-import { GlobeAltIcon } from '@heroicons/vue/24/solid';
+import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/solid';
import { createTestingPinia } from '@pinia/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -38,6 +38,7 @@ vi.mock('@unraid/shared-callbacks', () => ({
const mockAccountStore = {
signIn: vi.fn(),
+ signOut: vi.fn(),
};
vi.mock('~/store/account', () => ({
@@ -77,9 +78,10 @@ describe('Auth Component', () => {
// Patch the underlying state that `authAction` depends on
serverStore = useServerStore();
serverStore.$patch({
- state: 'ENOKEYFILE',
- registered: false,
+ state: 'PRO',
+ registered: true,
connectPluginInstalled: 'INSTALLED' as ServerconnectPluginInstalled,
+ keyfile: 'keyfile-present',
});
await nextTick();
@@ -87,8 +89,8 @@ describe('Auth Component', () => {
const button = wrapper.findComponent({ name: 'BrandButton' });
expect(button.exists()).toBe(true);
- expect(button.props('text')).toBe('Sign In with Unraid.net Account');
- expect(button.props('icon')).toBe(GlobeAltIcon);
+ expect(button.props('text')).toBe('Sign Out of Unraid.net');
+ expect(button.props('icon')).toBe(ArrowRightOnRectangleIcon);
});
it('displays error messages when stateData.error is true', () => {
@@ -124,16 +126,17 @@ describe('Auth Component', () => {
serverStore = useServerStore();
serverStore.$patch({
- state: 'ENOKEYFILE',
- registered: false,
+ state: 'PRO',
+ registered: true,
connectPluginInstalled: 'INSTALLED' as ServerconnectPluginInstalled,
+ keyfile: 'keyfile-present',
});
await nextTick();
await wrapper.findComponent({ name: 'BrandButton' }).vm.$emit('click');
- expect(mockAccountStore.signIn).toHaveBeenCalledTimes(1);
+ expect(mockAccountStore.signOut).toHaveBeenCalledTimes(1);
});
it('does not render button when authAction is undefined', () => {
@@ -145,8 +148,8 @@ describe('Auth Component', () => {
serverStore = useServerStore();
serverStore.$patch({
- state: 'PRO',
- registered: true,
+ state: 'ENOKEYFILE',
+ registered: false,
});
const button = wrapper.findComponent({ name: 'BrandButton' });
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index 2da30d2801..dc709dc834 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -226,6 +226,17 @@ describe('Registration.standalone.vue', () => {
expect(wrapper.find('[data-testid="key-linked-status"]').exists()).toBe(false);
});
+ it('does not show a connect sign-in action on the registration page', async () => {
+ serverStore.state = 'ENOKEYFILE';
+ serverStore.registered = false;
+ serverStore.connectPluginInstalled = 'INSTALLED' as ServerconnectPluginInstalled;
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.text()).not.toContain('Sign In');
+ expect(serverStore.stateData.actions?.some((action) => action.name === 'signIn')).toBe(false);
+ });
+
it('triggers expected action when key action is clicked', async () => {
serverStore.state = 'TRIAL';
diff --git a/web/src/components/Registration.standalone.vue b/web/src/components/Registration.standalone.vue
index f5f686b1ab..b8e3bfb273 100644
--- a/web/src/components/Registration.standalone.vue
+++ b/web/src/components/Registration.standalone.vue
@@ -20,7 +20,7 @@ import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { ShieldCheckIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
-import { BrandButton, CardWrapper, PageContainer, SettingsGrid } from '@unraid/ui';
+import { CardWrapper, PageContainer, SettingsGrid } from '@unraid/ui';
import type { RegistrationItemProps } from '~/types/registration';
import type { ServerStateDataAction } from '~/types/server';
@@ -43,7 +43,6 @@ const serverStore = useServerStore();
const { activationCode } = storeToRefs(useActivationCodeDataStore());
const {
- authAction,
bootDeviceType,
dateTimeFormat,
deviceCount,
@@ -396,15 +395,6 @@ const actionItems = computed((): RegistrationItemProps[] => {
class="prose text-base leading-relaxed whitespace-normal opacity-75"
v-html="subheading"
/>
-
-
-
diff --git a/web/src/store/server.ts b/web/src/store/server.ts
index 42208defa4..fbda05f5d4 100644
--- a/web/src/store/server.ts
+++ b/web/src/store/server.ts
@@ -10,7 +10,6 @@ import {
ArrowPathIcon,
ArrowRightOnRectangleIcon,
CogIcon,
- GlobeAltIcon,
InformationCircleIcon,
KeyIcon,
QuestionMarkCircleIcon,
@@ -402,19 +401,6 @@ export const useServerStore = defineStore('server', () => {
text: t('server.actions.replaceKey'),
};
});
- const signInAction = computed((): ServerStateDataAction => {
- return {
- click: () => {
- accountStore.signIn();
- },
- disabled: serverActionsDisable.value.disable,
- external: true,
- icon: GlobeAltIcon,
- name: 'signIn',
- text: t('server.actions.signIn'),
- title: serverActionsDisable.value.title,
- };
- });
/**
* The Sign Out action is a computed property because it depends on the state of the keyfile & unraid-api being online
*/
@@ -478,7 +464,6 @@ export const useServerStore = defineStore('server', () => {
case 'ENOKEYFILE':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(shouldUsePartnerActivationOnly.value
? [redeemAction.value, recoverAction.value, trialStartAction.value]
: [redeemAction.value, trialStartAction.value, purchaseAction.value, recoverAction.value]),
@@ -502,7 +487,6 @@ export const useServerStore = defineStore('server', () => {
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(shouldUsePartnerActivationOnly.value
? [redeemAction.value]
: [redeemAction.value, purchaseAction.value]),
@@ -518,7 +502,6 @@ export const useServerStore = defineStore('server', () => {
case 'EEXPIRED':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(shouldUsePartnerActivationOnly.value
? [redeemAction.value]
: [redeemAction.value, purchaseAction.value]),
@@ -536,7 +519,6 @@ export const useServerStore = defineStore('server', () => {
case 'STARTER':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(regUpdatesExpired.value ? [renewAction.value] : []),
...[upgradeAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
@@ -556,7 +538,6 @@ export const useServerStore = defineStore('server', () => {
case 'PLUS':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[upgradeAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -574,7 +555,6 @@ export const useServerStore = defineStore('server', () => {
case 'UNLEASHED':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(regUpdatesExpired.value ? [renewAction.value] : []),
...(state.value === 'UNLEASHED' ? [upgradeAction.value] : []),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
@@ -604,7 +584,6 @@ export const useServerStore = defineStore('server', () => {
}
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[replaceAction.value, purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -616,7 +595,6 @@ export const useServerStore = defineStore('server', () => {
case 'EGUID1':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -629,7 +607,6 @@ export const useServerStore = defineStore('server', () => {
case 'ENOKEYFILE2':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[recoverAction.value, purchaseAction.value, redeemAction.value],
...(registered.value ? [signOutAction.value] : []),
],
@@ -643,7 +620,6 @@ export const useServerStore = defineStore('server', () => {
case 'ETRIAL':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -655,7 +631,6 @@ export const useServerStore = defineStore('server', () => {
case 'ENOKEYFILE1':
return {
actions: [
- ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
From 70c5101f7602ad4d73f9df1eb2f14d97d9d2bc34 Mon Sep 17 00:00:00 2001
From: Eli Bosley
Date: Fri, 13 Mar 2026 14:23:41 -0400
Subject: [PATCH 9/9] Restore header sign in outside registration page
---
web/__test__/components/Auth.test.ts | 23 ++++++++----------
web/__test__/components/Registration.test.ts | 3 ++-
web/src/store/server.ts | 25 ++++++++++++++++++++
3 files changed, 37 insertions(+), 14 deletions(-)
diff --git a/web/__test__/components/Auth.test.ts b/web/__test__/components/Auth.test.ts
index a1ba3c04d2..eebfdea084 100644
--- a/web/__test__/components/Auth.test.ts
+++ b/web/__test__/components/Auth.test.ts
@@ -5,7 +5,7 @@
import { nextTick, ref } from 'vue';
import { mount } from '@vue/test-utils';
-import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/solid';
+import { GlobeAltIcon } from '@heroicons/vue/24/solid';
import { createTestingPinia } from '@pinia/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -38,7 +38,6 @@ vi.mock('@unraid/shared-callbacks', () => ({
const mockAccountStore = {
signIn: vi.fn(),
- signOut: vi.fn(),
};
vi.mock('~/store/account', () => ({
@@ -78,10 +77,9 @@ describe('Auth Component', () => {
// Patch the underlying state that `authAction` depends on
serverStore = useServerStore();
serverStore.$patch({
- state: 'PRO',
- registered: true,
+ state: 'ENOKEYFILE',
+ registered: false,
connectPluginInstalled: 'INSTALLED' as ServerconnectPluginInstalled,
- keyfile: 'keyfile-present',
});
await nextTick();
@@ -89,8 +87,8 @@ describe('Auth Component', () => {
const button = wrapper.findComponent({ name: 'BrandButton' });
expect(button.exists()).toBe(true);
- expect(button.props('text')).toBe('Sign Out of Unraid.net');
- expect(button.props('icon')).toBe(ArrowRightOnRectangleIcon);
+ expect(button.props('text')).toBe('Sign In with Unraid.net Account');
+ expect(button.props('icon')).toBe(GlobeAltIcon);
});
it('displays error messages when stateData.error is true', () => {
@@ -126,17 +124,16 @@ describe('Auth Component', () => {
serverStore = useServerStore();
serverStore.$patch({
- state: 'PRO',
- registered: true,
+ state: 'ENOKEYFILE',
+ registered: false,
connectPluginInstalled: 'INSTALLED' as ServerconnectPluginInstalled,
- keyfile: 'keyfile-present',
});
await nextTick();
await wrapper.findComponent({ name: 'BrandButton' }).vm.$emit('click');
- expect(mockAccountStore.signOut).toHaveBeenCalledTimes(1);
+ expect(mockAccountStore.signIn).toHaveBeenCalledTimes(1);
});
it('does not render button when authAction is undefined', () => {
@@ -148,8 +145,8 @@ describe('Auth Component', () => {
serverStore = useServerStore();
serverStore.$patch({
- state: 'ENOKEYFILE',
- registered: false,
+ state: 'PRO',
+ registered: true,
});
const button = wrapper.findComponent({ name: 'BrandButton' });
diff --git a/web/__test__/components/Registration.test.ts b/web/__test__/components/Registration.test.ts
index dc709dc834..a2ff2bde2e 100644
--- a/web/__test__/components/Registration.test.ts
+++ b/web/__test__/components/Registration.test.ts
@@ -233,8 +233,9 @@ describe('Registration.standalone.vue', () => {
await wrapper.vm.$nextTick();
+ expect(serverStore.authAction?.name).toBe('signIn');
expect(wrapper.text()).not.toContain('Sign In');
- expect(serverStore.stateData.actions?.some((action) => action.name === 'signIn')).toBe(false);
+ expect(serverStore.stateData.actions?.some((action) => action.name === 'signIn')).toBe(true);
});
it('triggers expected action when key action is clicked', async () => {
diff --git a/web/src/store/server.ts b/web/src/store/server.ts
index fbda05f5d4..42208defa4 100644
--- a/web/src/store/server.ts
+++ b/web/src/store/server.ts
@@ -10,6 +10,7 @@ import {
ArrowPathIcon,
ArrowRightOnRectangleIcon,
CogIcon,
+ GlobeAltIcon,
InformationCircleIcon,
KeyIcon,
QuestionMarkCircleIcon,
@@ -401,6 +402,19 @@ export const useServerStore = defineStore('server', () => {
text: t('server.actions.replaceKey'),
};
});
+ const signInAction = computed((): ServerStateDataAction => {
+ return {
+ click: () => {
+ accountStore.signIn();
+ },
+ disabled: serverActionsDisable.value.disable,
+ external: true,
+ icon: GlobeAltIcon,
+ name: 'signIn',
+ text: t('server.actions.signIn'),
+ title: serverActionsDisable.value.title,
+ };
+ });
/**
* The Sign Out action is a computed property because it depends on the state of the keyfile & unraid-api being online
*/
@@ -464,6 +478,7 @@ export const useServerStore = defineStore('server', () => {
case 'ENOKEYFILE':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(shouldUsePartnerActivationOnly.value
? [redeemAction.value, recoverAction.value, trialStartAction.value]
: [redeemAction.value, trialStartAction.value, purchaseAction.value, recoverAction.value]),
@@ -487,6 +502,7 @@ export const useServerStore = defineStore('server', () => {
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(shouldUsePartnerActivationOnly.value
? [redeemAction.value]
: [redeemAction.value, purchaseAction.value]),
@@ -502,6 +518,7 @@ export const useServerStore = defineStore('server', () => {
case 'EEXPIRED':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(shouldUsePartnerActivationOnly.value
? [redeemAction.value]
: [redeemAction.value, purchaseAction.value]),
@@ -519,6 +536,7 @@ export const useServerStore = defineStore('server', () => {
case 'STARTER':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(regUpdatesExpired.value ? [renewAction.value] : []),
...[upgradeAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
@@ -538,6 +556,7 @@ export const useServerStore = defineStore('server', () => {
case 'PLUS':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[upgradeAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -555,6 +574,7 @@ export const useServerStore = defineStore('server', () => {
case 'UNLEASHED':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...(regUpdatesExpired.value ? [renewAction.value] : []),
...(state.value === 'UNLEASHED' ? [upgradeAction.value] : []),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
@@ -584,6 +604,7 @@ export const useServerStore = defineStore('server', () => {
}
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[replaceAction.value, purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -595,6 +616,7 @@ export const useServerStore = defineStore('server', () => {
case 'EGUID1':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -607,6 +629,7 @@ export const useServerStore = defineStore('server', () => {
case 'ENOKEYFILE2':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[recoverAction.value, purchaseAction.value, redeemAction.value],
...(registered.value ? [signOutAction.value] : []),
],
@@ -620,6 +643,7 @@ export const useServerStore = defineStore('server', () => {
case 'ETRIAL':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
@@ -631,6 +655,7 @@ export const useServerStore = defineStore('server', () => {
case 'ENOKEYFILE1':
return {
actions: [
+ ...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],