From eaab66609cd714f994308cc1ca8801c696d83fb0 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Sun, 5 Apr 2026 19:21:33 -0700 Subject: [PATCH] Docs: update all markdown files to reflect workspace-settings-overhaul changes Update CLAUDE.md, PLAN.md, README.md, docs/API.md, and docs/DEVELOPMENT.md to reflect UUID-keyed workspaces, checksum-based conflict resolution, auto-sync lifecycle, tauri-plugin-credentials, .onyx-workspace.json rename, and new GUI features (remote folder browsing, workspace rename, sync interval, loading screen, safe area insets, task dedup). --- CLAUDE.md | 33 ++++++++++++++---------- PLAN.md | 63 +++++++++++++++++++++++++++++---------------- README.md | 5 ++-- docs/API.md | 45 ++++++++++++++++++++------------ docs/DEVELOPMENT.md | 5 ++++ 5 files changed, 97 insertions(+), 54 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2b776fa..e876302 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,19 +31,19 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app: - **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. `keyring` feature-gated behind `keyring-storage` (default on) for Android compatibility. - **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`. -- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android. +- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android. `tauri-plugin-credentials/` provides cross-platform credential storage (Android Keystore via EncryptedSharedPreferences, desktop via keyring crate). ### Key patterns - **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. -- **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` for credential fields. Scoped keyring keys (`com.onyx.webdav.::`); auto-migrates legacy dot-separated format on load. 10MB PROPFIND response cap. +- **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav), `webdav_url`, `webdav_path` (user-selected remote folder), `theme`, and `sync_interval_secs`. `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate. +- **Sync** (`sync.rs`): Three-way diff sync with offline queue. Checksum-based conflict resolution: downloads remote, compares SHA-256 — identical content is a false conflict (skipped); when different, remote wins and local is recovered as a duplicate with a new UUID and `[RECOVERED FROM CONFLICT]` prefix. Auto-sync lifecycle: periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold). 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. Rejects non-HTTPS URLs. `Zeroizing` for credential fields. `move_resource` method for WebDAV MOVE (workspace rename). 10MB PROPFIND response cap. Desktop credentials via `keyring` crate (feature-gated); Tauri GUI uses `tauri-plugin-credentials` for cross-platform support (Android Keystore + desktop keychain). ### On-disk format -Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. +Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories. ### Tauri GUI structure @@ -66,9 +66,9 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to ### Current state (2026-04-05) - **Phase 1** (Core + CLI): Complete -- **Phase 2** (WebDAV sync): In progress — reworking to let users browse and pick a remote folder instead of hardcoding `Onyx/` subfolder +- **Phase 2** (WebDAV sync): Complete — remote folder browsing, checksum-based conflict resolution, auto-sync lifecycle, per-workspace sync interval - **Phase 3** (GUI MVP): Complete -- **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build +- **Phase 4** (Mobile): Tauri Android cfg-gated with tauri-plugin-credentials and safe area insets; needs `tauri android init` + build ### GUI features done @@ -87,21 +87,26 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to - Group-by-due-date toggle per list (main panel kebab) - Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs) - Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus) -- 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 +- Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option, remote folder browsing +- WebDAV setup flow with connection test, credential storage via tauri-plugin-credentials (Android Keystore + desktop keychain) +- WebDAV sync: user-selected remote folder, 60s hard timeout, checksum-based conflict resolution (remote wins, local recovered as duplicate) +- Auto-sync lifecycle: periodic polling (configurable interval), debounced file-change (5s), window-focus (30s stale threshold); sync status dot (idle/synced/error/offline) and manual sync button in drawer +- Initial sync loading screen for new WebDAV workspaces +- Per-workspace sync interval (configurable in settings) +- Upload/download counts in drawer footer (hidden when zero) +- Transient sync error suppression (connectivity issues update status dot only, no error banner) - File watcher (notify crate, 500ms debounce, auto-reloads on external changes) -- Sync status indicators (last-sync time + upload/download counts chip) -- Push/pull/full sync mode selection (session-only, in settings) -- WorkspaceMode enum (local/webdav) with per-workspace config +- WorkspaceMode enum (local/webdav) with per-workspace config, UUID-keyed workspaces +- Workspace rename (local folder rename + WebDAV MOVE for remote folders, with confirmation dialog) - Desktop packaging (Linux: AppImage + .deb; Windows: MSI) - Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation +- Safe area insets for mobile (CSS variables --safe-top/--safe-bottom, viewport-fit=cover) +- Task deduplication on load (handles sync conflict duplicates) - 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) - Custom confirmation dialogs (ConfirmDialog component replaces native confirm()) ### GUI features NOT yet done -- Workspace retarget/migrate - Search/filter tasks - Desktop packaging for macOS diff --git a/PLAN.md b/PLAN.md index 55d432d..48579a8 100644 --- a/PLAN.md +++ b/PLAN.md @@ -106,12 +106,18 @@ TaskList { } AppConfig { - workspaces: HashMap, - current_workspace: Option, + workspaces: HashMap, // UUID keys + current_workspace: Option, // UUID } WorkspaceConfig { + name: String, // Display name path: PathBuf, + mode: WorkspaceMode, // Local or Webdav + webdav_url: Option, + webdav_path: Option, // User-selected remote folder + theme: Option, + sync_interval_secs: Option, } ``` @@ -119,7 +125,7 @@ WorkspaceConfig { ``` ~/Documents/Tasks/ # User-selected folder -├── .metadata.json # Global: list ordering, last opened list +├── .onyx-workspace.json # Global: list ordering, last opened list ├── My Tasks/ # Task list folder │ ├── .listdata.json # List metadata: task order, id, timestamps │ ├── Buy groceries.md # Title: "Buy groceries" (without .md) @@ -132,7 +138,7 @@ WorkspaceConfig { **Note**: Task titles are derived from filenames by removing the `.md` extension. -**`.metadata.json` (root level)**: +**`.onyx-workspace.json` (root level)**: ```json { "version": 1, @@ -169,14 +175,20 @@ WorkspaceConfig { ```json { "workspaces": { - "personal": { - "path": "/home/user/Documents/Tasks" + "a1b2c3d4-...": { + "name": "personal", + "path": "/home/user/Documents/Tasks", + "mode": "local" }, - "shared": { - "path": "/home/user/Dropbox/TeamTasks" + "e5f6g7h8-...": { + "name": "shared", + "path": "/home/user/Dropbox/TeamTasks", + "mode": "webdav", + "webdav_url": "https://nextcloud.example.com/remote.php/dav/files/user/", + "webdav_path": "TeamTasks" } }, - "current_workspace": "personal" + "current_workspace": "a1b2c3d4-..." } ``` @@ -394,7 +406,7 @@ $ onyx workspace migrate personal ~/Dropbox/Tasks ⚠ This will move all files from ~/Documents/Tasks to ~/Dropbox/Tasks Continue? (y/n): y Moving files... - Moved .metadata.json + Moved .onyx-workspace.json Moved My Tasks/ (15 files) Moved Work/ (8 files) ✓ Migrated 23 files to ~/Dropbox/Tasks @@ -453,11 +465,16 @@ cargo run -p onyx-cli -- workspace list Add WebDAV support to `onyx-core`: ```rust -// Update WorkspaceConfig to include WebDAV +// WorkspaceConfig with WebDAV support (UUID-keyed in AppConfig) WorkspaceConfig { + name: String, path: PathBuf, + mode: WorkspaceMode, // Local or Webdav webdav_url: Option, + webdav_path: Option, // User-selected remote folder last_sync: Option, + theme: Option, + sync_interval_secs: Option, } // AppConfig remains the same (workspaces + current_workspace) @@ -484,8 +501,8 @@ pub fn delete_credentials(domain: &str) -> Result<()>; ``` #### Sync Strategy -- **Trigger**: On app start (if connected), background timer (every 5 min), on modification (debounced) -- **Conflict Resolution**: Last-write-wins with timestamp +- **Trigger**: Auto-sync lifecycle — periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold) +- **Conflict Resolution**: Checksum-based — downloads remote, compares SHA-256. Identical = false conflict (skipped). Different = remote wins, local recovered as duplicate with new UUID and `[RECOVERED FROM CONFLICT]` prefix - **Offline Support**: Queue operations when offline, sync when online #### Authentication @@ -664,20 +681,22 @@ apps/tauri/ #### App Configuration (Phase 3+) -**Update AppConfig** to include UI preferences: +**AppConfig** with UI preferences (theme is per-workspace): ```rust AppConfig { - workspaces: HashMap, // From Phase 1 - current_workspace: Option, - theme: Theme, // NEW: light/dark mode - window_size: Option<(u32, u32)>, // NEW: remember window size - last_opened_list_per_workspace: HashMap, // NEW: per-workspace last view + workspaces: HashMap, // UUID keys + current_workspace: Option, // UUID } WorkspaceConfig { + name: String, // Display name path: PathBuf, - webdav_url: Option, // From Phase 2 + mode: WorkspaceMode, // Local or Webdav + webdav_url: Option, + webdav_path: Option, // User-selected remote folder last_sync: Option, + theme: Option, // Per-workspace theme + sync_interval_secs: Option, // Auto-sync interval } ``` @@ -1011,6 +1030,6 @@ This project is free and open-source software licensed under GPL v3. --- -**Last Updated**: 2026-04-03 -**Document Version**: 4.2 +**Last Updated**: 2026-04-05 +**Document Version**: 4.3 **Status**: Ready to Implement - Milestone-Driven Plan diff --git a/README.md b/README.md index 81b641c..cce9abd 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ onyx/ - Subtask hierarchy (parent_id) - WebDAV sync with three-way diff and offline queue - Platform keychain credential storage (feature-gated for Android) +- Checksum-based conflict resolution (remote wins, local recovered as duplicate) ### CLI (`onyx-cli`) - Workspace management (init, add, list, switch, remove, retarget, migrate) @@ -62,7 +63,7 @@ onyx/ - Keyboard shortcuts (Escape priority chain) - WebDAV setup flow with credential auto-population - File watcher (auto-reloads on external changes) -- Sync status indicators and push/pull/full mode selection +- Auto-sync with configurable interval, status indicators - Custom confirmation dialogs - Desktop packaging (Linux: AppImage + .deb; Windows: MSI) @@ -182,7 +183,7 @@ Task description and notes go here in **markdown** format. ``` ~/Documents/Tasks/ # User-selected folder -├── .metadata.json # Global: list ordering, last opened list +├── .onyx-workspace.json # Workspace metadata: list ordering, detection ├── My Tasks/ # Task list folder │ ├── .listdata.json # List metadata: task order, id, timestamps │ ├── Buy groceries.md # Individual task files diff --git a/docs/API.md b/docs/API.md index 0216504..d521f37 100644 --- a/docs/API.md +++ b/docs/API.md @@ -64,12 +64,12 @@ pub struct TaskList { #### AppConfig -Global application configuration supporting multiple workspaces. +Global application configuration supporting multiple workspaces. Workspaces are keyed by UUID string. ```rust pub struct AppConfig { - pub workspaces: HashMap, - pub current_workspace: Option, + pub workspaces: HashMap, // UUID keys + pub current_workspace: Option, // UUID of active workspace } ``` @@ -87,14 +87,18 @@ use onyx_core::AppConfig; let config_path = AppConfig::get_config_path(); let mut config = AppConfig::load_from_file(&config_path)?; -// Add workspace -config.add_workspace( - "personal".to_string(), - WorkspaceConfig::new(PathBuf::from("/home/user/tasks")) +// Add workspace (returns generated UUID) +let id = config.add_workspace( + WorkspaceConfig::new("personal".to_string(), PathBuf::from("/home/user/tasks")) ); -// Set current workspace -config.set_current_workspace("personal".to_string())?; +// Set current workspace by ID +config.set_current_workspace(id)?; + +// Find workspace by display name +if let Some((id, ws)) = config.find_by_name("personal") { + println!("Found: {} at {:?}", id, ws.path); +} // Save config config.save_to_file(&config_path)?; @@ -106,9 +110,14 @@ Configuration for a single workspace. ```rust pub struct WorkspaceConfig { + pub name: String, // Display name pub path: PathBuf, + pub mode: WorkspaceMode, // Local or Webdav pub webdav_url: Option, + pub webdav_path: Option, // User-selected remote folder path pub last_sync: Option>, + pub theme: Option, + pub sync_interval_secs: Option, // Auto-sync polling interval } ``` @@ -267,7 +276,7 @@ Each list folder contains a `.listdata.json` file: ### Root Metadata -The root folder contains a `.metadata.json` file: +The root folder contains a `.onyx-workspace.json` file: ```json { @@ -318,7 +327,7 @@ 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). -Keyring service keys use the format `com.onyx.webdav.::` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format (`com.onyx.webdav..`) are automatically migrated to the scoped format and the old entries are removed. +**Core library** (`onyx-core::webdav`): Keyring service keys use the format `com.onyx.webdav.::` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format are automatically migrated. ```rust use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials}; @@ -333,6 +342,8 @@ let (username, password) = load_credentials("nextcloud.example.com")?; delete_credentials("nextcloud.example.com")?; ``` +**Tauri GUI**: Uses `tauri-plugin-credentials` instead of direct keyring calls. This plugin provides cross-platform support: EncryptedSharedPreferences (Android Keystore) on Android, keyring crate on desktop. Plugin crate at `apps/tauri/tauri-plugin-credentials/`. + ### WebDAV Client ```rust @@ -342,7 +353,7 @@ let client = WebDavClient::new( "https://nextcloud.example.com/remote.php/dav/files/user/Tasks", "username", "password", -); +)?; // Returns Result — rejects non-HTTPS URLs // Test connection client.test_connection().await?; @@ -361,11 +372,13 @@ client.delete_file("old-task.md").await?; ### Sync Strategy -- **Three-way diff**: Compares local state, remote state, and last-known baseline to determine actions (upload, download, delete local/remote) -- **Conflict resolution**: Last-write-wins using file timestamps +- **Three-way diff**: Compares local state, remote state, and last-known baseline to determine actions (upload, download, delete local/remote, conflict) +- **Conflict resolution**: Checksum-based — downloads remote file and compares SHA-256 checksums. Identical content is a false conflict (skipped). When different, remote wins and the local version is recovered as a duplicate task with a new UUID and `[RECOVERED FROM CONFLICT]` prefix, inserted adjacent to the original in `.listdata.json` - **Offline queue**: Pending operations are queued and replayed when connectivity returns - **Sync state**: Stored in `.syncstate.json` within the workspace directory +- **Auto-sync**: Periodic polling (configurable `sync_interval_secs`), debounced file-change trigger (5s), window-focus trigger (30s stale threshold) - **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 +- **Syncable files**: Only processes files at expected depths — `.onyx-workspace.json` at root (depth 1), `.listdata.json` and `*.md` inside list directories (depth 2) ## Error Handling @@ -424,8 +437,8 @@ fn main() -> Result<(), Box> { // Configure workspace let mut config = AppConfig::new(); - config.add_workspace("personal".to_string(), WorkspaceConfig::new(path)); - config.set_current_workspace("personal".to_string())?; + let ws_id = config.add_workspace(WorkspaceConfig::new("personal".to_string(), path)); + config.set_current_workspace(ws_id)?; config.save_to_file(&AppConfig::get_config_path())?; Ok(()) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 13cdca5..66dcd2a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -76,6 +76,11 @@ onyx/ │ │ ├── components/ # Reusable UI components │ │ ├── stores/ # Svelte state (app.svelte.ts) │ │ └── types.ts # TypeScript type definitions +│ ├── tauri-plugin-credentials/ # Cross-platform credential storage plugin +│ │ ├── Cargo.toml +│ │ ├── src/ +│ │ │ └── lib.rs # Desktop (keyring) + plugin API +│ │ └── android/ # Android (EncryptedSharedPreferences) │ └── src-tauri/ # Rust backend (Tauri commands) │ ├── Cargo.toml │ ├── tauri.conf.json