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) <noreply@anthropic.com>
This commit is contained in:
parent
0fdf31c2e4
commit
1ad898bc8b
|
|
@ -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<SystemUpdateStatus['stage'], string> = {
|
||||
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<SystemUpdateStatus['stage']> = 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<Omit<Props, 'earlyAccess'>>(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 <IconAlertCircle className="h-12 w-12 text-desert-red" />
|
||||
if (isUpdating) return <IconReload className="h-12 w-12 text-desert-green animate-spin" />
|
||||
if (props.system.updateAvailable)
|
||||
if (versionInfo.updateAvailable)
|
||||
return <IconArrowBigUpLines className="h-16 w-16 text-desert-green" />
|
||||
return <IconCircleCheck className="h-16 w-16 text-desert-olive" />
|
||||
}
|
||||
|
|
@ -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 && (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-2">
|
||||
{props.system.updateAvailable ? 'Update Available' : 'System Up to Date'}
|
||||
{versionInfo.updateAvailable ? 'Update Available' : 'System Up to Date'}
|
||||
</h2>
|
||||
<p className="text-desert-stone-dark mb-6">
|
||||
{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!'}
|
||||
</p>
|
||||
</>
|
||||
|
|
@ -473,8 +511,8 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
|||
|
||||
{isUpdating && updateStatus && (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-2 capitalize">
|
||||
{updateStatus.stage === 'idle' ? 'Preparing Update' : updateStatus.stage}
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-2">
|
||||
{STAGE_LABELS[updateStatus.stage] ?? updateStatus.stage}
|
||||
</h2>
|
||||
<p className="text-desert-stone-dark mb-6">{updateStatus.message}</p>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in New Issue