Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 60 additions & 60 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,65 @@ type Info implements Node {
primaryNetwork: InfoNetworkInterface
}

type NotificationCounts {
info: Int!
warning: Int!
alert: Int!
total: Int!
}

type NotificationOverview {
unread: NotificationCounts!
archive: NotificationCounts!
}

type Notification implements Node {
id: PrefixedID!

"""Also known as 'event'"""
title: String!
subject: String!
description: String!
importance: NotificationImportance!
link: String
type: NotificationType!

"""ISO Timestamp for when the notification occurred"""
timestamp: String
formattedTimestamp: String
}

enum NotificationImportance {
ALERT
INFO
WARNING
}

enum NotificationType {
UNREAD
ARCHIVE
}

type Notifications implements Node {
id: PrefixedID!

"""A cached overview of the notifications in the system & their severity."""
overview: NotificationOverview!
list(filter: NotificationFilter!): [Notification!]!

"""
Deduplicated list of unread warning and alert notifications, sorted latest first.
"""
warningsAndAlerts: [Notification!]!
}

input NotificationFilter {
importance: NotificationImportance
type: NotificationType!
offset: Int!
limit: Int!
}

type ExplicitStatusItem {
name: String!
updateStatus: UpdateStatus!
Expand Down Expand Up @@ -2328,65 +2387,6 @@ type FlatOrganizerEntry {
meta: DockerContainer
}

type NotificationCounts {
info: Int!
warning: Int!
alert: Int!
total: Int!
}

type NotificationOverview {
unread: NotificationCounts!
archive: NotificationCounts!
}

type Notification implements Node {
id: PrefixedID!

"""Also known as 'event'"""
title: String!
subject: String!
description: String!
importance: NotificationImportance!
link: String
type: NotificationType!

"""ISO Timestamp for when the notification occurred"""
timestamp: String
formattedTimestamp: String
}

enum NotificationImportance {
ALERT
INFO
WARNING
}

enum NotificationType {
UNREAD
ARCHIVE
}

type Notifications implements Node {
id: PrefixedID!

"""A cached overview of the notifications in the system & their severity."""
overview: NotificationOverview!
list(filter: NotificationFilter!): [Notification!]!

"""
Deduplicated list of unread warning and alert notifications, sorted latest first.
"""
warningsAndAlerts: [Notification!]!
}

input NotificationFilter {
importance: NotificationImportance
type: NotificationType!
offset: Int!
limit: Int!
}

type FlashBackupStatus {
"""Status message indicating the outcome of the backup initiation."""
status: String!
Expand Down Expand Up @@ -3569,4 +3569,4 @@ type Subscription {
systemMetricsTemperature: TemperatureMetrics
upsUpdates: UPSDevice!
pluginInstallUpdates(operationId: ID!): PluginInstallEvent!
}
}
1 change: 1 addition & 0 deletions api/src/unraid-api/cli/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3361,6 +3361,7 @@ export type Vars = Node & {
sysFlashSlots?: Maybe<Scalars['Int']['output']>;
sysModel?: Maybe<Scalars['String']['output']>;
timeZone?: Maybe<Scalars['String']['output']>;
tpmGuid?: Maybe<Scalars['String']['output']>;
/** Should a NTP server be used for time sync? */
useNtp?: Maybe<Scalars['Boolean']['output']>;
useSsh?: Maybe<Scalars['Boolean']['output']>;
Expand Down
30 changes: 21 additions & 9 deletions api/src/unraid-api/config/api-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,13 @@ describe('OnboardingTracker', () => {
const tracker = createOnboardingTracker(configService);
await tracker.onApplicationBootstrap();

const state = tracker.getState();
expect(state.completed).toBe(false);
expect(state.completedAtVersion).toBeUndefined();
await expect(tracker.getStateResult()).resolves.toEqual({
kind: 'missing',
state: {
completed: false,
completedAtVersion: undefined,
},
});
});

it('returns completed state when previously marked', async () => {
Expand All @@ -185,9 +189,13 @@ describe('OnboardingTracker', () => {
const tracker = createOnboardingTracker(configService);
await tracker.onApplicationBootstrap();

const state = tracker.getState();
expect(state.completed).toBe(true);
expect(state.completedAtVersion).toBe('7.1.0');
await expect(tracker.getStateResult()).resolves.toEqual({
kind: 'ok',
state: {
completed: true,
completedAtVersion: '7.1.0',
},
});
});

it('marks onboarding as completed with current version', async () => {
Expand Down Expand Up @@ -272,9 +280,13 @@ describe('OnboardingTracker', () => {
const tracker = new OnboardingTrackerService(configService, overrides);
await tracker.onApplicationBootstrap();

const state = tracker.getState();
expect(state.completed).toBe(true);
expect(state.completedAtVersion).toBe('6.12.0');
await expect(tracker.getStateResult()).resolves.toEqual({
kind: 'ok',
state: {
completed: true,
completedAtVersion: '6.12.0',
},
});
});
});

Expand Down
115 changes: 115 additions & 0 deletions api/src/unraid-api/config/onboarding-tracker.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,118 @@ describe('OnboardingTrackerService write retries', () => {
expect(mockAtomicWriteFile).toHaveBeenCalledTimes(3);
});
});

describe('OnboardingTrackerService tracker state availability', () => {
beforeEach(() => {
mockReadFile.mockReset();
mockAtomicWriteFile.mockReset();
});

it('treats a missing tracker file as a valid empty onboarding state', 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();

await expect(tracker.getStateResult()).resolves.toEqual({
kind: 'missing',
state: {
completed: false,
completedAtVersion: undefined,
},
});
});

it('captures tracker read failures when reading the tracker file fails for a non-ENOENT reason', 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();

const stateResult = await tracker.getStateResult();
expect(stateResult.kind).toBe('error');
if (stateResult.kind === 'error') {
expect(stateResult.error).toBeInstanceOf(Error);
}
});

it('returns override-backed onboarding state as a successful read result', async () => {
const config = createConfigService();
const overrides = new OnboardingOverrideService();

overrides.setState({
onboarding: {
completed: true,
completedAtVersion: '7.2.0',
},
});

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();

await expect(tracker.getStateResult()).resolves.toEqual({
kind: 'ok',
state: {
completed: true,
completedAtVersion: '7.2.0',
},
});
});

it('propagates tracker read failures through isCompleted', 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();

await expect(tracker.isCompleted()).rejects.toThrow('permission denied');
});

it('propagates tracker read failures through getCompletedAtVersion', 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();

await expect(tracker.getCompletedAtVersion()).rejects.toThrow('permission denied');
});
});
Loading
Loading