feat(dates): container video dates, filesystem branch, manual edit (#115)

The exiftool branch of the date waterfall now pulls raw strings with
group prefixes (one `exiftool -j -q -G1` subprocess per batch, no
`-d %s`) and classifies each candidate:

- zoned strings (own offset / Z) are true instants;
- naive strings from container groups (QuickTime/Track*/Apple) are
  UTC-rendered creation_time fields -> true instants (utc_direct).
  Running them through the camera wall-clock cascade was the
  "videos off by 4-5 hours" bug: it reinterpreted a UTC instant in
  the server zone;
- naive strings from photo-EXIF groups are camera wall clocks and run
  the offset cascade (OffsetTime* pairing + GPS evidence pulled in
  the same exiftool pass).

The legacy date_taken column stays byte-identical to the old
server-local interpretation (reproduced in Rust, DST-aware); the
new-contract date_utc / camera_offset_minutes / date_taken_tz_source
columns carry the true instant.

Manual date edit (POST /image/exif/date) reinterprets the
naive-as-UTC epoch the client sent as a wall clock in the user's
display zone (camera_tz::manual_date_resolution, tz_source =
'manual'), snapshots the pre-override new-contract triple next to the
existing legacy snapshot pair (3 new columns, migration
2026-09-25-000000), and revert restores both pairs.

ADR-0001 records the container-date finding; CLAUDE.md date-pipeline
section updated.
This commit is contained in:
Cameron Cordes committed 2026-09-25 15:26:26 -04:00
1 parent 6f020f4201
commit d2263be6ac
13 files changed
+866 -178

No files matched your search

+10
View File
@@ -1519,6 +1519,9 @@ mod tests {
date_utc: None,
camera_offset_minutes: None,
date_taken_tz_source: None,
original_date_utc: None,
original_camera_offset_minutes: None,
original_date_taken_tz_source: None,
}
}
@@ -1563,6 +1566,9 @@ mod tests {
date_utc: data.date_utc,
camera_offset_minutes: data.camera_offset_minutes,
date_taken_tz_source: data.date_taken_tz_source.clone(),
original_date_utc: data.date_utc,
original_camera_offset_minutes: data.camera_offset_minutes,
original_date_taken_tz_source: data.date_taken_tz_source.clone(),
})
}
@@ -1614,6 +1620,9 @@ mod tests {
date_utc: data.date_utc,
camera_offset_minutes: data.camera_offset_minutes,
date_taken_tz_source: data.date_taken_tz_source.clone(),
original_date_utc: data.date_utc,
original_camera_offset_minutes: data.camera_offset_minutes,
original_date_taken_tz_source: data.date_taken_tz_source.clone(),
})
}
@@ -1751,6 +1760,7 @@ mod tests {
library_id: i32,
rel_path: &str,
date_taken: i64,
_display_tz: &str,
) -> Result<crate::database::models::ImageExif, DbError> {
// Mock — files.rs tests don't exercise the date-override endpoints.
// Returning a synthetic row keeps the trait satisfied without