Switch read paths to the true-instant date contract (ticket #119)
- 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
This commit is contained in:
1 parent
cd375f18db
commit
af87765bbb
13 files changed
+1590
-301
No files matched your search
+32
-25
@@ -417,9 +417,23 @@ pub async fn create_reel_handler(
|
||||
|
||||
let span = req.span.unwrap_or(MemoriesSpan::Day);
|
||||
let max_segments = req.max_segments.unwrap_or(selector::DEFAULT_MAX_SEGMENTS);
|
||||
// Grouping zone: explicit request param → stored pref → app default
|
||||
// (ADR-0001). The legacy device-offset param is still mirrored into the
|
||||
// frozen prefs column by `capture_prefs` for rollback but no longer
|
||||
// drives selection.
|
||||
let display_zone_name = {
|
||||
let req_tz = req.display_tz.clone().filter(|s| !s.trim().is_empty());
|
||||
let stored = app_state
|
||||
.user_ai_prefs_dao
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut dao| dao.get_prefs(&opentelemetry::Context::new()).ok().flatten())
|
||||
.and_then(|p| p.display_tz);
|
||||
crate::camera_tz::display_zone_for(req_tz.as_deref().or(stored.as_deref()))
|
||||
};
|
||||
let selector = ReelSelector::Memories {
|
||||
span,
|
||||
tz_offset_minutes: req.timezone_offset_minutes.unwrap_or(0),
|
||||
display_tz: display_zone_name,
|
||||
library: req.library.clone(),
|
||||
max_segments,
|
||||
};
|
||||
@@ -965,23 +979,26 @@ pub(crate) fn secs_until_next_run_hour(now: chrono::DateTime<chrono::Local>, run
|
||||
}
|
||||
|
||||
/// Load pre-gen parameters: tries the user_ai_prefs DB row first, falls back
|
||||
/// to env vars, then to server-local defaults.
|
||||
fn load_pregen_params(app_state: &AppState) -> (i32, Option<String>, String) {
|
||||
/// to env vars, then to the app default display zone. Returns the display
|
||||
/// zone as an IANA name (ADR-0001) — `REEL_PREGEN_DISPLAY_TZ` (e.g.
|
||||
/// "America/New_York") overrides the stored pref when set. IANA zones are
|
||||
/// DST-proof, unlike the old fixed-offset env vars.
|
||||
fn load_pregen_params(app_state: &AppState) -> (String, Option<String>, String) {
|
||||
// Try DB row first
|
||||
if let Ok(mut dao) = app_state.user_ai_prefs_dao.lock() {
|
||||
let ctx = opentelemetry::Context::new();
|
||||
if let Ok(Some(prefs)) = dao.get_prefs(&ctx) {
|
||||
let tz = prefs.tz_offset_minutes.unwrap_or_else(fixed_tz_offset);
|
||||
let tz = crate::camera_tz::display_zone_for(prefs.display_tz.as_deref());
|
||||
let voice = prefs.voice;
|
||||
let library = prefs.library.unwrap_or_else(|| "all".to_string());
|
||||
return (tz, voice, library);
|
||||
}
|
||||
}
|
||||
// Fall back to env (explicit offset overrides auto-detect)
|
||||
let tz = std::env::var("REEL_PREGEN_TZ_OFFSET_MINUTES")
|
||||
// Fall back to env (explicit zone overrides auto-detect)
|
||||
let tz = std::env::var("REEL_PREGEN_DISPLAY_TZ")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or_else(fixed_tz_offset);
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.unwrap_or_else(|| crate::camera_tz::display_zone_for(None));
|
||||
let voice = std::env::var("REEL_PREGEN_VOICE").ok();
|
||||
let library = std::env::var("REEL_PREGEN_LIBRARY")
|
||||
.ok()
|
||||
@@ -989,17 +1006,6 @@ fn load_pregen_params(app_state: &AppState) -> (i32, Option<String>, String) {
|
||||
(tz, voice, library)
|
||||
}
|
||||
|
||||
/// Fixed timezone offset: reads `REEL_PREGEN_TZ_FIXED_MINUTES` (e.g. "-480"
|
||||
/// for US Eastern) when set, falling back to the system local offset. Using
|
||||
/// a fixed offset avoids DST shifts changing the pre-gen schedule halfway
|
||||
/// through the year.
|
||||
fn fixed_tz_offset() -> i32 {
|
||||
std::env::var("REEL_PREGEN_TZ_FIXED_MINUTES")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse().ok())
|
||||
.unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc())
|
||||
}
|
||||
|
||||
/// Spawn the nightly pre-generation scheduler. Runs behind `REEL_PREGEN_ENABLED`.
|
||||
pub(crate) async fn spawn_pregen_scheduler(app_state: web::Data<AppState>) {
|
||||
if std::env::var("REEL_PREGEN_ENABLED").ok() != Some("1".to_string()) {
|
||||
@@ -1044,7 +1050,7 @@ async fn run_pregen_batch(app_state: &AppState) -> anyhow::Result<()> {
|
||||
let (tz, voice, library) = load_pregen_params(app_state);
|
||||
|
||||
for span in spans {
|
||||
if let Err(e) = pregen_one(app_state, span, tz, voice.clone(), &library).await {
|
||||
if let Err(e) = pregen_one(app_state, span, &tz, voice.clone(), &library).await {
|
||||
log::error!("Pre-gen failed for span={}: {}", span, e);
|
||||
}
|
||||
}
|
||||
@@ -1056,7 +1062,7 @@ async fn run_pregen_batch(app_state: &AppState) -> anyhow::Result<()> {
|
||||
async fn pregen_one(
|
||||
app_state: &AppState,
|
||||
span: &str,
|
||||
tz: i32,
|
||||
display_tz: &str,
|
||||
voice: Option<String>,
|
||||
library: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -1069,7 +1075,7 @@ async fn pregen_one(
|
||||
|
||||
let selector = ReelSelector::Memories {
|
||||
span: memories_span,
|
||||
tz_offset_minutes: tz,
|
||||
display_tz: display_tz.to_string(),
|
||||
library: if library == "all" {
|
||||
None
|
||||
} else {
|
||||
@@ -1179,7 +1185,8 @@ async fn pregen_one(
|
||||
title,
|
||||
media_count,
|
||||
render_version: RENDER_VERSION as i32,
|
||||
tz_offset_minutes: tz,
|
||||
tz_offset_minutes: crate::camera_tz::display_zone_offset_minutes(display_tz, now)
|
||||
.unwrap_or(0) as i32,
|
||||
voice: voice.clone(),
|
||||
generated_at: now,
|
||||
},
|
||||
@@ -1346,7 +1353,7 @@ mod tests {
|
||||
fn day_selector() -> ReelSelector {
|
||||
ReelSelector::Memories {
|
||||
span: MemoriesSpan::Day,
|
||||
tz_offset_minutes: 0,
|
||||
display_tz: "America/New_York".to_string(),
|
||||
library: None,
|
||||
max_segments: 24,
|
||||
}
|
||||
@@ -1379,7 +1386,7 @@ mod tests {
|
||||
// Span matters.
|
||||
let week = ReelSelector::Memories {
|
||||
span: MemoriesSpan::Week,
|
||||
tz_offset_minutes: 0,
|
||||
display_tz: "America/New_York".to_string(),
|
||||
library: None,
|
||||
max_segments: 24,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user