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 StyledSectionHeader from '~/components/StyledSectionHeader'
|
||||||
import ActiveDownloads from '~/components/ActiveDownloads'
|
import ActiveDownloads from '~/components/ActiveDownloads'
|
||||||
import Alert from '~/components/Alert'
|
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 { IconAlertCircle, IconArrowBigUpLines, IconCheck, IconCircleCheck, IconReload } from '@tabler/icons-react'
|
||||||
import { SystemUpdateStatus } from '../../../types/system'
|
import { SystemUpdateStatus } from '../../../types/system'
|
||||||
import type { ContentUpdateCheckResult, ResourceUpdateInfo } from '../../../types/collections'
|
import type { ContentUpdateCheckResult, ResourceUpdateInfo } from '../../../types/collections'
|
||||||
|
|
@ -24,6 +24,23 @@ type Props = {
|
||||||
earlyAccess: boolean
|
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() {
|
function ContentUpdatesSection() {
|
||||||
const { addNotification } = useNotifications()
|
const { addNotification } = useNotifications()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
@ -251,6 +268,12 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [versionInfo, setVersionInfo] = useState<Omit<Props, 'earlyAccess'>>(props.system)
|
const [versionInfo, setVersionInfo] = useState<Omit<Props, 'earlyAccess'>>(props.system)
|
||||||
const [showConnectionLostNotice, setShowConnectionLostNotice] = useState(false)
|
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({
|
const earlyAccessSetting = useSystemSetting({
|
||||||
key: 'system.earlyAccess', initialData: {
|
key: 'system.earlyAccess', initialData: {
|
||||||
|
|
@ -270,11 +293,22 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
}
|
}
|
||||||
setUpdateStatus(response)
|
setUpdateStatus(response)
|
||||||
|
|
||||||
|
if (ADVANCED_STAGES.has(response.stage)) {
|
||||||
|
seenAdvancedStageRef.current = true
|
||||||
|
}
|
||||||
|
|
||||||
// If we can connect again, hide the connection lost notice
|
// If we can connect again, hide the connection lost notice
|
||||||
setShowConnectionLostNotice(false)
|
setShowConnectionLostNotice(false)
|
||||||
|
|
||||||
// Check if update is complete or errored
|
// Check if update is complete or errored. We also treat a return to
|
||||||
if (response.stage === 'complete') {
|
// '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
|
// Re-check version so the KV store clears the stale "update available" flag
|
||||||
// before we reload, otherwise the banner shows "current → current"
|
// before we reload, otherwise the banner shows "current → current"
|
||||||
try {
|
try {
|
||||||
|
|
@ -304,6 +338,7 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
const handleStartUpdate = async () => {
|
const handleStartUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
seenAdvancedStageRef.current = false
|
||||||
setIsUpdating(true)
|
setIsUpdating(true)
|
||||||
const response = await api.startSystemUpdate()
|
const response = await api.startSystemUpdate()
|
||||||
if (!response || !response.success) {
|
if (!response || !response.success) {
|
||||||
|
|
@ -368,7 +403,7 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
if (updateStatus?.stage === 'error')
|
if (updateStatus?.stage === 'error')
|
||||||
return <IconAlertCircle className="h-12 w-12 text-desert-red" />
|
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 (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 <IconArrowBigUpLines className="h-16 w-16 text-desert-green" />
|
||||||
return <IconCircleCheck className="h-16 w-16 text-desert-olive" />
|
return <IconCircleCheck className="h-16 w-16 text-desert-olive" />
|
||||||
}
|
}
|
||||||
|
|
@ -380,6 +415,9 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
addNotification({ message: 'Setting updated successfully.', type: 'success' })
|
addNotification({ message: 'Setting updated successfully.', type: 'success' })
|
||||||
earlyAccessSetting.refetch()
|
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) => {
|
onError: (error) => {
|
||||||
console.error('Error updating setting:', error)
|
console.error('Error updating setting:', error)
|
||||||
|
|
@ -461,11 +499,11 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
{!isUpdating && (
|
{!isUpdating && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-2xl font-bold text-desert-green mb-2">
|
<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>
|
</h2>
|
||||||
<p className="text-desert-stone-dark mb-6">
|
<p className="text-desert-stone-dark mb-6">
|
||||||
{props.system.updateAvailable
|
{versionInfo.updateAvailable
|
||||||
? `A new version (${props.system.latestVersion}) is available for your Project N.O.M.A.D. instance.`
|
? `A new version (${versionInfo.latestVersion}) is available for your Project N.O.M.A.D. instance.`
|
||||||
: 'Your system is running the latest version!'}
|
: 'Your system is running the latest version!'}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
|
|
@ -473,8 +511,8 @@ export default function SystemUpdatePage(props: { system: Props }) {
|
||||||
|
|
||||||
{isUpdating && updateStatus && (
|
{isUpdating && updateStatus && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-2xl font-bold text-desert-green mb-2 capitalize">
|
<h2 className="text-2xl font-bold text-desert-green mb-2">
|
||||||
{updateStatus.stage === 'idle' ? 'Preparing Update' : updateStatus.stage}
|
{STAGE_LABELS[updateStatus.stage] ?? updateStatus.stage}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-desert-stone-dark mb-6">{updateStatus.message}</p>
|
<p className="text-desert-stone-dark mb-6">{updateStatus.message}</p>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue