Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions api/dev/dynamix/dynamix.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[display]
date="%c"
time="%I:%M %p"
number=".,"
scale="-1"
tabs="1"
Expand Down
29 changes: 29 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"request": "^2.88.2",
"semver": "^7.6.3",
"stoppable": "^1.1.0",
"strftime": "^0.10.3",
"systeminformation": "^5.23.5",
"ts-command-line-args": "^2.5.1",
"uuid": "^11.0.2",
Expand Down Expand Up @@ -155,6 +156,7 @@
"@types/semver": "^7.5.8",
"@types/sendmail": "^1.4.7",
"@types/stoppable": "^1.1.3",
"@types/strftime": "^0.9.8",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.13",
"@types/wtfnode": "^0.7.3",
Expand Down
71 changes: 71 additions & 0 deletions api/src/__test__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest';

import { formatDatetime } from '@app/utils';

describe('formatDatetime', () => {
const testDate = new Date('2024-02-14T12:34:56');

it('formats with default system time format and omits timezone', () => {
const result = formatDatetime(testDate);
// Default format is %c with timezone omitted
expect(result).toMatch('Wed 14 Feb 2024 12:34:56 PM');
});

it('includes timezone when omitTimezone is false', () => {
const result = formatDatetime(testDate, { omitTimezone: false });
// Should include timezone at the end
expect(result).toMatch(/^Wed 14 Feb 2024 12:34:56 PM .+$/);
});

it('formats with custom date and time formats', () => {
const result = formatDatetime(testDate, {
dateFormat: '%Y-%m-%d',
timeFormat: '%H:%M',
});
expect(result).toBe('2024-02-14 12:34');
});

it('formats with custom date format and default time format', () => {
const result = formatDatetime(testDate, {
dateFormat: '%d/%m/%Y',
});
expect(result).toBe('14/02/2024 12:34 PM');
});

describe('Unraid-style date formats', () => {
const dateFormats = [
'%A, %Y %B %e', // Day, YYYY Month D
'%A, %e %B %Y', // Day, D Month YYYY
'%A, %B %e, %Y', // Day, Month D, YYYY
'%A, %m/%d/%Y', // Day, MM/DD/YYYY
'%A, %d-%m-%Y', // Day, DD-MM-YYYY
'%A, %d.%m.%Y', // Day, DD.MM.YYYY
'%A, %Y-%m-%d', // Day, YYYY-MM-DD
];

const timeFormats = [
'%I:%M %p', // 12 hours
'%R', // 24 hours
];

it.each(dateFormats)('formats date with %s', (dateFormat) => {
const result = formatDatetime(testDate, { dateFormat });
expect(result).toMatch(/^Wednesday.*2024.*12:34 PM$/);
});

it.each(timeFormats)('formats time with %s', (timeFormat) => {
// specify a non-system-time date format for this test
const result = formatDatetime(testDate, { timeFormat, dateFormat: dateFormats[1] });
const expectedTime = timeFormat === '%R' ? '12:34' : '12:34 PM';
expect(result).toContain(expectedTime);
});

it.each(dateFormats.flatMap((d) => timeFormats.map((t) => [d, t])))(
'formats with date format %s and time format %s',
(dateFormat, timeFormat) => {
const result = formatDatetime(testDate, { dateFormat, timeFormat });
expect(result).toMatch(/^Wednesday.*2024.*(?:12:34 PM|12:34)$/);
}
);
});
});
114 changes: 66 additions & 48 deletions api/src/core/types/ini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,36 @@ export type IniStringBooleanOrAuto = 'auto' | 'no' | 'yes';
type Unit = 'C' | 'F';

interface Display {
align: string;
banner: string;
critical: string;
custom: string;
dashapps: string;
date: string;
hot: string;
max: string;
number: string;
refresh: string;
resize: string;
scale: string;
tabs: string;
text: string;
theme: string;
total: string;
unit: Unit;
usage: string;
warning: string;
wwn: string;
locale: string;
align: string;
banner: string;
critical: string;
custom: string;
dashapps: string;
/** a strftime format string */
date: string;
/** a strftime format string */
time?: string;
hot: string;
max: string;
number: string;
refresh: string;
resize: string;
scale: string;
tabs: string;
text: string;
theme: string;
total: string;
unit: Unit;
usage: string;
warning: string;
wwn: string;
locale: string;
}

