"use client"; import Link from "next/link"; import { useState } from "react"; import { Badge, Button, ConfidenceBadge, EmptyState, EventBadge, RelativeTime, ScopeBadge, SemanticsLabel, Sheet, SkeletonBlock, SkeletonRows, Tab, TabList, TabPanel, Tabs, Table, TBody, Td, Th, THead, Tr } from "@/components/ui"; import { cn } from "@/components/ui/cn"; import { DASH, fmtDateTimeUtc, fmtInt, fmtValue } from "@/lib/format"; import { CLAIM_STATUS_LABEL, ENTITY_TYPE_LABEL } from "@/lib/labels"; import { entityHref } from "@/lib/routes"; import type { AdminRun, RollbackResult, RunChanges } from "@/lib/admin-api"; import { durationBetween, ErrorNote, ExtLink, fmtMs, JsonView, KV, Mono, RefreshButton, RunStatusBadge, Stat } from "./primitives"; import { AdminPageHeader } from "./shell"; import { useToast } from "./toast"; import { useAdminMutation, useAdminQuery } from "./use-admin-query"; /* ------------------------------------------------------------------------------------------ /admin/runs/[id] — one connector run: stats, log, everything it wrote (provenance, claims, events, document versions) and a ROLLBACK with a confirm dialog. Rollback rejects the run's claims, retires its provenance rows (winner restored to the latest remaining row per field) and rejects its events; entity columns are re-derived by the worker's next reconciliation. ------------------------------------------------------------------------------------------ */ function n(v: unknown): number | null { return typeof v === "number" && Number.isFinite(v) ? v : null; } export function RunDetailView({ id }: { id: string }) { const q = useAdminQuery(`runs/${encodeURIComponent(id)}`, { timeoutMs: 30_000 }); const ch = useAdminQuery(`runs/${encodeURIComponent(id)}/changes`, { timeoutMs: 60_000 }); const { busy, run } = useAdminMutation(); const toast = useToast(); const [confirm, setConfirm] = useState(false); const [rolled, setRolled] = useState(null); const r = q.data; const c = ch.data; const stats = (r?.stats ?? {}) as Record; const capped = Boolean(c && Object.values(c.counts).some((v) => v >= 2000)); const rollback = async () => { const res = await run("rollback", `runs/${encodeURIComponent(id)}/rollback`, { body: {} }); setConfirm(false); if (res.ok) { setRolled(res.data); toast({ tone: "ok", title: "Run rolled back", description: `${fmtInt(res.data.claimsRejected)} claims rejected · ${fmtInt(res.data.provenanceRetired)} provenance rows retired · ${fmtInt(res.data.winnersRestored)} winners restored · ${fmtInt(res.data.eventsRejected)} events rejected`, duration: 9000 }); void ch.refresh(); } else toast({ tone: "danger", title: "Rollback failed", description: res.error }); }; if (q.error && !r) { return (
{id}} /> void q.refresh()} />
); } const statKeys = ["discovered", "selected", "fetched", "notModified", "changed", "unchanged", "extracted", "entities", "valid", "rejected", "created", "updated", "merged", "pendingMatches", "events", "provenanceRows", "claims", "failed", "robotsBlocked", "quarantined", "credits"]; return (
Connectors {r && ( <> / {r.connectorId} )} / run } title={{id}} description={ r ? ( {r.task} started {r.startedAt ? : DASH} · {fmtMs(durationBetween(r.startedAt, r.finishedAt))} {r.quarantined && ( quarantined (nothing published) )} ) : undefined } aside={ <> void Promise.all([q.refresh(), ch.refresh()])} busy={q.refreshing || ch.refreshing} updatedAt={q.updatedAt} /> } /> {r?.error && } {rolled && (
Rolled back: {fmtInt(rolled.claimsRejected)} claims rejected · {fmtInt(rolled.provenanceRetired)} provenance rows retired · {fmtInt(rolled.winnersRestored)} winners restored · {fmtInt(rolled.eventsRejected)} events rejected. Entity columns are re-derived by the worker’s next reconciliation.
)} {!r ? ( ) : (
{statKeys .filter((k) => n(stats[k]) !== null) .slice(0, 16) .map((k) => ( 0 ? "danger" : undefined) : k === "created" && (n(stats[k]) ?? 0) > 0 ? "ok" : undefined} /> ))}
)}

