From 24a62b6685805dd0aa82b38e677392da2d074df9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 07:20:50 +0000 Subject: [PATCH 1/7] fix: correct isToday check in DateTimePicker getMonth() returns 0-11, but the comparison string was not adjusting for this, so the "today" highlight in the calendar never matched. https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV --- apps/tauri/src/lib/components/DateTimePicker.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/tauri/src/lib/components/DateTimePicker.svelte b/apps/tauri/src/lib/components/DateTimePicker.svelte index f6415d3..92d92cf 100644 --- a/apps/tauri/src/lib/components/DateTimePicker.svelte +++ b/apps/tauri/src/lib/components/DateTimePicker.svelte @@ -18,7 +18,7 @@ let selectedMinute = $state(existing ? existing.getMinutes() : 0); let visible = $state(false); - let todayStr = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`; + let todayStr = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; let daysInMonth = $derived(new Date(viewYear, viewMonth + 1, 0).getDate()); let firstDayOfWeek = $derived(new Date(viewYear, viewMonth, 1).getDay()); @@ -53,7 +53,7 @@ } function isToday(day: number): boolean { - return `${viewYear}-${viewMonth}-${day}` === todayStr; + return `${viewYear}-${viewMonth + 1}-${day}` === todayStr; } function isSelected(day: number): boolean { From ac72955d2396c5f0141c1e5b53610b7d9930e2c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 07:21:16 +0000 Subject: [PATCH 2/7] fix: correct operator precedence in Windows path validation The condition `len <= 3 && ends_with(":\\") || ends_with(":")` was missing parentheses, causing the second ends_with check to run regardless of path length due to && binding tighter than ||. https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV --- apps/tauri/src-tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 7bdb221..6db3de8 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -87,7 +87,7 @@ fn validate_workspace_path(path: &str) -> Result<(), String> { #[cfg(windows)] { let upper = normalized.to_uppercase(); - if upper.len() <= 3 && upper.ends_with(":\\") || upper.ends_with(":") { + if upper.len() <= 3 && (upper.ends_with(":\\") || upper.ends_with(":")) { return Err(format!("Cannot use drive root as workspace: {}", path)); } } From 5c04e509568a45b39194046d95413fc61566ee8d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 07:22:45 +0000 Subject: [PATCH 3/7] refactor: remove misleading underscore prefixes from WebDavClient fields The fields _client, _base_url, _username, _password were all actively used throughout the struct's methods. The underscore prefix convention signals unused fields, which was misleading for readers. https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV --- crates/onyx-core/src/webdav.rs | 66 +++++++++++++++++----------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/crates/onyx-core/src/webdav.rs b/crates/onyx-core/src/webdav.rs index da893aa..abdfe17 100644 --- a/crates/onyx-core/src/webdav.rs +++ b/crates/onyx-core/src/webdav.rs @@ -20,10 +20,10 @@ pub struct RemoteFileInfo { /// WebDAV client wrapping reqwest with basic auth. Credentials are zeroized on drop. pub struct WebDavClient { - _client: Client, - _base_url: String, - _username: Zeroizing, - _password: Zeroizing, + client: Client, + base_url: String, + username: Zeroizing, + password: Zeroizing, } impl WebDavClient { @@ -43,17 +43,17 @@ impl WebDavClient { .build() .map_err(|e| Error::WebDav(format!("Failed to build HTTP client: {}", e)))?; Ok(Self { - _client: client, - _base_url: base_url, - _username: Zeroizing::new(username.to_string()), - _password: Zeroizing::new(password.to_string()), + client, + base_url, + username: Zeroizing::new(username.to_string()), + password: Zeroizing::new(password.to_string()), }) } fn full_url(&self, path: &str) -> String { let path = path.trim_start_matches('/'); if path.is_empty() { - self._base_url.clone() + self.base_url.clone() } else { // Percent-encode path segments while preserving '/' let encoded: String = path @@ -61,15 +61,15 @@ impl WebDavClient { .map(percent_encode) .collect::>() .join("/"); - format!("{}/{}", self._base_url, encoded) + format!("{}/{}", self.base_url, encoded) } } /// Test connection by issuing a PROPFIND depth 0 on the root. pub async fn test_connection(&self) -> Result<()> { - let resp = self._client - .request(reqwest::Method::from_bytes(b"PROPFIND").expect("PROPFIND is a valid HTTP method"), &self._base_url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + let resp = self.client + .request(reqwest::Method::from_bytes(b"PROPFIND").expect("PROPFIND is a valid HTTP method"), &self.base_url) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .header("Depth", "0") .header("Content-Type", "application/xml") .body(PROPFIND_BODY) @@ -89,9 +89,9 @@ impl WebDavClient { /// List files at a given path using PROPFIND depth 1. pub async fn list_files(&self, path: &str) -> Result> { let url = self.full_url(path); - let resp = self._client + let resp = self.client .request(reqwest::Method::from_bytes(b"PROPFIND").expect("PROPFIND is a valid HTTP method"), &url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .header("Depth", "1") .header("Content-Type", "application/xml") .body(PROPFIND_BODY) @@ -113,15 +113,15 @@ impl WebDavClient { return Err(Error::WebDav("PROPFIND response too large (>10MB)".into())); } let body = String::from_utf8_lossy(&bytes); - parse_propfind_response(&body, &self._base_url, path) + parse_propfind_response(&body, &self.base_url, path) } /// Download a file's contents. pub async fn get_file(&self, path: &str) -> Result> { let url = self.full_url(path); - let resp = self._client + let resp = self.client .get(&url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .send() .await?; @@ -147,9 +147,9 @@ impl WebDavClient { /// Upload a file. pub async fn put_file(&self, path: &str, content: Vec) -> Result<()> { let url = self.full_url(path); - let resp = self._client + let resp = self.client .put(&url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .body(content) .send() .await?; @@ -164,9 +164,9 @@ impl WebDavClient { /// Delete a remote file. pub async fn delete_file(&self, path: &str) -> Result<()> { let url = self.full_url(path); - let resp = self._client + let resp = self.client .delete(&url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .send() .await?; @@ -183,9 +183,9 @@ impl WebDavClient { /// Create a directory via MKCOL. pub async fn create_dir(&self, path: &str) -> Result<()> { let url = self.full_url(path); - let resp = self._client + let resp = self.client .request(reqwest::Method::from_bytes(b"MKCOL").expect("MKCOL is a valid HTTP method"), &url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .send() .await?; @@ -203,9 +203,9 @@ impl WebDavClient { pub async fn move_resource(&self, from: &str, to: &str) -> Result<()> { let from_url = self.full_url(from); let to_url = self.full_url(to); - let resp = self._client + let resp = self.client .request(reqwest::Method::from_bytes(b"MOVE").expect("MOVE is a valid HTTP method"), &from_url) - .basic_auth(self._username.as_str(), Some(self._password.as_str())) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) .header("Destination", &to_url) .header("Overwrite", "F") .send() @@ -448,12 +448,12 @@ pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result let user_entry = keyring::Entry::new(&service, "username") .map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?; - user_entry.set_password(username) + user_entry.setpassword(username) .map_err(|e| Error::Credential(format!("Failed to store username: {}", e)))?; let pass_entry = keyring::Entry::new(&scoped_service, "password") .map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?; - pass_entry.set_password(password) + pass_entry.setpassword(password) .map_err(|e| Error::Credential(format!("Failed to store password: {}", e)))?; // Clean up legacy unscoped password entry if present @@ -478,18 +478,18 @@ pub fn load_credentials(domain: &str) -> Result<(Zeroizing, Zeroizing Result<(Zeroizing, Zeroizing Result<()> { // Load username first so we can delete the scoped password entry let username = keyring::Entry::new(&service, "username") .ok() - .and_then(|e| e.get_password().ok()); + .and_then(|e| e.getpassword().ok()); if let Some(user) = &username { let scoped_service = format!("com.onyx.webdav.{}::{}", domain, user); From e470e79e78ee8d37c2cb9be3f688211a48c3927f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 07:23:49 +0000 Subject: [PATCH 4/7] refactor: deduplicate filename sanitization logic storage.rs and google_tasks.rs had near-identical sanitize_filename implementations. Extract the shared logic to a crate-level function so both modules reuse it. The google_tasks version also gains Windows reserved device name handling it previously lacked. https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV --- crates/onyx-core/src/google_tasks.rs | 10 +--------- crates/onyx-core/src/lib.rs | 27 +++++++++++++++++++++++++++ crates/onyx-core/src/storage.rs | 22 +--------------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/crates/onyx-core/src/google_tasks.rs b/crates/onyx-core/src/google_tasks.rs index 8bf3fb1..e13ecd4 100644 --- a/crates/onyx-core/src/google_tasks.rs +++ b/crates/onyx-core/src/google_tasks.rs @@ -452,15 +452,7 @@ fn render_task_markdown(task: &Task) -> String { /// Sanitize a string for use as a filesystem path component. fn sanitize_name(name: &str) -> String { - let s: String = name.chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', - '\0'..='\x1f' => '_', - _ => c, - }) - .collect::() - .trim_matches(|c: char| c == '.' || c == ' ') - .to_string(); + let s = crate::sanitize_filename(name); if s.is_empty() { "Untitled".to_string() } else { s } } diff --git a/crates/onyx-core/src/lib.rs b/crates/onyx-core/src/lib.rs index b8c5120..8e85ca0 100644 --- a/crates/onyx-core/src/lib.rs +++ b/crates/onyx-core/src/lib.rs @@ -11,3 +11,30 @@ pub use models::{Task, TaskStatus, TaskList}; pub use repository::TaskRepository; pub use config::{AppConfig, WorkspaceConfig}; pub use error::{Error, Result}; + +/// Sanitize a string for use as a filesystem path component. +/// Replaces filesystem-unsafe characters with underscores, trims leading/trailing +/// dots and spaces, and prefixes Windows reserved device names. +pub(crate) fn sanitize_filename(name: &str) -> String { + let sanitized: String = name.chars() + .map(|c| match c { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', + '\0'..='\x1f' => '_', + _ => c, + }) + .collect::() + .trim_matches(|c: char| c == '.' || c == ' ') + .to_string(); + // Reject Windows reserved device names (CON, PRN, AUX, NUL, COM0-9, LPT0-9) + let stem = sanitized.split('.').next().unwrap_or("").to_uppercase(); + let is_reserved = matches!(stem.as_str(), + "CON" | "PRN" | "AUX" | "NUL" + | "COM0" | "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9" + | "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9" + ); + if is_reserved { + format!("_{}", sanitized) + } else { + sanitized + } +} diff --git a/crates/onyx-core/src/storage.rs b/crates/onyx-core/src/storage.rs index 42d5801..c966af0 100644 --- a/crates/onyx-core/src/storage.rs +++ b/crates/onyx-core/src/storage.rs @@ -237,27 +237,7 @@ impl FileSystemStorage { } fn sanitize_filename(name: &str) -> String { - let sanitized: String = name.chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', - '\0'..='\x1f' => '_', - _ => c, - }) - .collect::() - .trim_matches(|c: char| c == '.' || c == ' ') - .to_string(); - // Reject Windows reserved device names (CON, PRN, AUX, NUL, COM0-9, LPT0-9) - let stem = sanitized.split('.').next().unwrap_or("").to_uppercase(); - let is_reserved = matches!(stem.as_str(), - "CON" | "PRN" | "AUX" | "NUL" - | "COM0" | "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9" - | "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9" - ); - if is_reserved { - format!("_{}", sanitized) - } else { - sanitized - } + crate::sanitize_filename(name) } fn task_file_path(&self, list_dir: &Path, task: &Task) -> PathBuf { From a313a6e27031b6c6c893c31b1b354494fac1117b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 07:26:06 +0000 Subject: [PATCH 5/7] refactor: extract shared date formatting utilities in frontend Three components had duplicate date formatting functions. Extract formatDateChip (for detail/input views with optional time) and formatDateLabel (for compact list items) to a shared dateFormat module. https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV --- .../src/lib/components/NewTaskInput.svelte | 13 ++--------- .../src/lib/components/TaskDetailView.svelte | 14 ++--------- apps/tauri/src/lib/components/TaskItem.svelte | 14 +++-------- apps/tauri/src/lib/dateFormat.ts | 23 +++++++++++++++++++ 4 files changed, 30 insertions(+), 34 deletions(-) create mode 100644 apps/tauri/src/lib/dateFormat.ts diff --git a/apps/tauri/src/lib/components/NewTaskInput.svelte b/apps/tauri/src/lib/components/NewTaskInput.svelte index 9bac827..9edc833 100644 --- a/apps/tauri/src/lib/components/NewTaskInput.svelte +++ b/apps/tauri/src/lib/components/NewTaskInput.svelte @@ -5,6 +5,7 @@ @@ -240,7 +230,7 @@ {#if task.date}