Merge pull request #26 from SteelDynamite/docs-webdav-sync-theme-updates

docs-webdav-sync-theme-updates
This commit is contained in:
SteelDynamite 2026-04-03 11:26:29 -07:00 committed by GitHub
commit 8bbd3128e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 40 additions and 13 deletions

View file

@ -37,7 +37,9 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
- **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files. - **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files.
- **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here. - **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here.
- **Config** (`config.rs`): `AppConfig` manages named workspaces with paths. Stored in platform-specific config dirs via the `directories` crate. - **Config** (`config.rs`): `AppConfig` manages named workspaces with paths, mode (local/webdav), theme, and WebDAV URL. Stored in platform-specific config dirs via the `directories` crate.
- **Sync** (`sync.rs`): Three-way diff sync with offline queue. Auto-appends `Onyx/` to WebDAV URL. Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows.
- **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Credentials stored via `keyring` crate (feature-gated). `Zeroizing<String>` for credential fields. Scoped keyring keys (`com.onyx.webdav.<domain>::<username>`); auto-migrates legacy dot-separated format on load. 10MB PROPFIND response cap.
### On-disk format ### On-disk format
@ -49,8 +51,8 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
- **Sliding drawer**: Left panel (lists) slides with main content as one piece via `translateX`. 80vw wide. List items show checkmark for active list and chevron on hover. - **Sliding drawer**: Left panel (lists) slides with main content as one piece via `translateX`. 80vw wide. List items show checkmark for active list and chevron on hover.
- **Three-panel slide**: Main content area is 300% wide with three panels (task list, task detail, subtask detail) that slide via `translateX` using a `taskStack` array. Stack depth 0 = list, 1 = task detail, 2 = subtask detail. - **Three-panel slide**: Main content area is 300% wide with three panels (task list, task detail, subtask detail) that slide via `translateX` using a `taskStack` array. Stack depth 0 = list, 1 = task detail, 2 = subtask detail.
- **Settings popup**: Floating overlay card with backdrop, not a sliding panel. - **Settings modal**: Per-workspace settings opened from workspace kebab menu. Shows WebDAV config (for webdav workspaces), sync controls, and theme selector.
- **Workspace switcher**: Custom drop-up menu in drawer footer (left), settings gear (right). - **Workspace switcher**: Custom drop-up menu in drawer footer (left), kebab menu per workspace (right) with Settings option.
- **Task animations**: Grid-rows `0fr`/`1fr` trick for smooth collapse/expand. Module-level `animateInIds` Set coordinates expand-in after toggle. - **Task animations**: Grid-rows `0fr`/`1fr` trick for smooth collapse/expand. Module-level `animateInIds` Set coordinates expand-in after toggle.
- **Inline editing**: Click task to edit, auto-save on blur. `debouncedSave` snapshots task before timer to prevent stale-reference errors on component destroy. - **Inline editing**: Click task to edit, auto-save on blur. `debouncedSave` snapshots task before timer to prevent stale-reference errors on component destroy.
- **Kebab menus**: Tasks and lists use kebab menus with custom `ConfirmDialog` component (not native `confirm()`). "Move to..." is inline in the menu (not a submenu) to avoid overflow. - **Kebab menus**: Tasks and lists use kebab menus with custom `ConfirmDialog` component (not native `confirm()`). "Move to..." is inline in the menu (not a submenu) to avoid overflow.
@ -60,7 +62,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
### Current state (2026-04-03) ### Current state (2026-04-03)
- **Phase 1** (Core + CLI): Complete - **Phase 1** (Core + CLI): Complete
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials) - **Phase 2** (WebDAV sync): Complete — CLI + GUI sync working, auto-creates `Onyx/` subfolder on remote
- **Phase 3** (GUI MVP): Complete - **Phase 3** (GUI MVP): Complete
- **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build - **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build
@ -73,7 +75,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
- Sliding lists drawer with checkmark selection - Sliding lists drawer with checkmark selection
- Settings popup overlay - Settings popup overlay
- Workspace switcher drop-up with add/remove - Workspace switcher drop-up with add/remove
- Dark mode (GNOME-style neutral grays, cyan-blue accent) - Per-workspace theme system (System default, Light, Dark, Nord, Dracula, Solarized Dark) via CSS `data-theme` attribute
- Completed tasks section with animated show/hide - Completed tasks section with animated show/hide
- Due date picker/editor (DateTimePicker in new task + task detail); `has_time: bool` field tracks whether time is set - Due date picker/editor (DateTimePicker in new task + task detail); `has_time: bool` field tracks whether time is set
- Move task between lists (inline list in kebab menu, no submenu) - Move task between lists (inline list in kebab menu, no submenu)
@ -81,11 +83,13 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
- Group-by-due-date toggle per list (main panel kebab) - Group-by-due-date toggle per list (main panel kebab)
- Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs) - Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs)
- Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus) - Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus)
- WebDAV setup flow (settings auto-populates URL/credentials from config + keychain) - Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option
- WebDAV setup flow with connection test, credential storage in system keychain
- WebDAV sync: auto-creates `Onyx/` subfolder on remote, 60s hard timeout, sync error display in settings
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes) - File watcher (notify crate, 500ms debounce, auto-reloads on external changes)
- Setup screen with window dragging + "Open Existing Folder" option
- Sync status indicators (last-sync time + upload/download counts chip) - Sync status indicators (last-sync time + upload/download counts chip)
- Push/pull/full sync mode selection (session-only, in settings) - Push/pull/full sync mode selection (session-only, in settings)
- WorkspaceMode enum (local/webdav) with per-workspace config
- Desktop packaging (Linux: AppImage + .deb; Windows: MSI) - Desktop packaging (Linux: AppImage + .deb; Windows: MSI)
- Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation - Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation
- Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning) - Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning)

