feat(topology): persist comparison runs + history
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled

Save each comparison and let users reload past ones.
- migration 0008: topology_runs (workspace-scoped; full comparison as JSONB).
- cm-db repo::topology_runs (insert / list_recent / get) + regenerated .sqlx.
- cm-api: compare persists best-effort (never loses the LLM result on a DB
  hiccup); GET /api/topology-runs (recent) + GET /api/topology-runs/{id}.
  Integration test asserts persist → list → get.
- frontend: "Recent comparisons" list on the Compare tab; click to reload a
  saved run. e2e p8 green (39 suite); offline build + clippy clean.

Server self-migrates at boot (cm_db::MIGRATOR), so 0008 applies on deploy.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 05:11:52 -07:00
co-authored by Claude Opus 4.8
parent 654ec0f511
commit 7baf2082d0
10 changed files with 345 additions and 3 deletions
@@ -1,9 +1,15 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import type { CatalogEntry, TopologyGraph } from "@/lib/api/topology";
interface RunSummary {
id: string;
task: string;
created_at: string;
}
interface CompareResult {
kind: string;
quality: number;
@@ -28,6 +34,33 @@ export function TopologyCompare({ catalog }: { catalog: CatalogEntry[] }) {
const [cmp, setCmp] = useState<Comparison | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [runs, setRuns] = useState<RunSummary[]>([]);
async function loadRuns() {
try {
const res = await fetch("/api/topology-runs");
if (res.ok) setRuns((await res.json()) as RunSummary[]);
} catch {
/* recent list is best-effort */
}
}
useEffect(() => {
void loadRuns();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function loadRun(id: string) {
try {
const res = await fetch(`/api/topology-runs/${id}`);
if (res.ok) {
const detail = (await res.json()) as { comparison: Comparison };
setCmp(detail.comparison);
}
} catch {
/* ignore */
}
}
function toggle(kind: string) {
setSelected((s) => {
@@ -63,6 +96,7 @@ export function TopologyCompare({ catalog }: { catalog: CatalogEntry[] }) {
});
if (!res.ok) throw new Error(`Compare failed (${res.status})`);
setCmp((await res.json()) as Comparison);
void loadRuns();
} catch (e) {
setError(e instanceof Error ? e.message : "Compare failed");
} finally {
@@ -120,6 +154,22 @@ export function TopologyCompare({ catalog }: { catalog: CatalogEntry[] }) {
{error && <p className="text-sm text-coral">{error}</p>}
{cmp && <Results cmp={cmp} />}
{runs.length > 0 && (
<div className="flex flex-col gap-1 border-t border-border pt-4">
<p className="text-xs font-medium text-muted-foreground">Recent comparisons</p>
{runs.map((r) => (
<button
key={r.id}
type="button"
onClick={() => loadRun(r.id)}
className="rounded-lg px-3 py-1.5 text-left text-xs text-muted-foreground transition-colors hover:bg-surface-warm/50"
>
{r.task}
</button>
))}
</div>
)}
</div>
);
}