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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user