Commit graph

307 commits

Author SHA1 Message Date
SteelDynamite c5a3840aea
Merge pull request #66 from SteelDynamite/claude/gracious-cray-yN12q
docs(api): clarify thread-safety bounds and multi-process limits
2026-04-29 02:45:47 +01:00
Claude c29f715c9e
docs(api): clarify thread-safety bounds and multi-process limits
The Storage trait itself does not declare `Send + Sync` bounds — only the
boxed instance held by `TaskRepository` does. Reword to describe what's
actually required of an implementation, and call out that
`FileSystemStorage` does not coordinate writes across processes outside
the `.sync.lock`-protected WebDAV flow.

https://claude.ai/code/session_01LweYBKMFbnTen7pCTdeQKq
2026-04-27 07:45:44 +00:00
SteelDynamite 6f4d00b912
Merge pull request #65 from SteelDynamite/claude/serene-ride-Gt8lp
audit: 2026-04-27 — sync clone, google metadata errors, dedup invariant
2026-04-27 08:40:13 +01:00
SteelDynamite 39718ef700
Merge pull request #64 from SteelDynamite/claude/dreamy-brown-4XuTd
docs: sync documentation with codebase state
2026-04-27 08:39:21 +01:00
Claude c57ffd3f55
docs(audit): log 2026-04-27 findings 2026-04-27 07:23:34 +00:00
Claude 12adfdc532
refactor(storage): drop unreachable error in dedup loop
The dedup loop wrapped its winner in `Option<Task>` and then mapped the
`None` case to `Error::InvalidData("Empty dedup entries for task")`.
That branch is unreachable: `by_id` is built by pushing every entry of
`file_tasks` into the vector for its UUID, so every group has at least
one entry, and the `len() > 1` branch keeps the first element after
`drain(1..)`.

Replace the spurious error with `expect` calls that document the
invariant and let the dedup loop yield `Task` directly instead of
`Option<Task>`.
2026-04-27 07:23:12 +00:00
Claude 6e161ba819
fix(google_tasks): surface metadata write failures
`sync_google_workspace` silently dropped errors from `.listdata.json`
and `.onyx-workspace.json` atomic writes via `let _ = ...`, so a sync
could report `downloaded: N` while the list/workspace ordering had not
been persisted.  Push those errors into the `errors` vec returned by
`GoogleSyncResult` so callers see the failure.
2026-04-27 07:22:27 +00:00
Claude e8a69a3222
perf(sync): avoid cloning upload payload
`SyncAction::Upload` cloned the file bytes solely so it could later read
`data.len()` for the sync-state record.  Capture the length up front and
move the buffer into `put_file`.
2026-04-27 07:22:01 +00:00
Claude 839b744720
docs: sync documentation with codebase state
- PLAN.md: uncheck push/pull sync mode selector (backend supports it
  via SyncMode enum, but no UI exists in SettingsScreen; always full sync)
- PLAN.md: bump Last Updated to 2026-04-27, Document Version to 4.5
- CLAUDE.md: update Current state date to 2026-04-27

https://claude.ai/code/session_01C7jV6wrzJVhHRKWsq87XwB
2026-04-27 00:55:46 +00:00
SteelDynamite 0506d44989
Merge pull request #62 from SteelDynamite/claude/serene-ride-JTRND
audit(2026-04-25): O(n²) sync-status + cascade-delete + atomic-write dedup
2026-04-27 01:50:09 +01:00
Claude e1c4fd7dfb
docs(audit): log 2026-04-25 findings 2026-04-25 07:28:33 +00:00
Claude 8c8735b2b4
refactor(config): reuse storage::atomic_write for save_to_file
`AppConfig::save_to_file` had its own copy of the temp-file + rename +
cleanup-on-failure dance.  `storage::atomic_write` is already
`pub(crate)` and does exactly that — `google_tasks.rs` was migrated to
use it earlier.  Drop the duplicate so there's one canonical atomic
write path in the crate.
2026-04-25 07:27:25 +00:00
Claude 069afe8d5e
perf(tauri): build child index once for cascade delete
`delete_task`'s descendant walk re-scanned the full task list on every
frontier pop, so the cost was O(n * depth) where n is the list size.
For a list of a few hundred tasks with even moderate nesting that's
already noticeable.

