The audit #485 called for: on SQLite (native installs), timestamp
columns written with a raw `new Date()` through knex store epoch-ms
numbers; Postgres returns ISO strings. Frontend code written against
Postgres calls parseISO() on them — parseISO(number) throws and crashes
the page. #485 fixed admin Users and listed api tokens / photos /
activity as out-of-scope follow-ups.
Verified crash on main: Timeline gallery layout parseISO(uploaded_at)
against photos written by the archive-RESTORE path (raw Date). Other
raw-write surfaces (api_tokens last_used_at/revoked_at, email_queue)
degrade rather than crash but violate the ISO contract.
- extract toIso() from adminUsers.js into utils/dateNormalize.js
(contract unchanged — the 10 existing #485 tests still pin it)
- write-side: archive-restore uploaded_at, api-token last_used_at /
revoked_at, email_queue created_at/sent_at now write ISO strings
- read-side (heals existing corrupted rows): gallery /photos normalizes
uploaded_at/captured_at; api-tokens list normalizes all four
timestamp fields
- frontend defence-in-depth: Timeline layout parses uploaded_at
tolerantly (typeof guard) for stale caches / old backends
- 2 regression tests seed literal epoch numbers and assert the API
serves ISO strings
activity_logs turned out safe (created_at comes from the DB default,
not a raw Date) — left untouched.
Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>