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).
This commit is contained in:
Tristan Michael 2026-04-05 19:21:33 -07:00
parent 7cc899470f
commit eaab66609c
5 changed files with 97 additions and 54 deletions

View file

@ -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<String>` for credential fields. Scoped keyring keys (`com.onyx.webdav.<domain>::<username>`); 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<String>` 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

63
PLAN.md
View file

@ -106,12 +106,18 @@ TaskList {
}
AppConfig {
workspaces: HashMap<String, WorkspaceConfig>,
current_workspace: Option<String>,
workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
current_workspace: Option<String>, // UUID
}
WorkspaceConfig {
name: String, // Display name
path: PathBuf,
mode: WorkspaceMode, // Local or Webdav
webdav_url: Option<String>,
webdav_path: Option<String>, // User-selected remote folder
theme: Option<String>,
sync_interval_secs: Option<u64>,
}
```
@ -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<String>,
webdav_path: Option<String>, // User-selected remote folder
last_sync: Option<DateTime>,
theme: Option<String>,
sync_interval_secs: Option<u64>,
}
// 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<String, WorkspaceConfig>, // From Phase 1
current_workspace: Option<String>,
theme: Theme, // NEW: light/dark mode
window_size: Option<(u32, u32)>, // NEW: remember window size
last_opened_list_per_workspace: HashMap<String, Uuid>, // NEW: per-workspace last view
workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
current_workspace: Option<String>, // UUID
}
WorkspaceConfig {
name: String, // Display name
path: PathBuf,
webdav_url: Option<String>, // From Phase 2
mode: WorkspaceMode, // Local or Webdav
webdav_url: Option<String>,
webdav_path: Option<String>, // User-selected remote folder
last_sync: Option<DateTime>,
theme: Option<String>, // Per-workspace theme
sync_interval_secs: Option<u64>, // 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

View file

@ -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

View file

@ -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<String, WorkspaceConfig>,
pub current_workspace: Option<String>,
pub workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
pub current_workspace: Option<String>, // 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<String>,
pub webdav_path: Option<String>, // User-selected remote folder path
pub last_sync: Option<DateTime<Utc>>,
pub theme: Option<String>,
pub sync_interval_secs: Option<u64>, // 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.<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.
**Core library** (`onyx-core::webdav`): 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 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<dyn std::error::Error>> {
// 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(())

View file

@ -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