Add new config fields for oneliner polling
Introduces `oneliner_poll_interval_seconds` and `oneliner_window_hours` to the client configuration. These fields allow users to customize how frequently the client polls for new oneliners and the time window for the data fetched from the server. Default values are provided for these fields to ensure backward compatibility and a sensible out-of-the-box experience. The `Config` struct now also includes helper methods `poll_interval()` and `window_hours()` for easier access to these settings, including the application of defaults. Additionally, new functions `cases_dir()` and `snapshot_cache_path()` are added to `client-desktop/src/paths.rs` to manage directories for case data and the oneliner snapshot cache, respectively. These paths follow OS-standard conventions for local application data.
This commit is contained in:
@@ -6,17 +6,20 @@ description = "Desktop dictation client for the doctate medical dictation system
|
||||
|
||||
[dependencies]
|
||||
doctate-common = { path = "../doctate-common" }
|
||||
doctate-client-core = { path = "../doctate-client-core" }
|
||||
eframe = "0.28"
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uuid = { workspace = true }
|
||||
directories = "5"
|
||||
thiserror = "1"
|
||||
webbrowser = "1"
|
||||
which = "6"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -6,6 +7,10 @@ use thiserror::Error;
|
||||
|
||||
const CONFIG_FILENAME: &str = "client.toml";
|
||||
|
||||
/// Fallback values used when the TOML file omits the field.
|
||||
const DEFAULT_POLL_INTERVAL_SECONDS: u64 = 5;
|
||||
const DEFAULT_ONELINER_WINDOW_HOURS: u32 = 72;
|
||||
|
||||
/// Client configuration loaded from a TOML file at OS-standard paths.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
@@ -13,6 +18,14 @@ pub struct Config {
|
||||
pub server_url: String,
|
||||
/// Per-user API key used in the `X-API-Key` header on upload.
|
||||
pub api_key: String,
|
||||
/// Polling cadence for `/api/oneliners`. Omit in TOML to use the
|
||||
/// built-in default (5 s).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub oneliner_poll_interval_seconds: Option<u64>,
|
||||
/// Time window handed to the server as `?hours=N`. Omit in TOML to
|
||||
/// use the built-in default (72 h = 3 days).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub oneliner_window_hours: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -92,6 +105,21 @@ impl Config {
|
||||
pub fn save(&self) -> Result<(), ConfigError> {
|
||||
self.save_to(&default_config_path()?)
|
||||
}
|
||||
|
||||
/// Polling cadence as a [`Duration`], substituting the default if
|
||||
/// the field was omitted.
|
||||
pub fn poll_interval(&self) -> Duration {
|
||||
Duration::from_secs(
|
||||
self.oneliner_poll_interval_seconds
|
||||
.unwrap_or(DEFAULT_POLL_INTERVAL_SECONDS),
|
||||
)
|
||||
}
|
||||
|
||||
/// Window-hours parameter for `/api/oneliners`, with default.
|
||||
pub fn window_hours(&self) -> u32 {
|
||||
self.oneliner_window_hours
|
||||
.unwrap_or(DEFAULT_ONELINER_WINDOW_HOURS)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -103,6 +131,8 @@ mod tests {
|
||||
Config {
|
||||
server_url: "https://doctate.example.org".into(),
|
||||
api_key: "sk-test-12345".into(),
|
||||
oneliner_poll_interval_seconds: None,
|
||||
oneliner_window_hours: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,4 +171,46 @@ mod tests {
|
||||
let err = Config::load_from(&path).unwrap_err();
|
||||
assert!(matches!(err, ConfigError::Parse { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_when_fields_omitted() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("minimal.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"server_url = \"http://x\"\napi_key = \"y\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let cfg = Config::load_from(&path).unwrap().unwrap();
|
||||
assert_eq!(cfg.poll_interval(), Duration::from_secs(5));
|
||||
assert_eq!(cfg.window_hours(), 72);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_values_override_defaults() {
|
||||
let cfg = Config {
|
||||
server_url: "http://x".into(),
|
||||
api_key: "y".into(),
|
||||
oneliner_poll_interval_seconds: Some(10),
|
||||
oneliner_window_hours: Some(24),
|
||||
};
|
||||
assert_eq!(cfg.poll_interval(), Duration::from_secs(10));
|
||||
assert_eq!(cfg.window_hours(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_preserves_new_fields() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("client.toml");
|
||||
let original = Config {
|
||||
server_url: "http://x".into(),
|
||||
api_key: "y".into(),
|
||||
oneliner_poll_interval_seconds: Some(3),
|
||||
oneliner_window_hours: Some(168),
|
||||
};
|
||||
original.save_to(&path).unwrap();
|
||||
let loaded = Config::load_from(&path).unwrap().unwrap();
|
||||
assert_eq!(loaded.oneliner_poll_interval_seconds, Some(3));
|
||||
assert_eq!(loaded.oneliner_window_hours, Some(168));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,20 @@ pub fn pending_dir() -> Result<PathBuf, PathError> {
|
||||
.map(|dirs| dirs.data_local_dir().join("pending"))
|
||||
.ok_or(PathError::NoDataDir)
|
||||
}
|
||||
|
||||
/// Directory holding one JSON marker per known case (locally created
|
||||
/// and/or server-synced). Persists across app restarts.
|
||||
/// Linux: `~/.local/share/doctate/cases/`
|
||||
pub fn cases_dir() -> Result<PathBuf, PathError> {
|
||||
ProjectDirs::from("", "", "doctate")
|
||||
.map(|dirs| dirs.data_local_dir().join("cases"))
|
||||
.ok_or(PathError::NoDataDir)
|
||||
}
|
||||
|
||||
/// File path for the last-successful `/api/oneliners` response. Feeds
|
||||
/// the UI at cold start before the first live poll completes.
|
||||
pub fn snapshot_cache_path() -> Result<PathBuf, PathError> {
|
||||
ProjectDirs::from("", "", "doctate")
|
||||
.map(|dirs| dirs.data_local_dir().join("oneliners_cache.json"))
|
||||
.ok_or(PathError::NoDataDir)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user