feat: critical notifications

This commit is contained in:
Pujit Mehrotra
2025-10-15 12:10:08 -04:00
parent 0224ba578e
commit 31dd8dd5d4
22 changed files with 397 additions and 20 deletions

View File

@@ -1831,6 +1831,11 @@ type Notifications implements Node {
"""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 {

View File

@@ -1,6 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsObject, IsOptional, IsArray, IsString } from 'class-validator';
import { IsArray, IsObject, IsOptional, IsString } from 'class-validator';
import { GraphQLJSON } from 'graphql-scalars';
@ObjectType()

View File

@@ -42,7 +42,7 @@ export class DockerConfigService extends ConfigFilePersister<DockerConfig> {
if (!cronExpression.valid) {
throw new AppError(`Cron expression not supported: ${dockerConfig.updateCheckCronSchedule}`);
}
return dockerConfig;
}
}

View File

@@ -23,7 +23,9 @@ export class DockerTemplateIconService {
return parsed.Container.Icon || null;
} catch (error) {
this.logger.debug(`Failed to read icon from template ${templatePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
this.logger.debug(
`Failed to read icon from template ${templatePath}: ${error instanceof Error ? error.message : 'Unknown error'}`
);
return null;
}
}
@@ -57,4 +59,3 @@ export class DockerTemplateIconService {
return iconMap;
}
}

View File

@@ -14,4 +14,3 @@ export class DockerTemplateSyncResult {
@Field(() => [String])
errors!: string[];
}

View File

@@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { mkdir, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DockerConfigService } from '@app/unraid-api/graph/resolvers/docker/docker-config.service.js';
@@ -151,9 +152,7 @@ describe('DockerTemplateScannerService', () => {
image: 'test/image:latest',
} as DockerContainer;
const templates = [
{ filePath: '/path/1', name: 'different', repository: 'other/image' },
];
const templates = [{ filePath: '/path/1', name: 'different', repository: 'other/image' }];
const result = (service as any).matchContainerToTemplate(container, templates);
@@ -424,4 +423,3 @@ describe('DockerTemplateScannerService', () => {
});
});
});

View File

@@ -65,7 +65,9 @@ export class DockerTemplateScannerService {
});
if (needsSync.length > 0) {
this.logger.log(`Found ${needsSync.length} containers without template mappings, triggering sync`);
this.logger.log(
`Found ${needsSync.length} containers without template mappings, triggering sync`
);
await this.scanTemplates();
return true;
}
@@ -178,7 +180,10 @@ export class DockerTemplateScannerService {
}
for (const template of templates) {
if (template.repository && this.normalizeRepository(template.repository) === containerImage) {
if (
template.repository &&
this.normalizeRepository(template.repository) === containerImage
) {
return template;
}
}
@@ -203,4 +208,3 @@ export class DockerTemplateScannerService {
this.dockerConfigService.replaceConfig(updated);
}
}

View File

@@ -7,8 +7,8 @@ import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
import { UseFeatureFlag } from '@app/unraid-api/decorators/use-feature-flag.decorator.js';
import { DockerFormService } from '@app/unraid-api/graph/resolvers/docker/docker-form.service.js';
import { DockerPhpService } from '@app/unraid-api/graph/resolvers/docker/docker-php.service.js';
import { DockerTemplateScannerService } from '@app/unraid-api/graph/resolvers/docker/docker-template-scanner.service.js';
import { DockerTemplateSyncResult } from '@app/unraid-api/graph/resolvers/docker/docker-template-scanner.model.js';
import { DockerTemplateScannerService } from '@app/unraid-api/graph/resolvers/docker/docker-template-scanner.service.js';
import { ExplicitStatusItem } from '@app/unraid-api/graph/resolvers/docker/docker-update-status.model.js';
import {
Docker,

View File

@@ -3,8 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
import type { ContainerListOptions } from 'dockerode';
import { AppError } from '@app/core/errors/app-error.js';
import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js';
import { DockerTemplateIconService } from '@app/unraid-api/graph/resolvers/docker/docker-template-icon.service.js';
import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
import { DockerOrganizerConfigService } from '@app/unraid-api/graph/resolvers/docker/organizer/docker-organizer-config.service.js';
import {

View File

@@ -164,4 +164,10 @@ export class Notifications extends Node {
@Field(() => [Notification])
@IsNotEmpty()
list!: Notification[];
@Field(() => [Notification], {
description: 'Deduplicated list of unread warning and alert notifications, sorted latest first.',
})
@IsNotEmpty()
warningsAndAlerts!: Notification[];
}

View File

@@ -49,6 +49,13 @@ export class NotificationsResolver {
return await this.notificationsService.getNotifications(filters);
}
@ResolveField(() => [Notification], {
description: 'Deduplicated list of unread warning and alert notifications.',
})
public async warningsAndAlerts(): Promise<Notification[]> {
return this.notificationsService.getWarningsAndAlerts();
}
/**============================================
* Mutations
*=============================================**/

View File

@@ -289,6 +289,73 @@ describe.sequential('NotificationsService', () => {
expect(loaded.length).toEqual(3);
});
describe('getWarningsAndAlerts', () => {
it('deduplicates unread warning and alert notifications', async ({ expect }) => {
const duplicateData = {
title: 'Array Status',
subject: 'Disk 1 is getting warm',
description: 'Disk temperature has exceeded threshold.',
importance: NotificationImportance.WARNING,
} as const;
// Create duplicate warnings and an alert with different content
await createNotification(duplicateData);
await createNotification(duplicateData);
await createNotification({
title: 'UPS Disconnected',
subject: 'The UPS connection has been lost',
description: 'Reconnect the UPS to restore protection.',
importance: NotificationImportance.ALERT,
});
await createNotification({
title: 'Parity Check Complete',
subject: 'A parity check has completed successfully',
description: 'No sync errors were detected.',
importance: NotificationImportance.INFO,
});
const results = await service.getWarningsAndAlerts();
const warningMatches = results.filter(
(notification) => notification.subject === duplicateData.subject
);
const alertMatches = results.filter((notification) =>
notification.subject.includes('UPS connection')
);
expect(results.length).toEqual(2);
expect(warningMatches).toHaveLength(1);
expect(alertMatches).toHaveLength(1);
expect(
results.every((notification) => notification.importance !== NotificationImportance.INFO)
).toBe(true);
});
it('respects the provided limit', async ({ expect }) => {
const limit = 2;
await createNotification({
title: 'Array Warning',
subject: 'Disk 2 is getting warm',
description: 'Disk temperature has exceeded threshold.',
importance: NotificationImportance.WARNING,
});
await createNotification({
title: 'Network Down',
subject: 'Ethernet link is down',
description: 'Physical link failure detected.',
importance: NotificationImportance.ALERT,
});
await createNotification({
title: 'Critical Temperature',
subject: 'CPU temperature exceeded',
description: 'CPU temperature has exceeded safe operating limits.',
importance: NotificationImportance.ALERT,
});
const results = await service.getWarningsAndAlerts(limit);
expect(results.length).toEqual(limit);
});
});
/**--------------------------------------------
* CRUD: Update Tests
*---------------------------------------------**/

View File

@@ -567,6 +567,49 @@ export class NotificationsService {
return notifications;
}
/**
* Returns a deduplicated list of unread warning and alert notifications.
*
* Deduplication is based on the combination of importance, title, subject, description, and link.
* This ensures repeated notifications with the same user-facing content are only shown once, while
* still prioritizing the most recent occurrence of each unique notification.
*
* @param limit Maximum number of unique notifications to return. Default: 50.
*/
public async getWarningsAndAlerts(limit = 50): Promise<Notification[]> {
const { UNREAD } = this.paths();
const files = await this.listFilesInFolder(UNREAD);
const [notifications] = await this.loadNotificationsFromPaths(files, {
type: NotificationType.UNREAD,
});
const deduped: Notification[] = [];
const seen = new Set<string>();
for (const notification of notifications) {
if (
notification.importance !== NotificationImportance.ALERT &&
notification.importance !== NotificationImportance.WARNING
) {
continue;
}
const key = this.getDeduplicationKey(notification);
if (seen.has(key)) {
continue;
}
seen.add(key);
deduped.push(notification);
if (deduped.length >= limit) {
break;
}
}
return deduped;
}
/**
* Given a path to a folder, returns the full (absolute) paths of the folder's top-level contents.
* Sorted latest-first by default.
@@ -791,4 +834,15 @@ export class NotificationsService {
const defaultTimestamp = 0;
return Number(b.timestamp ?? defaultTimestamp) - Number(a.timestamp ?? defaultTimestamp);
}
private getDeduplicationKey(notification: Notification): string {
const makePart = (value?: string | null) => (value ?? '').trim();
return [
notification.importance,
makePart(notification.title),
makePart(notification.subject),
makePart(notification.description),
makePart(notification.link),
].join('|');
}
}

View File

@@ -139,10 +139,10 @@ describe('Organizer Resolver', () => {
const resolved = resolveOrganizer(organizer);
const flatEntries = resolved.views[0].flatEntries;
// Should have 2 entries: root folder and the ref (kept as ref type since resource not found)
expect(flatEntries).toHaveLength(2);
const missingRefEntry = flatEntries[1];
expect(missingRefEntry.id).toBe('missing-ref');
expect(missingRefEntry.type).toBe('ref'); // Stays as ref when resource not found
@@ -172,7 +172,7 @@ describe('Organizer Resolver', () => {
const resolved = resolveOrganizer(organizer);
const flatEntries = resolved.views[0].flatEntries;
// Should only have root folder, missing entry is skipped
expect(flatEntries).toHaveLength(1);
expect(flatEntries[0].id).toBe('root-folder');

View File

@@ -86,7 +86,6 @@ export function addMissingResourcesToView(
return view;
}
/**
* Directly enriches flat entries from an organizer view without building an intermediate tree.
* This is more efficient than building a tree just to flatten it again.
@@ -228,7 +227,6 @@ export function resolveOrganizer(
};
}
export interface CreateFolderInViewParams {
view: OrganizerView;
folderId: string;

1
web/components.d.ts vendored
View File

@@ -37,6 +37,7 @@ declare module 'vue' {
ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
'ConnectSettings.standalone': typeof import('./src/components/ConnectSettings/ConnectSettings.standalone.vue')['default']
Console: typeof import('./src/components/Docker/Console.vue')['default']
'CriticalNotifications.standalone': typeof import('./src/components/Notifications/CriticalNotifications.standalone.vue')['default']
Detail: typeof import('./src/components/LayoutViews/Detail/Detail.vue')['default']
DetailContentHeader: typeof import('./src/components/LayoutViews/Detail/DetailContentHeader.vue')['default']
DetailLeftNavigation: typeof import('./src/components/LayoutViews/Detail/DetailLeftNavigation.vue')['default']

View File

@@ -73,6 +73,14 @@
</head>
<body>
<div class="container">
<div class="category-header">🔔 Notifications</div>
<div class="component-card" style="grid-column: 1 / -1;">
<h3>Critical Notifications</h3>
<span class="selector">&lt;unraid-critical-notifications&gt;</span>
<div class="component-mount">
<unraid-critical-notifications></unraid-critical-notifications>
</div>
</div>
<!-- Docker -->
<div class="category-header">🐳 Docker</div>
<div class="component-grid">
@@ -379,4 +387,4 @@
});
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,194 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useQuery, useSubscription } from '@vue/apollo-composable';
import { AlertTriangle, Octagon } from 'lucide-vue-next';
import type {
NotificationFragmentFragment,
WarningAndAlertNotificationsQuery,
WarningAndAlertNotificationsQueryVariables,
} from '~/composables/gql/graphql';
import {
NOTIFICATION_FRAGMENT,
warningsAndAlerts,
} from '~/components/Notifications/graphql/notification.query';
import {
notificationAddedSubscription,
notificationOverviewSubscription,
} from '~/components/Notifications/graphql/notification.subscription';
import { useFragment } from '~/composables/gql';
import { NotificationImportance } from '~/composables/gql/graphql';
const { result, loading, refetch, error } = useQuery<
WarningAndAlertNotificationsQuery,
WarningAndAlertNotificationsQueryVariables
>(warningsAndAlerts, undefined, {
fetchPolicy: 'network-only',
});
const extractNotifications = (
notifications: NotificationFragmentFragment[] | null | undefined
): NotificationFragmentFragment[] => {
if (!notifications?.length) {
return [];
}
return useFragment(NOTIFICATION_FRAGMENT, notifications) ?? [];
};
const notifications = computed<NotificationFragmentFragment[]>(() => {
const data = result.value?.notifications?.warningsAndAlerts;
return extractNotifications(data);
});
const formatTimestamp = (notification: NotificationFragmentFragment) => {
if (notification.formattedTimestamp) {
return notification.formattedTimestamp;
}
if (!notification.timestamp) {
return '';
}
const parsed = new Date(notification.timestamp);
if (Number.isNaN(parsed.getTime())) {
return '';
}
return parsed.toLocaleString();
};
const importanceMeta: Record<
NotificationImportance,
{ label: string; badge: string; icon: typeof AlertTriangle; accent: string }
> = {
[NotificationImportance.ALERT]: {
label: 'Alert',
badge: 'bg-red-100 text-red-700 border border-red-300',
icon: Octagon,
accent: 'text-red-600',
},
[NotificationImportance.WARNING]: {
label: 'Warning',
badge: 'bg-amber-100 text-amber-700 border border-amber-300',
icon: AlertTriangle,
accent: 'text-amber-600',
},
[NotificationImportance.INFO]: {
label: 'Info',
badge: 'bg-blue-100 text-blue-700 border border-blue-300',
icon: AlertTriangle,
accent: 'text-blue-600',
},
};
const enrichedNotifications = computed(() =>
notifications.value.map((notification) => ({
notification,
displayTimestamp: formatTimestamp(notification),
meta: importanceMeta[notification.importance],
}))
);
useSubscription(notificationAddedSubscription, null, {
onResult: ({ data }) => {
if (!data) {
return;
}
const notification = useFragment(NOTIFICATION_FRAGMENT, data.notificationAdded);
if (
!notification ||
(notification.importance !== NotificationImportance.ALERT &&
notification.importance !== NotificationImportance.WARNING)
) {
return;
}
void refetch();
},
});
useSubscription(notificationOverviewSubscription, null, {
onResult: () => {
void refetch();
},
});
</script>
<template>
<section class="flex flex-col gap-4 rounded-lg border border-amber-200 bg-white p-4 shadow-sm">
<header class="flex items-center justify-between gap-3">
<div class="flex items-center gap-2">
<AlertTriangle class="h-5 w-5 text-amber-600" aria-hidden="true" />
<h2 class="text-base font-semibold text-gray-900">Warnings & Alerts</h2>
</div>
<span
v-if="!loading"
class="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700"
>
{{ notifications.length }}
</span>
</header>
<div v-if="error" class="rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
Failed to load notifications. Please try again.
</div>
<div v-else-if="loading" class="flex items-center gap-2 text-sm text-gray-500">
<span class="h-2 w-2 animate-pulse rounded-full bg-amber-400" aria-hidden="true" />
Loading latest notifications
</div>
<ul v-else-if="enrichedNotifications.length" class="flex flex-col gap-3">
<li
v-for="{ notification, displayTimestamp, meta } in enrichedNotifications"
:key="notification.id"
class="grid gap-2 rounded-md border border-gray-200 p-3 transition hover:border-amber-300"
>
<div class="flex items-start gap-3">
<component
:is="meta.icon"
class="mt-0.5 h-5 w-5 flex-none"
:class="meta.accent"
aria-hidden="true"
/>
<div class="flex flex-1 flex-col gap-1">
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full px-2 py-0.5 text-xs font-medium" :class="meta.badge">
{{ meta.label }}
</span>
<span v-if="displayTimestamp" class="text-xs font-medium text-gray-500">
{{ displayTimestamp }}
</span>
</div>
<p class="text-sm font-semibold text-gray-900">
{{ notification.title }}
</p>
<p class="text-sm text-gray-600">
{{ notification.subject }}
</p>
<p v-if="notification.description" class="text-sm text-gray-500">
{{ notification.description }}
</p>
</div>
</div>
<a
v-if="notification.link"
:href="notification.link"
class="inline-flex w-fit items-center gap-1 text-sm font-medium text-amber-700 hover:text-amber-800"
target="_blank"
rel="noreferrer"
>
View details
</a>
</li>
</ul>
<div v-else class="flex flex-col items-start gap-2 rounded-md border border-gray-200 p-3">
<div class="flex items-center gap-2 text-sm font-medium text-gray-700">
<span class="h-2 w-2 rounded-full bg-emerald-400" aria-hidden="true" />
All clear. No active warnings or alerts.
</div>
<p class="text-sm text-gray-500">
This panel automatically refreshes when new warnings or alerts are generated.
</p>
</div>
</section>
</template>

View File

@@ -34,6 +34,17 @@ export const getNotifications = graphql(/* GraphQL */ `
}
`);
export const warningsAndAlerts = graphql(/* GraphQL */ `
query WarningAndAlertNotifications {
notifications {
id
warningsAndAlerts {
...NotificationFragment
}
}
}
`);
export const archiveNotification = graphql(/* GraphQL */ `
mutation ArchiveNotification($id: PrefixedID!) {
archiveNotification(id: $id) {

View File

@@ -96,6 +96,13 @@ export const componentMappings: ComponentMapping[] = [
selector: 'unraid-dev-settings',
appId: 'dev-settings',
},
{
component: defineAsyncComponent(
() => import('../Notifications/CriticalNotifications.standalone.vue')
),
selector: 'unraid-critical-notifications',
appId: 'critical-notifications',
},
{
component: defineAsyncComponent(() => import('../ApiKeyPage.standalone.vue')),
selector: ['unraid-apikey-page', 'unraid-api-key-manager'],

View File

@@ -48,6 +48,7 @@ type Documents = {
"\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n formattedTimestamp\n }\n": typeof types.NotificationFragmentFragmentDoc,
"\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n": typeof types.NotificationCountFragmentFragmentDoc,
"\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n": typeof types.NotificationsDocument,
"\n query WarningAndAlertNotifications {\n notifications {\n id\n warningsAndAlerts {\n ...NotificationFragment\n }\n }\n }\n": typeof types.WarningAndAlertNotificationsDocument,
"\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": typeof types.ArchiveNotificationDocument,
"\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n": typeof types.ArchiveAllNotificationsDocument,
"\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": typeof types.DeleteNotificationDocument,
@@ -107,6 +108,7 @@ const documents: Documents = {
"\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n formattedTimestamp\n }\n": types.NotificationFragmentFragmentDoc,
"\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n": types.NotificationCountFragmentFragmentDoc,
"\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n": types.NotificationsDocument,
"\n query WarningAndAlertNotifications {\n notifications {\n id\n warningsAndAlerts {\n ...NotificationFragment\n }\n }\n }\n": types.WarningAndAlertNotificationsDocument,
"\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": types.ArchiveNotificationDocument,
"\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n": types.ArchiveAllNotificationsDocument,
"\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": types.DeleteNotificationDocument,
@@ -282,6 +284,10 @@ export function graphql(source: "\n fragment NotificationCountFragment on Notif
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n"): (typeof documents)["\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query WarningAndAlertNotifications {\n notifications {\n id\n warningsAndAlerts {\n ...NotificationFragment\n }\n }\n }\n"): (typeof documents)["\n query WarningAndAlertNotifications {\n notifications {\n id\n warningsAndAlerts {\n ...NotificationFragment\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1510,6 +1510,8 @@ export type Notifications = Node & {
list: Array<Notification>;
/** A cached overview of the notifications in the system & their severity. */
overview: NotificationOverview;
/** Deduplicated list of unread warning and alert notifications, sorted latest first. */
warningsAndAlerts: Array<Notification>;
};
@@ -2863,6 +2865,14 @@ export type NotificationsQuery = { __typename?: 'Query', notifications: { __type
& { ' $fragmentRefs'?: { 'NotificationFragmentFragment': NotificationFragmentFragment } }
)> } };
export type WarningAndAlertNotificationsQueryVariables = Exact<{ [key: string]: never; }>;
export type WarningAndAlertNotificationsQuery = { __typename?: 'Query', notifications: { __typename?: 'Notifications', id: string, warningsAndAlerts: Array<(
{ __typename?: 'Notification' }
& { ' $fragmentRefs'?: { 'NotificationFragmentFragment': NotificationFragmentFragment } }
)> } };
export type ArchiveNotificationMutationVariables = Exact<{
id: Scalars['PrefixedID']['input'];
}>;
@@ -3044,6 +3054,7 @@ export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"Opera
export const LogFileContentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFileContent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"lines"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"lines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"lines"}}},{"kind":"Argument","name":{"kind":"Name","value":"startLine"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}},{"kind":"Field","name":{"kind":"Name","value":"startLine"}}]}}]}}]} as unknown as DocumentNode<LogFileContentQuery, LogFileContentQueryVariables>;
export const LogFileSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LogFileSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}}]}}]}}]} as unknown as DocumentNode<LogFileSubscriptionSubscription, LogFileSubscriptionSubscriptionVariables>;
export const NotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Notifications"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationFilter"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode<NotificationsQuery, NotificationsQueryVariables>;
export const WarningAndAlertNotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WarningAndAlertNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"warningsAndAlerts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode<WarningAndAlertNotificationsQuery, WarningAndAlertNotificationsQueryVariables>;
export const ArchiveNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode<ArchiveNotificationMutation, ArchiveNotificationMutationVariables>;
export const ArchiveAllNotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveAllNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveAll"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode<ArchiveAllNotificationsMutation, ArchiveAllNotificationsMutationVariables>;
export const DeleteNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode<DeleteNotificationMutation, DeleteNotificationMutationVariables>;