Index `parent_id -> [child_id]` once up-front; the BFS then visits each
descendant in O(1) amortised, dropping the total to O(n).
2026-04-25 07:26:56 +00:00
Claude 1cdf5dff90
perf(sync): hash-set membership check in get_sync_status
The deletion-detection loop in `get_sync_status` scanned `local_files`
linearly for every tracked path in `sync_state.files`, making the cost
quadratic in the file count.  The earlier "pending change" loop just
above already does the inverse direction via `sync_state.files.get`
(O(1)).  Build a `HashSet<&str>` of local paths once and check it
the same way to make the function O(n).

This is called by the GUI status indicator, so the win shows up as
soon as a workspace tracks more than a handful of files.
2026-04-25 07:25:36 +00:00
SteelDynamite 56944360e0
Merge pull request #60 from SteelDynamite/claude/serene-ride-1mX8o 2026-04-24 22:12:58 +01:00
SteelDynamite 16cf409f32
Merge pull request #59 from SteelDynamite/claude/dreamy-brown-Ss931 2026-04-24 22:12:10 +01:00
Claude 8611f55573
docs(audit): log 2026-04-24 findings 2026-04-24 07:38:54 +00:00
Claude a9fac2c1d8
refactor(storage): drop single-caller sanitize_filename wrapper
`FileSystemStorage::sanitize_filename` was a one-line forwarder to
`crate::sanitize_filename` with a single call site in
`task_file_path`. The extra method added a layer of indirection
without value. Inline the crate-level call.
2026-04-24 07:38:18 +00:00
Claude 1fcc6e7f6d
fix(sync): purge orphan base entries when both sides deleted
`compute_sync_actions` emits no action for files that are missing from
both local and remote but still tracked in the sync base (the
`(None, None, Some(_))` arm). Nothing else cleaned those entries, so
`.syncstate.json` grew forever every time a file was deleted both
locally and remotely — and on each subsequent sync the same
no-op match fired again.

Add a `prune_orphan_bases` pass that runs before `compute_sync_actions`
in `sync_workspace_inner`, dropping any base entry whose path is in
neither the local nor remote scan. Unit-tested in isolation.
2026-04-24 07:37:39 +00:00
Claude 970210b647
refactor(sync): destructure remote in deleted-local branch
The `(None, Some(_), Some(b))` arm re-checked the already-matched
`remote` via `remote.is_some_and(...)`, which obscures intent and
compiles to redundant None-branch code. Bind `Some(r)` in the match
and use `r` directly.

No behavior change.
2026-04-24 07:36:28 +00:00
Claude 66513519ab
docs: fix credential return type, add missing test dir, update plan date
- docs/API.md: load_credentials returns Zeroizing<String> (not String)
- docs/DEVELOPMENT.md: add src/test/ directory to project structure
- PLAN.md: update Last Updated to 2026-04-23, bump version to 4.4

https://claude.ai/code/session_01By1aj94LMM7muDV7AT4egk
2026-04-23 10:08:34 +00:00
SteelDynamite 1bb1b67977
Merge pull request #58 from SteelDynamite/claude/serene-ride-LeiSc 2026-04-23 11:05:17 +01:00
SteelDynamite 4c318705f6
Merge pull request #57 from SteelDynamite/claude/dreamy-brown-nRanS 2026-04-23 11:01:53 +01:00
Claude 890f0c2126
docs(audit): log 2026-04-20 findings 2026-04-20 07:37:54 +00:00
Claude f42697f4ed
refactor(tauri): extract parse_uuid helper
17 Tauri commands repeated `Uuid::parse_str(&s).map_err(|e| e.to_string())`
for each UUID argument. Collapse the pattern into a `parse_uuid`
helper so callers read as `let id = parse_uuid(&list_id)?;`.
2026-04-20 07:35:50 +00:00
Claude 7754ea4b45
fix(tauri): surface errors from toggle_task cascade
When a parent task was toggled, `update_task` failures on child tasks
were silently swallowed with `let _ = ...`, leaving subtasks out of
sync with the parent's status and giving the user no feedback. Map the
error and propagate so the UI can show it and the user can retry.
2026-04-20 07:35:12 +00:00
Claude 6abe95692e
perf(tauri): use HashSet for cascade-delete dedup
Descendant walking in delete_task called Vec::contains in the inner
loop, making the traversal O(n^2) in the number of tasks. Swap the
visited set to HashSet so membership tests are O(1); HashSet::insert
also folds the contains-check and record-new steps into one call.
2026-04-20 07:34:52 +00:00
Claude 70fe7420cd
refactor(sync): remove dead .listdata.json guard in conflict path
The `.listdata.json` check was unreachable: the branch is already
gated on `parts[1].ends_with(".md")`, which `.listdata.json` fails.
2026-04-20 07:33:12 +00:00
Claude 6e1921230a
docs: sync markdown files with current codebase state
- Remove BottomSheet.svelte from PLAN.md file structure (deleted in
  efb4cca — NewTaskInput hand-rolls its own sheet)