What this run wrote

Rows tagged run_id = {id}{capped ? " — lists capped at 2 000 rows per kind" : ""}.

{c && (
{fmtInt(c.counts.provenance)} provenance · {fmtInt(c.counts.claims)} claims · {fmtInt(c.counts.events)} events · {fmtInt(c.counts.documentVersions)} versions
)}
void ch.refresh()} className="m-3" /> {!c && !ch.error ? (
) : c ? ( Provenance Claims Events Document versions {c.provenance.length === 0 ? ( ) : (
{c.provenance.map((p, i) => ( ))}
Entity Field Value Source State Observed
{p.entityId}
{ENTITY_TYPE_LABEL[p.entityType] ?? p.entityType}
{p.field} {p.scope && } {fmtValue(p.value)} {p.isEstimate && ≈} {p.sourceName}
{p.sourceKind} {p.method && ` · ${p.method}`}
{p.isWinner && ( winner )} {p.isCurrent ? "current" : "retired"}
{fmtDateTimeUtc(p.lastObserved)}
)}
{c.claims.length === 0 ? ( ) : (
{c.claims.map((k) => ( ))}
Claim Predicate Value Scope Status Source
{k.id} {k.isWinner && ( shown )} {k.value !== null ? fmtValue(k.value, k.unit === "USD" ? "usd" : "mw") : k.valueText ?? DASH} {CLAIM_STATUS_LABEL[k.status]} {k.rejectionReason &&
{k.rejectionReason}
}
{k.sourceName}
)}
{c.events.length === 0 ? ( ) : (
    {c.events.map((e) => (
  • {e.entity ? ( {e.entity.name} ) : ( {e.title} )} {e.entity && e.title !== e.entity.name && — {e.title}} {e.reviewStatus} sig {e.significance} {e.id}
  • ))}
)}
{c.documentVersions.length === 0 ? ( ) : ( {c.documentVersions.map((v) => ( ))}
Document Version Fetched Significance Changes
{v.documentId} {v.url && (
{v.url.replace(/^https?:\/\//, "")}
)}
{v.id} {v.fetchedAt ? : DASH} {fmtInt(v.significance)} {fmtInt(v.changes)}
)}
) : null}

Run

{!r ? ( ) : ( {r.connectorId} }, { k: "Task", v: {r.task} }, { k: "Status", v: }, { k: "Started", v: fmtDateTimeUtc(r.startedAt) }, { k: "Finished", v: fmtDateTimeUtc(r.finishedAt) }, { k: "Duration", v: fmtMs(durationBetween(r.startedAt, r.finishedAt)) }, { k: "Avg fetch", v: n(stats.avgMs) !== null ? fmtMs(n(stats.avgMs)) : DASH }, ]} /> )}

Stats

{r ? : }

Log

{!r ? (
) : (r.log ?? []).length === 0 ? (

No log lines recorded.

) : (
{(r.log ?? []).map((l, i) => (
{l.t ? String(l.t).slice(11, 19) : ""} {l.level ?? ""} {String(l.msg ?? JSON.stringify(l))}
))}
)}
setConfirm(false)} title="Roll back this run?" description={`Run ${id}${r ? ` · ${r.connectorId} · ${r.task}` : ""}`} heightVh={60} footer={
} >

This will, for every row this run wrote:

  • {c ? fmtInt(c.counts.claims) : "…"} claims → status rejected (reason “rollback”)
  • {c ? fmtInt(c.counts.provenance) : "…"} provenance rows → retired; the winner of each affected field is restored to the latest remaining observation
  • {c ? fmtInt(c.counts.events) : "…"} events → review status rejected (removed from the live feed)

Entity columns (MW, status, coordinates…) are not rewritten here: the worker re-derives them on its next reconciliation. Document versions and archived bodies are kept. This action is recorded and cannot be undone from the console.

); }