Dates: serve the display wall clock (wall_clock) to clients
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>
This commit is contained in:
1 parent
f00a309a24
commit
7231383fa6
8 files changed
+259
-93
No files matched your search
@@ -184,6 +184,65 @@ pub fn row_display_day(
|
||||
)
|
||||
}
|
||||
|
||||
/// Wire format for a wall clock: naive ISO-8601, no zone designator
|
||||
/// (`2018-09-29T14:00:00`). Clients render it verbatim — no zone math —
|
||||
/// so the UI shows exactly the wall [`row_display_wall`] computed and
|
||||
/// insight prompts use (ADR-0001: consumers never re-anchor).
|
||||
pub fn format_wall_clock(wall: NaiveDateTime) -> String {
|
||||
wall.format("%Y-%m-%dT%H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
/// The owner's display zone for a request: prefs `display_tz`, else the
|
||||
/// app default (never fails — an unreadable prefs row is the default).
|
||||
pub fn owner_display_zone(
|
||||
prefs_dao: &std::sync::Mutex<Box<dyn crate::database::UserAiPrefsDao>>,
|
||||
context: &opentelemetry::Context,
|
||||
) -> Tz {
|
||||
use std::str::FromStr;
|
||||
let stored = prefs_dao
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut dao| dao.get_prefs(context).ok().flatten())
|
||||
.and_then(|p| p.display_tz);
|
||||
Tz::from_str(&camera_tz::display_zone_for(stored.as_deref()))
|
||||
.unwrap_or(chrono_tz::Tz::America__New_York)
|
||||
}
|
||||
|
||||
/// [`row_display_wall`] in wire format.
|
||||
pub fn row_wall_clock(
|
||||
row: &ImageExif,
|
||||
display_zone: Tz,
|
||||
server_zone: Option<Tz>,
|
||||
) -> Option<String> {
|
||||
row_display_wall(row, display_zone, server_zone).map(format_wall_clock)
|
||||
}
|
||||
|
||||
/// Wall clock for a file that may have no `image_exif` row yet (not yet
|
||||
/// ingested, or excluded): the row's wall when it has one, else the
|
||||
/// filename-class date (a wall pattern *is* the wall; a unix-timestamp
|
||||
/// pattern renders in the display zone), else the file's earliest
|
||||
/// filesystem time rendered in the display zone. The same precedence the
|
||||
/// ingest waterfall applies, so the value doesn't jump once the row lands.
|
||||
pub fn file_wall_clock(
|
||||
row: Option<&ImageExif>,
|
||||
rel_path: &str,
|
||||
fs_earliest: Option<i64>,
|
||||
display_zone: Tz,
|
||||
server_zone: Option<Tz>,
|
||||
) -> Option<String> {
|
||||
if let Some(wall) = row.and_then(|r| row_display_wall(r, display_zone, server_zone)) {
|
||||
return Some(format_wall_clock(wall));
|
||||
}
|
||||
let wall = match crate::memories::extract_date_from_filename(rel_path) {
|
||||
Some(crate::memories::FilenameDate::Wall(wall)) => Some(wall),
|
||||
Some(crate::memories::FilenameDate::Utc(dt)) => {
|
||||
render_in_zone_opt(dt.timestamp(), display_zone)
|
||||
}
|
||||
None => fs_earliest.and_then(|t| render_in_zone_opt(t, display_zone)),
|
||||
};
|
||||
wall.map(format_wall_clock)
|
||||
}
|
||||
|
||||
/// Render a unix instant as a naive wall in `zone`.
|
||||
fn render_in_zone_opt(instant: i64, zone: Tz) -> Option<NaiveDateTime> {
|
||||
zone.timestamp_opt(instant, 0)
|
||||
@@ -564,4 +623,102 @@ mod tests {
|
||||
);
|
||||
assert_eq!(row_true_instant(&fs, tokyo, Some(ny())), Some(instant));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wall_clock_is_the_camera_wall_for_a_travel_zone_photo() {
|
||||
// 14:00 in Tokyo (+09:00) = 05:00Z. The owner is in New York, but
|
||||
// the wall clock is the camera's: 14:00, not 01:00.
|
||||
let r = row(
|
||||
"otd.jpg",
|
||||
Some(utc_epoch(2018, 9, 29, 14, 0)),
|
||||
Some("exif"),
|
||||
Some(utc_epoch(2018, 9, 29, 5, 0)),
|
||||
Some(540),
|
||||
);
|
||||
assert_eq!(
|
||||
row_wall_clock(&r, ny(), Some(ny())),
|
||||
Some("2018-09-29T14:00:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wall_clock_without_offset_renders_the_instant_in_the_display_zone() {
|
||||
// Container date (utc_direct): 12:30Z is 08:30 in New York (EDT).
|
||||
let r = row(
|
||||
"summit.mp4",
|
||||
None,
|
||||
Some("exiftool"),
|
||||
Some(utc_epoch(2022, 8, 20, 12, 30)),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
row_wall_clock(&r, ny(), Some(ny())),
|
||||
Some("2022-08-20T08:30:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wall_clock_serves_unconverted_rows_by_their_legacy_convention() {
|
||||
// The drain's permanent floor never gets date_utc; an `exif` legacy
|
||||
// value is the wall stored as UTC and must come back unshifted.
|
||||
let r = row(
|
||||
"edited.jpg",
|
||||
Some(utc_epoch(2021, 5, 12, 10, 30)),
|
||||
Some("exif"),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
row_wall_clock(&r, ny(), Some(ny())),
|
||||
Some("2021-05-12T10:30:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_wall_clock_prefers_the_row() {
|
||||
let r = row(
|
||||
"Screenshots/Screenshot_20220315-101500.png",
|
||||
Some(utc_epoch(2022, 3, 15, 10, 15)),
|
||||
Some("filename"),
|
||||
Some(utc_epoch(2022, 3, 15, 14, 15)),
|
||||
Some(-240),
|
||||
);
|
||||
assert_eq!(
|
||||
file_wall_clock(Some(&r), &r.file_path, Some(0), ny(), Some(ny())),
|
||||
Some("2022-03-15T10:15:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_wall_clock_without_a_row_uses_the_filename_wall() {
|
||||
assert_eq!(
|
||||
file_wall_clock(
|
||||
None,
|
||||
"Screenshots/Screenshot_20220315-101500.png",
|
||||
Some(0),
|
||||
ny(),
|
||||
Some(ny())
|
||||
),
|
||||
Some("2022-03-15T10:15:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_wall_clock_without_a_row_or_filename_date_uses_fs_time_in_display_zone() {
|
||||
// 2015-03-01 17:00Z = 12:00 in New York (EST).
|
||||
assert_eq!(
|
||||
file_wall_clock(
|
||||
None,
|
||||
"Undated/scan_01.jpg",
|
||||
Some(utc_epoch(2015, 3, 1, 17, 0)),
|
||||
ny(),
|
||||
Some(ny())
|
||||
),
|
||||
Some("2015-03-01T12:00:00".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
file_wall_clock(None, "Undated/scan_01.jpg", None, ny(), Some(ny())),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user