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
+44
View File
@@ -26,6 +26,13 @@ pub struct RetentionUserSettings {
pub auto_delete_days: u32,
}
/// Default time window (hours) of visible cases when the user entry
/// omits `window_hours`. Chosen so Monday morning still shows Friday's
/// unfinished work. Server-authoritative: clients never override this.
fn default_window_hours() -> u32 {
72
}
/// A single user entry from users.toml.
#[derive(Debug, Deserialize)]
pub struct User {
@@ -37,6 +44,13 @@ pub struct User {
pub whisper: WhisperUserSettings,
#[serde(default)]
pub retention: RetentionUserSettings,
/// Time window (hours) that defines which cases are visible to this
/// user's clients. Server-authoritative: the `/api/oneliners` scan
/// filters recordings older than `now - window_hours` before they
/// reach any client. Clients render what the server returns and
/// apply no additional time filter.
#[serde(default = "default_window_hours")]
pub window_hours: u32,
}
/// Wrapper for deserializing the [[user]] array from TOML.
@@ -275,6 +289,7 @@ mod tests {
role: "doctor".into(),
whisper: WhisperUserSettings::default(),
retention: RetentionUserSettings::default(),
window_hours: default_window_hours(),
}
}
@@ -402,4 +417,33 @@ mod tests {
let err = validate_and_index_users(&users).unwrap_err();
assert!(err.contains("duplicate slug"), "got: {err}");
}
#[test]
fn parses_user_without_window_hours_defaults_to_72() {
let toml_str = r#"
[[user]]
slug = "a"
api_key = "k1"
web_password = ""
role = "doctor"
"#;
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
// Missing field falls back to the serde default so existing
// users.toml files continue to work unchanged.
assert_eq!(parsed.user[0].window_hours, 72);
}
#[test]
fn parses_user_with_explicit_window_hours() {
let toml_str = r#"
[[user]]
slug = "a"
api_key = "k1"
web_password = ""
role = "doctor"
window_hours = 168
"#;
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
assert_eq!(parsed.user[0].window_hours, 168);
}
}