- Expand workspace path validation description in API.md and CLAUDE.md
  to include filesystem root "/" alongside system directories, matching
  the forbidden list added in fix(tauri): reject "/" root path

https://claude.ai/code/session_015BSAnuhvMBLk7s4g7dSE53
2026-04-19 08:16:47 +00:00
SteelDynamite 6ae1006ab4
Merge pull request #56 from SteelDynamite/claude/serene-ride-XUY3D 2026-04-19 09:12:44 +01:00
SteelDynamite d8c6b9fc8e
Merge pull request #53 from SteelDynamite/claude/dreamy-brown-pFY5T 2026-04-19 09:12:08 +01:00
Claude 9a8a1a9f8e
style(sync): replace stray var with const in restartSyncInterval
Lone var in an otherwise let/const file — promote to const since the
value never gets reassigned. No behavior change.
2026-04-19 07:13:47 +00:00
Claude c952156491
refactor(date-picker): group selected-state declarations up top
selectedYear/selectedMonth were declared below selectDay, which writes
to them, and below isToday, which is declared nearby. Runtime worked
because the assignments only run on user click (after script init), but
the split made the initialization order confusing. Group all $state
fields at the top of the script.
2026-04-19 07:13:29 +00:00
Claude 62cf05480d
refactor(tauri): extract join_remote_path helper
Three call sites repeated the same "empty base -> child, otherwise
trim_end + slash + child" pattern. Pull it into a helper to keep the
join convention consistent across list_remote_folder, inspect, and
create_remote_workspace.
2026-04-19 07:12:37 +00:00
Claude e911ac1d94
refactor(tauri): extract credential_domain helper
Three call sites reproduced the same scheme://host parsing inline. Pull
it into a named helper so the domain-extraction convention lives in one
place.
2026-04-19 07:11:53 +00:00
Claude 937b6c2c7d
refactor(storage): read dedup mtimes once instead of in sort closure
sort_by may call the comparator many times, so the previous tiebreaker
re-read each duplicate file's metadata on every comparison. With N
duplicates that's O(N log N) stat calls, and the ordering could flip
mid-sort if a file was touched concurrently. Snapshot mtime per file up
front and sort on the cached values.
2026-04-19 07:09:49 +00:00
Claude 4e8f7c4536
fix(tauri): reject "/" root path in workspace validation
trim_end_matches('/') collapses "/" to "", which then isn't matched by
the forbidden list, so a root-filesystem workspace slipped through. Keep
"/" as the canonical form when the stripped value is empty.
2026-04-19 07:08:42 +00:00
Claude b977d275ba
docs: sync markdown docs with current codebase state
- CLAUDE.md: add `sync` to the CLI commands list (commands/sync.rs exists)
- PLAN.md: remove BottomSheet.svelte (deleted in efb4cca)
- DEVELOPMENT.md: add grouping.ts and paths.ts to the lib directory listing

https://claude.ai/code/session_01YbcpJqmwpEW5tCJFFkMSPZ
2026-04-18 08:53:10 +00:00
SteelDynamite 065118789f
Merge pull request #52 from SteelDynamite/claude/smoke-test-and-fixes-TwfSh 2026-04-18 09:49:21 +01:00
Claude a79dcc4617
test: cover CLI workspace resolver, date picker, saturating version
Add regression tests for the bugs found in this smoke test:

- resolve_workspace: by-name, by-UUID, unknown-identifier, current-fallback,
  actionable no-workspace message.
- DateTimePicker: selected-day highlight must be month-scoped; committing
  after navigating months uses the selected month, not the viewed one.
