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:
2026-04-18 10:09:00 +02:00
parent 9b9d68402f
commit 19d841e644
3 changed files with 92 additions and 0 deletions
+3
View File
@@ -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]
+72
View File
@@ -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));
}
}
+17
View File
@@ -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)
}