Skip to content

Commit e6427c7

Browse files
pmaxhoganclaude
andauthored
feat(core): live exclusion pickup and a visible pending-work queue (#313)
Closes #302, closes #303, closes #304. PR2 of the v2.12.0 wave. ## #302 - exclusion edits take effect mid-backup The exclusion matcher is snapshotted once per scan (`scanner.rs`), so saving a new rule only took effect on the NEXT cycle: a plan that runs for hours kept uploading a folder the user had just excluded, for the whole remainder of that plan. - `Executor::set_live_exclusions(source_id, generation, matcher)` swaps a freshly built `SourceMatcher` into the running executor (`std::sync::Mutex<HashMap<SourceId, _>>`; the dispatch loop clones an `Arc` per op and never holds the lock across an await). - `execute`'s dispatch loop re-checks each op against it EVERY iteration, right beside the existing manual-pause gate. Skipped ops are never dispatched and keep no `file_state` row, so the next scan re-plans cleanly. - Fail-open, mirroring the reconcile-time check at `executor.rs` `op_path_now_excluded`: a matcher that could not be built publishes `None` and everything keeps dispatching. A broken ignore file must not strand a backup. - Ordering: the generation is assigned BEFORE the (blocking) build starts, so a slow build cannot clobber a newer one that finished first. - A bundle op is dropped only when EVERY member is newly excluded - members are packed into one remote object and cannot be trimmed in flight. - `Orchestrator::refresh_source_exclusions(&SourceRow)`, called from `update_source` only when the patch actually touched include/exclude/`respect_gitignore`, does the build off-thread and arms ONE coalesced follow-up rescan 2s after the last edit of a burst (each edit aborts the previous timer), so the totals correct themselves and re-included paths get picked up. - Files already uploaded before the edit are left alone (no retro-trash), per the locked spec. ## #303 - an explicit pending-work queue `trigger()` used to `try_send` into a capacity-1 channel, so a burst of unrelated requests (a recovery, three watcher ticks, a manual click) collapsed into one anonymous follow-up that the user could not see or cancel. - New `crates/driven-core/src/queue.rs`: a plain synchronous `WorkQueue` (no channels, no async) so the coalescing / ordering / cancel rules are directly unit-testable. Items are `{ id, kind, source_id, enqueued_at, tick }`; kinds are recovery / watcher / manual / scheduled. - Coalescing is per `(kind, source_id)` among PENDING items only - work requested after a cycle started may have been missed by that cycle's scan, so it earns its own follow-up. Recovery jumps the queue (a fresh scan ahead of it would re-upload bytes already on the remote). - The old channel is now a capacity-1 wake NUDGE carrying no value; the queue is the single source of truth. The run loop runs at most one item per `select!` iteration and re-nudges itself if more is pending, so shutdown / pause / power arms are never starved. - Single-in-flight-cycle-per-account is unchanged: `start_next` refuses to hand out a second item while one is running. - Cancel: a pending item is dropped; the RUNNING item is stopped through the existing pause-drain (`executor.set_paused(true)`), and the run loop restores the gate from the account's real manual-pause state when that cycle returns, so a cancel cannot leak into the next item. Clear all = cancel every pending + the same graceful stop. - A crash-recovery item is queued at run-loop start when `pending_ops` shows an interrupted run (best-effort: a state-repo error costs the LABEL, never the recovery). The queue is cleared when the loop exits so a suspended account cannot leave phantom work on screen. - Wire-up: `OrchestratorEvent::WorkQueueChanged` -> a new `BridgeAction` -> `queue:changed` (whole snapshot, never a delta, so a missed event self-heals), plus `get_work_queue` / `cancel_work_item` / `clear_work_queue` IPC. In-memory only, per the locked spec. - `sync_now(source_id)` now attributes its trigger to that source so the queue can name it. ## #304 - the top-bar work-queue menu Follows section 5 of the approved mockup. - New `DropdownPanel.vue`, the app's first dropdown primitive (everything overlay-shaped so far has been a modal): real `<button>` trigger with `aria-expanded` + `aria-controls`, Escape closes and returns focus to the trigger, pointer-outside closes, listeners only while open. Deliberately `role="group"`, not `role="menu"` - the rows hold prose, progress bars and several controls, and menu semantics would promise arrow-key navigation that does not exist. - New `WorkQueueMenu.vue`: list glyph + pending-plus-running badge, default-collapsed ~360px panel, running row with its own per-account progress bar, one glyph per pending kind, Clear all in the header, the "Items run one at a time per account." footer, and the "No pending work - next scheduled backup HH:MM" empty state. - New `workQueue` Pinia setup store following the `progress.ts` subscribe/hydrate pattern, subscribed at the app root in `App.vue` so the badge is right the moment the window opens. Source display NAMES are resolved client-side from `list_sources` (the backend queue deals in ids); a name we do not have costs a label, never a row. - Typed IPC only (`ui/src/ipc/commands.ts` + `events.ts`), i18n via `t()`, Tailwind classes matching the shell's existing zinc/teal conventions. ## Scope note Queue items carry a source for DISPLAY and coalescing; a cycle still runs every enabled source of its account, which is exactly what the panel's footer tells the user. Per-source cycle execution would be a much larger orchestrator change and is not in this PR. ## Tests - `cargo test --workspace`: green (58 test binaries, 0 failures). New: 11 `queue.rs` unit tests (FIFO + coalescing + recovery-first + single-in-flight + cancel/clear/ids), 4 executor tests (mid-plan swap stops newly-excluded ops with no `file_state` left behind, fail-open on a failed build, stale-generation publish dropped, another source's rules are inert), 7 orchestrator tests (publish + rising generation, an edit burst earns exactly one rescan, triggers become visible items and coalesce, cancelling the running item drains it then restores the gate, clear-all, crash-recovery item present/absent). - `pnpm -C ui test:unit`: 813 passed / 61 files. New: 14 store tests + 10 mount tests covering the menu and the dropdown primitive. Coverage on the new files: `WorkQueueMenu.vue` 96.8%, `DropdownPanel.vue` 95.5%, `workQueue.ts` 92.1% (total 93.15%). - `cargo clippy --workspace --all-targets`: 0 warnings. `cargo fmt`, `prettier`, `eslint`: clean (eslint's 35 warnings are all pre-existing unused-i18n-key ones; none in the new files). - `just visual-update` regenerated the linux baselines in the Playwright container - 106 specs pass, including two new shell scenarios (`work-queue-busy`, `work-queue-empty`). All existing baselines shifted because the nav button lives in the sticky header on every page. - NOT run: `just e2e` (the containerized app-level suite) - left to the wave's integration pass. ## README Updated in this PR (repo rule): a Features bullet for immediate exclusion rules and one for the visible work queue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019xKUm9vH4ifb5LHR5szy1v Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6d8e1ab commit e6427c7

130 files changed

Lines changed: 3275 additions & 44 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ These move: check each project's current docs before relying on a cell.
123123
directories instead of descending them.
124124
- Live exclusion preview that re-classifies the folder tree as you edit a rule,
125125
from an in-memory tree rather than a fresh walk of the disk.
126+
- Exclusion rules that take effect immediately, even mid-backup: saving a new
127+
rule stops the running backup from uploading anything it newly excludes at the
128+
next file boundary (the file already in flight still finishes cleanly), and
129+
one follow-up scan re-derives the totals once your edits settle. Files
130+
uploaded before the edit are left alone - excluding a folder never deletes
131+
what is already backed up.
132+
- A visible work queue in the top bar: crash-recovery passes, watcher ticks,
133+
"Back up now" clicks, and due scheduled runs are listed with what is running
134+
and what is waiting, instead of silently collapsing into one anonymous
135+
follow-up. Items run one at a time per account, each can be cancelled, and
136+
cancelling the running one is a graceful stop - it stops starting new files,
137+
lets the in-flight ones finish and commit, and re-plans the rest later.
126138
- Nested backup sources, as long as they cannot overlap: a source may contain
127139
another source's folder when its own exclude rules cover that folder (e.g.
128140
back up your home folder with `Documents` excluded while `Documents` is

crates/driven-core/src/executor.rs

Lines changed: 335 additions & 0 deletions
Large diffs are not rendered by default.

crates/driven-core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub mod planner;
3535
/// outside `driven-core` never open a source file themselves.
3636
mod platform_open;
3737
pub mod priority;
38+
/// The per-account visible pending-work queue (issue #303): what is running,
39+
/// what is waiting, and the coalescing / ordering / cancel rules for both.
40+
pub mod queue;
3841
/// Headless restore (download + decrypt + verify + write) for round-trip
3942
/// testing without the GUI. The GUI's own restore path stays in `src-tauri`.
4043
pub mod restore_fetch;

crates/driven-core/src/orchestrator.rs

Lines changed: 835 additions & 39 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)