feat(dates): true_instant + wall_clock on /photos/exif (#113)

/photos/exif is Apollo's photo-to-track matching feed, but unlike
/image/metadata and /photos/gps-summary it only carried the raw
new-contract triple. date_utc is NULL for unconverted rows and, by
design, forever for the drain's edited/exported floor (#120), so a
client reading date_utc alone would have to re-implement the
per-source legacy conventions to cover those rows: the re-anchoring
ADR-0001 says consumers delete, not migrate.

ExifSummary now carries:
- true_instant: date_view::row_true_instant (date_utc when converted,
  else the per-source fallback), for instant-to-instant matching.
- wall_clock: date_view::row_wall_clock, rendered verbatim.

The row → wire mapping moves to ExifSummary::from_row so the tests
cover the real projection, including an unconverted row falling back
to the same instant and wall as its converted twin. Additive: legacy
fields are unchanged, so current clients see the same values.

The date_from/date_to filter still runs on the legacy column; that's
#152, which now explicitly covers /photos/exif too.

Unblocks Apollo #37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-10 21:15:41 -04:00
1 parent 991622af2a
commit f1a7fb4ecc
2 files changed
+84 -31

No files matched your search

+8 -16
View File
@@ -1216,6 +1216,11 @@ pub async fn list_exif_summary(
.map(|lib| (lib.id, lib.name.clone()))
.collect();
// Resolved before taking the DAO mutex so the prefs lock isn't held
// nested inside it.
let display_zone = crate::date_view::owner_display_zone(&app_state.user_ai_prefs_dao);
let server_zone = crate::camera_tz::server_zone();
let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao");
match exif_dao_guard.query_by_exif(
library_filter,
@@ -1229,22 +1234,9 @@ pub async fn list_exif_summary(
Ok(rows) => {
let photos: Vec<ExifSummary> = rows
.into_iter()
.map(|r| ExifSummary {
library_name: library_names.get(&r.library_id).cloned(),
file_path: r.file_path,
library_id: r.library_id,
camera_model: r.camera_model,
width: r.width,
height: r.height,
// image_exif stores GPS as f32 to keep row size small;
// widen for the JSON shape so clients don't need to
// know about the on-disk precision.
gps_latitude: r.gps_latitude.map(f64::from),
gps_longitude: r.gps_longitude.map(f64::from),
date_taken: r.date_taken,
date_utc: r.date_utc,
camera_offset_minutes: r.camera_offset_minutes,
date_taken_tz_source: r.date_taken_tz_source,
.map(|r| {
let name = library_names.get(&r.library_id).cloned();
ExifSummary::from_row(r, name, display_zone, server_zone)
})
.collect();