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 master but not deployed — needs pnpm --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-empty standup row.)

Symptoms

  • On the mobile app, opening “today’s” evening checklist showed last night’s answers already filled in (3 free-text EVENING_CHECKLIST fields + the BOTH-phase Energy datapoint).
  • No data was lost — the data was just attached to the wrong day. (standup has UNIQUE(person, date); standup_datapoint has UNIQUE(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:

  1. The app created the May-11 standup row.
  2. It saved the 3 EVENING_CHECKLIST datapoints (+ the BOTH-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 — new eveningStandupDate(now = new Date()): string. const EVENING_STANDUP_DAY_START_HOUR = 15; if now.getHours() < 15 → yesterday’s toLocalDateString, else today’s. Deliberately uses getHours() = local time (not UTC) — same lesson as the rest of the daily-ritual date code (toLocalDateString, not toISODateString).
  • 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-demand useEnsureStandup(eveningDate) (guarded by eveningDate !== date so 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 / eveningDate are no longer mount-frozen — they recompute on document.visibilitychange, so a long-lived PWA doesn’t go stale across midnight / 15:00.
  • web/src/lib/__tests__/date-utils.test.ts — added (4 cases for eveningStandupDate).

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:

  1. Compute it from local time, not UTCtoLocalDateString, not toISODateString. The desktop standup page gets this subtly wrong (and got lucky that the wrongness happens to cancel out for late-night CEST).
  2. Don’t mount-freeze it (useMemo(…, [])) in a PWA — recompute on visibilitychange. (MobilePlan was flagged for this in Mount-frozen date selectors as “deliberately not fixed” — it’s now partially fixed for this screen; MobileHours.active still freezes.)
  3. 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

  • Deploypnpm --filter web deploy. The fix is dead code on prod until then.