chore: groundwork for update from settings page

This commit is contained in:
fsylvain committed 2026-10-01 06:04:02 +02:00
1 parent e10a5e57e2
commit a49eec909c
8 files changed
+256 -35

No files matched your search

@@ -118,6 +118,18 @@ class Component extends DCLogic {
const rowFail = { n: 'roof.png', sz: '1.4 MB', st: 'fail', im: 10 }
const gone = [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12]
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 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'
const RESTART_FOOT = 'Restart takes about 10 seconds. Your site is unavailable while it restarts.'
const bn = (icon, title, text) => ({ icon, title, text, btn: 'Go to Settings', fg: 'var(--ui-info)', bg: 'var(--rc-info-soft)' })
const cmdLine = 'renewcms enable-browser-updates'
const notPrep = (copied) => ({ icon: 'info', tone: 'info', title: 'Version 2.3.0 is available', text: 'This site can\'t update itself from the browser yet. Whoever manages your server can set it up once, or update RenewCMS for you.', meta: LAST, cmd: { line: cmdLine, t: copied ? 'Copied' : 'Copy', icon: copied ? 'check' : 'copy', fg: copied ? 'var(--ui-success)' : 'var(--ui-text-highlighted)', note: 'They run this once on the server. After that, Update and Restart appear here.' }, link: 'How to update' })
const restartModal = (o) => ({ title: 'Restart the site?', desc: 'Your site will be unavailable for about 10 seconds. This page reloads on its own when the site is back. Save anything you\'re editing first.', cancel: 'Cancel', confirm: 'Restart site', ...o })
const installModal = (o) => ({ title: 'Restart to finish the update?', desc: 'RenewCMS will install version 2.3.0 and restart. Your site will be unavailable for about 10 seconds. This page reloads on its own when the site is back. Save anything you\'re editing first.', cancel: 'Restart later', confirm: 'Restart now', ...o })
return this.finish([
{ label: 'Boot', title: 'App boot', route: 'any route, first load', api: 'GET /v1/auth/me (pending)', note: 'While the session is checked, the shell renders with skeletons, never a spinner. The possible outcomes are: 200 with a verified user, which renders the route; 200 with an unverified user, which goes to /register/pending; and 401, which goes to /login?redirect=<path>. Public routes skip this check.', states: [
withM({ ...home, name: 'Checking session', trigger: 'The first paint on any back-office URL, and usually visible for well under a second.', boot: true })
@@ -126,7 +138,9 @@ class Component extends DCLogic {
withM({ ...home, name: 'Default', trigger: 'The articles loaded.', home: { sub: 'Here\'s where things stand on your site.', stats, recent } }),
withM({ ...home, name: 'Loading', trigger: 'The list request is in flight. Skeletons appear after 150 ms.', home: { sub: 'Here\'s where things stand on your site.', stats: stats.map(() => ({ skel: true })), recent: Array.from({ length: 5 }, () => ({ skel: true })) } }),
withM({ ...home, name: 'Empty, first run', trigger: 'Zero articles: a single call to action, and no stats.', home: { sub: 'Welcome to RenewCMS.', empty: true } }),
withM({ ...home, name: 'Error', trigger: 'A 500 or network failure on the list request.', home: { sub: 'Here\'s where things stand on your site.', error: true } })
withM({ ...home, name: 'Error', trigger: 'A 500 or network failure on the list request.', home: { sub: 'Here\'s where things stand on your site.', error: true } }),
withM({ ...home, navBadge: true, name: 'Update available (owner)', trigger: 'The owner only. A quiet banner above the content, and an "Update" pill on the Settings item (a dot on the menu icon on mobile, read as "Update available"). Dismissing hides the banner until the next version; the pill stays. Admins see neither.', home: { sub: 'Here\'s where things stand on your site.', stats, recent, banner: bn('circle-arrow-up', 'Version 2.3.0 is available', 'Update it from Settings. It takes about 2 minutes and the site stays online.') } }),
withM({ ...home, navBadge: true, name: 'Update ready to install (owner)', trigger: 'The download is checked. The same banner changes its copy, and a dismiss comes back on the next visit because the update is waiting.', home: { sub: 'Here\'s where things stand on your site.', stats, recent, banner: bn('circle-check', 'Version 2.3.0 is ready to install', 'Restart from Settings to finish. The site is unavailable for about 10 seconds.') } })
] },
{ label: 'Articles', title: 'Articles', route: '/article', api: 'GET /v1/article/ · PATCH …/publish · PATCH …/unpublish · DELETE /v1/article/{id}', note: 'Rows update when the server responds, not optimistically. Search, tab and sort are stored in the query string.', states: [
withM({ ...L(), name: 'Default', trigger: 'The list loaded, sorted by last updated.' }),
@@ -274,8 +288,50 @@ class Component extends DCLogic {
withM({ ...U({ toasts: [T('error', 'circle-x', 'Couldn\'t renew the invite', 'Try again in a moment.')] }), name: 'Renew invite: failed', trigger: '500 or network failure. The row stays Expired and the button can be pressed again.' }),
withM({ ...U({ users: { rows: [{ email: 'lea.martin@renew.example', role: 'admin', st: 'pending', c: 'Today', hl: true }, ...people] } , toasts: [T('success', 'circle-check', 'Invite sent', 'lea.martin@renew.example has 3 days to accept.')] }), name: 'Invite sent', trigger: '201: the dialog closes, the new invite appears at the top as Pending, and a toast confirms.' })
] },
{ label: 'Settings', title: 'Settings', route: '/settings', api: 'none (static)', note: 'Opened from the sidebar. For now it holds only the Version section.', states: [
withM({ kind: 'settings', nav: 'settings', crumbs: ['Settings'], mTitle: 'Settings', name: 'Default', trigger: 'Opened from the sidebar.' })
{ label: 'Settings', title: 'Settings: Updates', route: '/settings', api: 'GET /v1/system/update · POST …/check · POST …/download · POST …/restart (all pending)', note: 'New Updates card under Version, owner only. Update downloads and checks the new version and changes nothing. Restart installs it and restarts the site (about 10 seconds). "Restart later" is simply leaving the card or the dialog. A plain Restart shows only when no update is waiting. Admins see the version and a note. When the server isn\'t prepared, the card shows the notice and a "How to update" link with no buttons. Progress is a progressbar with a value, announced at 25% steps and when each step changes. The card is a polite live region. After the restart the page reloads, focus lands on the "Updates" heading, and a toast confirms. Buttons are disabled while any step runs, and any owner on any device sees the same state from the server. Cue elsewhere: see Home. Out of scope: choosing versions, downgrades, automatic updates, update history.', states: [
SU({ icon: 'circle-check', tone: 'success', title: 'You\'re up to date', text: 'You have the latest version of RenewCMS.', meta: LAST, btns: [B('Check for updates', 'sec', 'refresh-cw'), B('Restart', 'sec', 'power')], hint: RESTART_FOOT }, { name: 'Up to date', trigger: 'Opened with no newer version. Restart is available but not emphasised.' }),
SU({ icon: 'loader-circle', title: 'Checking for updates…', text: 'This usually takes a few seconds.', btns: [B('Checking…', 'sec', 'loader-circle', true)] }, { name: 'Checking', trigger: 'Check for updates pressed, or the first open after 24 hours. Announced as "Checking for updates". Buttons are disabled.' }),
SU({ icon: 'wifi-off', tone: 'warning', title: 'Couldn\'t check for updates', text: 'RenewCMS couldn\'t reach the update server. Check your connection and try again. Your site isn\'t affected.', meta: 'Last checked yesterday at 18:12', btns: [B('Try again', 'primary', 'rotate-cw'), B('Restart', 'sec', 'power')], hint: RESTART_FOOT }, { name: 'Check failed', trigger: 'Offline, or the update server didn\'t answer (500, network). The last known result is kept.' }),
SU({ icon: 'circle-arrow-up', tone: 'info', title: 'Version 2.3.0 is available', text: 'You have v2.2.0-beta. Update downloads the new version and checks it. Nothing changes until you restart.', meta: LAST, notes: NOTES(false), btns: [B('Update', 'primary', 'download')], hint: 'Takes about 2 minutes. Your site stays online.' }, { name: 'Update available', trigger: 'A newer version exists. Release notes are collapsed. Update is the one primary button.', navBadge: true }),
SU({ icon: 'circle-arrow-up', tone: 'info', title: 'Version 2.3.0 is available', text: 'You have v2.2.0-beta. Update downloads the new version and checks it. Nothing changes until you restart.', meta: LAST, notes: NOTES(true), btns: [B('Update', 'primary', 'download')], hint: 'Takes about 2 minutes. Your site stays online.' }, { name: 'Update available, notes open', trigger: '"What\'s new in 2.3.0" expanded. It is a button with aria-expanded, and the list is plain text.', navBadge: true }),
SU({ icon: 'download', tone: 'info', title: 'Downloading version 2.3.0…', text: 'You can leave this page. The download continues and your site stays online.', pct: 42, barLabel: 'Downloading version 2.3.0', barText: '42% · 11 of 26 MB', btns: [B('Updating…', 'sec', 'loader-circle', true)] }, { name: 'Downloading', trigger: 'Update pressed (202). Progress polls every second. Announced as "Downloading, 25 percent" and so on, never every tick.', navBadge: true }),
SU({ icon: 'shield-check', tone: 'info', title: 'Checking the download…', text: 'Making sure the file is complete and safe to install. This takes a few seconds.', indet: true, barLabel: 'Checking the download', barText: 'Almost there', btns: [B('Updating…', 'sec', 'loader-circle', true)] }, { name: 'Verifying', trigger: 'The download finished. No percentage, so the bar is indeterminate. Announced as "Checking the download".', navBadge: true }),
SU({ icon: 'circle-check', tone: 'success', title: 'Version 2.3.0 is ready to install', text: 'The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds.', meta: 'Nothing has changed yet.', notes: NOTES(false), btns: [B('Restart', 'primary', 'power')], hint: 'Not now? Leave this page. The update waits here, and the banner on Home reminds you.' }, { name: 'Ready to install', trigger: 'The file passed its checks. Restart becomes the primary action. "Restart later" is closing the dialog or leaving the page.', navBadge: true }),
SU({ icon: 'circle-check', tone: 'success', title: 'Version 2.3.0 is ready to install', text: 'The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds.', meta: 'Nothing has changed yet.', btns: [B('Restart', 'primary', 'power')] }, { name: 'Restart: confirm', trigger: '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.', navBadge: true, modal: installModal() }),
SU({ icon: 'circle-check', tone: 'success', title: 'Version 2.3.0 is ready to install', text: 'The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds.', meta: 'Nothing has changed yet.', btns: [B('Restart', 'primary', 'power')] }, { name: 'Restart: request failed', trigger: '500 or network failure before the restart began. The site is still running.', navBadge: true, modal: installModal({ notice: 'Couldn\'t start the restart. Nothing was changed and your site is still running. Try again in a moment.' }) }),
SU({ icon: 'circle-check', tone: 'success', title: 'Version 2.3.0 is ready to install', text: '', btns: [] }, { name: 'Restarting to install', trigger: '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."', overlay: { title: 'Installing version 2.3.0', text: 'Your site is restarting with the new version. This takes about 10 seconds. This page reloads on its own when the site is back.', meta: 'Started at 09:51', barL: '30%' } }),
SU({ icon: 'circle-check', tone: 'success', title: '', text: '', btns: [] }, { name: 'Restarting, taking longer', trigger: 'No answer after 60 seconds. The copy changes and a manual reload appears. The overlay stays until the site is back.', overlay: { title: 'Still restarting', text: 'This is taking longer than usual. Keep this page open. It reloads on its own when the site is back, or you can reload it now.', meta: 'Started at 09:51 · 1 minute ago', btn: 'Reload page', barL: '65%' } }),
SU({ icon: 'circle-check', tone: 'success', title: 'You\'re on version 2.3.0', text: 'The update is installed and your site is running again.', meta: 'Updated today at 09:52', btns: [B('Check for updates', 'sec', 'refresh-cw'), B('Restart', 'sec', 'power')], hint: RESTART_FOOT, link: 'Read the release notes', v: 'v2.3.0', focus: '2px solid var(--ui-primary)', toasts: undefined }, { name: 'Update done', trigger: 'The page reloaded on the new version. Focus lands on the "Updates" heading (ring shown), the toast is announced, and the version updates.', toasts: [T('success', 'circle-check', 'Update installed', 'RenewCMS is now on version 2.3.0.')] }),
SU({ icon: 'triangle-alert', tone: 'warning', title: 'The update didn\'t install', text: 'RenewCMS put the previous version back, so your site is running as before. Nothing was lost.', meta: 'What to do next: try again in a few minutes. If it fails again, ask whoever manages your server to check the update.', btns: [B('Try again', 'primary', 'rotate-cw')] }, { name: 'Update failed, rolled back', trigger: 'The site came back on v2.2.0-beta. The error toast stays until dismissed and the card keeps the explanation.', toasts: [T('error', 'circle-x', 'The update didn\'t install', 'Your site is back on v2.2.0-beta.')] }),
SU({ icon: 'circle-x', tone: 'error', title: 'The download didn\'t finish', text: 'The connection dropped before the file was complete. Nothing was changed and your site is running as before.', btns: [B('Try again', 'primary', 'rotate-cw')] }, { name: 'Download failed', trigger: 'A 500, or the connection dropped. Focus moves to the card heading. Try again restarts the download.', navBadge: true }),
SU({ icon: 'triangle-alert', tone: 'warning', title: 'We didn\'t use this update', text: 'The file didn\'t pass our safety check, so it was not installed. Nothing was changed. Try downloading it again. If this keeps happening, contact RenewCMS support.', btns: [B('Try again', 'primary', 'rotate-cw')] }, { name: 'Verification failed', trigger: 'The downloaded file isn\'t trusted. The tone is a warning, and the file is thrown away. Technical detail goes to the server log, not the screen.', navBadge: true }),
SU({ icon: 'info', tone: 'info', title: 'Too many tries', text: 'Wait a moment before trying again.', btns: [B('Try again in 0:42', 'sec', 'clock', true)] }, { name: 'Too many requests (429)', trigger: 'Retry-After counted down in the button. It re-enables at zero.' }),
SU(notPrep(false), { name: 'Not prepared', trigger: 'The server can\'t update from the browser. No buttons, not even Restart. The command is for the person who runs the server and is a single line. The placeholder command is subject to the real one.', navBadge: true }),
SU(notPrep(true), { name: 'Not prepared: command copied', trigger: 'Copy pressed. The button reads "Copied" for 2 s and it is announced.', navBadge: true }),
SU({ icon: 'circle-check', tone: 'success', title: 'You\'re up to date', text: 'You have the latest version of RenewCMS.', meta: LAST, btns: [B('Check for updates', 'sec', 'refresh-cw'), B('Restart', 'sec', 'power')], hint: RESTART_FOOT }, { name: 'Plain restart: confirm', trigger: 'Restart with no update waiting. Nothing is installed.', modal: restartModal() }),
SU({ icon: 'circle-check', tone: 'success', title: '', text: '', btns: [] }, { name: 'Plain restart: restarting', trigger: 'Same overlay as the update restart with shorter copy.', overlay: { title: 'Restarting your site', text: 'This takes about 10 seconds. This page reloads on its own when the site is back.', meta: 'Started at 09:51', barL: '30%' } }),
SU({ icon: 'circle-check', tone: 'success', title: 'Your site is back', text: '', btns: [] }, { name: 'Plain restart: done', trigger: 'The page reloaded. Focus lands on the "Updates" heading.', toasts: [T('success', 'circle-check', 'Site restarted', 'Your site is running again.')] }),
SU({ icon: 'circle-arrow-up', tone: 'info', title: 'Version 2.3.0 is available', text: 'You have v2.2.0-beta. Only the owner can update RenewCMS. Ask the owner of this site.', meta: LAST }, { name: 'Admin view', trigger: 'Admins see the version and the note, with no buttons. Up to date: "You\'re up to date" and "Only the owner can update or restart the site." No Home banner or Settings pill.' }),
SU({ icon: 'circle-arrow-up', tone: 'info', title: 'Version 2.3.0 is available', text: 'You have v2.2.0-beta. Only the owner can update RenewCMS. Ask the owner of this site.', meta: LAST }, { name: 'Role changed (403)', trigger: 'An owner was changed to admin while the page was open and pressed a button. The card reloads in the admin view.', toasts: [T('error', 'circle-x', 'Only the owner can do this', 'Ask the owner of this site to update RenewCMS.')] }),
SU({ icon: 'circle-arrow-up', tone: 'info', title: 'Version 2.3.0 is available', text: 'You have v2.2.0-beta. Update downloads the new version and checks it. Nothing changes until you restart.', meta: LAST, btns: [B('Update', 'primary', 'download', true)] }, { name: 'Update already running (409)', trigger: 'Another owner started it. The card switches to that run\'s progress.', navBadge: true, toasts: [T('info', 'info', 'An update is already running', 'Another owner started it. Progress shows here.')] })
], tableTitle: 'Status to copy', table: [
{ a: '200', b: 'Check, up to date', c: '"You\'re up to date" / "You have the latest version of RenewCMS." / "Last checked today at 09:41"' },
{ a: '200', b: 'Check, newer version', c: '"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."' },
{ a: '202', b: 'Update, Restart', c: 'Accepted. The card switches to progress. Announced: "Downloading version 2.3.0, 25 percent".' },
{ a: '200', b: 'Update finished', c: '"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."' },
{ a: '200', b: 'After restart', c: 'Toast: "Update installed" / "RenewCMS is now on version 2.3.0." Plain restart: "Site restarted" / "Your site is running again."' },
{ a: '400', b: 'Update, nothing to install', c: 'Toast (info): "Nothing to update" / "You already have the latest version." The card refreshes.' },
{ a: '401', b: 'Any call', c: 'Handled globally: /login?redirect=/settings&reason=expired. No local copy.' },
{ a: '403', b: 'Any button, not the owner', c: 'Toast (error): "Only the owner can do this" / "Ask the owner of this site to update RenewCMS." The card reloads as the admin view. An unverified account is handled globally.' },
{ a: '409', b: 'Update, Restart', c: 'Already running: toast (info) "An update is already running" / "Another owner started it. Progress shows here." Not prepared: the card shows the "Not prepared" state and "How to update".' },
{ a: '422', b: 'Update, file not trusted', c: '"We didn\'t use this update" / "The file didn\'t pass our safety check, so it was not installed. Nothing was changed. Try downloading it again. If this keeps happening, contact RenewCMS support."' },
{ a: '429', b: 'Check, Update', c: 'Card alert (info): "Too many tries" / "Wait a moment before trying again." Button reads "Try again in 0:42" and counts down.' },
{ a: '500 / network', b: 'Check', c: '"Couldn\'t check for updates" / "RenewCMS couldn\'t reach the update server. Check your connection and try again. Your site isn\'t affected." with "Try again".' },
{ a: '500 / network', b: 'Update', c: '"The download didn\'t finish" / "The connection dropped before the file was complete. Nothing was changed and your site is running as before." with "Try again".' },
{ a: '500 / network', b: 'Restart dialog', c: '"Couldn\'t start the restart. Nothing was changed and your site is still running. Try again in a moment."' },
{ a: 'Site back, old version', b: 'After restart', c: '"The update didn\'t install" / "RenewCMS put the previous version back, so your site is running as before. Nothing was lost." Next step: "try again in a few minutes. If it fails again, ask whoever manages your server to check the update."' },
{ a: 'No answer, 60 s', b: 'Restart overlay', c: '"Still restarting" / "This is taking longer than usual. Keep this page open. It reloads on its own when the site is back, or you can reload it now."' },
{ a: 'Offline', b: 'Whole page', c: 'Existing banner. Check, Update and Restart are disabled. During a restart, the overlay keeps polling and ignores the offline state.' }
] },
{ label: 'Profile', title: 'Profile', route: '/profile', api: 'GET /v1/auth/me · PATCH /v1/user/me (pending) · POST /v1/auth/password-reset', note: 'Opened from the account menu at the bottom of the sidebar. Username, email and password are read-only. The display name is optional, trimmed, and up to 60 characters. Once saved it replaces the username everywhere a person is named: the sidebar, the account menu, the Home greeting, the Users list (with the username beside it) and the emails RenewCMS sends, such as "Marta Alves invited you to RenewCMS". Saving it empty goes back to the username. The reset link uses the same flow as "Forgot password" and ends on the auth board\'s "Choose a new password" screen.', states: [
withM({ ...home, home: { sub: 'Here\'s where things stand on your site.', stats, recent }, userMenu: true, name: 'Account menu', trigger: 'The account chip at the bottom of the sidebar. On mobile, the account row in the navigation drawer opens it as a sheet. Escape or a click outside closes it.' }),