feat(ui): add an indefinite pause and a paused banner with one-click resume - #151
Merged
Merged
Conversation
pmaxhogan
force-pushed
the
fix/tray-pause-not-pausing
branch
from
July 25, 2026 03:53
eacf51b to
ab20272
Compare
pmaxhogan
force-pushed
the
feat/indefinite-pause-ui
branch
from
July 25, 2026 04:01
6d97f38 to
88fa297
Compare
…resume The tray could only pause for 30 minutes, and nothing in the window said backups were paused at all - the only signal was the tray icon. - Tray: "Pause until I resume" next to "Pause for 30 minutes". - A yellow banner at the top of the window whenever a manual pause is in force: "Backups paused indefinitely" or "Backups paused - 27m left" (counting down live), with a Resume button that unpauses on one click. - IPC: `get_pause_state` plus a `sync:pause_changed` event carrying the pause kind and deadline, so the banner appears/disappears with no refresh; a pause Pinia store subscribed at the app root. - The pause is now persisted, so it survives a restart as DESIGN s5.7 already claimed. A timed pause re-arms only the time it has LEFT; one that elapsed while the app was closed is cleared, not re-applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YnYLjHgYhhnJjUxojsRhn
Nothing in the run loop's select selected on the manual-pause signal, so
clearing it (tray "Resume sync", the paused banner's Resume button, or a
timed pause auto-expiring) left the next cycle waiting for the scheduled
tick - 600s in the shipped default. That made the banner's "unpauses
immediately" untrue.
Adds a pause-change arm. Resuming runs a cycle now; a pause re-derives
Paused{Manual} through the same gate check and issues zero remote calls.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016YnYLjHgYhhnJjUxojsRhn
pmaxhogan
force-pushed
the
feat/indefinite-pause-ui
branch
from
July 25, 2026 04:18
88fa297 to
1bdfec1
Compare
Contributor
Coverage
Gate: passed - no coverage regression (epsilon 0.1 pp). |
The hand-rolled env::temp_dir()+format!(tag) path tripped CodeQL rust/path-injection (2 high alerts on PR 151). RAII TempDir removes the tainted flow and cleans up after itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YnYLjHgYhhnJjUxojsRhn
pmaxhogan
added a commit
that referenced
this pull request
Jul 25, 2026
🤖 I have created a release *beep* *boop* --- ## [2.2.0](v2.1.0...v2.2.0) (2026-07-25) ### Features * **ui:** add an indefinite pause and a paused banner with one-click resume ([#151](#151)) ([090d70c](090d70c)) * **ui:** last-5m throughput sparkline behind the current-throughput stat ([#153](#153)) ([e60b27f](e60b27f)) ### Bug Fixes * **core:** make the manual pause actually halt in-flight upload dispatch ([#148](#148)) ([d984826](d984826)) * **core:** mirror local folder structure on Drive for plaintext sources ([#150](#150)) ([a5d0673](a5d0673)) * **ui:** stream live scan progress from the moment Run Now is clicked ([#149](#149)) ([12ddd1e](12ddd1e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
An indefinite pause. The tray menu gains "Pause until I resume" beside
"Pause for 30 minutes". It arms no auto-resume timer - only the tray's "Resume
sync" or the banner's Resume button clears it.
A paused banner. A full-width amber bar under the progress bar, shown
whenever a manual pause is in force:
Backups paused - 27m leftfor a timed pause, counting down live (aone-second tick that re-reads the wall clock, so it stays correct across a
laptop suspend instead of drifting).
Backups paused indefinitelyfor the open-ended one.Resumebutton that unpauses on one click. It clears the banneroptimistically and rolls back (keeping the banner, showing the error) if the
backend rejects - a failed resume must never look like a successful one.
Styling follows the existing amber-alert idiom already used for the
needs-reauth banner in
AccountList.vue(border-amber-400 bg-amber-50 text-amber-800with thedark:variants), and the show/hide fade mirrorsGlobalProgressBar.vue.Resume now actually restarts backups. Found while checking that the Resume
button does what the label says: nothing in the orchestrator's run-loop
select!selected on the manual-pause signal (orchestrator.rs:484threw thereceiver away), so clearing the pause left the next cycle waiting for the
scheduled tick - 600 s as shipped. Resume would clear the banner and then do
nothing visible for up to ten minutes. This was invisible before #148 (the
pause never stopped anything, so there was nothing to restart) and user-facing
after it. Fixed with a pause-change arm in the run loop, which covers every
resume path at once - tray, banner, and the timed auto-expiry - rather than
sprinkling a trigger at each call site. The pause direction goes through the
same gate check and issues zero remote calls.
Pause state over IPC.
get_pause_state()returns{"kind":"timed","until_ms":...}/{"kind":"indefinite"}/null, and a newsync:pause_changedevent carries the same payload on every pause, resume,auto-expiry, and boot-time restore. A
pausePinia store owns the subscriptionat the app root (same pattern as the updater and progress stores), so the banner
reacts live on any route - including to a pause set from the tray while the
window was closed.
The pause now actually persists. DESIGN s5.7 has always said "manual pause
persists across restarts" and it did not: the pause was an in-memory watch cell
plus a detached timer, so restarting silently resumed backups the user had
paused. It is now written to the settings table and re-applied at boot.
Deliberately, a timed pause re-arms only the time it has LEFT (pause 30m,
restart 25 minutes later, resume 5 minutes later - not a fresh 30), and one that
elapsed while the app was closed is cleared rather than re-applied. This is
implemented as the fix for that gap rather than as a second mechanism beside it,
so there is exactly one source of truth for "is sync paused".
Tests
Rust (6 new; 340 driven-core + 249 driven-app pass):
orchestrator::tests::run_loop_wakes_and_runs_a_cycle_when_the_manual_pause_clears-a one-hour scan interval, so if the resume did not WAKE the loop nothing could
run a cycle inside the timeout. Verified to fail without the new arm (times
out after 30 s) and pass with it.
pause_state_serializes_kind_tagged_snake_case- pins the wire shape theTypeScript
PauseStateunion mirrors, both directions. A rename on eitherside would otherwise silently blank the banner.
timed_pause_expires_at_its_deadline_indefinite_never_doesremaining_is_the_leftover_window_saturating_at_zero- the restart-resumes-the-remainder rule, and no negative/underflowed duration on an elapsed one.
pause_state_round_trips_through_the_settings_table_and_clears- bothvariants through a real
SqliteStateRepo, and that a resume CLEARS the record(a stale one would have a later boot re-apply a pause the user ended).
an_unparseable_pause_record_reads_as_not_paused- a corrupt record on theboot path degrades to "running", never to an error.
tray.pause_indefinite,so a menu item added in code but missing from the YAML still fails there
rather than shipping as a raw key.
Vitest (20 new; 309 pass):
pause-store.test.ts(13) - timed vs indefinite shapes, minutes rounding upso the last partial minute reads "1m left", the elapsed-pause floor at zero,
the tick recomputing from the wall clock rather than decrementing, live
ingest + clear, idempotent subscribe, hydrate (including a swallowed backend
failure), and resume's optimistic clear + rollback-and-rethrow.
paused-banner.test.ts(7) - a mount test per render branch: hidden whenunpaused, both copy variants, the live countdown, hiding when the pause
clears, the Resume click, and the failed-resume path keeping the banner and
showing the error.
app-shell.test.tsupdated for the fifth listener + the new hydrate call.Gates
cargo test -p driven-core -p driven-app- 340 + 249 + integration suitespass, 0 fail.
cargo clippy --workspace --all-targets -- -D warnings- clean.cargo fmt --all --check- clean.pnpm test:unit- 34 files, 309 tests pass.pnpm lint- 0 errors (17 pre-existing unused-i18n-key warnings, none new).pnpm format:check- clean.pnpm build(vue-tsc --noEmit && vite build) - clean.mainbase: UI linecoverage measured on this branch (89.89%) vs the base branch, whose UI tree is
identical to
main(89.71%) - it goes UP, and every added file is above themean (
PausedBanner.vue100%,pause.ts92.2%,App.vue97.5%). The Rusthalf is unaffected because
coverage.ymlrunscargo llvm-cov --workspace --exclude src-tauri.Notes
tray action), so "any in-app pause control" came down to the banner's Resume.
OrchestratorState, so it isspecifically about the MANUAL pause. An automatic pause (battery, metered,
offline, schedule) still surfaces through the tray icon and tooltip only -
giving those the same treatment is a separate change.
🤖 Generated with Claude Code
https://claude.ai/code/session_016YnYLjHgYhhnJjUxojsRhn