Fixed a bug where last evening’s standup answers showed up pre-filled in today’s evening-checklist inputs on the mobile app — because a post-midnight evening session was filing onto the new calendar day’s standup row. Also includes the one-shot data migration that moved the mis-filed datapoints back to the right day. Commit 1bf861a (fix(standup): evening checklist/reflection day starts at 15:00 local).
Not deployed yet
The fix is committed + pushed to
masterbut not deployed — needspnpm --filter web deploy(wrangler → CF Pages) for the mobile PWA to pick it up. Until then, a post-midnight evening-checklist session would still mis-file. (A before-midnight session tonight is fine — the deployed code targets May 11’s now-emptystanduprow.)
Symptoms
- On the mobile app, opening “today’s” evening checklist showed last night’s answers already filled in (3 free-text
EVENING_CHECKLISTfields + theBOTH-phase Energy datapoint). - No data was lost — the data was just attached to the wrong day. (
standuphasUNIQUE(person, date);standup_datapointhasUNIQUE(standup, config). No constraint was violated — the wrong day’s row simply held the answers.)
Root cause
web/src/mobile/plan/MobilePlan.tsx computed the standup “day” as toLocalDateString(new Date()) — the raw local calendar date — and froze it at mount via useMemo(…, []). It then passed that single standupId to every sheet, including the evening checklist and evening reflection.
On 2026-05-10 the user did their May-10 evening checklist at ~00:20 CEST on May 11 (procrastinated past midnight, tapped the new evening-checklist reminder push). Because it was technically May 11 by then:
- The app created the May-11
standuprow. - It saved the 3
EVENING_CHECKLISTdatapoints (+ theBOTH-phase Energy datapoint) onto that May-11 row.
Then on the evening of May 11, opening “today’s” (= May 11’s) evening checklist showed those answers — which were last night’s.
Fix (commit 1bf861a)
The model
An “evening-standup day D” now spans 15:00 on calendar-day D → 14:59 on calendar-day D+1. So a post-midnight session (00:00–14:59) is treated as still belonging to the previous evening’s day.
web/src/lib/date-utils.ts— neweveningStandupDate(now = new Date()): string.const EVENING_STANDUP_DAY_START_HOUR = 15; ifnow.getHours() < 15→ yesterday’stoLocalDateString, else today’s. Deliberately usesgetHours()= local time (not UTC) — same lesson as the rest of the daily-ritual date code (toLocalDateString, nottoISODateString).web/src/mobile/plan/MobilePlan.tsx:- The evening checklist + evening reflection sheets now target the row from
useStandup(person, eveningStandupDate())(eveningStandupId), with a second on-demanduseEnsureStandup(eveningDate)(guarded byeveningDate !== dateso it doesn’t double-ensure when the two coincide). - The “Evening” nudge button is now gated on that row’s
morning_completed_at/evening_completed_at. - Morning ritual, lunch checkin, day plan blocks, standup notes are UNCHANGED — still keyed on the calendar date (
standupId). date/eveningDateare no longer mount-frozen — they recompute ondocument.visibilitychange, so a long-lived PWA doesn’t go stale across midnight / 15:00.
- The evening checklist + evening reflection sheets now target the row from
web/src/lib/__tests__/date-utils.test.ts— added (4 cases foreveningStandupDate).
Desktop /standup page was NOT changed
pages/standup/index.tsx has a date picker (/standup/:date) and computes “today” via toISODateString(new Date()) (UTC). By accident this means a late-CEST-night session (00:00–02:00 CEST = still the previous day in UTC) lands on the previous day’s row anyway — so it doesn’t have the same bug. (Minor pre-existing nit: desktop uses UTC todayStr rather than local — could drift by a day near midnight UTC ≈ 1–2 am CEST. Not fixed — see tech-debt.)
Data migration (one-shot, applied to prod 2026-05-11 ~22:xx CEST)
UPDATE standup_datapoint
SET standup = '<2026-05-10 standup row id>'
WHERE standup = '<2026-05-11 standup row id>';Moved the 4 datapoints (proud_of, not_proud_of, standup.energy, overspent_today) from András Léderer’s May-11 standup row to his May-10 one. The May-10 row had 0 datapoints, so no conflict with UNIQUE(standup, config). The May-11 row is now empty.
Gotcha: “today’s date” for daily-ritual features
Worth re-stating because this class of bug keeps recurring (see the debugging log for the prior UTC-drift cases). For standup morning/evening:
- Compute it from local time, not UTC —
toLocalDateString, nottoISODateString. The desktop standup page gets this subtly wrong (and got lucky that the wrongness happens to cancel out for late-night CEST). - Don’t mount-freeze it (
useMemo(…, [])) in a PWA — recompute onvisibilitychange. (MobilePlanwas flagged for this in Mount-frozen date selectors as “deliberately not fixed” — it’s now partially fixed for this screen;MobileHours.activestill freezes.) - An evening ritual specifically should treat post-midnight sessions as the previous day — hence the 15:00 cutoff (
eveningStandupDate). A morning ritual does not want this.
Action item still open
- Deploy —
pnpm --filter web deploy. The fix is dead code on prod until then.
Related
- evening-checklist - The Evening Checklist feature (schema, frontend wiring, the timed-nudge reminder)
- mobile-day-rituals - The phone-native day-ritual sheets (where
MobilePlanwires them in) - Evening-Checklist Reminder - The 21:00/22:00 nudge that prompted the post-midnight session
- debugging-log-crm - Prior UTC-drift / timezone bugs in this codebase
- Mount-frozen date selectors - The mount-frozen-date item this partially closes
- day-planner - Day plan blocks (unchanged — still calendar-date keyed)