From 68401d66e05fdd296225e211886336bed843c636 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Sun, 4 Oct 2026 17:42:46 +0200 Subject: [PATCH] refac: revamp the update flow in settings so the user has better visibility that it has to restart when update downloaded --- docs/design/components.md | 2 +- docs/design/screens/settings.md | 14 ++++--- docs/design/source/AppFrame.dc.html | 2 +- .../RenewCMS Screens Back-office.dc.html | 2 +- features/updates_download_stage.feature | 2 +- web/src/components/UpdateStatusBlock.vue | 14 ++++++- web/src/components/UpdatesCard.vue | 38 +++++++++++++++++-- .../views/__tests__/SettingsRestart.spec.ts | 2 +- .../__tests__/SettingsUpdateDownload.spec.ts | 10 +++++ 9 files changed, 70 insertions(+), 16 deletions(-) diff --git a/docs/design/components.md b/docs/design/components.md index 8aa9bb2..9c8c3fc 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -802,7 +802,7 @@ Exact values live in `source/AppFrame.dc.html` (the Settings and Home states). C | Component | Nuxt UI base | Props | Emits | Used in | |---|---|---|---|---| | UpdatesCard | custom card, UProgress, UButton, UCollapsible | status: UpdateStatus, role | check, update, restart, copy-command | Settings | -| UpdateStatusBlock | custom (polite live region) | tone, icon, title, text, meta | none | UpdatesCard | +| UpdateStatusBlock | custom (polite live region) | tone, icon, title, text, meta, pendingTag?, action? (in-block Restart, Ready only) | none | UpdatesCard | | UpdateProgress | UProgress | label, text, value or indeterminate | none | UpdatesCard | | ReleaseNotes | UCollapsible | version, date, items | none | UpdatesCard | | ServerCommand | UButton, code block | line | copy | UpdatesCard (Not prepared) | diff --git a/docs/design/screens/settings.md b/docs/design/screens/settings.md index c2abc93..f64eed1 100644 --- a/docs/design/screens/settings.md +++ b/docs/design/screens/settings.md @@ -20,7 +20,9 @@ Title "Settings", subtitle "Workspace information.", 720 px at most, 20 px betwe | Part | Content | |---|---| -| Status block | A polite live region (`aria-live="polite"`): 14 px padding, 12 px gap, an 18 px icon, a semibold title, the text, and a muted `meta` line. Background and icon color follow the tone: neutral `--ui-bg-muted`, success, info, warning, error (`--rc--soft` fill, `--ui-` icon). | +| Status block | A polite live region (`aria-live="polite"`): 14 px padding, 12 px gap, an 18 px icon, a semibold title, the text, and a muted `meta` line. Background and icon color follow the tone: neutral `--ui-bg-muted`, success, info, warning, error (`--rc--soft` fill, `--ui-` icon). The title row is a flex row with an 8 px gap that wraps. In "Ready to install" it carries a **Pending tag** after the title and the block holds the **Restart** button (see below). | +| Pending tag | Only in "Ready to install", right after the title: 20 px high, 0 6 px padding, IBM Plex Mono 11 px medium, `--ui-bg` fill, `--ui-warning` text and 1 px `--ui-warning` border, square corners, text "Pending". | +| In-block button | Only in "Ready to install", "Restart: confirm" and "Restart: request failed": the primary Restart button sits inside the status block, under the `meta` line, after 8 px of extra top padding, left-aligned (content width, 14 px side padding, 32 px high, 44 px on mobile, not stretched). The buttons row under the block is not drawn in these states. The hint stays under the card. | | Progress | A `progressbar` with a value (`aria-valuenow`) while downloading, labelled for example "Downloading version 2.3.0", with the text `42% · 11 of 26 MB`. Indeterminate while verifying (35 % wide moving bar), labelled "Checking the download", text "Almost there". Announced at 25 % steps and when the step changes, never on every tick. | | Release notes | A button "What's new in 2.3.0" with `aria-expanded` and a chevron, collapsed by default. Open: "Released 24 Sep 2026" and a plain-text list. | | Server command | Only in the "Not prepared" state: "For whoever manages the server", a one-line `code` block, a Copy button (reads "Copied" with a check for 2 s, announced), and the note "They run this once on the server. After that, Update and Restart appear here." The command shown is `renewcms enable-browser-updates` and is subject to the real one. | @@ -28,7 +30,7 @@ Title "Settings", subtitle "Workspace information.", 720 px at most, 20 px betwe | Hint | 12 px muted line under the buttons. | | Link | Underlined, with an `arrow-up-right`: "How to update" or "Read the release notes". | -Buttons used: **Check for updates** (`refresh-cw`, secondary), **Update** (`download`, primary), **Restart** (`power`, primary when an update is ready, otherwise secondary), **Try again** (`rotate-cw`, primary), and the disabled "Checking…" and "Updating…" (`loader-circle`). Copy such as "Version 2.3.0" names the newer release. Decision for the spec: show the release tag without its leading `v` ("Version 2.3.0-beta" for tag `v2.3.0-beta`), while the Version card keeps the running build exactly as the server reports it. +Buttons used: **Check for updates** (`refresh-cw`, secondary), **Update** (`download`, primary), **Restart** (`power`, primary and inside the status block when an update is ready, otherwise secondary in the buttons row), **Try again** (`rotate-cw`, primary), and the disabled "Checking…" and "Updating…" (`loader-circle`). Copy such as "Version 2.3.0" names the newer release. Decision for the spec: show the release tag without its leading `v` ("Version 2.3.0-beta" for tag `v2.3.0-beta`), while the Version card keeps the running build exactly as the server reports it. ## Copy @@ -40,7 +42,7 @@ Buttons used: **Check for updates** (`refresh-cw`, secondary), **Update** (`down | Available | `circle-arrow-up`, info: "Version 2.3.0 is available" / "You have v2.2.0-beta. Update downloads the new version and checks it. Nothing changes until you restart."; "Update"; hint "Takes about 2 minutes. Your site stays online." | | Downloading | `download`, info: "Downloading version 2.3.0…" / "You can leave this page. The download continues and your site stays online."; "Updating…" disabled | | Verifying | `shield-check`, info: "Checking the download…" / "Making sure the file is complete and safe to install. This takes a few seconds." | -| Ready | `circle-check`, success: "Version 2.3.0 is ready to install" / "The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds." / "Nothing has changed yet."; "Restart"; hint "Not now? Leave this page. The update waits here, and the banner on Home reminds you." | +| Ready | `clock`, warning, with the "Pending" tag after the title: "Version 2.3.0 is ready to install" / "The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds." / "Nothing has changed yet."; "Restart" (primary, inside the block); hint "Not now? Leave this page. The update waits here, and the banner on Home reminds you." | | Done | "You're on version 2.3.0" / "The update is installed and your site is running again." / "Updated today at 09:52"; link "Read the release notes"; toast `circle-check` "Update installed" / "RenewCMS is now on version 2.3.0." | | Rolled back | `triangle-alert`, warning: "The update didn't install" / "RenewCMS put the previous version back, so your site is running as before. Nothing was lost." / "What to do next: try again in a few minutes. If it fails again, ask whoever manages your server to check the update."; "Try again"; toast (error, stays) `circle-x` "The update didn't install" / "Your site is back on v2.2.0-beta." | | Download failed | `circle-x`, error: "The download didn't finish" / "The connection dropped before the file was complete. Nothing was changed and your site is running as before."; "Try again" | @@ -105,9 +107,9 @@ For the owner only: an **Update** pill on the Settings sidebar item, and a dot o | Update available, notes open | "What's new in 2.3.0" expanded. It is a button with aria-expanded, and the list is plain text. | | Downloading | Update pressed (202). Progress polls every second. Announced as "Downloading, 25 percent" and so on, never every tick. | | Verifying | The download finished. No percentage, so the bar is indeterminate. Announced as "Checking the download". | -| Ready to install | The file passed its checks. Restart becomes the primary action. "Restart later" is closing the dialog or leaving the page. | -| Restart: confirm | Restart pressed. Same dialog behaviour as the delete confirm: focus moves to the first button, Escape and "Restart later" close it, and focus returns to Restart. The confirm button is neutral, not red, because nothing is lost. | -| Restart: request failed | 500 or network failure before the restart began. The site is still running. | +| Ready to install | The file passed its checks. The card turns warning (orange) with a "Pending" tag and a `clock` icon, and Restart, the primary action, sits inside it. "Restart later" is closing the dialog or leaving the page. | +| Restart: confirm | Restart pressed. The card behind the dialog keeps the Ready look. Same dialog behaviour as the delete confirm: focus moves to the first button, Escape and "Restart later" close it, and focus returns to Restart. The confirm button is neutral, not red, because nothing is lost. | +| Restart: request failed | 500 or network failure before the restart began. The site is still running. The card behind the dialog keeps the Ready look. | | Restarting to install | Full-page overlay, so it survives the server going away. The page polls until the site answers, then reloads. It is a live region announced once as "Installing version 2.3.0. Your site is restarting." | | Restarting, taking longer | No answer after 60 seconds. The copy changes and a manual reload appears. The overlay stays until the site is back. | | Update done | The page reloaded on the new version. Focus lands on the "Updates" heading (ring shown), the toast is announced, and the version updates. | diff --git a/docs/design/source/AppFrame.dc.html b/docs/design/source/AppFrame.dc.html index 837a490..537a70d 100644 --- a/docs/design/source/AppFrame.dc.html +++ b/docs/design/source/AppFrame.dc.html @@ -233,7 +233,7 @@
Version{{ s.upd.v }}
Updates -
{{ s.upd.title }}{{ s.upd.text }}
{{ s.upd.barText }}
{{ s.upd.meta }}
+
{{ s.upd.title }}Pending
{{ s.upd.text }}
{{ s.upd.barText }}
{{ s.upd.meta }}
{{ s.upd.inBtn.t }}
What's new in {{ s.upd.notes.ver }}
{{ s.upd.notes.date }}
{{ it.t }}
Read the full release notes
For whoever manages the server
{{ s.upd.cmd.line }}{{ s.upd.cmd.t }}
{{ s.upd.cmd.note }}
{{ b.t }}
diff --git a/docs/design/source/RenewCMS Screens Back-office.dc.html b/docs/design/source/RenewCMS Screens Back-office.dc.html index 291c4c2..af5a34d 100644 --- a/docs/design/source/RenewCMS Screens Back-office.dc.html +++ b/docs/design/source/RenewCMS Screens Back-office.dc.html @@ -120,7 +120,7 @@ class Component extends DCLogic { const pk = (o = {}, x = {}) => MS({ kind: 'picker', picker: true, pk: { tab: 'Library', ...o }, ...x }) const TB = { neutral: 'var(--ui-bg-muted)', success: 'var(--rc-success-soft)', info: 'var(--rc-info-soft)', warning: 'var(--rc-warning-soft)', error: 'var(--rc-error-soft)' } const B = (t, kind, icon, dis) => ({ t, icon, bg: kind === 'primary' ? 'var(--ui-primary)' : 'var(--ui-bg)', fg: kind === 'primary' ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)', bd: kind === 'primary' ? 'var(--ui-primary)' : 'var(--ui-border-accented)', op: dis ? '0.4' : '1' }) - const UPD = (o) => { const tone = o.tone || 'neutral'; return { v: 'v2.2.0-beta', focus: 'none', ...o, fg: tone === 'neutral' ? 'var(--ui-text-toned)' : 'var(--ui-' + tone + ')', bg: TB[tone], bar: o.pct !== undefined || !!o.indet, barW: o.indet ? '35%' : (o.pct || 0) + '%', barL: o.indet ? '30%' : '0', pct: o.indet ? undefined : o.pct } } + const UPD = (o) => { const rdy = o.title === 'Version 2.3.0 is ready to install' && o.btns && o.btns.length; if (rdy) o = { ...o, tone: 'warning', icon: 'clock', pendingTag: true, inBtn: o.btns[0], btns: undefined }; const tone = o.tone || 'neutral'; return { v: 'v2.2.0-beta', focus: 'none', ...o, fg: tone === 'neutral' ? 'var(--ui-text-toned)' : 'var(--ui-' + tone + ')', bg: TB[tone], bar: o.pct !== undefined || !!o.indet, barW: o.indet ? '35%' : (o.pct || 0) + '%', barL: o.indet ? '30%' : '0', pct: o.indet ? undefined : o.pct } } const NOTES = (open) => ({ open, chev: open ? 'chevron-down' : 'chevron-right', ver: '2.3.0', date: 'Released 24 Sep 2026', items: [{ t: 'Images upload faster, and large images no longer stall.' }, { t: 'Fixed a problem where a draft could show an old title after saving.' }, { t: 'Small improvements to the Users page.' }] }) const SU = (o, extra) => withM({ kind: 'settings', nav: 'settings', crumbs: ['Settings'], mTitle: 'Settings', ...extra, upd: UPD(o) }) const LAST = 'Last checked today at 09:41' diff --git a/features/updates_download_stage.feature b/features/updates_download_stage.feature index d60f6ad..e4ac5ce 100644 --- a/features/updates_download_stage.feature +++ b/features/updates_download_stage.feature @@ -254,7 +254,7 @@ Feature: Updates part 2: the Update button (download, verify, stage) Scenario: Settings, owner, Ready to install Given the poll answers state "ready" Then polling stops - And the block shows icon circle-check (success), "Version 2.3.0-beta is ready to install" / + And the block shows icon clock (warning tone) with a "Pending" tag after the title, "Version 2.3.0-beta is ready to install" / "The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds." / "Nothing has changed yet." And the notes button is shown collapsed diff --git a/web/src/components/UpdateStatusBlock.vue b/web/src/components/UpdateStatusBlock.vue index 2578903..0247a03 100644 --- a/web/src/components/UpdateStatusBlock.vue +++ b/web/src/components/UpdateStatusBlock.vue @@ -17,6 +17,7 @@ const props = defineProps<{ title: string text: string meta?: string + tag?: string live?: 'polite' | 'off' }>() @@ -37,10 +38,21 @@ const spinning = computed(() => props.icon === 'i-lucide-loader-circle') />
- {{ title }} +
+ {{ title }} + {{ tag }} +
{{ text }} {{ meta }} +
+ +
diff --git a/web/src/components/UpdatesCard.vue b/web/src/components/UpdatesCard.vue index b0cf8a9..4335e32 100644 --- a/web/src/components/UpdatesCard.vue +++ b/web/src/components/UpdatesCard.vue @@ -39,6 +39,7 @@ interface Block { title: string text: string meta?: string + tag?: string } interface ActionButton { @@ -187,8 +188,11 @@ const applyBlocks = computed>(() => ({ }, }, ready: { - tone: 'success', - icon: 'i-lucide-circle-check', + tone: 'warning', + icon: 'i-lucide-clock', + get tag() { + return t('Pending') + }, get title() { return t('Version {0} is ready to install', [newVersion.value]) }, @@ -498,6 +502,12 @@ const actionButtons = computed(() => { }) }) +const inBlockButton = computed(() => + applyView.value === 'ready' ? (actionButtons.value[0] ?? null) : null, +) + +const rowButtons = computed(() => (inBlockButton.value ? [] : actionButtons.value)) + const showRestartHint = computed(() => actionButtons.value.includes(plainRestartButton)) function withPlainRestart(button: ActionButton) { @@ -577,6 +587,7 @@ const notes = computed(() => { :title="block.title" :text="block.text" :meta="block.meta" + :tag="block.tag" :live="progressView ? 'off' : 'polite'" > { :text="progressView.text" :value="progressView.value" /> +

@@ -595,9 +625,9 @@ const notes = computed(() => { -

+
{ it('ready: Restart is the enabled primary button', async () => { await mountSettings({ role: 'owner', status: ready() }) - expect(cardButtons()).toEqual(["What's new in 2.3.0-beta", 'Restart']) + expect(cardButtons()).toEqual(['Restart', "What's new in 2.3.0-beta"]) const restart = findButton('Restart') as HTMLButtonElement expect(restart.disabled).toBe(false) expect(restart.className).not.toContain('border-accented') diff --git a/web/src/views/__tests__/SettingsUpdateDownload.spec.ts b/web/src/views/__tests__/SettingsUpdateDownload.spec.ts index 11a3c3f..3f8ca34 100644 --- a/web/src/views/__tests__/SettingsUpdateDownload.spec.ts +++ b/web/src/views/__tests__/SettingsUpdateDownload.spec.ts @@ -311,6 +311,16 @@ describe('Settings, owner, Ready to install', () => { expect(findButton("What's new in 2.3.0-beta")!.getAttribute('aria-expanded')).toBe('false') const restart = findButton('Restart') as HTMLButtonElement expect(restart.disabled).toBe(false) + const tag = updatesCard().querySelector('[data-testid="update-tag"]') + expect(tag?.textContent?.trim()).toBe('Pending') + expect(updatesCard().querySelector('svg.text-warning')).not.toBeNull() + expect(updatesCard().querySelector('svg.text-success')).toBeNull() + const block = updatesCard().querySelector('[aria-live]') as HTMLElement + expect(block.className).toContain('--rc-warning-soft') + expect(block.contains(restart)).toBe(true) + expect(restart.className).toContain('h-11') + expect(restart.className).toContain('md:h-8') + expect(updatesCard().querySelector('[class*="md:flex-row"]')).toBeNull() const readsBefore = callsTo(STATUS_PATH).length await tick(3000)