release(v1.7.26-alpha): mirror list + origin-relative download URLs
Adds a multi-mirror manifest fetch. `check_for_updates` walks a configurable list (data_dir/update-mirrors.json) in priority order and falls through to the next mirror on any HTTP / parse / timeout failure. Two defaults bake in: Server 1 (git.tx1138.com) and Server 2 (23.182.128.160:3000). Critical fix: after parsing a manifest, rewrite every component's `download_url` so its origin matches the manifest URL we fetched. Before this, the manifest hard-coded absolute URLs pointing at one specific server — so even when a node fetched the manifest from a faster mirror, the actual 200MB download went back to the slow original. Now the faster mirror wins end-to-end. New RPCs: update.list-mirrors, update.add-mirror, update.remove-mirror, update.set-primary-mirror. New UI section on the System Update page for operator management. 5 new unit tests for origin parsing and manifest rewriting (21/21 green). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1c1416cc1a
commit
0d15ca588a
@@ -172,6 +172,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mirrors -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="flex items-start justify-between gap-4 mb-2">
|
||||
<h2 class="text-lg font-semibold text-white">Update mirrors</h2>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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 -->
|
||||
<div class="glass-card p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.actions') }}</h2>
|
||||
@@ -304,7 +364,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
@@ -346,6 +406,79 @@ const statusIsError = ref(false)
|
||||
const downloadPercent = ref(0)
|
||||
const downloadPercentFormatted = computed(() => downloadPercent.value.toFixed(2))
|
||||
|
||||
// Mirrors — servers this node tries for the manifest, in priority
|
||||
// order. First entry is the primary. Add/remove/set-primary are wired
|
||||
// to update.*-mirror RPCs; downloads automatically go to the mirror
|
||||
// that served the manifest.
|
||||
interface UpdateMirror { url: string; label: string }
|
||||
const mirrors = ref<UpdateMirror[]>([])
|
||||
const addingMirror = ref(false)
|
||||
const mirrorSaving = ref(false)
|
||||
const mirrorDraft = reactive({ url: '', label: '' })
|
||||
|
||||
async function loadMirrors() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({ method: 'update.list-mirrors' })
|
||||
mirrors.value = res.mirrors
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('update.list-mirrors failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitMirror() {
|
||||
const url = mirrorDraft.url.trim()
|
||||
if (!url) return
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
showStatus('Mirror URL must start with http:// or https://', true)
|
||||
return
|
||||
}
|
||||
mirrorSaving.value = true
|
||||
try {
|
||||
const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({
|
||||
method: 'update.add-mirror',
|
||||
params: { url, label: mirrorDraft.label.trim() },
|
||||
})
|
||||
mirrors.value = res.mirrors
|
||||
mirrorDraft.url = ''
|
||||
mirrorDraft.label = ''
|
||||
addingMirror.value = false
|
||||
showStatus('Mirror added.')
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
showStatus(`Add mirror failed: ${msg}`, true)
|
||||
} finally {
|
||||
mirrorSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMirror(url: string) {
|
||||
try {
|
||||
const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({
|
||||
method: 'update.remove-mirror',
|
||||
params: { url },
|
||||
})
|
||||
mirrors.value = res.mirrors
|
||||
showStatus('Mirror removed.')
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
showStatus(`Remove failed: ${msg}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function setPrimaryMirror(url: string) {
|
||||
try {
|
||||
const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({
|
||||
method: 'update.set-primary-mirror',
|
||||
params: { url },
|
||||
})
|
||||
mirrors.value = res.mirrors
|
||||
showStatus('Primary mirror updated. Next update check will try it first.')
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
showStatus(`Set primary failed: ${msg}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Poll the backend for the real bytes_downloaded / total_bytes so the
|
||||
// progress bar tracks actual download state (and survives route
|
||||
// changes). Returns true if a download is currently in progress.
|
||||
@@ -685,7 +818,7 @@ async function setSchedule(value: ScheduleValue) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadStatus(), loadSchedule(), checkForUpdates()])
|
||||
await Promise.all([loadStatus(), loadSchedule(), loadMirrors(), checkForUpdates()])
|
||||
// If a download was already running when the user navigated here
|
||||
// (or refreshed), pick up the progress bar where it is and keep
|
||||
// polling until the backend reports done. No RPC call to start the
|
||||
|
||||
Reference in New Issue
Block a user