Files
picpeak/frontend
Paul NothaftandPaul Nothaft 6891769124 fix(admin): stop marking events expired up to 24h early (#909) (stable) (#917)
* fix(admin): stop marking events expired up to 24h early (#909) (stable)

differenceInDays truncates to whole days, so an event expiring in a few
hours returned 0 and three admin surfaces treated it as gone:

- EventsListPage: status chip said 'Expired' (days <= 0) while the
  public gallery — which compares real timestamps — correctly showed
  'expires in X hours'. This is the reporter's exact symptom.
- EventDetailsPage: same isExpired math on the detail view.
- AdminDashboard: the expiring-soon card showed '0 days left' on the
  final day.

Expired is now gated on the actual timestamp (expires_at <= now), and
the countdown chips use ceiling days so the last day reads '1 day
left' instead of flipping to Expired/0.

* fix(admin): drop already-expired events from the dashboard card (#909 review round)

The expiring-soon card ran Math.max(1, ceil(delta)), so an event that
expired while the dashboard sat open (its query isn't polled) showed
'1 day left' indefinitely from the stale cached row. Expired rows are
now filtered out before render; the delta is therefore always positive
and the clamp is gone.

* fix(admin): refresh expiry status live at the boundary (#909 review round 2)

Two review findings on the admin expiry surfaces:

- The dashboard 'expiring soon' card, list badges, and detail banner are
  all computed inline from Date.now() at render, so a page left open
  across an event's expiry kept showing 'active'/'1 day left' until an
  unrelated render — which for editor/viewer roles (no health poll)
  never happens.
- My round-1 client-side filter on the dashboard desynced the visible
  list from the cached total/stat ('no events expiring' beside 'view
  all N').

Both are fixed by new useExpiryRefresh: it fires once at the soonest
future expiry (setTimeout, overflow-guarded). The dashboard refetches
its expiring + stats queries — the backend already excludes expired
events, so rows/total/stats come back consistent (filter removed). The
list and detail pages bump a tick so the inline badges recompute. Hooks
are placed above the loading early-returns (rules-of-hooks is disabled
in eslint, so this was a latent crash otherwise).

* fix(admin): expiry-refresh precision + filtered refetch (#909 review round 3)

Three refinements to round-2's live-expiry work:

- useExpiryRefresh now re-arms past setTimeout's ~24.8-day overflow
  limit (capped wake-up that re-evaluates) instead of dropping the timer,
  so a page mounted for weeks still updates.
- The dashboard requests the expiring list ordered by expires_at asc, so
  the five shown rows ARE the soonest to expire — the timer schedules
  against the true next boundary even when >5 events are expiring
  (getEvents gains optional sortBy/sortOrder; backend already whitelists
  expires_at).
- EventsListPage refetches instead of only re-rendering at the boundary:
  under the 'expiring' filter the backend drops expired rows, so a plain
  tick would leave a stale 'Expired' row + total. refetch keeps rows and
  totals correct under every filter.

---------

Co-authored-by: Paul Nothaft <[email protected]>
2026-07-30 12:14:54 +02:00
..