Add a Date & Time setting and fix inconsistent date formats app-wide

Different tables used different date formats: Servers & Tasks used a
fixed "YYYY-MM-DD HH:mm:ss" (24h), while Audit Log, DNS, Integrations
(Tailscale/Semaphore), and Users called plain .toLocaleString() with
no options, which renders using the browser's own locale — different
per browser/OS, and inconsistent with the other pages' fixed format.

- New Settings -> Date & Time page: pick date order (YYYY-MM-DD,
  DD/MM/YYYY, MM/DD/YYYY) and 12h vs 24h clock, with a live preview.
- utils/date.ts's formatDateTime() now reads these settings instead of
  being hardcoded to sv-SE/24h; the setting is fetched once at app
  startup (alongside /api/me) via a new non-secret GET
  /api/settings/display (any signed-in user, same rationale as the
  badge-color endpoints) and applied immediately on save too, without
  needing a page reload.
- Switched every remaining raw new Date(...).toLocaleString() call
  (Audit Log, DNS zone sync time, Tailscale/Semaphore last-seen,
  Users' last login) over to the shared formatter, so every table now
  renders dates identically.

Verified the formatter's date-order x 12h logic against all six
combinations plus the midnight/noon 12h edge cases, and the settings
endpoints end-to-end (defaults, partial updates, validation
rejection, audit logging) against the real dev server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-15 22:17:06 +02:00
1 parent f7357141eb
commit f92f8de96e
12 files changed
+206 -17

No files matched your search

+8
View File
@@ -27,6 +27,13 @@ settingsRouter.get("/integration-colors", asyncHandler(async (_req, res) => {
res.json({ integrationColors });
}));
// Display prefs (date/time format) affect every page, so any signed-in user
// can read them — same non-secret rationale as the badge-color endpoints.
settingsRouter.get("/display", asyncHandler(async (_req, res) => {
const { display } = await getSettings();
res.json({ display });
}));
const updateSchema = z.object({
gotify: z.object({ enabled: z.boolean(), url: z.string(), token: z.string(), priority: z.number() }).partial().optional(),
ntfy: z.object({ enabled: z.boolean(), url: z.string(), topic: z.string(), token: z.string(), priority: z.number() }).partial().optional(),
@@ -57,6 +64,7 @@ const updateSchema = z.object({
.optional(),
providerColors: z.record(z.string()).optional(),
integrationColors: z.record(z.string()).optional(),
display: z.object({ dateFormat: z.enum(["ymd", "dmy", "mdy"]), timeFormat: z.enum(["24h", "12h"]) }).partial().optional(),
});
settingsRouter.put("/", requireRole("admin"), asyncHandler(async (req, res) => {
+10
View File
@@ -46,6 +46,14 @@ export interface NotificationEvents {
export type ProviderColors = Record<string, string>;
export type IntegrationColors = Record<string, string>;
export type DateFormat = "ymd" | "dmy" | "mdy";
export type TimeFormat = "24h" | "12h";
export interface DisplaySettings {
dateFormat: DateFormat;
timeFormat: TimeFormat;
}
export interface AppSettings {
gotify: GotifySettings;
ntfy: NtfySettings;
@@ -54,6 +62,7 @@ export interface AppSettings {
notifications: NotificationEvents;
providerColors: ProviderColors;
integrationColors: IntegrationColors;
display: DisplaySettings;
}
const DEFAULTS: AppSettings = {
@@ -71,6 +80,7 @@ const DEFAULTS: AppSettings = {
},
providerColors: {},
integrationColors: {},
display: { dateFormat: "ymd", timeFormat: "24h" },
};
const KEYS = Object.keys(DEFAULTS) as (keyof AppSettings)[];