release(v1.7.28-alpha): reboot progress overlay + VPS default primary
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 20m56s

- New reboot progress overlay: full-screen black with the screensaver's
  pulsing ring, rebooting → reconnecting → back-online → stalled stages,
  elapsed counter, auto-reload on health-check success, manual reload
  button at 3 min stall. Mirrors the existing update overlay.
- Ring extracted from Screensaver.vue into a reusable ScreensaverRing
  component so the reboot overlay reuses the same animation.
- default_mirrors() now puts the VPS as Server 1 (primary) and tx1138 as
  Server 2 — new nodes fetch manifests from VPS first; existing nodes
  keep whatever mirror order they've customized.
- What's New entry prepended for v1.7.28-alpha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-21 15:06:37 -04:00
parent c3b3b03ee1
commit 79ae14a127
10 changed files with 273 additions and 133 deletions

View File

@@ -180,6 +180,18 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.7.28-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.28-alpha</span>
<span class="text-xs text-white/40">Apr 21, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Reboot now shows a proper progress screen. Click Reboot and you'll see a full-screen overlay with the familiar pulsing ring animation, a rebooting / reconnecting / back-online status, and an elapsed counter — no more black screen of mystery while you wait.</p>
<p>The overlay auto-reloads the page the moment your node is back up; if it takes longer than three minutes it surfaces a manual Reload button.</p>
<p>New nodes now default to the VPS mirror as Server 1 (primary) and tx1138 as Server 2 (fallback). Existing nodes keep whatever mirror order they've already set use Set Primary on the System Update page to change it.</p>
</div>
</div>
<!-- v1.7.27-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { rpcClient } from '@/api/rpc-client'
import ScreensaverRing from '@/components/ScreensaverRing.vue'
const router = useRouter()
const { t } = useI18n()
@@ -13,6 +14,59 @@ const rebooting = ref(false)
const rebootPassword = ref('')
const rebootError = ref('')
// Reboot overlay — full-screen progress shown once the reboot is committed.
// Mirrors the update overlay pattern in SystemUpdate.vue: poll /health,
// auto-reload when the backend returns, stall fallback at 3 min.
type RebootStage = 'rebooting' | 'reconnecting' | 'ready' | 'stalled'
const rebootOverlay = ref(false)
const rebootStage = ref<RebootStage>('rebooting')
const rebootStartedAt = ref(0)
const rebootElapsedSec = ref(0)
let rebootPollTimer: ReturnType<typeof setInterval> | null = null
let rebootElapsedTimer: ReturnType<typeof setInterval> | null = null
const rebootElapsedLabel = computed(() => {
const s = rebootElapsedSec.value
if (s < 60) return `Elapsed: ${s}s`
return `Elapsed: ${Math.floor(s / 60)}m${s % 60 < 10 ? '0' : ''}${s % 60}s`
})
function startRebootOverlay() {
rebootOverlay.value = true
rebootStage.value = 'rebooting'
rebootStartedAt.value = Date.now()
rebootElapsedSec.value = 0
rebootElapsedTimer = setInterval(() => {
rebootElapsedSec.value = Math.floor((Date.now() - rebootStartedAt.value) / 1000)
if (rebootElapsedSec.value >= 180 && rebootStage.value !== 'ready') {
rebootStage.value = 'stalled'
}
}, 1000)
// Start health polling after 2.5s — the kernel has to go down before
// /health can disappear, and we don't want to see the pre-reboot health
// reply and mis-report "ready".
setTimeout(() => {
rebootStage.value = 'reconnecting'
rebootPollTimer = setInterval(pollRebootHealth, 1500)
}, 2500)
}
async function pollRebootHealth() {
if (rebootStage.value === 'ready' || rebootStage.value === 'stalled') return
try {
const res = await fetch('/health', { signal: AbortSignal.timeout(2000) })
if (!res.ok) throw new Error(`health ${res.status}`)
rebootStage.value = 'ready'
if (rebootPollTimer) { clearInterval(rebootPollTimer); rebootPollTimer = null }
setTimeout(() => { window.location.reload() }, 1200)
} catch {
// Fetch failing is the normal state while the host is down.
}
}
function rebootReloadNow() { window.location.reload() }
onBeforeUnmount(() => {
if (rebootPollTimer) clearInterval(rebootPollTimer)
if (rebootElapsedTimer) clearInterval(rebootElapsedTimer)
})
async function performReboot() {
if (!rebootPassword.value) return
rebooting.value = true
@@ -21,6 +75,7 @@ async function performReboot() {
await rpcClient.call({ method: 'system.reboot', params: { password: rebootPassword.value } })
showRebootConfirm.value = false
rebootPassword.value = ''
startRebootOverlay()
} catch (e) {
rebootError.value = e instanceof Error ? e.message : 'Reboot failed'
rebooting.value = false
@@ -108,6 +163,50 @@ async function performFactoryReset() {
</div>
</Teleport>
<!-- Reboot Progress Overlay -->
<Teleport to="body">
<Transition name="fade">
<div
v-if="rebootOverlay"
class="fixed inset-0 z-[3000] bg-black flex flex-col items-center justify-center overflow-hidden"
>
<!-- Centered animated ring same segments as the screensaver -->
<ScreensaverRing />
<!-- Stage text + progress bar underneath -->
<div class="mt-8 w-[min(520px,80vw)] text-center">
<h2 class="text-xl font-semibold text-white mb-1">
{{ rebootStage === 'rebooting' ? 'Rebooting…'
: rebootStage === 'reconnecting' ? 'Reconnecting to your node…'
: rebootStage === 'ready' ? 'Back online'
: 'Reboot is taking longer than expected' }}
</h2>
<p class="text-sm text-white/60 mb-4">
Your node is restarting. This page will refresh automatically once it's back.
</p>
<!-- Animated progress bar: indeterminate stripe while working,
solid green when ready, paused at half while stalled. -->
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden mb-3 relative">
<div v-if="rebootStage === 'ready'" class="absolute inset-0 bg-green-400"></div>
<div v-else-if="rebootStage === '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 reboot-overlay-bar-anim"></div>
</div>
<p class="text-xs text-white/40">{{ rebootElapsedLabel }}</p>
<button
v-if="rebootStage === 'stalled'"
@click="rebootReloadNow"
class="mt-5 glass-button rounded-lg px-5 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
>
Reload now
</button>
</div>
</div>
</Transition>
</Teleport>
<!-- Factory Reset Section -->
<div class="glass-card px-6 py-6 mb-6 border border-red-500/30">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
@@ -148,3 +247,23 @@ async function performFactoryReset() {
</div>
</Teleport>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.reboot-overlay-bar-anim {
animation: rebootBarSlide 1.8s ease-in-out infinite;
}
@keyframes rebootBarSlide {
0% { transform: translateX(-100%); }
50% { transform: translateX(120%); }
100% { transform: translateX(300%); }
}
</style>