release(v1.7.14-alpha): install overlay + FIPS real fix + AIUI restore
Install UX SystemUpdate.vue now shows a full-screen overlay after apply: the BitcoinFaceAscii logo, a target-version label, an indeterminate progress stripe (solid orange; solid green on ready), and an elapsed-time readout. Polls /health every 1.5s and auto-reloads once the backend reports the new version. 3-min stall → "Reload now" button. Download UI also shows a spinner + "Finishing download — verifying checksum…" while the fake bar sits at 95%. FIPS reconnect — for real this time New fips.reconnect RPC does stop → start → wait 20s → re-poll → classify. Classification buckets: connected / daemon_down / no_seed_key / no_outbound_udp_or_anchor_down / peers_but_no_anchor, each with a plain-language hint surfaced verbatim by the Reconnect button. The real reason nodes like .198/.253 couldn't reach the anchor: identity::write_fips_key_from_seed was writing fips_key.pub as a bech32 npub TEXT file, but upstream fips expects 32 raw bytes. The daemon silently authenticated with garbage. Fix: PublicKey::to_bytes() → raw 32 bytes, and new fips::config::normalize_pub_file migrates legacy files by decoding the npub and rewriting in place. fips.reconnect also re-installs the config + healed keys to /etc/fips before restarting. AIUI preservation + restore apply_update was wiping /opt/archipelago/web-ui/aiui because the Vue build doesn't include it — every OTA lost the Claude sidebar. The preserve block now copies aiui/ + archipelago-companion.apk from the old web-ui into the staging dir before the swap, and prefers new-tar versions if present. To restore it on the three nodes that already lost it (.116/.198/.253), this release bundles the 85 MB aiui build into the frontend tarball. Frontend component size is now ~155 MB. Download / install timeouts Backend download client timeout 1800s → 3600s (1 h). Larger tarball + slow gitea raw throughput put us above the old cap. Frontend update.download rpc timeout 30 min → 65 min to match. package.install rpc timeout 15 min → 45 min — IndeedHub pulls 6 images and was timing out mid-install. UI nit "Rollback to Previous" → "Rollback Available". App-catalog proxy already landed in v1.7.13. Artefacts: archipelago 725e18e6…3c525e6 40462288 archipelago-frontend-1.7.14-alpha.tar.gz c35284be…ff2c16 162077052 (+aiui) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,14 @@
|
||||
:style="{ width: downloadPercentFormatted + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-white/60">{{ t('systemUpdate.percentComplete', { percent: downloadPercentFormatted }) }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="downloadFinishing" class="w-3 h-3 border-2 border-orange-400 border-t-transparent rounded-full animate-spin shrink-0"></div>
|
||||
<p class="text-xs text-white/60">
|
||||
{{ downloadFinishing
|
||||
? t('systemUpdate.finishingDownload')
|
||||
: t('systemUpdate.percentComplete', { percent: downloadPercentFormatted }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Applying -->
|
||||
@@ -176,6 +183,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install progress overlay — covers the UI while the backend
|
||||
swaps files, restarts, and comes back up on the new version.
|
||||
Auto-reloads the page as soon as /health reports the target
|
||||
version. Styled to match the screensaver (ASCII logo, full-
|
||||
screen black). -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="installing"
|
||||
class="fixed inset-0 z-[3000] bg-black flex flex-col items-center justify-center overflow-hidden"
|
||||
>
|
||||
<!-- Centered ASCII logo — same asset used by the screensaver -->
|
||||
<div class="install-overlay-ascii">
|
||||
<BitcoinFaceAscii />
|
||||
</div>
|
||||
|
||||
<!-- Status text + progress bar underneath -->
|
||||
<div class="mt-8 w-[min(520px,80vw)] text-center">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">
|
||||
{{ installStage === 'applying' ? t('systemUpdate.overlayApplying')
|
||||
: installStage === 'restarting' ? t('systemUpdate.overlayRestarting')
|
||||
: installStage === 'reconnecting' ? t('systemUpdate.overlayReconnecting')
|
||||
: installStage === 'ready' ? t('systemUpdate.overlayReady')
|
||||
: t('systemUpdate.overlayStalled') }}
|
||||
</h2>
|
||||
<p v-if="installTargetVersion" class="text-sm text-white/60 mb-4">
|
||||
{{ t('systemUpdate.overlayTarget', { version: installTargetVersion }) }}
|
||||
</p>
|
||||
|
||||
<!-- Animated bar: indeterminate stripe while working; full
|
||||
orange when ready; steady at 50% (paused look) when
|
||||
stalled so it reads as "something needs the user". -->
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden mb-3 relative">
|
||||
<div
|
||||
v-if="installStage === 'ready'"
|
||||
class="absolute inset-0 bg-green-400"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="installStage === 'stalled'"
|
||||
class="absolute inset-y-0 left-0 w-1/2 bg-orange-400/60"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="absolute inset-y-0 w-1/3 bg-orange-400 rounded-full install-overlay-bar-anim"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-white/40">{{ installElapsedLabel }}</p>
|
||||
|
||||
<button
|
||||
v-if="installStage === 'stalled'"
|
||||
@click="reloadNow"
|
||||
class="mt-5 glass-button rounded-lg px-5 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
|
||||
>
|
||||
{{ t('systemUpdate.overlayReloadNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelConfirm">
|
||||
@@ -221,6 +289,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'
|
||||
|
||||
interface UpdateDetail {
|
||||
version: string
|
||||
@@ -255,6 +324,78 @@ const statusMessage = ref('')
|
||||
const statusIsError = ref(false)
|
||||
const downloadPercent = ref(0)
|
||||
const downloadPercentFormatted = computed(() => downloadPercent.value.toFixed(2))
|
||||
// Shown next to the progress bar when the fake increment has maxed out
|
||||
// at 95% but the real RPC hasn't returned yet — lets the user know the
|
||||
// UI hasn't frozen while SHA verification and disk writes finish.
|
||||
const downloadFinishing = computed(() => downloading.value && downloadPercent.value >= 95)
|
||||
|
||||
// Install overlay state — drives the full-screen progress modal shown
|
||||
// while the backend swaps files, restarts, and comes back up on the
|
||||
// new version. The overlay polls /health and auto-reloads the browser
|
||||
// as soon as the backend reports the target version, so the user
|
||||
// doesn't need to manually refresh.
|
||||
type InstallStage = 'applying' | 'restarting' | 'reconnecting' | 'ready' | 'stalled'
|
||||
const installing = ref(false)
|
||||
const installStage = ref<InstallStage>('applying')
|
||||
const installTargetVersion = ref<string | null>(null)
|
||||
const installStartedAt = ref<number>(0)
|
||||
const installElapsedSec = ref(0)
|
||||
let installPollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let installElapsedTimer: ReturnType<typeof setInterval> | null = null
|
||||
const installElapsedLabel = computed(() => {
|
||||
const s = installElapsedSec.value
|
||||
if (s < 60) return `Elapsed: ${s}s`
|
||||
return `Elapsed: ${Math.floor(s / 60)}m${s % 60 < 10 ? '0' : ''}${s % 60}s`
|
||||
})
|
||||
function startInstallOverlay(targetVersion: string) {
|
||||
installing.value = true
|
||||
installStage.value = 'applying'
|
||||
installTargetVersion.value = targetVersion
|
||||
installStartedAt.value = Date.now()
|
||||
installElapsedSec.value = 0
|
||||
// Tick an elapsed counter once per second for the UI.
|
||||
installElapsedTimer = setInterval(() => {
|
||||
installElapsedSec.value = Math.floor((Date.now() - installStartedAt.value) / 1000)
|
||||
// Stop polling after 3 min — surface the manual reload button.
|
||||
if (installElapsedSec.value >= 180 && installStage.value !== 'ready') {
|
||||
installStage.value = 'stalled'
|
||||
}
|
||||
}, 1000)
|
||||
// Start polling /health after a short delay — the backend restarts 2s
|
||||
// after replying to update.apply, so an immediate poll would see the
|
||||
// old backend and conclude nothing happened.
|
||||
setTimeout(() => {
|
||||
installStage.value = 'restarting'
|
||||
installPollTimer = setInterval(pollHealth, 1500)
|
||||
}, 2500)
|
||||
}
|
||||
async function pollHealth() {
|
||||
if (installStage.value === 'ready' || installStage.value === 'stalled') return
|
||||
try {
|
||||
const res = await fetch('/health', { signal: AbortSignal.timeout(2000) })
|
||||
if (!res.ok) throw new Error(`health ${res.status}`)
|
||||
const data = await res.json() as { version?: string }
|
||||
if (data.version && data.version === installTargetVersion.value) {
|
||||
installStage.value = 'ready'
|
||||
if (installPollTimer) { clearInterval(installPollTimer); installPollTimer = null }
|
||||
// Brief pause so the user sees the "Ready" state before the reload.
|
||||
setTimeout(() => { window.location.reload() }, 1200)
|
||||
} else {
|
||||
// Backend is up but still reporting the old version — frontend
|
||||
// and backend are mid-swap. Signal to the user.
|
||||
installStage.value = 'reconnecting'
|
||||
}
|
||||
} catch {
|
||||
// Fetch fails while the server is mid-restart. Stay in 'restarting'.
|
||||
}
|
||||
}
|
||||
function reloadNow() { window.location.reload() }
|
||||
// Cleanup if the component is torn down mid-install (unlikely but safe).
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
onBeforeUnmount(() => {
|
||||
if (installPollTimer) clearInterval(installPollTimer)
|
||||
if (installElapsedTimer) clearInterval(installElapsedTimer)
|
||||
})
|
||||
|
||||
const lastCheckDisplay = computed(() => {
|
||||
if (!lastCheck.value) return t('common.never')
|
||||
@@ -359,7 +500,7 @@ async function downloadUpdate() {
|
||||
total_bytes: number
|
||||
downloaded_bytes: number
|
||||
components_downloaded: number
|
||||
}>({ method: 'update.download', timeout: 1_800_000 })
|
||||
}>({ method: 'update.download', timeout: 3_900_000 })
|
||||
downloadPercent.value = 100
|
||||
downloaded.value = true
|
||||
const sizeMB = (res.downloaded_bytes / 1_048_576).toFixed(1)
|
||||
@@ -395,40 +536,50 @@ async function executeConfirm() {
|
||||
if (action === 'apply') {
|
||||
await applyUpdate()
|
||||
} else if (action === 'git-apply') {
|
||||
await applyUpdateGit()
|
||||
await applyUpdateGitWithOverlay()
|
||||
} else if (action === 'rollback') {
|
||||
await rollbackUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUpdateGit() {
|
||||
applying.value = true
|
||||
statusMessage.value = ''
|
||||
try {
|
||||
await rpcClient.call({ method: 'update.git-apply', timeout: 900_000 })
|
||||
showStatus(t('systemUpdate.gitApplyStarted'))
|
||||
updateInfo.value = null
|
||||
} catch (e) {
|
||||
showStatus(t('systemUpdate.applyFailed'), true)
|
||||
if (import.meta.env.DEV) console.warn('Git apply failed', e)
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUpdate() {
|
||||
applying.value = true
|
||||
statusMessage.value = ''
|
||||
const target = updateInfo.value?.version || null
|
||||
try {
|
||||
await rpcClient.call({ method: 'update.apply', timeout: 300_000 })
|
||||
showStatus(t('systemUpdate.applySuccess'))
|
||||
updateInfo.value = null
|
||||
downloaded.value = false
|
||||
await loadStatus()
|
||||
// Apply succeeded. Backend scheduled a restart 2s after returning;
|
||||
// show the full-screen overlay while we wait for the new backend
|
||||
// to report the target version, then auto-reload.
|
||||
applying.value = false
|
||||
if (target) {
|
||||
startInstallOverlay(target)
|
||||
} else {
|
||||
// No target version known (legacy path) — fall back to the old
|
||||
// flash-and-reload behaviour.
|
||||
showStatus(t('systemUpdate.applySuccess'))
|
||||
setTimeout(() => window.location.reload(), 3000)
|
||||
}
|
||||
} catch (e) {
|
||||
showStatus(t('systemUpdate.applyFailed'), true)
|
||||
if (import.meta.env.DEV) console.warn('Apply failed', e)
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUpdateGitWithOverlay() {
|
||||
// Git-apply (dev path) also restarts the service — reuse the overlay
|
||||
// so the UX matches the manifest path. Target version isn't known up
|
||||
// front for git-apply; we just wait for a version change on /health.
|
||||
applying.value = true
|
||||
statusMessage.value = ''
|
||||
try {
|
||||
await rpcClient.call({ method: 'update.git-apply', timeout: 900_000 })
|
||||
applying.value = false
|
||||
startInstallOverlay(updateInfo.value?.version || currentVersion.value)
|
||||
} catch (e) {
|
||||
showStatus(t('systemUpdate.applyFailed'), true)
|
||||
if (import.meta.env.DEV) console.warn('Git apply failed', e)
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
@@ -469,3 +620,25 @@ onMounted(() => {
|
||||
Promise.all([loadStatus(), loadSchedule(), checkForUpdates()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Centered ASCII logo — clamped so the overlay doesn't blow out on
|
||||
narrow viewports. :deep so the rule reaches BitcoinFaceAscii's
|
||||
inner <pre>. */
|
||||
.install-overlay-ascii :deep(pre) {
|
||||
font-size: clamp(6px, 1.2vw, 12px);
|
||||
line-height: 1.1;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Indeterminate progress stripe that slides left-to-right. */
|
||||
.install-overlay-bar-anim {
|
||||
animation: installBarSlide 1.8s ease-in-out infinite;
|
||||
}
|
||||
@keyframes installBarSlide {
|
||||
0% { transform: translateX(-100%); }
|
||||
50% { transform: translateX(120%); }
|
||||
100% { transform: translateX(300%); }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user