View file

@ -99,7 +99,16 @@ impl WebDavClient {
return Err(Error::WebDav(format!("PROPFIND failed with status {}", status))); return Err(Error::WebDav(format!("PROPFIND failed with status {}", status)));
} }
let body = resp.text().await?; // Reject oversized responses to prevent memory exhaustion from malicious servers
const MAX_PROPFIND_BYTES: u64 = 10 * 1024 * 1024;
if resp.content_length().unwrap_or(0) > MAX_PROPFIND_BYTES {
return Err(Error::WebDav("PROPFIND response too large (>10MB)".into()));
}
let bytes = resp.bytes().await?;
if bytes.len() as u64 > MAX_PROPFIND_BYTES {
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)
} }
@ -401,7 +410,7 @@ fn extract_relative_path(href: &str, base_url: &str, request_path: &str) -> Stri
/// to prevent collisions when multiple accounts exist on the same server. /// to prevent collisions when multiple accounts exist on the same server.
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> { pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
let service = format!("com.onyx.webdav.{}", domain); let service = format!("com.onyx.webdav.{}", domain);
let scoped_service = format!("com.onyx.webdav.{}.{}", domain, username); let scoped_service = format!("com.onyx.webdav.{}::{}", domain, username);
let user_entry = keyring::Entry::new(&service, "username") let user_entry = keyring::Entry::new(&service, "username")
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?; .map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
@ -437,18 +446,29 @@ pub fn load_credentials(domain: &str) -> Result<(Zeroizing<String>, Zeroizing<St
if let Ok(user) = user_entry.get_password() { if let Ok(user) = user_entry.get_password() {
// Try scoped password key first (domain+username), fall back to legacy unscoped key // Try scoped password key first (domain+username), fall back to legacy unscoped key
let scoped_service = format!("com.onyx.webdav.{}.{}", domain, user); let scoped_service = format!("com.onyx.webdav.{}::{}", domain, user);
let pass = keyring::Entry::new(&scoped_service, "password") let found = keyring::Entry::new(&scoped_service, "password")
.ok() .ok()
.and_then(|e| e.get_password().ok()) .and_then(|e| e.get_password().ok())
.map(|p| (p, false))
.or_else(|| { .or_else(|| {
// Migration fallback: try legacy unscoped password entry // Migration fallback: try legacy unscoped password entry
keyring::Entry::new(&service, "password") keyring::Entry::new(&service, "password")
.ok() .ok()
.and_then(|e| e.get_password().ok()) .and_then(|e| e.get_password().ok())
.map(|p| (p, true))
}); });
if let Some(pass) = pass { if let Some((pass, needs_migration)) = found {
// Auto-migrate legacy credentials to scoped format
if needs_migration {
if let Ok(entry) = keyring::Entry::new(&scoped_service, "password") {
let _ = entry.set_password(&pass);
}
if let Ok(legacy) = keyring::Entry::new(&service, "password") {
let _ = legacy.delete_credential();
}
}
return Ok((Zeroizing::new(user), Zeroizing::new(pass))); return Ok((Zeroizing::new(user), Zeroizing::new(pass)));
} }
} }
@ -496,7 +516,7 @@ pub fn delete_credentials(domain: &str) -> Result<()> {
.and_then(|e| e.get_password().ok()); .and_then(|e| e.get_password().ok());
if let Some(user) = &username { if let Some(user) = &username {
let scoped_service = format!("com.onyx.webdav.{}.{}", domain, user); let scoped_service = format!("com.onyx.webdav.{}::{}", domain, user);
if let Ok(entry) = keyring::Entry::new(&scoped_service, "password") { if let Ok(entry) = keyring::Entry::new(&scoped_service, "password") {
let _ = entry.delete_credential(); let _ = entry.delete_credential();
} }

View file

@ -318,6 +318,8 @@ let status = get_sync_status(Path::new("/home/user/tasks"))?;
Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service). Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service).
Keyring service keys use the format `com.onyx.webdav.<domain>::<username>` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format (`com.onyx.webdav.<domain>.<username>`) are automatically migrated to the scoped format and the old entries are removed.
```rust ```rust
use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials}; use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials};
@ -363,6 +365,7 @@ client.delete_file("old-task.md").await?;
- **Conflict resolution**: Last-write-wins using file timestamps - **Conflict resolution**: Last-write-wins using file timestamps
- **Offline queue**: Pending operations are queued and replayed when connectivity returns - **Offline queue**: Pending operations are queued and replayed when connectivity returns
- **Sync state**: Stored in `.syncstate.json` within the workspace directory - **Sync state**: Stored in `.syncstate.json` within the workspace directory
- **Response size cap**: PROPFIND responses are limited to 10 MB (checked via `Content-Length` header and actual body size) to prevent memory exhaustion from malicious servers
## Error Handling ## Error Handling