diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 2f51f85b5b..54fc8a34e9 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -3567,4 +3567,4 @@ type Subscription { systemMetricsTemperature: TemperatureMetrics upsUpdates: UPSDevice! pluginInstallUpdates(operationId: ID!): PluginInstallEvent! -} +} \ No newline at end of file diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index 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/config/onboarding-tracker.service.spec.ts b/api/src/unraid-api/config/onboarding-tracker.service.spec.ts index 4a17584954..34351a4abd 100644 --- a/api/src/unraid-api/config/onboarding-tracker.service.spec.ts +++ b/api/src/unraid-api/config/onboarding-tracker.service.spec.ts @@ -87,3 +87,76 @@ describe('OnboardingTrackerService write retries', () => { expect(mockAtomicWriteFile).toHaveBeenCalledTimes(3); }); }); + +describe('OnboardingTrackerService tracker state availability', () => { + beforeEach(() => { + mockReadFile.mockReset(); + mockAtomicWriteFile.mockReset(); + }); + + it('keeps tracker state available when the tracker file does not exist yet', async () => { + const config = createConfigService(); + const overrides = new OnboardingOverrideService(); + + mockReadFile.mockImplementation(async (filePath) => { + if (String(filePath).includes('unraid-version')) { + return 'version="7.2.0"\n'; + } + throw Object.assign(new Error('Not found'), { code: 'ENOENT' }); + }); + + const tracker = new OnboardingTrackerService(config, overrides); + await tracker.onApplicationBootstrap(); + + expect(tracker.didTrackerStateReadFail()).toBe(false); + }); + + it('marks tracker state unavailable when reading the tracker file fails', async () => { + const config = createConfigService(); + const overrides = new OnboardingOverrideService(); + + mockReadFile.mockImplementation(async (filePath) => { + if (String(filePath).includes('unraid-version')) { + return 'version="7.2.0"\n'; + } + throw new Error('permission denied'); + }); + + const tracker = new OnboardingTrackerService(config, overrides); + await tracker.onApplicationBootstrap(); + + expect(tracker.didTrackerStateReadFail()).toBe(true); + }); + + it('refreshes tracker state after a transient read failure', async () => { + const config = createConfigService(); + const overrides = new OnboardingOverrideService(); + + mockReadFile.mockImplementation(async (filePath) => { + if (String(filePath).includes('unraid-version')) { + return 'version="7.2.0"\n'; + } + throw new Error('permission denied'); + }); + + const tracker = new OnboardingTrackerService(config, overrides); + await tracker.onApplicationBootstrap(); + + mockReadFile.mockImplementation(async (filePath) => { + if (String(filePath).includes('unraid-version')) { + return 'version="7.2.0"\n'; + } + return JSON.stringify({ + completed: true, + completedAtVersion: '7.2.0', + }); + }); + + await expect(tracker.refreshTrackerStateIfNeeded()).resolves.toBe(true); + expect(tracker.didTrackerStateReadFail()).toBe(false); + expect(tracker.getState()).toEqual({ + completed: true, + completedAtVersion: '7.2.0', + }); + }); +}); diff --git a/api/src/unraid-api/config/onboarding-tracker.service.ts b/api/src/unraid-api/config/onboarding-tracker.service.ts index a7fb9d785f..1f754c3328 100644 --- a/api/src/unraid-api/config/onboarding-tracker.service.ts +++ b/api/src/unraid-api/config/onboarding-tracker.service.ts @@ -26,6 +26,7 @@ export class OnboardingTrackerService implements OnApplicationBootstrap { private state: TrackerState = {}; private currentVersion?: string; private readonly versionFilePath: string; + private trackerStateReadFailed = false; constructor( private readonly configService: ConfigService, @@ -84,6 +85,25 @@ export class OnboardingTrackerService implements OnApplicationBootstrap { return this.currentVersion; } + didTrackerStateReadFail(): boolean { + return this.trackerStateReadFailed; + } + + async refreshTrackerStateIfNeeded(): Promise { + if (!this.trackerStateReadFailed) { + return true; + } + + const previousState = await this.readTrackerState(); + if (this.trackerStateReadFailed) { + return false; + } + + this.state = previousState ?? {}; + this.syncConfig(); + return true; + } + /** * Mark onboarding as completed for the current OS version. */ @@ -167,8 +187,16 @@ export class OnboardingTrackerService implements OnApplicationBootstrap { private async readTrackerState(): Promise { try { const content = await readFile(this.trackerPath, 'utf8'); + this.trackerStateReadFailed = false; return JSON.parse(content) as TrackerState; } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + this.trackerStateReadFailed = false; + this.logger.debug(`Onboarding tracker state does not exist yet at ${this.trackerPath}.`); + return undefined; + } + + this.trackerStateReadFailed = true; this.logger.debug( `Unable to read onboarding tracker state at ${this.trackerPath}: ${error}` ); diff --git a/api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts index a2faeddab1..326cfd4395 100644 --- a/api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts +++ b/api/src/unraid-api/graph/resolvers/customization/customization.resolver.spec.ts @@ -16,6 +16,8 @@ describe('CustomizationResolver', () => { getOnboardingState: vi.fn(), } as unknown as OnboardingService; const onboardingTracker = { + didTrackerStateReadFail: vi.fn(), + refreshTrackerStateIfNeeded: vi.fn(), getState: vi.fn(), getCurrentVersion: vi.fn(), } as unknown as OnboardingTrackerService; @@ -27,6 +29,8 @@ describe('CustomizationResolver', () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(onboardingTracker.didTrackerStateReadFail).mockReturnValue(false); + vi.mocked(onboardingTracker.refreshTrackerStateIfNeeded).mockResolvedValue(true); vi.mocked(onboardingTracker.getCurrentVersion).mockReturnValue('7.2.0'); vi.mocked(onboardingService.getPublicPartnerInfo).mockResolvedValue(null); vi.mocked(onboardingService.getActivationDataForPublic).mockResolvedValue(null); @@ -62,6 +66,29 @@ describe('CustomizationResolver', () => { }); }); + it('throws when tracker state could not be read', async () => { + vi.mocked(onboardingTracker.didTrackerStateReadFail).mockReturnValue(true); + vi.mocked(onboardingTracker.refreshTrackerStateIfNeeded).mockResolvedValue(false); + + await expect(resolver.resolveOnboarding()).rejects.toThrow(); + expect(onboardingTracker.refreshTrackerStateIfNeeded).toHaveBeenCalledTimes(1); + expect(onboardingTracker.getState).not.toHaveBeenCalled(); + }); + + it('retries tracker state before resolving onboarding', async () => { + vi.mocked(onboardingTracker.didTrackerStateReadFail).mockReturnValue(true); + vi.mocked(onboardingTracker.refreshTrackerStateIfNeeded).mockResolvedValue(true); + vi.mocked(onboardingTracker.getState).mockReturnValue({ + completed: false, + completedAtVersion: undefined, + }); + + const result = await resolver.resolveOnboarding(); + + expect(onboardingTracker.refreshTrackerStateIfNeeded).toHaveBeenCalledTimes(1); + expect(result.status).toBe(OnboardingStatus.INCOMPLETE); + }); + it('returns COMPLETED status when completed on current version', async () => { vi.mocked(onboardingTracker.getState).mockReturnValue({ completed: true, diff --git a/api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts b/api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts index 1f4c770495..f7c35148c6 100644 --- a/api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts @@ -2,6 +2,7 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthAction, Resource } from '@unraid/shared/graphql.model.js'; import { UsePermissions } from '@unraid/shared/use-permissions.directive.js'; +import { GraphQLError } from 'graphql'; import { Public } from '@app/unraid-api/auth/public.decorator.js'; import { OnboardingTrackerService } from '@app/unraid-api/config/onboarding-tracker.module.js'; @@ -59,6 +60,13 @@ export class CustomizationResolver { resource: Resource.CUSTOMIZATIONS, }) async resolveOnboarding(): Promise { + if ( + this.onboardingTracker.didTrackerStateReadFail() && + !(await this.onboardingTracker.refreshTrackerStateIfNeeded()) + ) { + throw new GraphQLError('Onboarding tracker state is unavailable.'); + } + const state = this.onboardingTracker.getState(); const currentVersion = this.onboardingTracker.getCurrentVersion() ?? 'unknown'; const partnerInfo = await this.onboardingService.getPublicPartnerInfo(); diff --git a/plugin/source/dynamix.unraid.net/install/doinst.sh b/plugin/source/dynamix.unraid.net/install/doinst.sh index 710522e626..e18f5f64eb 100644 --- a/plugin/source/dynamix.unraid.net/install/doinst.sh +++ b/plugin/source/dynamix.unraid.net/install/doinst.sh @@ -31,9 +31,3 @@ cp usr/local/unraid-api/.env.production usr/local/unraid-api/.env ( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npm-cli.js npm ) ( cd usr/local/bin ; rm -rf npx ) ( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npx-cli.js npx ) -( cd usr/local/bin ; rm -rf corepack ) -( cd usr/local/bin ; ln -sf ../lib/node_modules/corepack/dist/corepack.js corepack ) -( cd usr/local/bin ; rm -rf npm ) -( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npm-cli.js npm ) -( cd usr/local/bin ; rm -rf npx ) -( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npx-cli.js npx ) diff --git a/web/__test__/store/onboardingStatus.test.ts b/web/__test__/store/onboardingStatus.test.ts new file mode 100644 index 0000000000..cb96c958d7 --- /dev/null +++ b/web/__test__/store/onboardingStatus.test.ts @@ -0,0 +1,99 @@ +import { ref } from 'vue'; +import { createPinia, setActivePinia } from 'pinia'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useOnboardingStore } from '~/components/Onboarding/store/onboardingStatus'; +import { useServerStore } from '~/store/server'; + +vi.mock('@vue/apollo-composable', () => ({ + useQuery: () => useQueryMock(), +})); + +vi.mock('~/store/server', () => ({ + useServerStore: vi.fn(), +})); + +type OnboardingQueryResult = { + customization?: { + onboarding?: { + status?: 'INCOMPLETE' | 'UPGRADE' | 'DOWNGRADE' | 'COMPLETED'; + isPartnerBuild?: boolean; + completed?: boolean; + completedAtVersion?: string | null; + }; + }; +}; + +const { state, refetchMock, useQueryMock } = vi.hoisted(() => ({ + state: { + onboardingResult: null as unknown as ReturnType>, + onboardingLoading: null as unknown as ReturnType>, + onboardingError: null as unknown as ReturnType>, + osVersionRef: null as unknown as ReturnType>, + }, + refetchMock: vi.fn(), + useQueryMock: vi.fn(), +})); + +const createOnboardingResult = (): OnboardingQueryResult => ({ + customization: { + onboarding: { + status: 'INCOMPLETE', + isPartnerBuild: false, + completed: false, + completedAtVersion: null, + }, + }, +}); + +describe('onboardingStatus store', () => { + beforeEach(() => { + vi.clearAllMocks(); + setActivePinia(createPinia()); + + state.onboardingResult = ref(createOnboardingResult()); + state.onboardingLoading = ref(false); + state.onboardingError = ref(null); + state.osVersionRef = ref('7.3.0'); + refetchMock.mockResolvedValue(undefined); + + useQueryMock.mockReturnValue({ + result: state.onboardingResult, + loading: state.onboardingLoading, + error: state.onboardingError, + refetch: refetchMock, + }); + + vi.mocked(useServerStore).mockReturnValue({ + osVersion: state.osVersionRef, + } as unknown as ReturnType); + }); + + it('blocks onboarding modal while the onboarding query is still loading', () => { + state.onboardingLoading.value = true; + + const store = useOnboardingStore(); + + expect(store.canDisplayOnboardingModal).toBe(false); + expect(store.shouldShowOnboarding).toBe(false); + }); + + it('blocks onboarding modal when the onboarding query errors', () => { + state.onboardingError.value = new Error('Network error'); + + const store = useOnboardingStore(); + + expect(store.hasOnboardingQueryError).toBe(true); + expect(store.canDisplayOnboardingModal).toBe(false); + expect(store.shouldShowOnboarding).toBe(false); + }); + + it('allows onboarding modal when the onboarding query succeeds', () => { + const store = useOnboardingStore(); + + expect(store.hasOnboardingQueryError).toBe(false); + expect(store.canDisplayOnboardingModal).toBe(true); + expect(store.shouldShowOnboarding).toBe(true); + }); +}); diff --git a/web/src/components/Onboarding/store/onboardingStatus.ts b/web/src/components/Onboarding/store/onboardingStatus.ts index 4a6797c414..3c7ea162cf 100644 --- a/web/src/components/Onboarding/store/onboardingStatus.ts +++ b/web/src/components/Onboarding/store/onboardingStatus.ts @@ -159,7 +159,14 @@ export const useOnboardingStore = defineStore('onboarding', () => { const isUnauthenticated = computed( () => mockUnauthenticated.value || isUnauthenticatedApolloError(onboardingError.value) ); - const canDisplayOnboardingModal = computed(() => isVersionSupported.value && !isUnauthenticated.value); + const hasOnboardingQueryError = computed(() => Boolean(onboardingError.value)); + const canDisplayOnboardingModal = computed( + () => + isVersionSupported.value && + !onboardingLoading.value && + !hasOnboardingQueryError.value && + !isUnauthenticated.value + ); // Automatic onboarding should only run for initial setup. const shouldShowOnboarding = computed(() => { @@ -189,6 +196,7 @@ export const useOnboardingStore = defineStore('onboarding', () => { mockUnauthenticated, mockOsVersion, isUnauthenticated, + hasOnboardingQueryError, canDisplayOnboardingModal, shouldShowOnboarding, // Actions diff --git a/web/src/composables/gql/graphql.ts b/web/src/composables/gql/graphql.ts index 782ba84823..5aaf56c004 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -4115,4 +4115,4 @@ export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"Operat 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 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