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

+110
View File
@@ -0,0 +1,110 @@
import { useEffect, useState } from "react";
import { api, type AppSettings, type DateFormat, type TimeFormat } from "../../api/client";
import { setDateTimeSettings } from "../../utils/date";
const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string; example: string }[] = [
{ value: "ymd", label: "Year-Month-Day", example: "2026-09-15" },
{ value: "dmy", label: "Day/Month/Year", example: "15/09/2026" },
{ value: "mdy", label: "Month/Day/Year", example: "09/15/2026" },
];
function previewTime(timeFormat: TimeFormat): string {
return timeFormat === "12h" ? "2:30:00 PM" : "14:30:00";
}
export default function DateTimeSettings() {
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [dateFormat, setDateFormatState] = useState<DateFormat>("ymd");
const [timeFormat, setTimeFormatState] = useState<TimeFormat>("24h");
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
useEffect(() => {
api.settings
.get()
.then((res: { settings: AppSettings }) => {
setDateFormatState(res.settings.display.dateFormat);
setTimeFormatState(res.settings.display.timeFormat);
})
.catch((err) => setLoadError(err instanceof Error ? err.message : String(err)))
.finally(() => setLoading(false));
}, []);
async function handleSave() {
setSaving(true);
setSaveError(null);
setSaved(false);
try {
const res = await api.settings.update({ display: { dateFormat, timeFormat } });
setDateTimeSettings(res.settings.display);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
} catch (err) {
setSaveError(err instanceof Error ? err.message : String(err));
} finally {
setSaving(false);
}
}
if (loading) return <div className="text-secondary">Loading…</div>;
const dateExample = DATE_FORMAT_OPTIONS.find((o) => o.value === dateFormat)?.example ?? "";
return (
<>
<div className="d-flex align-items-center justify-content-between mb-3">
<h3 className="mb-0">Date &amp; Time</h3>
<div className="d-flex align-items-center gap-2">
{saveError && <span className="text-danger small">{saveError}</span>}
{saved && <span className="text-success small">✓ Settings saved</span>}
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
{saving ? "Saving…" : "Save Settings"}
</button>
</div>
</div>
{loadError && <div className="alert alert-danger">{loadError}</div>}
<div className="card">
<div className="card-header">
<h3 className="card-title">Display format</h3>
</div>
<div className="card-body">
<p className="text-secondary">
Controls how dates and times are shown throughout the app — tables previously mixed formats depending on
the page; this makes them consistent everywhere.
</p>
<div className="row g-3">
<div className="col-md-6">
<label className="form-label">Date format</label>
<select className="form-select" value={dateFormat} onChange={(e) => setDateFormatState(e.target.value as DateFormat)}>
{DATE_FORMAT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label} ({opt.example})
</option>
))}
</select>
</div>
<div className="col-md-6">
<label className="form-label">Clock</label>
<select className="form-select" value={timeFormat} onChange={(e) => setTimeFormatState(e.target.value as TimeFormat)}>
<option value="24h">24-hour (14:30:00)</option>
<option value="12h">12-hour (2:30:00 PM)</option>
</select>
</div>
</div>
<div className="mt-3">
<div className="text-secondary small">Preview</div>
<div>
<code>
{dateExample} {previewTime(timeFormat)}
</code>
</div>
</div>
</div>
</div>
</>
);
}