feat(dates): serve wall_clock + true_instant to clients (#113, #120) #146

Merged
cameron merged 3 commits from feature/wall-clock-dates into master 2026-09-29 22:01:24 +00:00
Owner

Summary

Serve the display wall clock and true instant to clients, so consumers render/compare dates without any zone math — the server half of the FileViewer-React #33 / Apollo #37 flips (spec #113, cutover #120).

Rebased onto the tracing migration (#148). DAO calls and date_view::owner_display_zone follow its API (no &opentelemetry::Context params; trace_db_call(op, name, |span|)). Conflicts were only in get_all_with_gps and the metadata handler plumbing, plus four auto-merged call sites still passing the old context, fixed before building. Re-verified after the rebase: fmt, clippy, full suite, e2e.sh check 97/97.

Why server-side: #120's test run showed the drain has a permanent floor (0.5–2% of rows — edited/exported files — never get date_utc). A client reading only the triple would have to carry the per-source legacy conventions forever. row_display_wall already handles every row and already feeds the insight prompts, so prompt and UI agree by construction.

src/date_view.rs      # format_wall_clock / row_wall_clock / file_wall_clock / file_true_instant
                      #   owner_display_zone (prefs -> app default)
src/data/mod.rs       # MetadataResponse.{wall_clock, true_instant}; GpsPhotoSummary.wall_clock
src/memories.rs       # MemoryItem.wall_clock
src/handlers/image.rs # set_capture_dates at all 4 metadata response sites (get + GPS/date/clear)
src/files.rs          # gps-summary computes wall_clock from full rows
src/database/mod.rs   # get_all_with_gps returns ImageExif rows (the 7-tuple dropped
                      #   date_taken_source, which unconverted rows need)
dev/e2e/              # `e2e.sh check`; deterministic fixture dates; TZ pinned (#120 preflight)

wall_clock   = row_display_wall(row)            # naive ISO "2018-09-29T14:00:00" — render verbatim
               ?? filename-class wall            # no row yet (unindexed file)
               ?? earliest fs time in display zone
true_instant = row_true_instant(row) ?? filename instant ?? earliest fs time
                                                 # SMS/location window anchor — never displayed

Additive only: legacy fields byte-identical, so current clients are unaffected and this can deploy ahead of the client flip (#120 step 3).

Found while doing this: the mobile SMS window was anchored on the legacy value (wall-as-UTC) while SMS-API stores true instants (verified: 40/40 sampled rows equal their backup epochs), putting the window 4–5 h off for photos taken at home — #113 user story 3. true_instant is the fix. Also: #113's schema table says "wall = instant − offset"; the code (correctly) adds the offset.

Test plan

  • dev/e2e/e2e.sh check (before and after the #148 rebase): 97/97 fixtures report the expected wall_clock on metadata / memories / gps-summary and the expected true_instant (EXIF+GPS in Tokyo/Paris/LA, no-GPS, filename-class, container video, mtime, duplicate)
  • Unit tests in date_view (travel-zone wall, no-offset rendering, unconverted floor row, file fallbacks, instants)
  • End to end on the emulator with the FileViewer #33 branch: Tokyo 14:00 photo shows 2:00 PM in viewer and memories (was 10:00 AM / 1:00 AM)
  • fmt; clippy (only pre-existing filename_audit warning); full suite (only the pre-existing filename_audit failure)

🤖 Generated with Claude Code

## Summary Serve the **display wall clock** and **true instant** to clients, so consumers render/compare dates without any zone math — the server half of the FileViewer-React #33 / Apollo #37 flips (spec #113, cutover #120). > **Rebased onto the tracing migration (#148).** DAO calls and `date_view::owner_display_zone` follow its API (no `&opentelemetry::Context` params; `trace_db_call(op, name, |span|)`). Conflicts were only in `get_all_with_gps` and the metadata handler plumbing, plus four auto-merged call sites still passing the old context, fixed before building. Re-verified after the rebase: fmt, clippy, full suite, `e2e.sh check` 97/97. Why server-side: #120's test run showed the drain has a **permanent floor** (0.5–2% of rows — edited/exported files — never get `date_utc`). A client reading only the triple would have to carry the per-source legacy conventions forever. `row_display_wall` already handles every row and already feeds the insight prompts, so prompt and UI agree by construction. src/date_view.rs # format_wall_clock / row_wall_clock / file_wall_clock / file_true_instant # owner_display_zone (prefs -> app default) src/data/mod.rs # MetadataResponse.{wall_clock, true_instant}; GpsPhotoSummary.wall_clock src/memories.rs # MemoryItem.wall_clock src/handlers/image.rs # set_capture_dates at all 4 metadata response sites (get + GPS/date/clear) src/files.rs # gps-summary computes wall_clock from full rows src/database/mod.rs # get_all_with_gps returns ImageExif rows (the 7-tuple dropped # date_taken_source, which unconverted rows need) dev/e2e/ # `e2e.sh check`; deterministic fixture dates; TZ pinned (#120 preflight) wall_clock = row_display_wall(row) # naive ISO "2018-09-29T14:00:00" — render verbatim ?? filename-class wall # no row yet (unindexed file) ?? earliest fs time in display zone true_instant = row_true_instant(row) ?? filename instant ?? earliest fs time # SMS/location window anchor — never displayed Additive only: legacy fields byte-identical, so current clients are unaffected and this can deploy ahead of the client flip (#120 step 3). Found while doing this: the mobile SMS window was anchored on the legacy value (wall-as-UTC) while SMS-API stores true instants (verified: 40/40 sampled rows equal their backup epochs), putting the window 4–5 h off for photos taken at home — #113 user story 3. `true_instant` is the fix. Also: #113's schema table says "wall = instant − offset"; the code (correctly) adds the offset. ## Test plan - [x] `dev/e2e/e2e.sh check` (before and after the #148 rebase): **97/97** fixtures report the expected `wall_clock` on metadata / memories / gps-summary and the expected `true_instant` (EXIF+GPS in Tokyo/Paris/LA, no-GPS, filename-class, container video, mtime, duplicate) - [x] Unit tests in `date_view` (travel-zone wall, no-offset rendering, unconverted floor row, file fallbacks, instants) - [x] End to end on the emulator with the FileViewer #33 branch: Tokyo 14:00 photo shows 2:00 PM in viewer and memories (was 10:00 AM / 1:00 AM) - [x] fmt; clippy (only pre-existing `filename_audit` warning); full suite (only the pre-existing `filename_audit` failure) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
cameron added 3 commits 2026-09-29 21:59:11 +00:00
Every payload a client renders a capture time from now carries
`wall_clock`, a naive ISO-8601 string computed by the existing
`date_view::row_display_wall`: the camera's wall clock (true instant +
camera offset; the owner's display zone without an offset), with the
per-source fallback for unconverted rows. Clients render it verbatim and
do no zone math, which is the FileViewer-React #33 / Apollo #37 flip with
the rule kept server-side: the drain's permanent `date_utc IS NULL` floor
(#120: edited/exported files) would otherwise force every client to
carry the per-source legacy conventions forever, and insight prompts
already use the same function, so prompt and UI agree by construction.

- `/image/metadata` (+ the GPS / date / date-clear mutation responses):
  `file_wall_clock` — the row's wall, else the filename-class date, else
  the earliest fs time in the display zone (unindexed files).
- `/memories` items and `/photos/gps-summary` photos: `row_wall_clock`.
- `get_all_with_gps` returns full rows instead of a 7-tuple that dropped
  `date_taken_source`, which unconverted rows need.
- `date_view::owner_display_zone` resolves prefs -> app default per request.

Legacy fields are untouched, so current clients see byte-identical
responses (additive; safe to deploy ahead of the client flip, per #120).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`e2e.sh check` compares every indexed fixture's manifest wall clock with
the `wall_clock` reported by /image/metadata, /memories and
/photos/gps-summary; any disagreement fails.

- Fixture videos are stamped like phone containers: the America/New_York
  wall as a UTC creation_time (the label previously showed the UTC time).
- mtimes are pinned in America/New_York instead of the host's zone.
- The e2e env sets TZ=America/New_York (the #120 preflight): the Windows
  exe has no /etc/timezone, so server_zone() was otherwise None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mobile viewer anchored its "messages around this photo" window on
the legacy date_taken (a wall clock stored as UTC for EXIF rows), while
SMS-API dates are true instants: the window was off by the photo's UTC
offset (4-5 h for photos taken at home; ImageApi #113 user story 3).
Verified SMS-API side: importers store backup epochs as UTC, and 40/40
sampled rows equal their source epochs exactly.

`/image/metadata` (+ GPS / date mutation responses) now carries
`true_instant` from `date_view::file_true_instant`: `row_true_instant`,
else the filename-class instant, else the earliest fs time. Server-side
for the same reason as `wall_clock`: the drain's permanent floor rows
have no `date_utc`. `set_capture_dates` fills both fields at all four
response sites.

dev/e2e: the manifest records each fixture's expected instant (the wall
read in its GPS zone, else America/New_York) and `e2e.sh check` asserts
it (97/97).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cameron force-pushed feature/wall-clock-dates from e3477a4710 to 9269b5c9fd 2026-09-29 21:59:11 +00:00 Compare
cameron merged commit 3ee2517a9e into master 2026-09-29 22:01:24 +00:00
cameron deleted branch feature/wall-clock-dates 2026-09-29 22:01:24 +00:00
Sign in to join this conversation.