162 lines
6.7 KiB
TypeScript
162 lines
6.7 KiB
TypeScript
import Link from "next/link";
|
|
import { LedgerRow, type ReadoutCell } from "./LedgerRow";
|
|
import { NoticeState } from "./NoticeState";
|
|
import { ProvenanceChip } from "./ProvenanceChip";
|
|
import { STATUS_TONE_CLASSES, statusToneFor } from "./tokens";
|
|
import type { AarResult } from "@/lib/operations/surfaceQuery";
|
|
|
|
const headCell = "px-3 py-2 text-left font-mono text-[9px] uppercase tracking-widest text-white/50";
|
|
const cell = "px-3 py-2 align-middle text-xs";
|
|
|
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
return (
|
|
<section data-slot="aar-section" className="flex flex-col gap-2">
|
|
<h2 className="font-mono text-[9px] uppercase tracking-widest text-white/60">{title}</h2>
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function countsRow(aar: AarResult): ReadoutCell[] {
|
|
return [
|
|
{ label: "Events", value: String(aar.counts.events), mono: true },
|
|
{ label: "Validated", value: String(aar.counts.validated), mono: true },
|
|
{ label: "Rejected", value: String(aar.counts.rejected), mono: true },
|
|
{ label: "Applied", value: String(aar.counts.applied), mono: true },
|
|
{ label: "Failed", value: String(aar.counts.failed), mono: true },
|
|
{ label: "Pending", value: String(aar.counts.pending), mono: true },
|
|
];
|
|
}
|
|
|
|
export function AarView({ aar }: { aar: AarResult }) {
|
|
const failedEffects = aar.effects.filter((e) => e.status === "failed" || e.status === "reversed");
|
|
const appliedEffects = aar.effects.filter((e) => e.status === "applied");
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6" data-slot="operations-aar-view">
|
|
<LedgerRow cells={countsRow(aar)} />
|
|
|
|
<Section title="Timeline">
|
|
{aar.events.length === 0 ? (
|
|
<NoticeState variant="empty" message="No events recorded for this operation." />
|
|
) : (
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="border-b border-white/10">
|
|
<th className={headCell}>Event</th>
|
|
<th className={headCell}>Type</th>
|
|
<th className={headCell}>Status</th>
|
|
<th className={headCell}>Occurred</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{aar.events.map((ev) => (
|
|
<tr key={ev.id} className="border-b border-white/5 last:border-0">
|
|
<td className={`${cell} font-mono text-white/85`}>{ev.eventId}</td>
|
|
<td className={`${cell} font-mono`}>{ev.type}</td>
|
|
<td className={cell}>
|
|
<span
|
|
className={`border px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-widest ${STATUS_TONE_CLASSES[statusToneFor(ev.status)]}`}
|
|
>
|
|
{ev.status}
|
|
</span>
|
|
{ev.errorCode && (
|
|
<span className="ml-2 text-[10px] text-rose-300">
|
|
{ev.errorCode}: {ev.errorMessage ?? "see notes"}
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td className={`${cell} font-mono tabular-nums text-white/70`}>{ev.occurredAt}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Applied effects">
|
|
{appliedEffects.length === 0 ? (
|
|
<NoticeState variant="empty" message="No applied effects yet for this operation." />
|
|
) : (
|
|
<ul className="flex flex-col gap-1">
|
|
{appliedEffects.map((eff) => (
|
|
<li
|
|
key={eff.id}
|
|
className="flex items-center justify-between gap-2 border border-white/10 px-2 py-1.5"
|
|
>
|
|
<span className="min-w-0 truncate font-mono text-xs text-white/80">{eff.effectKey}</span>
|
|
<span className="flex shrink-0 items-center gap-2">
|
|
<span className="font-mono text-[10px] uppercase tracking-widest text-white/50">
|
|
{eff.effectType}
|
|
</span>
|
|
<ProvenanceChip label={eff.status} href="/operations/ledger" />
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title="Failed or reversed effects">
|
|
{failedEffects.length === 0 ? (
|
|
<NoticeState variant="empty" message="No failed or reversed effects. Clean run." />
|
|
) : (
|
|
<ul className="flex flex-col gap-1">
|
|
{failedEffects.map((eff) => (
|
|
<li key={eff.id} className="border border-rose-500/30 px-2 py-1.5">
|
|
<p className="font-mono text-xs text-rose-300">{eff.effectKey}</p>
|
|
<p className="text-xs text-white/70">
|
|
{eff.notes ?? "Effect did not apply; see the operations ledger for reconciliation state."}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Section>
|
|
|
|
{aar.canSeePressure ? (
|
|
<Section title="Zone pressure">
|
|
{aar.pressure.length === 0 ? (
|
|
<NoticeState variant="empty" message="No pressure effects recorded for this operation." />
|
|
) : (
|
|
<ul className="flex flex-col gap-1">
|
|
{aar.pressure.map((p, i) => (
|
|
<li key={`${p.zoneId}-${i}`} className="flex items-center justify-between border border-white/10 px-2 py-1.5">
|
|
<span className="font-mono text-xs text-white/80">Zone {p.zoneId}</span>
|
|
<span
|
|
className={`font-mono text-[10px] uppercase tracking-widest ${p.delta >= 0 ? "text-emerald-400" : "text-rose-400"}`}
|
|
>
|
|
{p.delta >= 0 ? `+${p.delta}` : p.delta}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Section>
|
|
) : (
|
|
<NoticeState variant="empty" message="Zone pressure is currently disabled." />
|
|
)}
|
|
|
|
<Section title="Reservations">
|
|
{aar.reservations.length === 0 ? (
|
|
<NoticeState variant="empty" message="No reservations linked to this operation." />
|
|
) : (
|
|
<ul className="flex flex-col gap-1">
|
|
{aar.reservations.map((r) => (
|
|
<li key={r.reservationKey} className="flex items-center justify-between border border-white/10 px-2 py-1.5">
|
|
<span className="min-w-0 truncate font-mono text-xs text-white/80">{r.reservationKey}</span>
|
|
<span className="font-mono text-[10px] uppercase tracking-widest text-white/50">{r.status}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Section>
|
|
|
|
<p className="text-xs text-white/50">
|
|
<Link href="/operations/ledger" className="underline-offset-4 hover:underline">
|
|
Open the full operations ledger
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|