diff --git a/api/src/graphql/generated/api/operations.ts b/api/src/graphql/generated/api/operations.ts index 85f0c4f9c..78318f73e 100755 --- a/api/src/graphql/generated/api/operations.ts +++ b/api/src/graphql/generated/api/operations.ts @@ -591,6 +591,7 @@ export function NotificationSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Notification').optional(), description: z.string(), + formattedTimestamp: z.string().nullish(), id: z.string(), importance: ImportanceSchema, link: z.string().nullish(), diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts index 410a1945b..252817c2f 100644 --- a/api/src/graphql/generated/api/types.ts +++ b/api/src/graphql/generated/api/types.ts @@ -817,6 +817,7 @@ export type Node = { export type Notification = Node & { __typename?: 'Notification'; description: Scalars['String']['output']; + formattedTimestamp?: Maybe; id: Scalars['ID']['output']; importance: Importance; link?: Maybe; @@ -2403,6 +2404,7 @@ export type NodeResolvers = ResolversObject<{ description?: Resolver; + formattedTimestamp?: Resolver, ParentType, ContextType>; id?: Resolver; importance?: Resolver; link?: Resolver, ParentType, ContextType>; diff --git a/api/src/graphql/schema/types/notifications/notifications.graphql b/api/src/graphql/schema/types/notifications/notifications.graphql index f54648961..1381e5dcd 100644 --- a/api/src/graphql/schema/types/notifications/notifications.graphql +++ b/api/src/graphql/schema/types/notifications/notifications.graphql @@ -17,15 +17,21 @@ type Query { type Mutation { createNotification(input: NotificationData!): Notification! deleteNotification(id: String!, type: NotificationType!): NotificationOverview! - """Marks a notification as archived.""" + """ + Marks a notification as archived. + """ archiveNotification(id: String!): Notification! - """Marks a notification as unread.""" + """ + Marks a notification as unread. + """ unreadNotification(id: String!): Notification! archiveNotifications(ids: [String!]): NotificationOverview! unarchiveNotifications(ids: [String!]): NotificationOverview! archiveAll(importance: Importance): NotificationOverview! unarchiveAll(importance: Importance): NotificationOverview! - """Reads each notification to recompute & update the overview.""" + """ + Reads each notification to recompute & update the overview. + """ recalculateOverview: NotificationOverview! } @@ -42,7 +48,9 @@ enum Importance { type Notifications implements Node { id: ID! - """A cached overview of the notifications in the system & their severity.""" + """ + A cached overview of the notifications in the system & their severity. + """ overview: NotificationOverview! list(filter: NotificationFilter!): [Notification!]! } @@ -62,6 +70,7 @@ type Notification implements Node { ISO Timestamp for when the notification occurred """ timestamp: String + formattedTimestamp: String } input NotificationData { diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts index 30aa9e146..d20f65d05 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts @@ -10,6 +10,7 @@ import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub'; import { NotificationsService } from './notifications.service'; import { Importance } from '@app/graphql/generated/client/graphql'; import { AppError } from '@app/core/errors/app-error'; +import { formatTimestamp } from '@app/utils'; @Resolver('Notifications') export class NotificationsResolver { @@ -41,7 +42,11 @@ export class NotificationsResolver { @Args('filter') filters: NotificationFilter ) { - return await this.notificationsService.getNotifications(filters); + const notifications = await this.notificationsService.getNotifications(filters); + return notifications.map((notification) => ({ + ...notification, + formattedTimestamp: formatTimestamp(notification.timestamp), + })); } /**============================================ @@ -97,7 +102,9 @@ export class NotificationsResolver { } @Mutation() - public async unarchiveAll(@Args('importance') importance?: Importance): Promise { + public async unarchiveAll( + @Args('importance') importance?: Importance + ): Promise { const { overview } = await this.notificationsService.unarchiveAll(importance); return overview; } @@ -106,7 +113,7 @@ export class NotificationsResolver { public async recalculateOverview() { const { overview, error } = await this.notificationsService.recalculateOverview(); if (error) { - throw new AppError("Failed to refresh overview", 500); + throw new AppError('Failed to refresh overview', 500); } return overview; } diff --git a/api/src/utils.ts b/api/src/utils.ts index a7483ba3e..f76785cc6 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -101,3 +101,29 @@ export function updateObject( iterations++; } } + +/** + * Formats a timestamp into a human-readable format: "MMM D, YYYY" + * Example: "Oct 24, 2024" + * + * @param timestamp - ISO date string or Unix timestamp in seconds + * @returns Formatted date string or null if timestamp is invalid + */ +export function formatTimestamp(timestamp: string | number | null | undefined): string | null { + if (!timestamp) return null; + + try { + // Convert Unix timestamp (seconds) to milliseconds if needed + const date = typeof timestamp === 'number' ? new Date(timestamp * 1_000) : new Date(timestamp); + + if (isNaN(date.getTime())) return null; + + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + } catch { + return null; + } +}