release(v1.7.28-alpha): reboot progress overlay + VPS default primary
- 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:
parent
c3b3b03ee1
commit
79ae14a127
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.27-alpha"
|
||||
version = "1.7.28-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.27-alpha"
|
||||
version = "1.7.28-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@ -90,12 +90,12 @@ fn mirrors_path(data_dir: &Path) -> std::path::PathBuf {
|
||||
fn default_mirrors() -> Vec<UpdateMirror> {
|
||||
vec![
|
||||
UpdateMirror {
|
||||
url: DEFAULT_UPDATE_MANIFEST_URL.to_string(),
|
||||
label: "Server 1 (tx1138)".to_string(),
|
||||
url: DEFAULT_SECONDARY_MIRROR_URL.to_string(),
|
||||
label: "Server 1 (VPS)".to_string(),
|
||||
},
|
||||
UpdateMirror {
|
||||
url: DEFAULT_SECONDARY_MIRROR_URL.to_string(),
|
||||
label: "Server 2 (VPS)".to_string(),
|
||||
url: DEFAULT_UPDATE_MANIFEST_URL.to_string(),
|
||||
label: "Server 2 (tx1138)".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -13,15 +13,7 @@
|
||||
</div>
|
||||
<!-- Normal logo with audio viz ring -->
|
||||
<div v-else class="screensaver-content">
|
||||
<!-- Radial audio visualization - bars around the logo -->
|
||||
<div class="screensaver-viz-ring">
|
||||
<div
|
||||
v-for="(_, i) in segmentCount"
|
||||
:key="i"
|
||||
class="screensaver-viz-segment"
|
||||
:style="getSegmentStyle(i)"
|
||||
/>
|
||||
</div>
|
||||
<ScreensaverRing />
|
||||
<!-- Logo in center -->
|
||||
<div class="screensaver-logo-wrapper">
|
||||
<ScreensaverLogo />
|
||||
@ -35,21 +27,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount } from 'vue'
|
||||
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
|
||||
import ScreensaverRing from '@/components/ScreensaverRing.vue'
|
||||
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'
|
||||
import { useScreensaverStore } from '@/stores/screensaver'
|
||||
|
||||
const store = useScreensaverStore()
|
||||
|
||||
const segmentCount = 48
|
||||
|
||||
function getSegmentStyle(i: number) {
|
||||
const deg = (i / segmentCount) * 360
|
||||
return {
|
||||
'--segment-index': i,
|
||||
'--segment-deg': `${deg}deg`,
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss on any key (except when typing)
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (store.isActive) {
|
||||
@ -86,102 +69,15 @@ onBeforeUnmount(() => {
|
||||
|
||||
.screensaver-content {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.screensaver-content {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.screensaver-content {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ring of segments around the logo - audio viz style (behind logo) */
|
||||
.screensaver-viz-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
--viz-radius: 140px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.screensaver-viz-ring {
|
||||
--viz-radius: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.screensaver-viz-ring {
|
||||
--viz-radius: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.screensaver-viz-segment {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
margin-left: -2px;
|
||||
margin-top: -12px;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
|
||||
border-radius: 2px;
|
||||
/* Origin at segment center = ring center (segment is centered via left/top 50%) */
|
||||
transform-origin: center center;
|
||||
transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius)));
|
||||
animation: segment-pulse 14s ease-in-out infinite;
|
||||
animation-delay: calc(var(--segment-index) * 0.02s);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.screensaver-viz-segment {
|
||||
height: 28px;
|
||||
margin-top: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 5 normal loops (10s) then stronger longer expression (4s) - total 14s */
|
||||
@keyframes segment-pulse {
|
||||
/* Loop 1 */
|
||||
0% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
7.1% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
14.3% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
/* Loop 2 */
|
||||
21.4% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
28.6% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
/* Loop 3 */
|
||||
35.7% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
42.9% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
/* Loop 4 */
|
||||
50% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
57.1% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
/* Loop 5 */
|
||||
64.3% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
71.4% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
/* Strong expression: ramp up (1.5s), hold (2s), ease back (0.5s) */
|
||||
78.6% { opacity: 1; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1.5); }
|
||||
85.7% { opacity: 1; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1.5); }
|
||||
92.9% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
100% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.screensaver-logo-wrapper {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 10;
|
||||
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
|
||||
}
|
||||
|
||||
114
neode-ui/src/components/ScreensaverRing.vue
Normal file
114
neode-ui/src/components/ScreensaverRing.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="viz-ring" :class="sizeClass">
|
||||
<div
|
||||
v-for="(_, i) in segmentCount"
|
||||
:key="i"
|
||||
class="viz-segment"
|
||||
:style="getSegmentStyle(i)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** Visual size: 'default' matches the screensaver; 'compact' drops the
|
||||
* min-width breakpoints (useful inside overlays on narrower canvases). */
|
||||
size?: 'default' | 'compact'
|
||||
/** Override segment count. Defaults to 48 (screensaver standard). */
|
||||
segmentCount?: number
|
||||
}>(), { size: 'default', segmentCount: 48 })
|
||||
|
||||
const sizeClass = computed(() => props.size === 'compact' ? 'viz-ring-compact' : 'viz-ring-default')
|
||||
|
||||
function getSegmentStyle(i: number) {
|
||||
const deg = (i / props.segmentCount) * 360
|
||||
return {
|
||||
'--segment-index': i,
|
||||
'--segment-deg': `${deg}deg`,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.viz-ring {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.viz-ring-default {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
--viz-radius: 140px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.viz-ring-default {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
--viz-radius: 180px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.viz-ring-default {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
--viz-radius: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.viz-ring-compact {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
--viz-radius: 120px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.viz-ring-compact {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
--viz-radius: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.viz-segment {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
margin-left: -2px;
|
||||
margin-top: -12px;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
|
||||
border-radius: 2px;
|
||||
transform-origin: center center;
|
||||
transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius)));
|
||||
animation: segment-pulse 14s ease-in-out infinite;
|
||||
animation-delay: calc(var(--segment-index) * 0.02s);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.viz-segment {
|
||||
height: 28px;
|
||||
margin-top: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 5 normal loops (10s) then stronger longer expression (4s) — total 14s */
|
||||
@keyframes segment-pulse {
|
||||
0% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
7.1% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
14.3%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
21.4%{ opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
28.6%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
35.7%{ opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
42.9%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
50% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
57.1%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
64.3%{ opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
||||
71.4%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
78.6%{ opacity: 1; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1.5); }
|
||||
85.7%{ opacity: 1; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1.5); }
|
||||
92.9%{ opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
100% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
||||
}
|
||||
</style>
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,27 +1,26 @@
|
||||
{
|
||||
"version": "1.7.27-alpha",
|
||||
"version": "1.7.28-alpha",
|
||||
"release_date": "2026-04-21",
|
||||
"changelog": [
|
||||
"The Update page now shows which mirror delivered your update — a small 'Served by' line under the new version tells you whether Server 1, Server 2, or a custom mirror was the one your node actually reached. Useful for confirming mirror fallback is doing its job.",
|
||||
"Every mirror row has a new lightning-bolt button that pings the mirror and shows reachable / unreachable plus the round-trip latency in milliseconds. No more guessing if a mirror you just added is actually responding.",
|
||||
"The Update mirrors section got a visual refresh: Set Primary, Remove, and the new Test action are compact icon buttons instead of crowded text, and adding a mirror now happens in a dedicated dialog that matches the rest of the UI."
|
||||
"Reboot now shows a proper progress screen. Click Reboot and you'll see a full-screen overlay with the familiar pulsing ring, a rebooting / reconnecting / back-online status, and an elapsed counter. The page auto-reloads the moment your node is back up; if it takes longer than three minutes, a manual Reload button appears.",
|
||||
"New nodes 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 any time."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.26-alpha",
|
||||
"new_version": "1.7.27-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.27-alpha/archipelago",
|
||||
"sha256": "00a592041430ec62fd2f5d52b501af863dc9a4404f94147d9f3cd7ed3df05950",
|
||||
"size_bytes": 40889384
|
||||
"current_version": "1.7.27-alpha",
|
||||
"new_version": "1.7.28-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.28-alpha/archipelago",
|
||||
"sha256": "eba06b1c10a90a796e14b9e810900f77a6774ba7dfc782bb26b41d7f7800a605",
|
||||
"size_bytes": 40888296
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.27-alpha.tar.gz",
|
||||
"current_version": "1.7.26-alpha",
|
||||
"new_version": "1.7.27-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.27-alpha/archipelago-frontend-1.7.27-alpha.tar.gz",
|
||||
"sha256": "05604617ca1977905c6b941fb0d471df34d235ea70a921f40191bf8ff2e45c47",
|
||||
"size_bytes": 77000601
|
||||
"name": "archipelago-frontend-1.7.28-alpha.tar.gz",
|
||||
"current_version": "1.7.27-alpha",
|
||||
"new_version": "1.7.28-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.28-alpha/archipelago-frontend-1.7.28-alpha.tar.gz",
|
||||
"sha256": "c7eec98be09b0f7f9f04fcbfea594bea4f2a738943cdaa3427251b402ac62620",
|
||||
"size_bytes": 77000104
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.28-alpha/archipelago
Executable file
BIN
releases/v1.7.28-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.28-alpha/archipelago-frontend-1.7.28-alpha.tar.gz
Normal file
BIN
releases/v1.7.28-alpha/archipelago-frontend-1.7.28-alpha.tar.gz
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user