Read-side switch of every date consumer onto the ADR-0001 new contract (date_utc, camera_offset_minutes, date_taken_tz_source), with per-source fallback conventions for rows the #118 drain hasn't converted yet. No writes, no migrations — the legacy date_taken column stays frozen.
src/
├── date_view.rs # NEW — pure read views, no DB/IO
│ # row_true_instant → window anchors (SMS/location/calendar)
│ # row_display_wall → prompt/UI date (must match metadata)
│ # row_display_day → grouping key (owner's day, always)
├── memories.rs # /memories groups in the owner's display zone (prefs),
│ # not the client offset; items carry the triple
├── ai/insight_generator.rs # timestamp = true instant; prompt date = display wall
├── ai/insight_chat.rs # chat bootstrap date = display wall (same waterfall)
├── reels/ # pregen + selector take an IANA zone (REEL_PREGEN_DISPLAY_TZ)
└── data/mod.rs # GpsPhotoSummary carries the triple
Per row, the read path is:
read(row)
if row.date_utc is set # converted by #118
instant = row.date_utc
wall = instant + camera_offset # camera wall when offset known
else # per-source convention (ADR-0001)
(instant, wall) = convention[row.date_taken_source]
day = render(instant, owner_display_zone) # grouping key — always owner's day
Unconverted-row conventions (the mixed-row safety net until the drain finishes):
exif / filename-wall / manual: legacy is a wall clock → instant = wall in server (manual: display) zone
exiftool video: legacy is the container's UTC naive rendered in the server zone → recovered by one server-zone round-trip, read back as UTC (kills the /memories double-shift)
exiftool photo / filename-ts / fs_time: legacy is already the right thing under its own convention
Evidence
Before: an unconverted container row in /memories rendered its legacy epoch in the client offset — a 05:30 UTC video landed on July 5 instead of the owner's July 4 evening (off by one day, exactly the #115 bug on the read path). A Japan +9h photo told the AI "July 5" (the UTC date) while the owner saw July 6.
After: four new tests at the seams the spec calls out:
memories::api::memories_mixed_rows_group_into_owner_month
converted + unconverted-exif + unconverted-container fixtures →
all three bucket into the owner's month; `created` = true instant
under each row's convention; triple populated/None as expected
memories::api::memories_response_shape_carries_triple
serialized item carries date_utc / camera_offset_minutes /
date_taken_tz_source (null while unconverted)
insight_chat::japan_offset_row_ai_date_is_camera_wall_and_anchor_unshifted
23:30 UTC + 9h → AI told 2024-07-06 (camera wall), window anchor
= date_utc with zero offset math
insight_chat::unconverted_video_row_ai_date_matches_display_wall
container "2024-07-06 01:30 UTC" → AI told 2024-07-05 (owner's
evening), not 2024-07-06
Full suite: cargo test → 1,410 passed, 0 failed. cargo fmt --check clean, cargo clippy --all-targets clean.
Spec + standards review (two-axis, #119 branch): spec READY with 4 test-gap notes — all closed above. Standards: no blockers; duplicated-code and naming findings fixed in-branch; the (display_zone, server_zone) data-clump bundle is a P2 judgement call, deferred.
Merge Danger
Door: two-way. Read-only switch; no schema change, no new writes, legacy columns byte-identical. Reverting restores the old render paths with zero data migration.
Blast Radius: display. /memories day buckets, precomputed-reel pregen dates, AI insight/chat prompt dates, and the GPS summary payload change for unconverted rows (that's the point — the double-shift goes away). Clients that still send timezone_offset_minutes to /memories keep working (param accepted, ignored for grouping). Reels pregen rows written under a different display zone before the cutover will be regenerated on their normal staleness cycle.
## Summary
Read-side switch of every date consumer onto the ADR-0001 new contract (`date_utc`, `camera_offset_minutes`, `date_taken_tz_source`), with per-source fallback conventions for rows the #118 drain hasn't converted yet. No writes, no migrations — the legacy `date_taken` column stays frozen.
```
src/
├── date_view.rs # NEW — pure read views, no DB/IO
│ # row_true_instant → window anchors (SMS/location/calendar)
│ # row_display_wall → prompt/UI date (must match metadata)
│ # row_display_day → grouping key (owner's day, always)
├── memories.rs # /memories groups in the owner's display zone (prefs),
│ # not the client offset; items carry the triple
├── ai/insight_generator.rs # timestamp = true instant; prompt date = display wall
├── ai/insight_chat.rs # chat bootstrap date = display wall (same waterfall)
├── reels/ # pregen + selector take an IANA zone (REEL_PREGEN_DISPLAY_TZ)
└── data/mod.rs # GpsPhotoSummary carries the triple
```
Per row, the read path is:
```
read(row)
if row.date_utc is set # converted by #118
instant = row.date_utc
wall = instant + camera_offset # camera wall when offset known
else # per-source convention (ADR-0001)
(instant, wall) = convention[row.date_taken_source]
day = render(instant, owner_display_zone) # grouping key — always owner's day
```
Unconverted-row conventions (the mixed-row safety net until the drain finishes):
- `exif` / filename-wall / `manual`: legacy is a wall clock → instant = wall in server (manual: display) zone
- `exiftool` video: legacy is the container's UTC naive rendered in the server zone → recovered by one server-zone round-trip, read back as UTC (kills the /memories double-shift)
- `exiftool` photo / filename-ts / `fs_time`: legacy is already the right thing under its own convention
## Evidence
**Before:** an unconverted container row in `/memories` rendered its legacy epoch in the client offset — a 05:30 UTC video landed on July 5 instead of the owner's July 4 evening (off by one day, exactly the #115 bug on the read path). A Japan +9h photo told the AI "July 5" (the UTC date) while the owner saw July 6.
**After:** four new tests at the seams the spec calls out:
```
memories::api::memories_mixed_rows_group_into_owner_month
converted + unconverted-exif + unconverted-container fixtures →
all three bucket into the owner's month; `created` = true instant
under each row's convention; triple populated/None as expected
memories::api::memories_response_shape_carries_triple
serialized item carries date_utc / camera_offset_minutes /
date_taken_tz_source (null while unconverted)
insight_chat::japan_offset_row_ai_date_is_camera_wall_and_anchor_unshifted
23:30 UTC + 9h → AI told 2024-07-06 (camera wall), window anchor
= date_utc with zero offset math
insight_chat::unconverted_video_row_ai_date_matches_display_wall
container "2024-07-06 01:30 UTC" → AI told 2024-07-05 (owner's
evening), not 2024-07-06
```
Full suite: `cargo test` → 1,410 passed, 0 failed. `cargo fmt --check` clean, `cargo clippy --all-targets` clean.
Spec + standards review (two-axis, #119 branch): spec READY with 4 test-gap notes — all closed above. Standards: no blockers; duplicated-code and naming findings fixed in-branch; the `(display_zone, server_zone)` data-clump bundle is a P2 judgement call, deferred.
## Merge Danger
**Door:** two-way. Read-only switch; no schema change, no new writes, legacy columns byte-identical. Reverting restores the old render paths with zero data migration.
**Blast Radius:** display. `/memories` day buckets, precomputed-reel pregen dates, AI insight/chat prompt dates, and the GPS summary payload change for unconverted rows (that's the point — the double-shift goes away). Clients that still send `timezone_offset_minutes` to `/memories` keep working (param accepted, ignored for grouping). Reels pregen rows written under a different display zone before the cutover will be regenerated on their normal staleness cycle.
- New src/date_view.rs: pure read-side functions (row_true_instant,
row_display_wall, row_display_day, filename_true_instant) applying the
per-source convention table for unconverted rows and the new-contract
triple for converted rows (ADR-0001).
- /memories: grouping on the row's true instant rendered in the owner's
display zone (prefs -> app default), not the client device offset.
SQL is a coarse interval pre-filter over the full years-back window;
exact month-day/week/month matching across past years happens in Rust
(memories::day_in_span, sqlite_w_week).
- Reels: ReelSelector::Memories carries an IANA display_tz instead of a
fixed minute offset; pre-gen params read REEL_PREGEN_DISPLAY_TZ; the
tz_offset_minutes ledger column stores the zone offset at render time.
- Insights: prompt-facing dates render as the display wall; the
proximity timestamp and chat context block use the true instant;
InsightGenerator reads the owner's display zone from prefs per call.
- GpsPhotoSummary carries the new-contract triple alongside the frozen
legacy date_taken.
Closes#119
A container whose /etc/localtime is a copy rather than a symlink makes
server_zone() return None; the assumed_server rung then fell back to
Local::now()'s offset, which is only correct for captures from the
current season. Historical winter walls on a DST host skewed date_utc
by an hour. Resolve the process-local zone at the wall instead
(earliest reading for ambiguous walls).
Surfaced by test-server validation of #119.
cameron
merged commit 7f348022d4 into master2026-09-27 15:07:13 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Read-side switch of every date consumer onto the ADR-0001 new contract (
date_utc,camera_offset_minutes,date_taken_tz_source), with per-source fallback conventions for rows the #118 drain hasn't converted yet. No writes, no migrations — the legacydate_takencolumn stays frozen.Per row, the read path is:
Unconverted-row conventions (the mixed-row safety net until the drain finishes):
exif/ filename-wall /manual: legacy is a wall clock → instant = wall in server (manual: display) zoneexiftoolvideo: legacy is the container's UTC naive rendered in the server zone → recovered by one server-zone round-trip, read back as UTC (kills the /memories double-shift)exiftoolphoto / filename-ts /fs_time: legacy is already the right thing under its own conventionEvidence
Before: an unconverted container row in
/memoriesrendered its legacy epoch in the client offset — a 05:30 UTC video landed on July 5 instead of the owner's July 4 evening (off by one day, exactly the #115 bug on the read path). A Japan +9h photo told the AI "July 5" (the UTC date) while the owner saw July 6.After: four new tests at the seams the spec calls out:
Full suite:
cargo test→ 1,410 passed, 0 failed.cargo fmt --checkclean,cargo clippy --all-targetsclean.Spec + standards review (two-axis, #119 branch): spec READY with 4 test-gap notes — all closed above. Standards: no blockers; duplicated-code and naming findings fixed in-branch; the
(display_zone, server_zone)data-clump bundle is a P2 judgement call, deferred.Merge Danger
Door: two-way. Read-only switch; no schema change, no new writes, legacy columns byte-identical. Reverting restores the old render paths with zero data migration.
Blast Radius: display.
/memoriesday buckets, precomputed-reel pregen dates, AI insight/chat prompt dates, and the GPS summary payload change for unconverted rows (that's the point — the double-shift goes away). Clients that still sendtimezone_offset_minutesto/memorieskeep working (param accepted, ignored for grouping). Reels pregen rows written under a different display zone before the cutover will be regenerated on their normal staleness cycle.