/**
* Represents [Notification Settings](http://tower.local/Settings/Notifications),
* which live in `/boot/config/plugins/dynamix/dynamix.cfg` under the `[notify]` section.
*/
interface Notify {
entity: string;
normal: string;
Expand All @@ -37,49 +44,60 @@ interface Notify {
plugin: string;
docker_notify: string;
report: string;
date: string;
time: string;
/** @deprecated (will remove in future release). Date format: DD-MM-YYYY, MM-DD-YYY, or YYYY-MM-DD */
date: 'd-m-Y' | 'm-d-Y' | 'Y-m-d';
/**
* @deprecated (will remove in future release). Time format:
* - `hi: A` => 12 hr
* - `H:i` => 24 hr (default)
*/
time: 'h:i A' | 'H:i';
position: string;
/** path for notifications (defaults to '/tmp/notifications') */
path: string;
display: string;
/**
* The 'Notifications Display' field:
* - 0 => Detailed (default)
* - 1 => Summarized
*/
display: '0' | '1';
system: string;
version: string;
docker_update: string;
}

interface Ssmtp {
service: string;
root: string;
rcptTo: string;
setEmailPriority: string;
subject: string;
server: string;
port: string;
useTls: string;
useStarttls: string;
useTlsCert: string;
authMethod: string;
authUser: string;
authPass: string;
service: string;
root: string;
rcptTo: string;
setEmailPriority: string;
subject: string;
server: string;
port: string;
useTls: string;
useStarttls: string;
useTlsCert: string;
authMethod: string;
authUser: string;
authPass: string;
}

interface Parity {
mode: string;
dotm: string;
hour: string;
mode: string;
dotm: string;
hour: string;
}

interface Remote {
wanaccess: string;
wanport: string;
apikey: string;
wanaccess: string;
wanport: string;
apikey: string;
}

export interface DynamixConfig extends Record<string, unknown> {
display: Display;
notify: Notify;
ssmtp: Ssmtp;
parity: Parity;
remote: Remote;
display: Display;
notify: Notify;
ssmtp: Ssmtp;
parity: Parity;
remote: Remote;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
import { AppError } from '@app/core/errors/app-error';
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub';
import { Importance } from '@app/graphql/generated/client/graphql';
import { formatTimestamp } from '@app/utils';

import { NotificationsService } from './notifications.service';

Expand Down Expand Up @@ -46,11 +45,7 @@ export class NotificationsResolver {
@Args('filter')
filters: NotificationFilter
) {
const notifications = await this.notificationsService.getNotifications(filters);
return notifications.map((notification) => ({
...notification,
formattedTimestamp: formatTimestamp(notification.timestamp),
}));
return await this.notificationsService.getNotifications(filters);
}

/**============================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { NotificationSchema } from '@app/graphql/generated/api/operations';
import { Importance, NotificationType } from '@app/graphql/generated/api/types';
import { getters } from '@app/store';
import { SortFn } from '@app/unraid-api/types/util';
import { batchProcess, isFulfilled, isRejected, unraidTimestamp } from '@app/utils';
import { batchProcess, formatDatetime, isFulfilled, isRejected, unraidTimestamp } from '@app/utils';

@Injectable()
export class NotificationsService {
Expand Down Expand Up @@ -672,7 +672,8 @@ export class NotificationsService {
title,
description,
importance: this.fileImportanceToGqlImportance(importance),
timestamp: this.parseNotificationDateToIsoDate(timestamp),
timestamp: this.parseNotificationDateToIsoDate(timestamp)?.toISOString(),
formattedTimestamp: this.formatTimestamp(timestamp),
};
}

Expand All @@ -698,13 +699,36 @@ export class NotificationsService {
}
}

private parseNotificationDateToIsoDate(unixStringSeconds: string | undefined): string | null {
if (unixStringSeconds && !isNaN(Number(unixStringSeconds))) {
return new Date(Number(unixStringSeconds) * 1_000).toISOString();
private parseNotificationDateToIsoDate(unixStringSeconds: string | undefined): Date | null {
const timeStamp = Number(unixStringSeconds)
if (unixStringSeconds && !Number.isNaN(timeStamp)) {
return new Date(timeStamp * 1_000);
}
// i.e. if unixStringSeconds is an empty string or represents a non-numberS
return null;
}

private formatTimestamp(timestamp: string) {
const { display: settings } = getters.dynamix();
const date = this.parseNotificationDateToIsoDate(timestamp);

if (!settings) {
this.logger.warn(
'[formatTimestamp] Dynamix display settings not found. Cannot apply user settings.'
);
return timestamp;
} else if (!date) {
this.logger.warn(`[formatTimestamp] Could not parse date from timestamp: ${date}`);
return timestamp;
}
this.logger.debug(`[formatTimestamp] ${settings.date} :: ${settings.time} :: ${date}`);
return formatDatetime(date, {
dateFormat: settings.date,
timeFormat: settings.time,
omitTimezone: true,
});
}

/**------------------------------------------------------------------------
* Helpers
*------------------------------------------------------------------------**/
Expand Down
Loading