snapshots from another Dockhand instance sharing the repo now appear as restorable orphans
This commit is contained in:
4 files changed
+110
-40
No files matched your search
@@ -10,6 +10,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { SnapshotLoadProgress } from '$lib/components/ui/snapshot-load-progress';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { DataGrid } from '$lib/components/data-grid';
|
||||
@@ -106,6 +107,12 @@
|
||||
// Snapshot counts loaded async per destination, keyed by targetName:destId
|
||||
let snapshotCounts = $state<Map<string, number>>(new Map());
|
||||
let snapshotCountsLoading = $state(true);
|
||||
// Progress of the per-destination snapshot count sweep: how many destinations have
|
||||
// responded out of the total. Drives an "X / N" indicator so a slow/unreachable
|
||||
// repo (backend restic timeout is 5 min) shows visible progress instead of an
|
||||
// indeterminate spinner. done === total means the sweep finished.
|
||||
let snapshotCountsDone = $state(0);
|
||||
let snapshotCountsTotal = $state(0);
|
||||
// Orphan targets: found in repos but no config exists
|
||||
interface OrphanTarget { key: string; targetName: string; type: string; destinationId: number; destinationName?: string; destinationRepository?: string; snapshotCount: number; latestSnapshot?: string; }
|
||||
let orphanTargets = $state<OrphanTarget[]>([]);
|
||||
@@ -169,6 +176,8 @@
|
||||
let restoreSnapshotId = $state('');
|
||||
let restoreContainerName = $state('');
|
||||
let restoreEnvId = $state<number | undefined>(undefined);
|
||||
let restoreDestName = $state('');
|
||||
let restoreDestRepo = $state('');
|
||||
|
||||
// Snapshot diff
|
||||
let showDiff = $state(false);
|
||||
@@ -313,6 +322,8 @@
|
||||
|
||||
async function loadSnapshotCounts() {
|
||||
snapshotCountsLoading = true;
|
||||
snapshotCountsDone = 0;
|
||||
snapshotCountsTotal = destinations.length;
|
||||
// Live config ids per destination — a snapshot's dockhand:configid tag matching
|
||||
// one of these belongs to a shown config; anything else is orphaned.
|
||||
const liveConfigIds = new Set(configs.map(c => c.id));
|
||||
@@ -322,10 +333,19 @@
|
||||
// (snapshots whose config was deleted). The right-column count must match the
|
||||
// expanded per-config list, so it's keyed by config, NOT by target name — two
|
||||
// configs backing the same container to the same repo have DIFFERENT counts.
|
||||
//
|
||||
// Each destination updates snapshotCounts as soon as IT responds, and the
|
||||
// "Loading" indicator clears after the FIRST one finishes - so a single slow or
|
||||
// unreachable repo can't hold the whole list hostage for its 30s timeout. The
|
||||
// rest keep filling in counts progressively behind the scenes.
|
||||
await Promise.allSettled(destinations.map(async (dest) => {
|
||||
try {
|
||||
const res = await fetch(`/api/backup/snapshots?destinationId=${dest.id}`);
|
||||
if (!res.ok) return;
|
||||
// This install id: a snapshot belongs to a LOCAL config only when THIS instance wrote it.
|
||||
// configid collides across instances (a fresh install reuses 1,2,3...), so a foreign
|
||||
// snapshot with configid=1 must be an orphan, not mis-bucketed into local config #1 (#1351).
|
||||
const thisInstance = res.headers.get("X-Dockhand-Instance") || "";
|
||||
const d = await res.json();
|
||||
const snaps: any[] = d.snapshots ?? d;
|
||||
if (!Array.isArray(snaps)) return;
|
||||
@@ -338,8 +358,9 @@
|
||||
const perConfig = new Map<number, number>();
|
||||
const orphanByName = new Map<string, any[]>();
|
||||
for (const snap of snaps) {
|
||||
const cid = Number(tagVal(snap, 'dockhand:configid='));
|
||||
if (Number.isInteger(cid) && liveConfigIds.has(cid)) {
|
||||
const cid = Number(tagVal(snap, "dockhand:configid="));
|
||||
const isOwn = tagVal(snap, "dockhand:instance=") === thisInstance;
|
||||
if (isOwn && Number.isInteger(cid) && liveConfigIds.has(cid)) {
|
||||
perConfig.set(cid, (perConfig.get(cid) || 0) + 1);
|
||||
} else {
|
||||
const name = tagVal(snap, 'dockhand:name=') || 'unknown';
|
||||
@@ -371,9 +392,10 @@
|
||||
}
|
||||
snapshotCounts = newCounts;
|
||||
orphanTargets = newOrphans;
|
||||
} catch {}
|
||||
} catch {} finally { snapshotCountsDone += 1; }
|
||||
}));
|
||||
|
||||
// Backstop: if there were zero destinations, nothing cleared it above.
|
||||
snapshotCountsLoading = false;
|
||||
}
|
||||
|
||||
@@ -622,6 +644,8 @@
|
||||
restoreSnapshotId = snapshot.id;
|
||||
restoreContainerName = config.targetName;
|
||||
restoreEnvId = config.environmentId ?? undefined;
|
||||
restoreDestName = snapshot._destinationName ?? config.destinationName ?? '';
|
||||
restoreDestRepo = snapshot._destinationRepository ?? config.destinationRepository ?? '';
|
||||
showRestore = true;
|
||||
}
|
||||
|
||||
@@ -651,11 +675,13 @@
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-3 overflow-hidden">
|
||||
<div class="shrink-0 flex flex-wrap justify-between items-center gap-3 min-h-8">
|
||||
<PageHeader title="Backups" icon={Archive} count={filteredConfigs.length}>
|
||||
{#if loading || snapshotCountsLoading}
|
||||
{#if loading}
|
||||
<span class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Loader2 class="w-3.5 h-3.5 animate-spin" />
|
||||
{loading ? 'Loading…' : 'Loading snapshots…'}
|
||||
Loading…
|
||||
</span>
|
||||
{:else if snapshotCountsLoading}
|
||||
<SnapshotLoadProgress done={snapshotCountsDone} total={snapshotCountsTotal} />
|
||||
{/if}
|
||||
</PageHeader>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -1045,6 +1071,8 @@
|
||||
destinationId={restoreDestId}
|
||||
snapshotId={restoreSnapshotId}
|
||||
containerName={restoreContainerName}
|
||||
destinationName={restoreDestName}
|
||||
destinationRepository={restoreDestRepo}
|
||||
environmentId={restoreEnvId}
|
||||
onDone={fetchData}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user