feat(updater): support updates after verified SeaweedFS migration

This commit is contained in:
2026-09-28 15:47:06 +02:00
parent 790f20b499
commit 2b947c5e8f
20 changed files with 1074 additions and 67 deletions

View File

@@ -47,7 +47,8 @@ const reload = () => window.location.reload()
<p class="text-sm text-muted">Aktualisiert die gesamte Selfhost-Instanz einschließlich Docker Compose und Datenbank. Vor Änderungen wird ein Backup erstellt.</p>
<div role="status" aria-live="polite" class="space-y-3">
<UAlert v-if="message" :color="busy ? 'warning' : 'neutral'" variant="soft" :title="message" />
<UAlert v-if="recovery" color="error" title="Wiederherstellung auf dem Host erforderlich" description="Weitere Updates sind gesperrt. Den letzten Auftrag und die Backups auf dem Host prüfen." />
<UAlert v-if="recovery && overview?.storage?.migrationPending" color="warning" title="Speicherumstellung auf dem Host abschließen" description="Updates bleiben gesperrt, bis die geprüfte Migration an den neuen Host-Updater übergeben wurde. Die bestehende FEDEO-Version kann danach regulär aktualisiert werden." />
<UAlert v-else-if="recovery" color="error" title="Wiederherstellung auf dem Host erforderlich" description="Weitere Updates sind gesperrt. Den letzten Auftrag und die Backups auf dem Host prüfen." />
<UAlert v-if="job?.error" color="error" :title="job.error.message" />
<UAlert v-if="job?.plan?.requiresComposeAdoption" color="warning" title="Compose-Datei zuerst auf dem Host übernehmen" description="Die bestehende Konfiguration benötigt eine manuelle Prüfung mit dem CLI-Updater. Anschließend hier erneut nach Updates suchen." />
<p v-if="job || busy" class="text-sm font-medium">{{ statusLabel }}</p>
@@ -58,6 +59,8 @@ const reload = () => window.location.reload()
<div><dt class="text-muted">Commit</dt><dd class="font-mono" :title="job.plan.revision">{{ job.plan.revision?.slice(0, 8) }}</dd></div>
<div><dt class="text-muted">Geschätztes Backup</dt><dd>{{ backupSize }}</dd></div>
</dl>
<p v-if="job?.plan?.storageProvider === 'seaweedfs'" class="text-sm text-muted">Dateispeicher: SeaweedFS. Die geprüfte Speicherversion bleibt erhalten und wird zusammen mit der Datenbank gesichert.</p>
<p v-else-if="job?.plan?.storageProvider === 'minio'" class="text-sm text-muted">Dateispeicher: MinIO. Eine Umstellung auf SeaweedFS erfolgt als eigener, geprüfter Wartungsschritt auf dem Host.</p>
<p v-if="job?.plan?.retiredServices.length" class="text-sm text-muted">Alte Matrix-Dienste werden stillgelegt; Daten und Container bleiben erhalten: {{ job.plan.retiredServices.join(', ') }}.</p>
<p v-if="job?.requestId" class="break-all text-xs text-muted">Auftrag: {{ job.requestId }}</p>
<section v-if="job?.requestId && !accessDenied" aria-label="Update-Protokoll" class="space-y-3 rounded-lg border border-default p-3">

View File

@@ -5,13 +5,15 @@ export type UpdateState = {
export type UpdateJob = UpdateState & {
progress?: UpdateState; error?: { code: string; message: string };
plan?: { version: string; revision: string; previousVersion?: string; updateAvailable: boolean;
requiresComposeAdoption: boolean; estimatedBackupBytes?: number; retiredServices: string[] };
requiresComposeAdoption: boolean; estimatedBackupBytes?: number; retiredServices: string[];
storageProvider?: 'minio' | 'seaweedfs'; storageImageRetained?: boolean };
}
export type UpdateLogs = { requestId: string; logs: { source: 'service' | 'events' | 'commands'; content: string; truncated: boolean }[] }
type Overview = {
acceptingRequests: boolean; engine: UpdateState; request?: UpdateJob;
installed: { version?: string }; backend: { version: string; revision: string };
capabilities?: { requestLogs: boolean };
storage?: { provider: 'seaweedfs'; migrationPending: boolean };
}
type Submission = { command: 'check' | 'apply'; body: { requestId: string; version?: string; checkRequestId?: string } }

View File

@@ -82,6 +82,19 @@ test('recovery, up-to-date, unmanaged compose and other active jobs prevent appl
}
})
test('a completed storage migration stays blocked until the host handover is complete', async t => {
let state = { ...overview(check, { status: 'failed', recoveryRequired: true }),
storage: { provider: 'seaweedfs', migrationPending: true } }
const f = await setup(t, async path => path.endsWith('/status') ? state : state.request)
assert.equal(f.ui.overview.value.storage.migrationPending, true)
assert.equal(f.ui.canApply.value, false)
state = { ...overview({ ...check, plan: { ...check.plan, storageProvider: 'seaweedfs', storageImageRetained: true } }),
storage: { provider: 'seaweedfs', migrationPending: false } }
await f.ui.refresh()
assert.equal(f.ui.canApply.value, true)
assert.equal(f.ui.job.value.plan.storageProvider, 'seaweedfs')
})
test('an older polling response cannot erase a new submission whose response was lost', async t => {
let hold = false, release
const f = await setup(t, async (path, options) => {