- create_task: version is saturating_add on u64::MAX (doesn't panic/wrap).

Also fixes the three pre-existing clippy warnings (WorkspaceMode now uses
#[derive(Default)] + #[default], repository test drops unused binding,
sync test uses struct-update syntax instead of field-reassign-default).
2026-04-17 16:32:22 +00:00
Claude efb4ccaaef
chore(cleanup): remove unused BottomSheet component and dead testConnection
BottomSheet.svelte is not imported anywhere — NewTaskInput hand-rolls
its own sheet. SetupScreen had a standalone testConnection() function
that was only ever reachable through connectAndBrowse which calls
test_webdav_connection directly; the standalone variant had no
callers.
2026-04-17 16:29:04 +00:00
Claude f6c8dfc951
fix(cli): create task-edit scratch file with mode 0600 on unix
onyx task edit wrote the task body to /tmp/onyx-<uuid>.md with the
default umask, leaving it world-readable on shared multi-user systems
for the duration of the editor session. Open with O_CREAT|O_TRUNC +
mode 0600 via OpenOptionsExt on unix; Windows keeps the existing
behaviour since unix-style mode bits don't apply.
2026-04-17 16:28:20 +00:00
Claude 3acc4c3f5d
fix(empty-state): replace misleading hint with an actual create button
The no-lists empty state said 'Tap the list name above to create one' —
but there is no list name above, just a static 'Tasks' label. The
actual affordance (+ New list) lives in the drawer, which may not be
open. Add a primary-button shortcut that opens the drawer and puts
focus in the new-list input in one click. Google Tasks workspaces are
read-only so they still get the explanatory text instead.
2026-04-17 16:27:18 +00:00
Claude 391c42aa18
fix(rename): imperatively focus + select rename inputs
Svelte's native autofocus attribute is unreliable for inputs rendered
via conditional blocks (prior smoke-test fixed this for the new-list
input). Apply the same bind:this + $effect pattern to the list-rename
input (TasksScreen) and the workspace-rename input (SettingsScreen),
and select() the existing text so typing replaces the old name
cleanly.
2026-04-17 16:26:29 +00:00
Claude 6283f9ab2c
fix(store): guard fs-changed listener against setup/missing screens
The module-scope fs-changed listener fired unconditionally, calling
loadLists even when the user was on the setup or missing-workspace
screens (where no current workspace exists). The invoke would fail
silently and a WebDAV debounced sync could kick off against an
incomplete state. Bail when there's no active workspace or the tasks
screen isn't mounted.
2026-04-17 16:25:39 +00:00
Claude 5869c305aa
fix(bulk-delete): snapshot targets and bail on first failure
executeDeleteCompleted and executeDeleteCompletedSubtasks iterated over
the reactive completedTasks/completedSubtasks lists with no error
handling: the array shrinks with every successful delete, skipping
subsequent entries, and a failed delete silently left a half-deleted
state. Snapshot the target list up front and abort as soon as a delete
returns false — matching the subtask-cascade path.
2026-04-17 16:25:03 +00:00
Claude d213e523ec
fix(sync): narrow transient-error detection so real errors aren't hidden
The connectivity-vs-real-error classifier tested the message against
/timeout|connect|network|unreachable|refused/i, matching any error
whose text happened to include one of those words. A server-side
permission error like 'network share access refused' was silently
classified as transient, updating only the status dot — the user
never saw the actual problem.

Tighten the regex to well-known connectivity phrases and lowercase
error codes (ENOTFOUND/ECONNREFUSED/etc), using word boundaries so
substrings in unrelated messages don't match.
2026-04-17 16:24:20 +00:00
Claude 0fc1f16c9d
fix(new-task): attach date in a single create_task call to prevent loss
The new-task bottom sheet called createTask then, if a date was set,
made a follow-up updateTask to attach the date. If the update failed
(e.g. filesystem error between the two writes) the user was left with
a dateless task and, because transient sync errors are already
suppressed, often no visible error either.

Extend the create_task Tauri command to accept optional date/has_time
fields and pass them through. The frontend now creates the task in one
round-trip. No separate update path needed.
2026-04-17 16:23:51 +00:00
Claude d01bd9d280
fix(settings): stop clobbering WebDAV edits and save without a successful test
Two coupled issues in workspace settings:

1. The credentials-loading effect re-ran whenever ws.webdav_url changed,
   so any config mutation (e.g. changing sync interval) would trigger a
   re-load of the stored username/password, overwriting whatever the
   user was typing into those fields. Gate with a one-shot credsLoaded
   flag.

2. Save would persist whatever was in the URL input even if the user
   had never tested it — a typo'd host silently pointed the workspace
   at a dead server. Now saveWebdav auto-runs the connection test and
   bails if it fails; any edit to the three inputs clears the "ok"
   status via markDirty() so the next Save is forced to re-verify.

Also replaces the ASCII "Failed -- Retry" with an em dash.
2026-04-17 16:22:31 +00:00
Claude b437b0b7b2
fix(sync): use atomic_write for all payload file writes during sync
Sync's conflict-resolution and download paths wrote the local file with
plain fs::write. A crash or I/O error mid-write left a truncated .md
or .listdata.json that would then fail YAML/JSON parsing on the next
list_tasks. All other callers in this crate use atomic_write; route
the four sync call sites through it for consistency and crash safety.
2026-04-17 16:21:24 +00:00