feat: update button (download, verify, stage)

This commit is contained in:
fsylvain committed 2026-10-01 18:13:45 +02:00
1 parent 834493a4f5
commit 70460e4170
41 files changed
+5345 -97

No files matched your search

+80
View File
@@ -7,6 +7,16 @@ import { useSessionStore } from '@/stores/session'
export type UpdateLoadState = 'idle' | 'loading' | 'ready' | 'error'
export type UpdateCheckPhase = 'idle' | 'checking' | 'failed' | 'limited'
export type UpdateCheckResult = 'updated' | 'failed' | 'limited' | 'forbidden' | 'busy'
export type UpdateApplyPhase = 'idle' | 'starting' | 'failed' | 'untrusted' | 'limited'
export type UpdateStartResult =
| 'started'
| 'failed'
| 'untrusted'
| 'limited'
| 'forbidden'
| 'nothing'
| 'conflict'
| 'busy'
export const DISMISSED_VERSION_KEY = 'renewcms:update-dismissed'
const DEFAULT_RETRY_SECONDS = 60
@@ -32,6 +42,7 @@ export const useUpdateStore = defineStore('update', () => {
const status = ref<UpdateStatus | null>(null)
const loadState = ref<UpdateLoadState>('idle')
const phase = ref<UpdateCheckPhase>('idle')
const applyPhase = ref<UpdateApplyPhase>('idle')
const retryAfter = ref(0)
const dismissedVersion = ref<string | null>(readDismissedVersion())
let pendingLoad: Promise<void> | null = null
@@ -42,6 +53,13 @@ export const useUpdateStore = defineStore('update', () => {
const bannerVisible = computed(
() => showCue.value && status.value?.latest !== dismissedVersion.value,
)
const jobRunning = computed(
() =>
isOwner.value &&
status.value?.can_apply === true &&
(status.value.state === 'downloading' || status.value.state === 'verifying'),
)
const readyToInstall = computed(() => isOwner.value && status.value?.state === 'ready')
function load() {
if (pendingLoad) {
@@ -49,6 +67,9 @@ export const useUpdateStore = defineStore('update', () => {
}
const { getUpdateStatus } = useUpdate()
loadState.value = 'loading'
if (applyPhase.value !== 'starting') {
applyPhase.value = 'idle'
}
pendingLoad = Promise.resolve()
.then(getUpdateStatus)
.then((result) => {
@@ -64,6 +85,60 @@ export const useUpdateStore = defineStore('update', () => {
return pendingLoad
}
let pendingPoll = false
async function poll() {
if (pendingPoll) {
return
}
const { getUpdateStatus } = useUpdate()
pendingPoll = true
try {
status.value = await getUpdateStatus()
} catch {
return
} finally {
pendingPoll = false
}
}
async function startDownload(): Promise<UpdateStartResult> {
if (applyPhase.value === 'starting') {
return 'busy'
}
const { startUpdateDownload } = useUpdate()
applyPhase.value = 'starting'
try {
status.value = await startUpdateDownload()
loadState.value = 'ready'
applyPhase.value = 'idle'
return 'started'
} catch (error) {
return settleStartFailure(error)
}
}
function settleStartFailure(error: unknown): UpdateStartResult {
const outcomes: Record<number, UpdateStartResult> = {
400: 'nothing',
403: 'forbidden',
409: 'conflict',
422: 'untrusted',
429: 'limited',
}
const outcome = error instanceof ApiError ? outcomes[error.status] : undefined
if (outcome === 'limited' && error instanceof ApiError) {
retryAfter.value = error.retryAfter ?? DEFAULT_RETRY_SECONDS
}
const phases: Partial<Record<UpdateStartResult, UpdateApplyPhase>> = {
limited: 'limited',
untrusted: 'untrusted',
}
const result = outcome ?? 'failed'
applyPhase.value = phases[result] ?? (result === 'failed' ? 'failed' : 'idle')
return result
}
async function check(): Promise<UpdateCheckResult> {
if (phase.value === 'checking') {
return 'busy'
@@ -101,12 +176,17 @@ export const useUpdateStore = defineStore('update', () => {
status,
loadState,
phase,
applyPhase,
retryAfter,
isOwner,
available,
showCue,
bannerVisible,
jobRunning,
readyToInstall,
load,
poll,
startDownload,
check,
dismissBanner,
}