diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 5487ca472..05bf74a79 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -929,15 +929,6 @@ type UpgradeStep { """Version of Unraid when this step was introduced""" introducedIn: String - - """Display title for the onboarding step""" - title: String! - - """Display description for the onboarding step""" - description: String! - - """Icon identifier for the onboarding step""" - icon: String } type UpgradeInfo { diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index eb4f2a2e8..0e033b689 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -148,7 +148,8 @@ export type ActivationOnboardingStep = { export enum ActivationOnboardingStepId { ACTIVATION = 'ACTIVATION', PLUGINS = 'PLUGINS', - TIMEZONE = 'TIMEZONE' + TIMEZONE = 'TIMEZONE', + WELCOME = 'WELCOME' } export type AddPermissionInput = { @@ -2682,18 +2683,12 @@ export type UpgradeInfo = { export type UpgradeStep = { __typename?: 'UpgradeStep'; - /** Display description for the onboarding step */ - description: Scalars['String']['output']; - /** Icon identifier for the onboarding step */ - icon?: Maybe; /** Identifier of the onboarding step */ id: Scalars['String']['output']; /** Version of Unraid when this step was introduced */ introducedIn?: Maybe; /** Whether the step is required to continue */ required: Scalars['Boolean']['output']; - /** Display title for the onboarding step */ - title: Scalars['String']['output']; }; export type Uptime = { diff --git a/api/src/unraid-api/config/api-config.test.ts b/api/src/unraid-api/config/api-config.test.ts index b11ffda60..218a7aec2 100644 --- a/api/src/unraid-api/config/api-config.test.ts +++ b/api/src/unraid-api/config/api-config.test.ts @@ -44,6 +44,7 @@ const mockReadFile = vi.mocked(readFile); const mockReaddir = vi.mocked(readdir); const mockAccess = vi.mocked(access); const mockAtomicWriteFile = vi.mocked(atomicWriteFile); +type ReaddirResult = Awaited>; describe('ApiConfigPersistence', () => { let service: ApiConfigPersistence; @@ -84,7 +85,6 @@ describe('ApiConfigPersistence', () => { ssoSubIds: [], plugins: [], }); - expect(defaultConfig.lastSeenOsVersion).toBeUndefined(); }); it('should migrate config from legacy format', async () => { @@ -110,7 +110,6 @@ describe('ApiConfigPersistence', () => { ssoSubIds: ['sub1', 'sub2'], plugins: [], }); - expect(result.lastSeenOsVersion).toBeUndefined(); }); it('sets api.version on bootstrap', async () => { @@ -142,7 +141,7 @@ describe('OnboardingTracker', () => { mockReadFile.mockReset(); mockReaddir.mockReset(); mockAccess.mockReset(); - mockReaddir.mockResolvedValue([]); + mockReaddir.mockResolvedValue([] as unknown as ReaddirResult); mockAccess.mockResolvedValue(undefined); mockAtomicWriteFile.mockReset(); @@ -165,7 +164,6 @@ describe('OnboardingTracker', () => { expect(setMock).toHaveBeenCalledWith('store.emhttp.var.version', '7.2.0-beta.3.4'); expect(setMock).toHaveBeenCalledWith('onboardingTracker.lastTrackedVersion', undefined); expect(setMock).toHaveBeenCalledWith('onboardingTracker.completedSteps', {}); - expect(configStore['api.lastSeenOsVersion']).toBeUndefined(); expect(mockAtomicWriteFile).not.toHaveBeenCalled(); await tracker.onApplicationShutdown(); @@ -209,7 +207,6 @@ describe('OnboardingTracker', () => { TIMEZONE: expect.objectContaining({ version: '6.12.0' }), }) ); - expect(configStore['api.lastSeenOsVersion']).toBe('6.12.0'); expect(mockAtomicWriteFile).not.toHaveBeenCalled(); await tracker.onApplicationShutdown(); @@ -265,7 +262,6 @@ describe('OnboardingTracker', () => { expect(configStore['onboardingTracker.lastTrackedVersion']).toBe('7.0.0'); expect(configStore['store.emhttp.var.version']).toBe('7.1.0'); expect(configStore['onboardingTracker.completedSteps']).toEqual({}); - expect(configStore['api.lastSeenOsVersion']).toBe('7.0.0'); expect(mockAtomicWriteFile).not.toHaveBeenCalled(); }); @@ -281,7 +277,6 @@ describe('OnboardingTracker', () => { expect(setMock).toHaveBeenCalledWith('onboardingTracker.lastTrackedVersion', undefined); expect(setMock).toHaveBeenCalledWith('onboardingTracker.completedSteps', {}); expect(mockAtomicWriteFile).not.toHaveBeenCalled(); - expect(configStore['api.lastSeenOsVersion']).toBeUndefined(); }); it('marks onboarding steps complete for the current version without clearing upgrade flag', async () => { @@ -304,7 +299,6 @@ describe('OnboardingTracker', () => { expect(configStore['store.emhttp.var.version']).toBe('7.2.0'); expect(configStore['onboardingTracker.lastTrackedVersion']).toBe('6.12.0'); - expect(configStore['api.lastSeenOsVersion']).toBe('6.12.0'); setMock.mockClear(); mockAtomicWriteFile.mockReset(); @@ -427,7 +421,7 @@ describe('OnboardingTracker', () => { throw Object.assign(new Error('Not found'), { code: 'ENOENT' }); }); - mockReaddir.mockResolvedValueOnce(['pending.activationcode']); + mockReaddir.mockResolvedValueOnce(['pending.activationcode'] as unknown as ReaddirResult); mockEmhttpState.var.regState = 'ENOKEYFILE'; const tracker = new OnboardingTracker(configService); @@ -463,7 +457,6 @@ describe('loadApiConfig', () => { ssoSubIds: [], plugins: [], }); - expect(result.lastSeenOsVersion).toBeUndefined(); }); it('should handle errors gracefully and return defaults', async () => { @@ -476,6 +469,5 @@ describe('loadApiConfig', () => { ssoSubIds: [], plugins: [], }); - expect(result.lastSeenOsVersion).toBeUndefined(); }); }); diff --git a/api/src/unraid-api/config/onboarding-tracker.module.ts b/api/src/unraid-api/config/onboarding-tracker.module.ts index 48aea3e68..b35a00dd0 100644 --- a/api/src/unraid-api/config/onboarding-tracker.module.ts +++ b/api/src/unraid-api/config/onboarding-tracker.module.ts @@ -96,7 +96,6 @@ export class OnboardingTracker implements OnApplicationBootstrap, OnApplicationS const lastTrackedVersion = this.sessionLastTrackedVersion ?? this.configService.get(`${CONFIG_PREFIX}.lastTrackedVersion`) ?? - this.configService.get('api.lastSeenOsVersion') ?? undefined; await this.ensureStateLoaded(); @@ -248,7 +247,6 @@ export class OnboardingTracker implements OnApplicationBootstrap, OnApplicationS this.configService.set('store.emhttp.var.version', currentVersion); this.configService.set(`${CONFIG_PREFIX}.lastTrackedVersion`, this.sessionLastTrackedVersion); this.configService.set(`${CONFIG_PREFIX}.completedSteps`, completedStepsMap); - this.configService.set('api.lastSeenOsVersion', this.sessionLastTrackedVersion); } private async readCurrentVersion(): Promise { diff --git a/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts b/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts index 71be0e554..4a657d4f9 100644 --- a/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts +++ b/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts @@ -59,18 +59,6 @@ export class UpgradeStep { description: 'Version of Unraid when this step was introduced', }) introducedIn?: string; - - @Field(() => String, { description: 'Display title for the onboarding step' }) - title!: string; - - @Field(() => String, { description: 'Display description for the onboarding step' }) - description!: string; - - @Field(() => String, { - nullable: true, - description: 'Icon identifier for the onboarding step', - }) - icon?: string; } @ObjectType() diff --git a/web/src/components/Activation/UPGRADE_ONBOARDING.md b/web/src/components/Activation/UPGRADE_ONBOARDING.md index 02673e664..ea5d4ccd4 100644 --- a/web/src/components/Activation/UPGRADE_ONBOARDING.md +++ b/web/src/components/Activation/UPGRADE_ONBOARDING.md @@ -2,16 +2,16 @@ ## Overview -This system shows contextual onboarding steps to users when they upgrade their Unraid OS to a new version. It tracks the last seen OS version in the API config and allows you to define which steps should be shown for specific version upgrades. +This system shows contextual onboarding steps to users when they upgrade their Unraid OS to a new version. It tracks the last seen OS version using the onboarding tracker state and allows you to define which steps should be shown for specific version upgrades. ## How It Works ### Backend (API) -1. **Version Tracking** - `api/src/unraid-api/config/api-config.module.ts` - - On boot, compares current OS version with `lastSeenOsVersion` in API config - - Automatically updates `lastSeenOsVersion` when version changes - - Persists to `/boot/config/modules/api.json` +1. **Version Tracking** - `api/src/unraid-api/config/onboarding-tracker.module.ts` + - On boot, compares current OS version with `lastTrackedVersion` in the onboarding tracker + - Automatically updates `lastTrackedVersion` when version changes + - Persists to `/boot/config/modules/onboarding-tracker.json` 2. **GraphQL API** - `api/src/unraid-api/graph/resolvers/customization/` - Exposes `activationOnboarding` query which returns: @@ -122,7 +122,7 @@ The `ActivationModal` is already integrated into the app and automatically handl To test the upgrade flow: -1. Edit `/boot/config/modules/api.json` and set `lastSeenOsVersion` to an older version +1. Edit `/boot/config/modules/onboarding-tracker.json` and set `lastTrackedVersion` to an older version 2. Ensure an activation code exists (or remove it to test conditional logic) 3. Restart the API 4. The modal should appear on next page load with relevant steps from `activationOnboarding` diff --git a/web/src/composables/gql/graphql.ts b/web/src/composables/gql/graphql.ts index 582cd9e59..fbda1992c 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -2683,18 +2683,12 @@ export type UpgradeInfo = { export type UpgradeStep = { __typename?: 'UpgradeStep'; - /** Display description for the onboarding step */ - description: Scalars['String']['output']; - /** Icon identifier for the onboarding step */ - icon?: Maybe; /** Identifier of the onboarding step */ id: Scalars['String']['output']; /** Version of Unraid when this step was introduced */ introducedIn?: Maybe; /** Whether the step is required to continue */ required: Scalars['Boolean']['output']; - /** Display title for the onboarding step */ - title: Scalars['String']['output']; }; export type Uptime = {