Commit e6427c7
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
- crates/driven-core/src
- src-tauri/src
- commands
- ui
- e2e-visual
- __screenshots__/linux
- dark
- activity.spec.ts
- restore.spec.ts
- settings.spec.ts
- setup-wizard.spec.ts
- shell.spec.ts
- light
- activity.spec.ts
- restore.spec.ts
- settings.spec.ts
- setup-wizard.spec.ts
- shell.spec.ts
- src
- __tests__
- components
- ipc
- locales
- stores
- test-support
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
126 | 138 | | |
127 | 139 | | |
128 | 140 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
38 | 41 | | |
39 | 42 | | |
40 | 43 | | |
| |||
Large diffs are not rendered by default.
0 commit comments