Skip to content

A server-side label reads the family's clock, not the host's - #937

Merged
NewWorldVenture merged 1 commit into
mainfrom
claude/a-server-label-reads-the-familys-clock-20261003
Oct 4, 2026
Merged

NewWorldVenture merged 1 commit into
mainfrom
claude/a-server-label-reads-the-familys-clock-20261003

Conversation

@NewWorldVenture

Copy link
Copy Markdown
Owner

What was wrong

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 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_at is 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 were getHours() >= 16 on 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

  • The document body renders the date as the date it is (timeZone: 'UTC').
  • The conflict resolver, chef, flyer, import and weekly brief use ctx.active.family.timezone (falling back to UTC). The chef uses hourInTz; the flyer's toIso resolves the family's wall time with instantForLocalTime; 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.
  • buildPantryChefPrompt takes 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.
  • The admin digest uses an explicit UTC label.

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. The pantry-chef and school-sports-desk pins carry the zone.

32 suites green in UTC and the new suite in America/Los_Angeles (the zone CI reruns under); tsc clean; eslint clean on every changed source file. tests/school-sports-desk.test.ts carries a pre-existing lint error on an unrelated line (a module variable), untouched here.

Mutation controls (run under America/Los_Angeles, each restored, checksums verified)

Mutation Failures
Document expiry rendered on the host 2
Chef back to the host's hour 1
Fridge chef's today on the host 2
Magic Import's timed summary on the host 1

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

`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
@vercel

vercel Bot commented Oct 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 3, 2026 11:13pm UTC

Request Review

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review October 4, 2026 11:57
@NewWorldVenture
NewWorldVenture merged commit a3712a9 into main Oct 4, 2026
7 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants