feat(awooop): surface status chain in operator console
This commit is contained in:
195
apps/web/src/components/awooop/status-chain.tsx
Normal file
195
apps/web/src/components/awooop/status-chain.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import { Activity, CheckCircle2, Route, ShieldAlert, TriangleAlert } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface AwoooPStatusChain {
|
||||
schema_version?: string;
|
||||
source?: string;
|
||||
source_id?: string | null;
|
||||
incident_ids?: string[];
|
||||
current_stage?: string | null;
|
||||
stage_status?: string | null;
|
||||
verdict?: string | null;
|
||||
repair_state?: string | null;
|
||||
verification?: string | null;
|
||||
needs_human?: boolean | null;
|
||||
next_step?: string | null;
|
||||
blockers?: string[];
|
||||
fetch_error?: string | null;
|
||||
evidence?: {
|
||||
auto_repair_records?: number | null;
|
||||
operation_records?: number | null;
|
||||
mcp_gateway_total?: number | null;
|
||||
knowledge_entries?: number | null;
|
||||
remediation_total?: number | null;
|
||||
remediation_state?: string | null;
|
||||
latest_route?: string | null;
|
||||
latest_mode?: string | null;
|
||||
latest_at?: string | null;
|
||||
latest_preview?: string | null;
|
||||
};
|
||||
writes?: {
|
||||
incident?: boolean | null;
|
||||
auto_repair?: boolean | null;
|
||||
};
|
||||
}
|
||||
|
||||
function toneClass(chain?: AwoooPStatusChain | null) {
|
||||
if (!chain) return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
|
||||
if (chain.needs_human) return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
|
||||
if (chain.repair_state === "auto_repaired_verified") {
|
||||
return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
|
||||
}
|
||||
if (chain.repair_state === "executed" || chain.stage_status === "success") {
|
||||
return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
|
||||
}
|
||||
return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
|
||||
}
|
||||
|
||||
function chainIcon(chain?: AwoooPStatusChain | null) {
|
||||
if (!chain) return Route;
|
||||
if (chain.needs_human) return TriangleAlert;
|
||||
if (chain.repair_state === "auto_repaired_verified") return CheckCircle2;
|
||||
if (chain.repair_state === "blocked_manual_required") return ShieldAlert;
|
||||
return Activity;
|
||||
}
|
||||
|
||||
function boolValue(value: boolean | null | undefined, emptyLabel: string) {
|
||||
if (value === true) return "true";
|
||||
if (value === false) return "false";
|
||||
return emptyLabel;
|
||||
}
|
||||
|
||||
function valueOrEmpty(value: unknown, emptyLabel: string) {
|
||||
if (value === null || value === undefined || value === "") return emptyLabel;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function AwoooPStatusChainPanel({
|
||||
chain,
|
||||
compact = false,
|
||||
className,
|
||||
}: {
|
||||
chain?: AwoooPStatusChain | null;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const t = useTranslations("awooop.statusChain");
|
||||
const Icon = chainIcon(chain);
|
||||
const tone = toneClass(chain);
|
||||
const emptyLabel = t("emptyValue");
|
||||
const evidence = chain?.evidence ?? {};
|
||||
const blockers = chain?.blockers ?? [];
|
||||
|
||||
if (!chain) {
|
||||
return (
|
||||
<section className={cn("border border-[#e0ddd4] bg-white", compact ? "p-3" : "", className)}>
|
||||
<div className={cn("flex items-start gap-3", !compact && "px-4 py-3")}>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center border border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]">
|
||||
<Route className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t("empty")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics = [
|
||||
{ label: t("fields.stage"), value: `${valueOrEmpty(chain.current_stage, emptyLabel)} / ${valueOrEmpty(chain.stage_status, emptyLabel)}` },
|
||||
{ label: t("fields.repair"), value: valueOrEmpty(chain.repair_state, emptyLabel) },
|
||||
{ label: t("fields.verification"), value: valueOrEmpty(chain.verification, emptyLabel) },
|
||||
{ label: t("fields.nextStep"), value: valueOrEmpty(chain.next_step, emptyLabel) },
|
||||
];
|
||||
const evidenceMetrics = [
|
||||
{ label: t("evidence.autoRepair"), value: evidence.auto_repair_records ?? 0 },
|
||||
{ label: t("evidence.ops"), value: evidence.operation_records ?? 0 },
|
||||
{ label: t("evidence.mcp"), value: evidence.mcp_gateway_total ?? 0 },
|
||||
{ label: t("evidence.km"), value: evidence.knowledge_entries ?? 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
|
||||
<div className={cn(
|
||||
"flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3]",
|
||||
compact ? "px-3 py-3" : "px-4 py-3"
|
||||
)}>
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className={cn("flex h-9 w-9 shrink-0 items-center justify-center border", tone)}>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-[#77736a]">
|
||||
{t("subtitle", {
|
||||
source: valueOrEmpty(chain.source, emptyLabel),
|
||||
sourceId: valueOrEmpty(chain.source_id, emptyLabel),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={cn("shrink-0 border px-2 py-0.5 text-xs font-semibold", tone)}>
|
||||
{chain.needs_human ? t("human.yes") : t("human.no")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={cn("grid gap-px bg-[#e0ddd4]", compact ? "md:grid-cols-2" : "md:grid-cols-4")}>
|
||||
{metrics.map((item) => (
|
||||
<div key={item.label} className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm font-semibold text-[#141413]" title={item.value}>
|
||||
{item.value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!compact && (
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-5">
|
||||
{evidenceMetrics.map((item) => (
|
||||
<div key={item.label} className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-2 font-mono text-xl font-semibold text-[#141413]">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("evidence.adr100")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(evidence.latest_route, emptyLabel)}>
|
||||
{valueOrEmpty(evidence.latest_route, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("fields.writes")}</p>
|
||||
<p className="mt-2 font-mono text-sm text-[#141413]">
|
||||
{t("writeFlags", {
|
||||
incident: boolValue(chain.writes?.incident, emptyLabel),
|
||||
autoRepair: boolValue(chain.writes?.auto_repair, emptyLabel),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("fields.verdict")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={valueOrEmpty(chain.verdict, emptyLabel)}>
|
||||
{valueOrEmpty(chain.verdict, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{blockers.length > 0 && (
|
||||
<div className="border-t border-[#eee9dd] bg-[#fff7e8] px-4 py-3 text-xs leading-5 text-[#8a5a08]">
|
||||
<span className="font-semibold">{t("blockers")}</span>{" "}
|
||||
<span className="font-mono">{blockers.slice(0, compact ? 2 : 4).join(", ")}</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user