diff --git a/server/src/config.rs b/server/src/config.rs index 1de2bd9..9327072 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -12,6 +12,20 @@ pub struct WhisperUserSettings { pub initial_prompt: Option, } +/// Per-user retention settings from users.toml. +/// +/// `auto_close_days = 0` disables the auto-close sweep (a case is never +/// closed for age alone). `auto_delete_days = 0` disables the auto-purge +/// sweep (closed cases stay forever until manual purge) — important for +/// admin / dev setups where data must survive across restarts. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct RetentionUserSettings { + #[serde(default)] + pub auto_close_days: u32, + #[serde(default)] + pub auto_delete_days: u32, +} + /// A single user entry from users.toml. #[derive(Debug, Deserialize)] pub struct User { @@ -21,6 +35,8 @@ pub struct User { pub role: String, #[serde(default)] pub whisper: WhisperUserSettings, + #[serde(default)] + pub retention: RetentionUserSettings, } /// Wrapper for deserializing the [[user]] array from TOML. @@ -258,6 +274,7 @@ mod tests { web_password: String::new(), role: "doctor".into(), whisper: WhisperUserSettings::default(), + retention: RetentionUserSettings::default(), } } @@ -277,6 +294,60 @@ mod tests { assert!(u.whisper.initial_prompt.is_none()); } + #[test] + fn parses_user_without_retention_block_defaults_to_zero() { + let toml_str = r#" + [[user]] + slug = "a" + api_key = "k1" + web_password = "" + role = "doctor" + "#; + let parsed: UsersFile = toml::from_str(toml_str).unwrap(); + let r = &parsed.user[0].retention; + // Both zero means "feature disabled", matching the pre-migration + // status quo — no case is ever auto-closed or auto-purged. + assert_eq!(r.auto_close_days, 0); + assert_eq!(r.auto_delete_days, 0); + } + + #[test] + fn parses_user_with_retention_block() { + let toml_str = r#" + [[user]] + slug = "a" + api_key = "k1" + web_password = "" + role = "doctor" + [user.retention] + auto_close_days = 7 + auto_delete_days = 30 + "#; + let parsed: UsersFile = toml::from_str(toml_str).unwrap(); + let r = &parsed.user[0].retention; + assert_eq!(r.auto_close_days, 7); + assert_eq!(r.auto_delete_days, 30); + } + + #[test] + fn parses_user_with_partial_retention_block() { + let toml_str = r#" + [[user]] + slug = "a" + api_key = "k1" + web_password = "" + role = "doctor" + [user.retention] + auto_close_days = 5 + "#; + let parsed: UsersFile = toml::from_str(toml_str).unwrap(); + let r = &parsed.user[0].retention; + assert_eq!(r.auto_close_days, 5); + // Missing field defaults to 0 = disabled — lets admins test auto-close + // in isolation without losing data to auto-purge. + assert_eq!(r.auto_delete_days, 0); + } + #[test] fn parses_user_with_full_whisper_block() { let toml_str = r#" diff --git a/server/tests/analyze_test.rs b/server/tests/analyze_test.rs index 393c793..7722541 100644 --- a/server/tests/analyze_test.rs +++ b/server/tests/analyze_test.rs @@ -31,6 +31,7 @@ fn make_user(slug: &str) -> User { web_password: bcrypt::hash("s", 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/auth_test.rs b/server/tests/auth_test.rs index e9996af..d615be3 100644 --- a/server/tests/auth_test.rs +++ b/server/tests/auth_test.rs @@ -17,6 +17,7 @@ fn test_config() -> Arc { web_password: "unused".into(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), }], api_keys: HashMap::from([("test-key-123".into(), "dr_test".into())]), ..Config::test_default() diff --git a/server/tests/case_page_test.rs b/server/tests/case_page_test.rs index 7cc4870..3e873bf 100644 --- a/server/tests/case_page_test.rs +++ b/server/tests/case_page_test.rs @@ -23,6 +23,7 @@ fn make_user(slug: &str) -> User { web_password: bcrypt::hash("s", 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/close_watermark_test.rs b/server/tests/close_watermark_test.rs index 0031edf..1df74b2 100644 --- a/server/tests/close_watermark_test.rs +++ b/server/tests/close_watermark_test.rs @@ -23,6 +23,7 @@ fn make_user(slug: &str, password_plain: &str) -> User { web_password: bcrypt::hash(password_plain, 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/delete_recording_test.rs b/server/tests/delete_recording_test.rs index e19b52b..e4427fb 100644 --- a/server/tests/delete_recording_test.rs +++ b/server/tests/delete_recording_test.rs @@ -23,6 +23,7 @@ fn make_user(slug: &str, password_plain: &str) -> User { web_password: bcrypt::hash(password_plain, 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/login_test.rs b/server/tests/login_test.rs index 635af5a..ea4527c 100644 --- a/server/tests/login_test.rs +++ b/server/tests/login_test.rs @@ -14,6 +14,7 @@ fn make_user(slug: &str, password_plain: &str) -> User { web_password: bcrypt::hash(password_plain, 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/magic_link_test.rs b/server/tests/magic_link_test.rs index ed47b2a..4b01f2b 100644 --- a/server/tests/magic_link_test.rs +++ b/server/tests/magic_link_test.rs @@ -22,6 +22,7 @@ fn make_user() -> User { web_password: bcrypt::hash("unused", 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/oneliners_api_test.rs b/server/tests/oneliners_api_test.rs index dc4898a..29dc62d 100644 --- a/server/tests/oneliners_api_test.rs +++ b/server/tests/oneliners_api_test.rs @@ -28,6 +28,7 @@ fn test_config() -> (Arc, PathBuf) { web_password: "unused".into(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), }], api_keys: HashMap::from([(TEST_KEY.into(), TEST_SLUG.into())]), ..Config::test_default() diff --git a/server/tests/sse_integration.rs b/server/tests/sse_integration.rs index 22cbc15..12c133d 100644 --- a/server/tests/sse_integration.rs +++ b/server/tests/sse_integration.rs @@ -37,6 +37,7 @@ fn make_user(slug: &str) -> User { web_password: bcrypt::hash("s", 4).unwrap(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), } } diff --git a/server/tests/transcribe_test.rs b/server/tests/transcribe_test.rs index 71e1d7a..36ec9fa 100644 --- a/server/tests/transcribe_test.rs +++ b/server/tests/transcribe_test.rs @@ -265,6 +265,7 @@ fn test_config_with_whisper(whisper_url: String) -> Arc { web_password: "unused".into(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), }], whisper_url, whisper_timeout_seconds: 5, diff --git a/server/tests/upload_test.rs b/server/tests/upload_test.rs index 4469487..a7a5520 100644 --- a/server/tests/upload_test.rs +++ b/server/tests/upload_test.rs @@ -22,6 +22,7 @@ fn test_config() -> Arc { web_password: "unused".into(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), }], api_keys: HashMap::from([("test-key-123".into(), "dr_test".into())]), ..Config::test_default() diff --git a/server/tests/web_test.rs b/server/tests/web_test.rs index fe0c935..0b443a7 100644 --- a/server/tests/web_test.rs +++ b/server/tests/web_test.rs @@ -21,6 +21,7 @@ fn test_config() -> Arc { web_password: "unused".into(), role: "doctor".into(), whisper: Default::default(), + retention: Default::default(), }], api_keys: HashMap::from([("test-key".into(), "dr_test".into())]), ..Config::test_default() diff --git a/server/users.toml.example b/server/users.toml.example index 050b83e..2062f2c 100644 --- a/server/users.toml.example +++ b/server/users.toml.example @@ -8,6 +8,13 @@ slug = "dr_mueller" api_key = "change-me-to-a-secure-key" web_password = "$2b$12$..." role = "doctor" + # Optional per-user retention. Both fields default to 0 (feature + # disabled). auto_close_days = 7 + auto_delete_days = 30 means cases + # without new recordings auto-close after 7 days and are irreversibly + # removed 30 days later. Changes require a server restart. + [user.retention] + auto_close_days = 7 + auto_delete_days = 30 [[user]] slug = "dr_schmidt"