Repository navigation
A server-side label reads the family's clock, not the host's - #937
Merged
NewWorldVenture merged 1 commit intoOct 4, 2026
Merged
Conversation
`toLocaleDateString` / `toLocaleTimeString` / `toLocaleString` with no
`timeZone` render in the zone of the machine running the code. In a browser
that is the person's own clock. On a server it is the host's, and nine sites
in server-reachable code still did it:
- lib/server/notifications.ts: a document's expiry body. `documents.expires_at`
is a DATE, UTC midnight when parsed, so a US host said "Expires Oct 7" about
a passport expiring on the 8th. Rendered as the date it is (UTC).
- app/api/ai/resolve-conflict: both event descriptions, which the model
reasons about and the family reads back, in the family's zone.
- app/api/ai/chef: busy evenings were `getHours() >= 16` on the host — a 6pm
Californian practice read as 1am and was never busy, with tomorrow's
weekday. Now `hourInTz` and a zoned weekday.
- app/api/ai/flyer: the INSTANTS were built on the host's clock
(`new Date('…T18:00:00')`), so a flyer's 6pm event imported at 11am for a
Californian family on a UTC server; `toIso` now resolves the family's wall
time with `instantForLocalTime`. Summaries and "Today is" in the zone.
- app/api/ai/import: item summaries (timed in the zone, date-only as the date
it is) and "Today is" with the family's day key.
- app/api/ai/weekly-briefing: the "generated" stamp.
- lib/meals/pantry-chef.ts + its route: "Today is" takes the family's zone
(a required parameter, so the arity guard sees a missing one).
- app/api/cron/admin-digest: an explicit UTC, so the subject does not name a
different day from one cron host to the next.
Tests: tests/a-server-label-reads-the-familys-clock.test.ts (8): the engine
says "Expires Oct 8" whatever zone the host runs in, the fridge chef names
the family's weekday, each repaired site is pinned, and a ratchet over the
server-reachable roots (app/api, lib/server, lib/notifications, lib/meals,
lib/briefing, lib/admin) fails on any Date render that omits a zone.
pantry-chef and school-sports-desk pins carry the zone. 32 suites green in
UTC and the new suite in America/Los_Angeles; tsc and eslint clean on the
changed sources. Mutation controls run under America/Los_Angeles (restored,
checksums verified): document expiry on the host 2 failures; chef on the
host's hour 2; fridge chef on the host 3; Magic Import on the host 2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
NewWorldVenture
marked this pull request as ready for review
October 4, 2026 11:57
NewWorldVenture
pushed a commit
that referenced
this pull request
Oct 4, 2026
…wal-is-reminded-again-20261003 One both-sides-edited conflict in lib/server/notifications.ts: main's document-expiry label now renders the DATE in UTC (#937); this branch's expiryDay key line sits beside it. Both kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture
pushed a commit
that referenced
this pull request
Oct 4, 2026
…t-ticked-twice-is-sent-once-20261004 One both-sides conflict in app/api/cron/admin-digest/route.ts: main (#937) renders the digest's date label in UTC from the clock; this branch labels it by the slot's date, in UTC, so every tick renders the same bytes. The slot label is kept — it is the superset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture
pushed a commit
that referenced
this pull request
Oct 4, 2026
…dar, so its declaration goes CI on e10ff9a failed one assertion: "Declared as still reading the host's calendar on a server path, but no longer does. Delete these lines: app/api/ai/chef/route.ts". The merge of main brought #937 (a3712a9), which reads the chef's busy evenings on the family's clock, so the declaration this ratchet carried for that file is stale — and the ratchet says so by design: the list may only shrink, and a fixed file must leave it. Deleted. Nothing else changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture
pushed a commit
that referenced
this pull request
Oct 4, 2026
Merging main into #788 put main's code next to #788's tests, and two of them pinned the side that changed: - a-retried-assistant-turn-counts-once: main's chat route now reads the upcoming calendar through lib/calendar/occurrences, which filters with PostgREST `or` and `neq`. This file's hand-written client fake had neither, so the snapshot read threw and every /api/ai/chat case streamed nothing. The fake gains both (`neq` with SQL's null semantics; `or` narrows nothing, as no case here seeds calendar rows). No assertion changed. - a-server-label-reads-the-familys-clock (main, #937) pinned main's flyer line `toIso(r.date as string, (r.time as string) ?? null, tz)`; #788 had already rewritten it as `toIso(date, textOrNull(r.time), tz)`. Same zone argument, which is what the test proves; the pin follows the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
NewWorldVenture
added a commit
that referenced
this pull request
Oct 4, 2026
…et over the import graph, and the eleven more sites it found (#945) * A server path does not read the host's calendar: a reachability ratchet over the import graph, and the eleven more sites it found `d.getFullYear()`, `.getMonth()`, `.getDate()`, `.getDay()` and `.getHours()` answer in the zone of the machine running the code — the reader's own day in a browser, Greenwich's on the UTC host that renders a server component, answers a service call or runs a cron. Four units closed the sites this class had grown; this one keeps it closed and fixes what the closing scan found. tests/a-server-path-does-not-read-the-hosts-calendar.test.ts - Walks the import graph from every server entry point (each non-client file under app/), stops at a 'use client' boundary, and lists every reached file that reads a local calendar part. Not every file with a getter: a module imported only by client components runs in the browser, where the local calendar is right, and listing fifty of those would bury the dozen that matter. 711 entry points, 1,525 reachable files. - Like tests/a-server-day-is-the-familys-day.test.ts, every remaining read is DECLARED with the reason it is still there — browser-side paths kept beside the zoned ones (birthdays, celebrations, on-this-day, memories, age, prep, vacations, maintenance, time-of-day, ambient, capture's LOCAL_OPS); correct as it stands (guardian weekday in-zone, adherence's wall clock by contract, the moving planner's local-in/local-out strings, reminder recurrence rolling, home-data's isoDate for client modules); reachable but called only from the browser (inventory finder, marketplace handoff, departure defaults); and two fixes in flight (the chef route on #932, the insights planner's host year). A new read fails; a removed one fails too, which is the moment to delete its line. Non-vacuity: the graph reaches the home page and, through home-data and the family service, lib/members/age.ts; no client file is in it; the getter is seen on a planted line and not in a comment; import resolution is checked on an alias, a relative path and a package. The eleven sites the scan found, fixed on the family's (or the admin's) calendar: - admin support tickets: the ticket number's date stamp, from the admin zone's day key; admin users: "new this month" from utcMonthStartIso(); the admin growth chart: cumulative counts at the end of each of the last N whole admin-zone days (lib/admin/clock.ts lastUtcDays) instead of rolling host midnights. - family COO: an event's weekday, `DAYS[d.getDay()]` in English on the host's calendar, becomes fmtDate(e.starts_at, 'EEE') from the formatter the page already binds to tz. - memories: the year the counts are over starts at the FAMILY's 1 January. - moments: the signals' hour and weekday from localPartsAt(now, tz) and the family's day key. - the family and personal dashboards: a formatter bound to the family zone (createFormat(locale.code, undefined, tz)) for each event's clock, which the bare fmtTime rendered in the host's zone, and for its badge month and day, which toLocaleDateString(locale.code, …) and d.getDate() did too; the meal strip's weekday is read off the DATE in no zone. - the trip layout's countdown: daysUntil / countdownLabel take the family's day key (a Date still means a browser's local midnight); the maintenance page's season: currentSeason takes the day key likewise. - six copyright years (five emails, the site footer): getUTCFullYear(), so the year does not depend on which side of midnight the host is on. Mutation controls, each against the unmodified test with the file restored byte-for-byte: a host read planted back on the COO page (2 cases fail), a declared file losing its read (1: the stale-declaration case), a read planted in a module reached only through others, lib/services/scope.ts (1), the growth chart back on the host's hours (2). Validation: the 77 touching test files (1,107 cases) green under UTC and TZ=America/Los_Angeles except the react-email render file, which fails identically on clean main in this container; tsc and eslint clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K * Server clock ratchet: the chef route no longer reads the host's calendar, so its declaration goes CI on e10ff9a failed one assertion: "Declared as still reading the host's calendar on a server path, but no longer does. Delete these lines: app/api/ai/chef/route.ts". The merge of main brought #937 (a3712a9), which reads the chef's busy evenings on the family's clock, so the declaration this ratchet carried for that file is stale — and the ratchet says so by design: the list may only shrink, and a fixed file must leave it. Deleted. Nothing else changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
toLocaleDateString/toLocaleTimeString/toLocaleStringwith notimeZonerender in the zone of the machine running the code. In a browser that is the person's own clock and is right. On a server it is the host's, and nine sites in server-reachable code still did it:lib/server/notifications.ts: the document-expiry body.documents.expires_atis a DATE column, UTC midnight when parsed, so a US host wrote "Expires Oct 7" about a passport expiring on the 8th.app/api/ai/resolve-conflict: both event descriptions, which the model reasons about and the family reads back.app/api/ai/chef: busy evenings weregetHours() >= 16on the host. A 6pm Californian practice read as 1am and was never busy, with tomorrow's weekday.app/api/ai/flyer: the event instants were built on the host's clock (new Date('…T18:00:00')), so a flyer's 6pm event imported at 11am for a Californian family on a UTC server. Summaries and "Today is" were host-zoned too.app/api/ai/import: item summaries and "Today is".app/api/ai/weekly-briefing: the "generated" stamp.lib/meals/pantry-chef.ts: "Today is" in the fridge-chef prompt.app/api/cron/admin-digest: the date label (no family; its day depended on which cron host ran).What changed
timeZone: 'UTC').ctx.active.family.timezone(falling back to UTC). The chef useshourInTz; the flyer'stoIsoresolves the family's wall time withinstantForLocalTime; Magic Import renders timed values in the zone and date-only values as the date they say, and its "Today is" carries the family's day key.buildPantryChefPrompttakes the zone as a required parameter, so the existing arity guard (a-zone-aware-helper-called-without-the-zone) sees a missing one; its route passes the family's.No migration, no schema change.
Tests
tests/a-server-label-reads-the-familys-clock.test.ts(8): the engine says "Expires Oct 8" whatever zone the host runs in (real engine over the in-memory Supabase); the fridge chef names the family's weekday (Friday in Los Angeles, Saturday in UTC, same instant); each repaired site is pinned; and a ratchet over the server-reachable roots (app/api,lib/server,lib/notifications,lib/meals,lib/briefing,lib/admin) fails on any Date render that omits a zone, so the class does not come back one site at a time. Client components keep their own guard. Thepantry-chefandschool-sports-deskpins carry the zone.32 suites green in UTC and the new suite in America/Los_Angeles (the zone CI reruns under);
tscclean;eslintclean on every changed source file.tests/school-sports-desk.test.tscarries a pre-existing lint error on an unrelated line (amodulevariable), untouched here.Mutation controls (run under America/Los_Angeles, each restored, checksums verified)
The commit message rounded two of these up (2/2/3/2); the table above is what the runs reported.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Generated by Claude Code