feat(ui): land the background update scheduler + one-click install button#101
Merged
Conversation
* feat(ui): single-flight update pipeline, sticky toasts, save-pending probe
Groundwork for the background update scheduler (OB-344):
- lib/updateRunner: module-singleton single-flight for checkForUpdate and
downloadAndInstall, shared by Settings check-now and the scheduler so the
two paths can never double-run; stamps lastCheckAt on every attempt and
lastCheckSuccessAt only on completion (policy moves here from the section).
- UpdatesSection: check-now goes through the runner; inline result behavior
unchanged (error keeps the old 'Last checked', success refreshes it).
- toast: durationMs Infinity = persistent toast (no auto-dismiss timer) for
the staged-security-update case.
- pageSaveStatus.anyPageSavePending(): 'is a restart risky right now?' probe.
- updateCheck.semverMajor(): major extraction for the never-cross-majors rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(ui): background update scheduler — cadence, jitter, decision logic, update-flow UX
The final build item of the auto-updates epic (OB-344):
- lib/updateScheduler: pure decision module. isBackgroundCheckDue (daily>24h,
weekly>7d, never=zero requests; ±10% jitter against release-day thundering
herd; future lastCheckAt = clock skew = stale) and decideUpdateAction
(security-only gates installs, not checks; a newer major is NEVER installed —
informational toast at most once per major, persisted in
updates.majorAnnounced; majors announce even from an up-to-date result).
- components/UpdateScheduler: host mounted in DefaultLayout (PluginBoot
pattern — inside ConfirmProvider, next to ToastHost, so both shells get it
with zero per-shell wiring; inert without platform.updates). Launch tick +
15min interval; acts via the shared runner; download then toast 'Update
ready — Restart to update' (persistent for security fixes); background
failures are console.debug-silent, retried next due tick.
- Restart action relaunches directly (autosave flushes edits); a confirm
dialog guards only the risky case where a page save is mid-flight/failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(ui,web): update-scheduler decision matrix, staleness math, runner + host units, e2e
- updateScheduler.test: exact 30-cell table {daily,weekly,never} ×
{securityOnly on/off} × {no update, minor, security, new major, error} →
{no-op, install-update, install-security, announce-major}; staleness incl.
clock-skew and jitter bounds; once-per-major persistence.
- updateRunner.test: single-flight for check + install, attempt-vs-success
stamping, never-rejects, rejection propagation.
- UpdateScheduler.test: host-level matrix with a mocked platform — never =
zero calls, security-only filtering, persistent security toast whose action
relaunches, silent error/download failures, once-per-major announce.
- web e2e (update-scheduler.spec): via the ?updates= seam, which now counts
check/install/relaunch calls on window — security-only ON + non-security
update → no toast/install; security update → persistent toast (outlives the
7s auto-dismiss) whose action relaunches; cadence never → zero checks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(ui,web): durable major-availability surface + update-flow polish (review advisories)
Design-gate advisories on the update scheduler (OB-344):
- M1: the once-per-major toast was the ONLY major signal (~7s, no action;
Settings read 'Up to date' when the current line is current). The shared
runner now records updates.latestMajorSeen on every successful check
(clearing it when none is reported, so a stale '2.x is available' can't
outlive an upgrade), and the Updates section renders it as a persistent
informational line — 'OpenBook {major}.x is available' — between checks
and across sessions. Toast behavior unchanged (still once per major).
- L1: the restart-while-saves-pending confirm is destructive:true
(data-loss class, matches the trash idiom).
- L2: confirm body reworded to 'Some changes haven't been saved yet' —
anyPageSavePending() also matches save-FAILED, which 'still saving' misled.
- L3: ?updates=major seam mode (up-to-date + latestMajor, the shape the check
API produces for a major-only bump) so the major path is observable in
browser/e2e/Chromatic; new e2e covers toast-once + durable Settings line
surviving a reload with no re-toast.
Tests: +5 unit (runner records/clears/preserves latestMajorSeen; persistence
round-trip), +1 host assertion, +1 e2e. updates.majorAvailableToast renamed
majorAvailable (now shared by toast + Settings line).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add an "Install & restart" action to UpdatesSection, shown when a manual check reports status 'update-available'. It runs the existing single-flight install pipeline — runDownloadAndInstall(updates) then updates.relaunch() — so a background download in flight is joined, never doubled. - Progress: button disabled + aria-busy with "Downloading…" (stage) then "Installing…" (relaunch) phase labels; spinner icon while in progress. - Error: a role="alert" inline surface on download/verify/relaunch failure; cleared on the next check. The check status span keeps its aria-live. - Restart guard: reuses the UpdateScheduler.restart() idiom — confirm via restartConfirm* i18n only when anyPageSavePending(). - Desktop-only (gated on platform.updates); scheduler + apply-on-restart toast flow untouched (additive). - i18n: updates.installAction/downloading/installing/installError (en). - SettingsPanel unit test now wraps ConfirmProvider (UpdatesSection consumes useConfirm, as sibling settings tabs already do). - e2e: update-scheduler.spec asserts the button appears post-check and one click drives install+relaunch via the web-stub counters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Jul 5, 2026
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.
Problem
main already ships the desktop updater runtime + signing + install plumbing (PRs #84/#85), but the background scheduler (#86) never reached main due to a merge-ordering quirk, and there was no install affordance in Settings. Net: the app can check for updates but nothing drives auto-download, and an available update dead-ends at a text label — it never installs and there's no one-click action. Board: Epic OB-338 (auto-updates) + the install-button task.
Solution
Re-land two already-reviewed pieces onto current main:
updateScheduler.ts(cadence daily>24h / weekly>7d / never; ±10% jitter; security-only gates installs not checks; new major = announce-only, never auto-installed),updateRunner.ts(single-flight check+install shared with Settings),UpdateScheduler.tsxmounted inDefaultLayout(launch tick + 15-min interval; silent background failures), persistent "Restart to update" toast for security updates, and a durable "OpenBook N.x is available" line in Settings. Code (Quinn) + design (Devon) previously cleared, all advisories folded.update-available, runsrunDownloadAndInstall→ save-guardedrelaunch(), with progress + error + a11y states; additive to the scheduler/toast. Design (Devon) + code (Quinn) previously cleared.Before / After
Test procedure
pnpm verifygreen (report counts).pnpm --filter @book.dev/web test:e2e update-scheduler.spec.ts— scheduler + install-button e2e via the?updates=seam (absence→presence→install/relaunch counters; cadence gating; security persistent toast).Operational notes
account.book.pub/api/updates/*endpoints to be deployed for real end-to-end update delivery (separate repo).Footer: verify green (counts above) + web e2e; re-landing code that previously cleared code (Quinn) + design (Devon) gates — no new review surface.