From 1ad898bc8b7222961e07cbaf4e1fc67258372e4e Mon Sep 17 00:00:00 2001 From: chriscrosstalk <49691103+chriscrosstalk@users.noreply.github.com> Date: Tue, 5 May 2026 10:33:18 -0700 Subject: [PATCH] fix(UI): four fixes for the System Update page (#827) Closes #826. 1. Heading and subtext now read from `versionInfo` state (which the Check Again mutation already populates) instead of the server-rendered `props.system`. Previously the card kept showing "System Up to Date / Your system is running the latest version!" alongside the new `Latest Version` row + Start Update button after a successful recheck. Status icon also switched to `versionInfo` for consistency. 2. The pulling-state heading rendered the lowercase status enum (`pulling`, `pulled`, ...) and relied on a Tailwind `capitalize` class for the visible glyph. Screen readers and other accessible-name consumers got the lowercase value with no transform applied. Replaced with a `STAGE_LABELS` map so visual + accessible names match. 3. The sidecar (install/sidecar-updater/update-watcher.sh) writes `complete` for ~5s, then resets the status file to `idle`. The SPA could miss that window across the admin container restart, leaving the page parked on its last observed progress percentage indefinitely while the upgrade was actually finished on disk. A `seenAdvancedStageRef` now records whether the session ever observed an advanced stage; a later poll seeing `idle` is treated as the missed completion, and the page reloads as advertised in step 3 of the on-screen process. Reset on each Start Update. 4. Toggling Enable Early Access now triggers a recheck on success, so the eligible-version list updates immediately instead of requiring a manual Check Again click. Single file touched: admin/inertia/pages/settings/update.tsx. Typecheck (tsc --noEmit) passes; static UI changes verified in source. Co-authored-by: Claude Opus 4.7 (1M context) --- admin/inertia/pages/settings/update.tsx | 56 +++++++++++++++++++++---- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/admin/inertia/pages/settings/update.tsx b/admin/inertia/pages/settings/update.tsx index 348040d..06db3bb 100644 --- a/admin/inertia/pages/settings/update.tsx +++ b/admin/inertia/pages/settings/update.tsx @@ -5,7 +5,7 @@ import StyledTable from '~/components/StyledTable' import StyledSectionHeader from '~/components/StyledSectionHeader' import ActiveDownloads from '~/components/ActiveDownloads' import Alert from '~/components/Alert' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { IconAlertCircle, IconArrowBigUpLines, IconCheck, IconCircleCheck, IconReload } from '@tabler/icons-react' import { SystemUpdateStatus } from '../../../types/system' import type { ContentUpdateCheckResult, ResourceUpdateInfo } from '../../../types/collections' @@ -24,6 +24,23 @@ type Props = { earlyAccess: boolean } +const STAGE_LABELS: Record = { + idle: 'Preparing Update', + starting: 'Starting Update', + pulling: 'Pulling Images', + pulled: 'Images Pulled', + recreating: 'Recreating Containers', + complete: 'Update Complete', + error: 'Update Failed', +} + +const ADVANCED_STAGES: ReadonlySet = new Set([ + 'pulling', + 'pulled', + 'recreating', + 'complete', +]) + function ContentUpdatesSection() { const { addNotification } = useNotifications() const queryClient = useQueryClient() @@ -251,6 +268,12 @@ export default function SystemUpdatePage(props: { system: Props }) { const [email, setEmail] = useState('') const [versionInfo, setVersionInfo] = useState>(props.system) const [showConnectionLostNotice, setShowConnectionLostNotice] = useState(false) + // Tracks whether this update session has progressed past 'idle'/'starting'. + // The sidecar sits on 'complete' for ~5s before resetting to 'idle' (see + // install/sidecar-updater/update-watcher.sh), and the SPA can miss that + // window across the admin container restart. If we resurface to 'idle' + // after seeing an advanced stage, treat it as the missed completion. + const seenAdvancedStageRef = useRef(false) const earlyAccessSetting = useSystemSetting({ key: 'system.earlyAccess', initialData: { @@ -270,11 +293,22 @@ export default function SystemUpdatePage(props: { system: Props }) { } setUpdateStatus(response) + if (ADVANCED_STAGES.has(response.stage)) { + seenAdvancedStageRef.current = true + } + // If we can connect again, hide the connection lost notice setShowConnectionLostNotice(false) - // Check if update is complete or errored - if (response.stage === 'complete') { + // Check if update is complete or errored. We also treat a return to + // 'idle' as completion if we previously saw an advanced stage — this + // catches the race where the sidecar's brief 'complete' window passes + // while we're disconnected during the admin container restart. + const isComplete = + response.stage === 'complete' || + (response.stage === 'idle' && seenAdvancedStageRef.current) + + if (isComplete) { // Re-check version so the KV store clears the stale "update available" flag // before we reload, otherwise the banner shows "current → current" try { @@ -304,6 +338,7 @@ export default function SystemUpdatePage(props: { system: Props }) { const handleStartUpdate = async () => { try { setError(null) + seenAdvancedStageRef.current = false setIsUpdating(true) const response = await api.startSystemUpdate() if (!response || !response.success) { @@ -368,7 +403,7 @@ export default function SystemUpdatePage(props: { system: Props }) { if (updateStatus?.stage === 'error') return if (isUpdating) return - if (props.system.updateAvailable) + if (versionInfo.updateAvailable) return return } @@ -380,6 +415,9 @@ export default function SystemUpdatePage(props: { system: Props }) { onSuccess: () => { addNotification({ message: 'Setting updated successfully.', type: 'success' }) earlyAccessSetting.refetch() + // Toggling Early Access changes which versions are eligible, so re-evaluate + // immediately rather than making the user click Check Again. + checkVersionMutation.mutate() }, onError: (error) => { console.error('Error updating setting:', error) @@ -461,11 +499,11 @@ export default function SystemUpdatePage(props: { system: Props }) { {!isUpdating && ( <>

- {props.system.updateAvailable ? 'Update Available' : 'System Up to Date'} + {versionInfo.updateAvailable ? 'Update Available' : 'System Up to Date'}

- {props.system.updateAvailable - ? `A new version (${props.system.latestVersion}) is available for your Project N.O.M.A.D. instance.` + {versionInfo.updateAvailable + ? `A new version (${versionInfo.latestVersion}) is available for your Project N.O.M.A.D. instance.` : 'Your system is running the latest version!'}

@@ -473,8 +511,8 @@ export default function SystemUpdatePage(props: { system: Props }) { {isUpdating && updateStatus && ( <> -

- {updateStatus.stage === 'idle' ? 'Preparing Update' : updateStatus.stage} +

+ {STAGE_LABELS[updateStatus.stage] ?? updateStatus.stage}

{updateStatus.message}