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 <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
2f05fcc39d
commit
c6ec93eef9
@@ -41,12 +41,19 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
const showDates = gallerySettings.timelineShowDates !== false;
|
||||
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
|
||||
|
||||
// Tolerant timestamp parse: SQLite installs can hold epoch numbers in
|
||||
// uploaded_at (pre-fix archive restores) — parseISO throws on numbers
|
||||
// and crashed the whole Timeline view (#485 class). The API normalizes
|
||||
// to ISO now; this guard covers stale caches and old backends.
|
||||
const parseUploadedAt = (value: string | number) =>
|
||||
typeof value === 'string' ? parseISO(value) : new Date(value);
|
||||
|
||||
// Group photos by date
|
||||
const groupedPhotos = useMemo(() => {
|
||||
const groups = new Map<string, Photo[]>();
|
||||
|
||||
photos.forEach(photo => {
|
||||
const date = parseISO(photo.uploaded_at);
|
||||
const date = parseUploadedAt(photo.uploaded_at);
|
||||
let groupKey: string;
|
||||
|
||||
switch (grouping) {
|
||||
@@ -76,7 +83,7 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
return Array.from(groups.entries())
|
||||
.map(([date, photos]) => ({
|
||||
date,
|
||||
label: photos[0] ? format(parseISO(photos[0].uploaded_at), grouping === 'month' ? 'MMMM yyyy' : grouping === 'week' ? "'Week of' MMM d, yyyy" : 'EEEE, MMMM d, yyyy') : date,
|
||||
label: photos[0] ? format(parseUploadedAt(photos[0].uploaded_at), grouping === 'month' ? 'MMMM yyyy' : grouping === 'week' ? "'Week of' MMM d, yyyy" : 'EEEE, MMMM d, yyyy') : date,
|
||||
photos: photos.sort((a, b) => new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime())
|
||||
}))
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
Reference in New Issue
Block a user