release(v1.7.27-alpha): mirror transparency — served-by line + one-click test button
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 15m40s
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 15m40s
- New "Served by {mirror}" line on the System Update page so operators can see
which mirror actually served the available manifest (vs. which is configured
primary). Backend threads the served URL through UpdateState.manifest_mirror.
- New update.test-mirror RPC + per-row lightning-bolt button that pings a
mirror and renders reachable/latency or error inline under the URL.
- UI polish on the mirrors section: Set Primary, Remove, and the new Test
action are compact icon buttons; add-mirror form moved into a dialog.
- "What's New" block prepended for v1.7.27-alpha.
21/21 update module tests pass. vue-tsc + vite build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.26-alpha"
|
||||
version = "1.7.27-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.26-alpha"
|
||||
version = "1.7.27-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -444,6 +444,10 @@ impl RpcHandler {
|
||||
let p = params.unwrap_or(serde_json::json!({}));
|
||||
self.handle_update_set_primary_mirror(&p).await
|
||||
}
|
||||
"update.test-mirror" => {
|
||||
let p = params.unwrap_or(serde_json::json!({}));
|
||||
self.handle_update_test_mirror(&p).await
|
||||
}
|
||||
"update.apply" => self.handle_update_apply().await,
|
||||
"update.git-apply" => self.handle_update_git_apply().await,
|
||||
"update.rollback" => self.handle_update_rollback().await,
|
||||
|
||||
@@ -40,6 +40,7 @@ impl RpcHandler {
|
||||
"last_check": state.last_check,
|
||||
"update_available": update_info.is_some(),
|
||||
"update": update_info,
|
||||
"manifest_mirror": state.manifest_mirror,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -195,6 +196,7 @@ impl RpcHandler {
|
||||
"update_available": state.available_update.is_some(),
|
||||
"update_in_progress": state.update_in_progress,
|
||||
"rollback_available": state.rollback_available,
|
||||
"manifest_mirror": state.manifest_mirror,
|
||||
"download_progress": if active || completed {
|
||||
Some(serde_json::json!({
|
||||
"bytes_downloaded": downloaded,
|
||||
@@ -290,6 +292,20 @@ impl RpcHandler {
|
||||
Ok(serde_json::json!({ "mirrors": list }))
|
||||
}
|
||||
|
||||
/// Ping a mirror's manifest URL. Returns reachability, wall-clock
|
||||
/// latency, and HTTP status. Params: `{ url }`.
|
||||
pub(super) async fn handle_update_test_mirror(
|
||||
&self,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<serde_json::Value> {
|
||||
let url = params
|
||||
.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("missing url"))?;
|
||||
let result = update::test_mirror(url).await;
|
||||
Ok(serde_json::to_value(result)?)
|
||||
}
|
||||
|
||||
/// Move a mirror to the top of the list so it's tried first.
|
||||
/// Params: `{ url }`.
|
||||
pub(super) async fn handle_update_set_primary_mirror(
|
||||
|
||||
@@ -218,6 +218,11 @@ pub struct UpdateState {
|
||||
pub rollback_available: bool,
|
||||
#[serde(default)]
|
||||
pub schedule: UpdateSchedule,
|
||||
/// URL of the mirror whose manifest populated `available_update`.
|
||||
/// Surfaces in the UI so operators can tell at a glance which mirror
|
||||
/// their node actually hit (vs. just which is configured primary).
|
||||
#[serde(default)]
|
||||
pub manifest_mirror: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for UpdateState {
|
||||
@@ -229,6 +234,7 @@ impl Default for UpdateState {
|
||||
update_in_progress: false,
|
||||
rollback_available: false,
|
||||
schedule: UpdateSchedule::DailyCheck,
|
||||
manifest_mirror: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +266,7 @@ pub async fn load_state(data_dir: &Path) -> Result<UpdateState> {
|
||||
// it unconditionally; the next check_for_updates will repopulate
|
||||
// if there's genuinely something newer.
|
||||
state.available_update = None;
|
||||
state.manifest_mirror = None;
|
||||
save_state(data_dir, &state).await?;
|
||||
}
|
||||
Ok(state)
|
||||
@@ -328,6 +335,7 @@ pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
||||
"Update available"
|
||||
);
|
||||
state.available_update = Some(manifest);
|
||||
state.manifest_mirror = Some(manifest_url.clone());
|
||||
} else {
|
||||
// Manifest version matches us or is behind
|
||||
// us — either we're current, or this mirror
|
||||
@@ -347,6 +355,7 @@ pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
||||
// break: another mirror could be ahead.
|
||||
continue 'mirrors;
|
||||
}
|
||||
state.manifest_mirror = None;
|
||||
state.available_update = None;
|
||||
}
|
||||
handled = true;
|
||||
@@ -375,6 +384,66 @@ pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MirrorTestResult {
|
||||
pub reachable: bool,
|
||||
pub latency_ms: u64,
|
||||
pub http_status: Option<u16>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Ping a mirror's manifest URL and return reachability + wall-clock
|
||||
/// latency. Used by the "Test mirror" button so operators can sanity-
|
||||
/// check a newly added mirror without running a full update check.
|
||||
pub async fn test_mirror(url: &str) -> MirrorTestResult {
|
||||
let client = match reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return MirrorTestResult {
|
||||
reachable: false,
|
||||
latency_ms: 0,
|
||||
http_status: None,
|
||||
error: Some(format!("client build failed: {}", e)),
|
||||
}
|
||||
}
|
||||
};
|
||||
let start = std::time::Instant::now();
|
||||
match client.get(url).send().await {
|
||||
Ok(resp) => {
|
||||
let latency_ms = start.elapsed().as_millis() as u64;
|
||||
let status = resp.status();
|
||||
if status.is_success() {
|
||||
MirrorTestResult {
|
||||
reachable: true,
|
||||
latency_ms,
|
||||
http_status: Some(status.as_u16()),
|
||||
error: None,
|
||||
}
|
||||
} else {
|
||||
MirrorTestResult {
|
||||
reachable: false,
|
||||
latency_ms,
|
||||
http_status: Some(status.as_u16()),
|
||||
error: Some(format!("HTTP {}", status.as_u16())),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let latency_ms = start.elapsed().as_millis() as u64;
|
||||
MirrorTestResult {
|
||||
reachable: false,
|
||||
latency_ms,
|
||||
http_status: None,
|
||||
error: Some(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current update status without checking remote.
|
||||
pub async fn get_status(data_dir: &Path) -> Result<UpdateState> {
|
||||
load_state(data_dir).await
|
||||
@@ -1155,6 +1224,7 @@ mod tests {
|
||||
update_in_progress: false,
|
||||
rollback_available: true,
|
||||
schedule: UpdateSchedule::AutoApply,
|
||||
manifest_mirror: None,
|
||||
};
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
let deserialized: UpdateState = serde_json::from_str(&json).unwrap();
|
||||
@@ -1265,6 +1335,10 @@ mod tests {
|
||||
update_in_progress: true,
|
||||
rollback_available: false,
|
||||
schedule: UpdateSchedule::Manual,
|
||||
manifest_mirror: Some(
|
||||
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
save_state(dir.path(), &state).await.unwrap();
|
||||
let loaded = load_state(dir.path()).await.unwrap();
|
||||
|
||||
@@ -42,6 +42,12 @@
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ t('systemUpdate.updateAvailable') }}</h2>
|
||||
<p class="text-sm text-white/60">Version {{ updateInfo.version }} — {{ updateInfo.release_date }}</p>
|
||||
<p v-if="manifestMirrorLabel" class="text-xs text-white/40 mt-1 flex items-center gap-1.5">
|
||||
<svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span>Served by <span class="text-white/70">{{ manifestMirrorLabel }}</span></span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="px-3 py-1 bg-orange-500/20 text-orange-400 text-xs font-medium rounded-full">{{ t('systemUpdate.new') }}</span>
|
||||
</div>
|
||||
@@ -179,57 +185,78 @@
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs px-3 py-1.5 rounded-md bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors"
|
||||
@click="addingMirror = !addingMirror"
|
||||
>{{ addingMirror ? 'Cancel' : '+ Add mirror' }}</button>
|
||||
@click="openAddMirror"
|
||||
>+ Add mirror</button>
|
||||
</div>
|
||||
<p class="text-sm text-white/60 mb-4">
|
||||
Servers this node checks for updates. The primary is tried first; if it's slow or unreachable, the next one in the list is tried automatically. Downloads always come from the mirror that served the manifest — switching primary switches where files come from.
|
||||
</p>
|
||||
<ul v-if="mirrors.length" class="space-y-2 mb-3">
|
||||
<li v-for="(m, i) in mirrors" :key="m.url" class="p-3 bg-white/5 rounded-lg flex items-start gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<p class="text-sm font-medium text-white truncate">{{ m.label || `Mirror ${i + 1}` }}</p>
|
||||
<span v-if="i === 0" class="text-[10px] font-mono px-2 py-0.5 rounded bg-green-500/20 text-green-300">PRIMARY</span>
|
||||
<ul v-if="mirrors.length" class="space-y-2">
|
||||
<li v-for="(m, i) in mirrors" :key="m.url" class="p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
<p class="text-sm font-medium text-white truncate">{{ m.label || `Mirror ${i + 1}` }}</p>
|
||||
<span v-if="i === 0" class="text-[10px] font-mono px-2 py-0.5 rounded bg-green-500/20 text-green-300">PRIMARY</span>
|
||||
</div>
|
||||
<p class="text-xs text-white/50 font-mono break-all">{{ m.url }}</p>
|
||||
</div>
|
||||
<div class="shrink-0 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-md text-white/60 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="mirrorTests[m.url]?.testing"
|
||||
title="Test reachability"
|
||||
@click="testMirror(m.url)"
|
||||
>
|
||||
<svg v-if="mirrorTests[m.url]?.testing" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25"></circle>
|
||||
<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="i !== 0"
|
||||
type="button"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-md text-white/60 hover:text-yellow-300 hover:bg-white/10 transition-colors"
|
||||
title="Make primary"
|
||||
@click="setPrimaryMirror(m.url)"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="mirrors.length > 1"
|
||||
type="button"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-md text-white/60 hover:text-red-300 hover:bg-red-400/10 transition-colors"
|
||||
title="Remove mirror"
|
||||
@click="removeMirror(m.url)"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-white/50 font-mono break-all">{{ m.url }}</p>
|
||||
</div>
|
||||
<div class="shrink-0 flex flex-col gap-1">
|
||||
<button
|
||||
v-if="i !== 0"
|
||||
type="button"
|
||||
class="text-xs px-2 py-1 rounded-md text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
||||
title="Make this the primary mirror"
|
||||
@click="setPrimaryMirror(m.url)"
|
||||
>Set primary</button>
|
||||
<button
|
||||
v-if="mirrors.length > 1"
|
||||
type="button"
|
||||
class="text-xs px-2 py-1 rounded-md text-red-300 hover:bg-red-400/10 transition-colors"
|
||||
@click="removeMirror(m.url)"
|
||||
>Remove</button>
|
||||
<div v-if="mirrorTests[m.url] && !mirrorTests[m.url]?.testing" class="mt-2 pt-2 border-t border-white/5 text-xs">
|
||||
<span v-if="mirrorTests[m.url]?.reachable" class="inline-flex items-center gap-1.5 text-green-300">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Reachable · {{ mirrorTests[m.url]?.latency_ms }}ms
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center gap-1.5 text-red-300">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span class="truncate">{{ mirrorTests[m.url]?.error || 'Unreachable' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<form v-if="addingMirror" class="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-3" @submit.prevent="submitMirror">
|
||||
<input
|
||||
v-model="mirrorDraft.url"
|
||||
type="text"
|
||||
placeholder="https://host/.../manifest.json"
|
||||
class="sm:col-span-2 px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
|
||||
/>
|
||||
<input
|
||||
v-model="mirrorDraft.label"
|
||||
type="text"
|
||||
placeholder="Label (optional)"
|
||||
class="px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="sm:col-span-3 min-h-[40px] glass-button rounded-lg text-sm font-medium disabled:opacity-60"
|
||||
:disabled="mirrorSaving || !mirrorDraft.url.trim()"
|
||||
>{{ mirrorSaving ? 'Adding…' : 'Add mirror' }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Actions row -->
|
||||
@@ -317,6 +344,51 @@
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Add-mirror modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="addingMirror" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelAddMirror">
|
||||
<div class="glass-card p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Add update mirror</h3>
|
||||
<p class="text-sm text-white/60 mb-5">
|
||||
The URL should point directly at a <span class="font-mono text-white/80">manifest.json</span> served by a Gitea mirror or equivalent. It's added to the end of the list; use "Make primary" to change order.
|
||||
</p>
|
||||
<form class="space-y-3" @submit.prevent="submitMirror">
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Manifest URL</label>
|
||||
<input
|
||||
v-model="mirrorDraft.url"
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder="https://host/.../manifest.json"
|
||||
class="w-full px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Label (optional)</label>
|
||||
<input
|
||||
v-model="mirrorDraft.label"
|
||||
type="text"
|
||||
placeholder="Home VPS"
|
||||
class="w-full px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="cancelAddMirror"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
>{{ t('common.cancel') }}</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="mirrorSaving || !mirrorDraft.url.trim()"
|
||||
>{{ mirrorSaving ? 'Adding…' : 'Add mirror' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 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">
|
||||
@@ -416,6 +488,60 @@ const addingMirror = ref(false)
|
||||
const mirrorSaving = ref(false)
|
||||
const mirrorDraft = reactive({ url: '', label: '' })
|
||||
|
||||
// URL of the mirror that served the currently-available-update manifest.
|
||||
// Backend reports it in update.status and update.check responses; the UI
|
||||
// resolves it to a friendly label by matching against the mirrors list.
|
||||
const manifestMirror = ref<string | null>(null)
|
||||
const manifestMirrorLabel = computed(() => {
|
||||
const url = manifestMirror.value
|
||||
if (!url) return null
|
||||
const match = mirrors.value.find(m => m.url === url)
|
||||
if (match && match.label) return match.label
|
||||
try {
|
||||
const u = new URL(url)
|
||||
return u.host
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
})
|
||||
|
||||
// Per-mirror test state. Populated by testMirror(); each entry is either
|
||||
// { testing: true } while in flight or the backend response shape on
|
||||
// completion. Rendered inline under each mirror row.
|
||||
interface MirrorTestState {
|
||||
testing?: boolean
|
||||
reachable?: boolean
|
||||
latency_ms?: number
|
||||
http_status?: number | null
|
||||
error?: string | null
|
||||
}
|
||||
const mirrorTests = ref<Record<string, MirrorTestState>>({})
|
||||
|
||||
async function testMirror(url: string) {
|
||||
mirrorTests.value = { ...mirrorTests.value, [url]: { testing: true } }
|
||||
try {
|
||||
const res = await rpcClient.call<{
|
||||
reachable: boolean
|
||||
latency_ms: number
|
||||
http_status: number | null
|
||||
error: string | null
|
||||
}>({ method: 'update.test-mirror', params: { url } })
|
||||
mirrorTests.value = { ...mirrorTests.value, [url]: { ...res, testing: false } }
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
mirrorTests.value = { ...mirrorTests.value, [url]: { testing: false, reachable: false, error: msg } }
|
||||
}
|
||||
}
|
||||
|
||||
function openAddMirror() {
|
||||
mirrorDraft.url = ''
|
||||
mirrorDraft.label = ''
|
||||
addingMirror.value = true
|
||||
}
|
||||
function cancelAddMirror() {
|
||||
addingMirror.value = false
|
||||
}
|
||||
|
||||
async function loadMirrors() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({ method: 'update.list-mirrors' })
|
||||
@@ -621,11 +747,13 @@ async function loadStatus() {
|
||||
update_available: boolean
|
||||
update_in_progress: boolean
|
||||
rollback_available: boolean
|
||||
manifest_mirror: string | null
|
||||
}>({ method: 'update.status' })
|
||||
currentVersion.value = res.current_version
|
||||
lastCheck.value = res.last_check
|
||||
updateInProgress.value = res.update_in_progress
|
||||
rollbackAvailable.value = res.rollback_available
|
||||
manifestMirror.value = res.manifest_mirror ?? null
|
||||
|
||||
if (res.update_in_progress) {
|
||||
downloaded.value = true
|
||||
@@ -645,11 +773,13 @@ async function checkForUpdates() {
|
||||
update_available: boolean
|
||||
update: UpdateDetail | null
|
||||
update_method?: string
|
||||
manifest_mirror?: string | null
|
||||
}>({ method: 'update.check' })
|
||||
currentVersion.value = res.current_version
|
||||
lastCheck.value = res.last_check
|
||||
updateInfo.value = res.update
|
||||
updateMethod.value = res.update_method === 'git' ? 'git' : 'manifest'
|
||||
manifestMirror.value = res.manifest_mirror ?? null
|
||||
if (!res.update_available) {
|
||||
showStatus(t('systemUpdate.upToDateMessage'))
|
||||
}
|
||||
|
||||
@@ -180,6 +180,18 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.27-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.27-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>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. Great for spot-checking that mirror fallback is doing its job.</p>
|
||||
<p>Every mirror row has a new lightning-bolt button that pings the mirror and shows whether it's reachable, plus the round-trip latency in milliseconds. No more guessing if a mirror you just added is responding.</p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.26-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
{
|
||||
"version": "1.7.26-alpha",
|
||||
"version": "1.7.27-alpha",
|
||||
"release_date": "2026-04-21",
|
||||
"changelog": [
|
||||
"Update downloads now fall through a mirror list. If the primary update server is slow or unreachable, your node automatically tries the next mirror and fetches files from there — no more stalling on a single wedged server.",
|
||||
"New 'Update mirrors' section on the System Update page: see the list, add your own mirror, reorder which one is tried first (Set primary), or remove one. The primary is tagged with a green PRIMARY pill.",
|
||||
"Downloads automatically follow the mirror that served the manifest. Before this, every mirror handed out the same manifest with download URLs hardcoded to one specific server, so even picking a faster mirror couldn't speed up the binary + frontend fetch. Now the backend rewrites download URLs to match whichever mirror actually responded.",
|
||||
"Ships with two defaults: Server 1 (tx1138) and Server 2 (VPS). Custom mirrors take the format https://host/.../releases/manifest.json."
|
||||
"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."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.25-alpha",
|
||||
"new_version": "1.7.26-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago",
|
||||
"sha256": "9b9be929e668525e05550bf07930018846115a8249463a9100b2acc777b5268a",
|
||||
"size_bytes": 40700088
|
||||
"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
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.26-alpha.tar.gz",
|
||||
"current_version": "1.7.25-alpha",
|
||||
"new_version": "1.7.26-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago-frontend-1.7.26-alpha.tar.gz",
|
||||
"sha256": "b5981aba616bd15aa768c610cf3e44972d519c2a8204d4401da10bf2e4bd5886",
|
||||
"size_bytes": 162084251
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.27-alpha/archipelago
Executable file
BIN
releases/v1.7.27-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.27-alpha/archipelago-frontend-1.7.27-alpha.tar.gz
Normal file
BIN
releases/v1.7.27-alpha/archipelago-frontend-1.7.27-alpha.tar.gz
Normal file
Binary file not shown.
Reference in New Issue
Block a user