feat: server-authoritative case window via users.toml

The /api/oneliners time window is now read per-user from users.toml
(window_hours, default 72h). Clients no longer carry a window:
client.toml oneliner_window_hours, SyncConfig.window_hours, the
?hours=N query param, and the render_case_list cutoff filter are
gone. ETag suffix keeps the effective hours so an admin edit to
users.toml invalidates client caches on the next request.
OnelinersResponse.window_hours stays in the wire format, but now
exists solely to anchor client reconciliation.
This commit is contained in:
2026-04-21 16:48:01 +02:00
parent c8186c9f88
commit 3218fc62bd
24 changed files with 167 additions and 132 deletions
+4 -13
View File
@@ -19,9 +19,7 @@ use doctate_client_core::{
pick_footer_status, run_startup_cleanup, write_sidecar,
};
use doctate_common::oneliners::OnelinerState;
use doctate_common::timestamp::{
extract_hhmm, now_rfc3339, parse_rfc3339, recorded_at_to_filename_stem,
};
use doctate_common::timestamp::{extract_hhmm, now_rfc3339, recorded_at_to_filename_stem};
use eframe::egui;
use tokio::runtime::Runtime;
use tokio::sync::{mpsc, watch};
@@ -158,7 +156,6 @@ impl DoctateApp {
.config
.as_ref()
.and_then(|c| c.oneliner_poll_interval_seconds),
oneliner_window_hours: self.config.as_ref().and_then(|c| c.oneliner_window_hours),
};
if let Err(e) = crate::config::save(&cfg) {
error!(error = %e, "config save failed");
@@ -474,13 +471,9 @@ impl DoctateApp {
fn render_case_list(&self, ui: &mut egui::Ui) -> Option<UiAction> {
let snapshot = self.snapshot_rx.borrow().clone();
let window = self.config.as_ref().map(Config::window_hours).unwrap_or(72);
let cutoff = time::OffsetDateTime::now_utc() - time::Duration::hours(window as i64);
let visible: Vec<&CaseMarker> = snapshot
.iter()
.filter(|m| parse_rfc3339(&m.last_activity_at).is_none_or(|t| t >= cutoff))
.collect();
// Snapshot is already server-filtered — the server's `window_hours`
// per user (users.toml) is the single source of truth.
let visible: Vec<&CaseMarker> = snapshot.iter().collect();
if visible.is_empty() {
ui.add_space(8.0);
@@ -634,12 +627,10 @@ fn spawn_worker(
) {
let _guard = runtime.enter();
let poll_interval = config.poll_interval();
let window_hours = config.window_hours();
let sync_config = SyncConfig {
server_url: config.server_url,
api_key: config.api_key,
poll_interval,
window_hours,
};
let (sync, events_rx) =
ServerSync::spawn(http, sync_config, case_store, snapshot_cache, pending_dir);