diff --git a/Cargo.lock b/Cargo.lock index 5951cde..db4a005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,25 +1145,6 @@ dependencies = [ "libloading 0.8.9", ] -[[package]] -name = "doctate-client-core" -version = "0.1.0" -dependencies = [ - "doctate-common", - "filetime", - "reqwest", - "serde", - "serde_json", - "tempfile", - "thiserror 1.0.69", - "time", - "tokio", - "toml", - "tracing", - "uuid", - "wiremock", -] - [[package]] name = "doctate-common" version = "0.1.0" @@ -1179,9 +1160,9 @@ name = "doctate-desktop" version = "0.1.0" dependencies = [ "directories", - "doctate-client-core", "doctate-common", "eframe", + "filetime", "libc", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index 75726b5..6c69e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["server", "doctate-common", "doctate-client-core", "client-desktop"] +members = ["server", "doctate-common", "client-desktop"] resolver = "3" [workspace.package] diff --git a/client-desktop/Cargo.toml b/client-desktop/Cargo.toml index 253a731..1cf9ea8 100644 --- a/client-desktop/Cargo.toml +++ b/client-desktop/Cargo.toml @@ -4,9 +4,12 @@ version.workspace = true edition.workspace = true description = "Desktop dictation client for the doctate medical dictation system" +[[bin]] +name = "doctate" +path = "src/main.rs" + [dependencies] doctate-common = { path = "../doctate-common" } -doctate-client-core = { path = "../doctate-client-core" } eframe = "0.28" reqwest = { workspace = true } serde = { workspace = true } @@ -29,3 +32,4 @@ libc = "0.2" tempfile = "3" tokio = { workspace = true, features = ["test-util"] } wiremock = "0.6" +filetime = "0.2" diff --git a/client-desktop/src/app.rs b/client-desktop/src/app.rs index 36a9fe2..f88d2cf 100644 --- a/client-desktop/src/app.rs +++ b/client-desktop/src/app.rs @@ -13,12 +13,13 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant}; -use doctate_client_core::{ - CaseMarker, CaseStore, DEFAULT_MARKER_RETENTION, DEFAULT_ORPHAN_AUDIO_RETENTION, FooterStatus, - PendingUpload, PutOnelinerError, ServerSync, SnapshotCache, SyncConfig, UploadEvent, - WorkerSnapshot, WorkerState, pick_footer_status, put_oneliner, run_startup_cleanup, - write_sidecar, -}; +use crate::case_store::{CaseMarker, CaseStore}; +use crate::case_update::{PutOnelinerError, put_oneliner}; +use crate::footer_status::{FooterStatus, pick_footer_status}; +use crate::server_sync::{ServerSync, SyncConfig, WorkerSnapshot, WorkerState}; +use crate::snapshot_cache::SnapshotCache; +use crate::startup::{DEFAULT_MARKER_RETENTION, DEFAULT_ORPHAN_AUDIO_RETENTION, run_startup_cleanup}; +use crate::upload::{PendingUpload, UploadEvent, write_sidecar}; use doctate_common::join_url; use doctate_common::oneliners::OnelinerState; use doctate_common::timestamp::{extract_hhmm, now_rfc3339, recorded_at_to_filename_stem}; @@ -118,7 +119,7 @@ impl DoctateApp { cases_dir: PathBuf, snapshot_cache_path: PathBuf, ) -> Self { - let config = match crate::config::load() { + let config = match crate::config_path::load() { Ok(Some(c)) => Some(c), Ok(None) => None, Err(e) => { @@ -207,7 +208,7 @@ impl DoctateApp { .as_ref() .and_then(|c| c.oneliner_poll_interval_seconds), }; - if let Err(e) = crate::config::save(&cfg) { + if let Err(e) = crate::config_path::save(&cfg) { error!(error = %e, "config save failed"); self.state = AppState::Error { message: format!("Speichern: {e}"), diff --git a/doctate-client-core/src/case_store.rs b/client-desktop/src/case_store.rs similarity index 100% rename from doctate-client-core/src/case_store.rs rename to client-desktop/src/case_store.rs diff --git a/doctate-client-core/src/case_update.rs b/client-desktop/src/case_update.rs similarity index 100% rename from doctate-client-core/src/case_update.rs rename to client-desktop/src/case_update.rs diff --git a/client-desktop/src/config.rs b/client-desktop/src/config.rs index af9762a..58012e1 100644 --- a/client-desktop/src/config.rs +++ b/client-desktop/src/config.rs @@ -1,47 +1,180 @@ -//! Desktop-side config helpers. The `Config` struct itself lives in -//! `doctate-client-core` — this module only provides the per-platform -//! default path via the `directories` crate, plus two convenience -//! functions for load/save against that default path. +//! Shared client configuration: server URL, API key, polling cadence, +//! oneliner window. Every native client (desktop, watch, phone) reads +//! the same schema — only the on-disk path differs per platform. +//! +//! Path resolution is deliberately **not** part of this module. Callers +//! provide a concrete `&Path` to `load_from` / `save_to`; the +//! per-platform path helper lives in each client crate. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::time::Duration; -use directories::ProjectDirs; -pub use doctate_client_core::config::{Config, ConfigError}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; -const CONFIG_FILENAME: &str = "client.toml"; +/// Fallback polling cadence for `/api/oneliners` in seconds. Chosen to +/// feel real-time without hammering the server. +pub const DEFAULT_POLL_INTERVAL_SECONDS: u64 = 5; -/// Default config path, following OS conventions: -/// Linux: `~/.config/doctate/client.toml` -/// Windows: `%APPDATA%\doctate\client.toml` -/// macOS: `~/Library/Application Support/doctate/client.toml` -pub fn default_config_path() -> Result { - ProjectDirs::from("", "", "doctate") - .map(|dirs| dirs.config_dir().join(CONFIG_FILENAME)) - .ok_or(ConfigError::NoConfigDir) +/// Client configuration loaded from a TOML file at a platform-specific +/// path. All fields except the two durations are mandatory. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Config { + /// Base URL of the doctate server, e.g. `https://doctate.example.org`. + pub server_url: String, + /// Per-user API key used in the `X-API-Key` header. + pub api_key: String, + /// Polling cadence for `/api/oneliners`. Omit in TOML to use the + /// built-in default. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oneliner_poll_interval_seconds: Option, } -/// Convenience: load from `default_config_path()`. -pub fn load() -> Result, ConfigError> { - Config::load_from(&default_config_path()?) +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("cannot determine OS config directory (is $HOME set?)")] + NoConfigDir, + #[error("I/O error at {path}: {source}")] + Io { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("parse error at {path}: {source}")] + Parse { + path: PathBuf, + #[source] + source: toml::de::Error, + }, + #[error("serialize error: {0}")] + Serialize(#[from] toml::ser::Error), } -/// Convenience: save to `default_config_path()`. -pub fn save(cfg: &Config) -> Result<(), ConfigError> { - cfg.save_to(&default_config_path()?) +impl Config { + /// Load from `path`. Returns `Ok(None)` if the file does not exist + /// (first-run case); `Ok(Some(_))` on success; `Err` on I/O or parse + /// errors — caller should surface these to the user, not swallow. + pub fn load_from(path: &Path) -> Result, ConfigError> { + let text = match std::fs::read_to_string(path) { + Ok(t) => t, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => { + return Err(ConfigError::Io { + path: path.into(), + source: e, + }); + } + }; + let cfg: Config = toml::from_str(&text).map_err(|e| ConfigError::Parse { + path: path.into(), + source: e, + })?; + Ok(Some(cfg)) + } + + /// Save to `path`, creating parent directories as needed. + pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| ConfigError::Io { + path: parent.into(), + source: e, + })?; + } + let text = toml::to_string_pretty(self)?; + std::fs::write(path, text).map_err(|e| ConfigError::Io { + path: path.into(), + source: e, + })?; + Ok(()) + } + + /// 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), + ) + } } #[cfg(test)] mod tests { use super::*; + use tempfile::TempDir; + + fn sample() -> Config { + Config { + server_url: "https://doctate.example.org".into(), + api_key: "sk-test-12345".into(), + oneliner_poll_interval_seconds: None, + } + } #[test] - fn default_path_ends_with_client_toml() { - // Sanity check on the ProjectDirs wiring — can't assert the - // exact path (varies per host), but the filename is deterministic. - let path = default_config_path().expect("config path available in test env"); - assert_eq!( - path.file_name().and_then(|s| s.to_str()), - Some("client.toml") - ); + fn roundtrip_through_file() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("client.toml"); + let original = sample(); + original.save_to(&path).unwrap(); + let loaded = Config::load_from(&path).unwrap(); + assert_eq!(loaded, Some(original)); + } + + #[test] + fn load_missing_file_returns_none() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("nonexistent.toml"); + assert!(Config::load_from(&path).unwrap().is_none()); + } + + #[test] + fn save_creates_parent_dirs() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("deep/nested/client.toml"); + sample().save_to(&path).unwrap(); + assert!(path.exists()); + } + + #[test] + fn load_invalid_toml_returns_parse_error() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("bad.toml"); + std::fs::write(&path, "this is not [valid toml").unwrap(); + 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)); + } + + #[test] + fn explicit_values_override_defaults() { + let cfg = Config { + server_url: "http://x".into(), + api_key: "y".into(), + oneliner_poll_interval_seconds: Some(10), + }; + assert_eq!(cfg.poll_interval(), Duration::from_secs(10)); + } + + #[test] + fn roundtrip_preserves_optional_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), + }; + original.save_to(&path).unwrap(); + let loaded = Config::load_from(&path).unwrap().unwrap(); + assert_eq!(loaded.oneliner_poll_interval_seconds, Some(3)); } } diff --git a/client-desktop/src/config_path.rs b/client-desktop/src/config_path.rs new file mode 100644 index 0000000..48649c7 --- /dev/null +++ b/client-desktop/src/config_path.rs @@ -0,0 +1,48 @@ +//! Desktop-side config helpers. The `Config` struct itself lives in +//! the sibling `config` module (TOML schema, platform-agnostic). This +//! module only provides the per-platform default path via the +//! `directories` crate, plus two convenience functions for load/save +//! against that default path. + +use std::path::PathBuf; + +use directories::ProjectDirs; +pub use crate::config::{Config, ConfigError}; + +const CONFIG_FILENAME: &str = "client.toml"; + +/// Default config path, following OS conventions: +/// Linux: `~/.config/doctate/client.toml` +/// Windows: `%APPDATA%\doctate\client.toml` +/// macOS: `~/Library/Application Support/doctate/client.toml` +pub fn default_config_path() -> Result { + ProjectDirs::from("", "", "doctate") + .map(|dirs| dirs.config_dir().join(CONFIG_FILENAME)) + .ok_or(ConfigError::NoConfigDir) +} + +/// Convenience: load from `default_config_path()`. +pub fn load() -> Result, ConfigError> { + Config::load_from(&default_config_path()?) +} + +/// Convenience: save to `default_config_path()`. +pub fn save(cfg: &Config) -> Result<(), ConfigError> { + cfg.save_to(&default_config_path()?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_path_ends_with_client_toml() { + // Sanity check on the ProjectDirs wiring — can't assert the + // exact path (varies per host), but the filename is deterministic. + let path = default_config_path().expect("config path available in test env"); + assert_eq!( + path.file_name().and_then(|s| s.to_str()), + Some("client.toml") + ); + } +} diff --git a/doctate-client-core/src/footer_status.rs b/client-desktop/src/footer_status.rs similarity index 100% rename from doctate-client-core/src/footer_status.rs rename to client-desktop/src/footer_status.rs diff --git a/client-desktop/src/lib.rs b/client-desktop/src/lib.rs new file mode 100644 index 0000000..d73be6b --- /dev/null +++ b/client-desktop/src/lib.rs @@ -0,0 +1,21 @@ +//! Desktop dictation client — Linux + Windows. +//! +//! Modules listed here are intentionally `pub` so that the Wear-OS +//! mirror (Kotlin) can reference canonical Rust paths via doc anchors +//! like `doctate-desktop::pending_cleanup::cleanup_orphan_audio`. + +pub mod app; +pub mod case_store; +pub mod case_update; +pub mod config; +pub mod config_path; +pub mod footer_status; +pub mod magic_link; +pub mod paths; +pub mod pending_cleanup; +pub mod recorder; +pub mod server_sync; +pub mod snapshot_cache; +pub mod startup; +pub mod state; +pub mod upload; diff --git a/client-desktop/src/main.rs b/client-desktop/src/main.rs index 6701a2a..a96704e 100644 --- a/client-desktop/src/main.rs +++ b/client-desktop/src/main.rs @@ -1,10 +1,3 @@ -mod app; -mod config; -mod magic_link; -mod paths; -mod recorder; -mod state; - use std::sync::Arc; use eframe::egui; @@ -13,8 +6,8 @@ use tracing_subscriber::EnvFilter; use std::fs::{File, OpenOptions, TryLockError}; -use crate::app::DoctateApp; -use crate::paths::{cases_dir, lock_file_path, pending_dir, snapshot_cache_path}; +use doctate_desktop::app::DoctateApp; +use doctate_desktop::paths::{cases_dir, lock_file_path, pending_dir, snapshot_cache_path}; /// Unwrap a startup `Result` or print `context: {err}` to stderr and /// `exit(1)`. Used for unrecoverable bootstrap failures before the diff --git a/doctate-client-core/src/pending_cleanup.rs b/client-desktop/src/pending_cleanup.rs similarity index 100% rename from doctate-client-core/src/pending_cleanup.rs rename to client-desktop/src/pending_cleanup.rs diff --git a/doctate-client-core/src/server_sync.rs b/client-desktop/src/server_sync.rs similarity index 100% rename from doctate-client-core/src/server_sync.rs rename to client-desktop/src/server_sync.rs diff --git a/doctate-client-core/src/snapshot_cache.rs b/client-desktop/src/snapshot_cache.rs similarity index 100% rename from doctate-client-core/src/snapshot_cache.rs rename to client-desktop/src/snapshot_cache.rs diff --git a/doctate-client-core/src/startup.rs b/client-desktop/src/startup.rs similarity index 100% rename from doctate-client-core/src/startup.rs rename to client-desktop/src/startup.rs diff --git a/doctate-client-core/src/upload.rs b/client-desktop/src/upload.rs similarity index 100% rename from doctate-client-core/src/upload.rs rename to client-desktop/src/upload.rs diff --git a/doctate-client-core/Cargo.toml b/doctate-client-core/Cargo.toml deleted file mode 100644 index 5c20244..0000000 --- a/doctate-client-core/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "doctate-client-core" -version.workspace = true -edition.workspace = true -description = "Shared client logic for the doctate medical dictation system: case store, server sync worker, config, snapshot cache" - -[dependencies] -doctate-common = { path = "../doctate-common" } -tokio = { workspace = true } -reqwest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -toml = { workspace = true } -uuid = { workspace = true } -time = { workspace = true } -tracing = { workspace = true } -thiserror = "1" - -[dev-dependencies] -tempfile = "3" -tokio = { workspace = true, features = ["test-util"] } -wiremock = "0.6" -filetime = "0.2" diff --git a/doctate-client-core/src/config.rs b/doctate-client-core/src/config.rs deleted file mode 100644 index 58012e1..0000000 --- a/doctate-client-core/src/config.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Shared client configuration: server URL, API key, polling cadence, -//! oneliner window. Every native client (desktop, watch, phone) reads -//! the same schema — only the on-disk path differs per platform. -//! -//! Path resolution is deliberately **not** part of this module. Callers -//! provide a concrete `&Path` to `load_from` / `save_to`; the -//! per-platform path helper lives in each client crate. - -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -/// Fallback polling cadence for `/api/oneliners` in seconds. Chosen to -/// feel real-time without hammering the server. -pub const DEFAULT_POLL_INTERVAL_SECONDS: u64 = 5; - -/// Client configuration loaded from a TOML file at a platform-specific -/// path. All fields except the two durations are mandatory. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct Config { - /// Base URL of the doctate server, e.g. `https://doctate.example.org`. - pub server_url: String, - /// Per-user API key used in the `X-API-Key` header. - pub api_key: String, - /// Polling cadence for `/api/oneliners`. Omit in TOML to use the - /// built-in default. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub oneliner_poll_interval_seconds: Option, -} - -#[derive(Debug, Error)] -pub enum ConfigError { - #[error("cannot determine OS config directory (is $HOME set?)")] - NoConfigDir, - #[error("I/O error at {path}: {source}")] - Io { - path: PathBuf, - #[source] - source: std::io::Error, - }, - #[error("parse error at {path}: {source}")] - Parse { - path: PathBuf, - #[source] - source: toml::de::Error, - }, - #[error("serialize error: {0}")] - Serialize(#[from] toml::ser::Error), -} - -impl Config { - /// Load from `path`. Returns `Ok(None)` if the file does not exist - /// (first-run case); `Ok(Some(_))` on success; `Err` on I/O or parse - /// errors — caller should surface these to the user, not swallow. - pub fn load_from(path: &Path) -> Result, ConfigError> { - let text = match std::fs::read_to_string(path) { - Ok(t) => t, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(e) => { - return Err(ConfigError::Io { - path: path.into(), - source: e, - }); - } - }; - let cfg: Config = toml::from_str(&text).map_err(|e| ConfigError::Parse { - path: path.into(), - source: e, - })?; - Ok(Some(cfg)) - } - - /// Save to `path`, creating parent directories as needed. - pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|e| ConfigError::Io { - path: parent.into(), - source: e, - })?; - } - let text = toml::to_string_pretty(self)?; - std::fs::write(path, text).map_err(|e| ConfigError::Io { - path: path.into(), - source: e, - })?; - Ok(()) - } - - /// 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), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - fn sample() -> Config { - Config { - server_url: "https://doctate.example.org".into(), - api_key: "sk-test-12345".into(), - oneliner_poll_interval_seconds: None, - } - } - - #[test] - fn roundtrip_through_file() { - let tmp = TempDir::new().unwrap(); - let path = tmp.path().join("client.toml"); - let original = sample(); - original.save_to(&path).unwrap(); - let loaded = Config::load_from(&path).unwrap(); - assert_eq!(loaded, Some(original)); - } - - #[test] - fn load_missing_file_returns_none() { - let tmp = TempDir::new().unwrap(); - let path = tmp.path().join("nonexistent.toml"); - assert!(Config::load_from(&path).unwrap().is_none()); - } - - #[test] - fn save_creates_parent_dirs() { - let tmp = TempDir::new().unwrap(); - let path = tmp.path().join("deep/nested/client.toml"); - sample().save_to(&path).unwrap(); - assert!(path.exists()); - } - - #[test] - fn load_invalid_toml_returns_parse_error() { - let tmp = TempDir::new().unwrap(); - let path = tmp.path().join("bad.toml"); - std::fs::write(&path, "this is not [valid toml").unwrap(); - 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)); - } - - #[test] - fn explicit_values_override_defaults() { - let cfg = Config { - server_url: "http://x".into(), - api_key: "y".into(), - oneliner_poll_interval_seconds: Some(10), - }; - assert_eq!(cfg.poll_interval(), Duration::from_secs(10)); - } - - #[test] - fn roundtrip_preserves_optional_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), - }; - original.save_to(&path).unwrap(); - let loaded = Config::load_from(&path).unwrap().unwrap(); - assert_eq!(loaded.oneliner_poll_interval_seconds, Some(3)); - } -} diff --git a/doctate-client-core/src/lib.rs b/doctate-client-core/src/lib.rs deleted file mode 100644 index feacdbd..0000000 --- a/doctate-client-core/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Shared client logic for the doctate dictation system. -//! -//! This crate is filesystem- and network-aware (unlike `doctate-common`, -//! which is DTO-only), but path-agnostic: callers pass concrete paths so -//! the same code works on Linux desktop, Windows desktop, Android, iOS. -//! -//! Modules: -//! - [`case_store`]: per-case marker files, merge-with-server-snapshot logic -//! - [`snapshot_cache`]: last successful `/api/oneliners` response, cached -//! on disk so the UI has data at cold start -//! - [`server_sync`]: unified background worker — uploads pending -//! recordings and polls `/api/oneliners` from a single HTTP client -//! - [`upload`]: pending-queue IO primitives (sidecar write/scan) -//! - [`pending_cleanup`]: orphan reaper for the pending-upload directory - -pub mod case_store; -pub mod case_update; -pub mod config; -pub mod footer_status; -pub mod pending_cleanup; -pub mod server_sync; -pub mod snapshot_cache; -pub mod startup; -pub mod upload; - -pub use case_store::{CaseMarker, CaseStore, CaseStoreError}; -pub use case_update::{PutOnelinerError, put_oneliner}; -pub use config::{Config, ConfigError, DEFAULT_POLL_INTERVAL_SECONDS}; -pub use footer_status::{FooterStatus, pick_footer_status}; -pub use pending_cleanup::cleanup_orphan_audio; -pub use server_sync::{ServerSync, SyncConfig, WorkerSnapshot, WorkerState}; -pub use snapshot_cache::{CachedSnapshot, SnapshotCache}; -pub use startup::{DEFAULT_MARKER_RETENTION, DEFAULT_ORPHAN_AUDIO_RETENTION, run_startup_cleanup}; -pub use upload::{ - PendingUpload, UploadEvent, UploadIoError, scan_pending_dir, sidecar_path_for, write_sidecar, -};