feat(awooop): surface source evidence flow
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Activity, CheckCircle2, Route, ShieldAlert, TriangleAlert } from "lucide-react";
|
||||
import { Activity, CheckCircle2, Link2, RadioTower, Route, ShieldAlert, TriangleAlert } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -176,6 +176,15 @@ function valueOrEmpty(value: unknown, emptyLabel: string) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
type SourceFlowTone = "success" | "warning" | "blocked" | "neutral";
|
||||
|
||||
function sourceFlowToneClass(tone: SourceFlowTone) {
|
||||
if (tone === "success") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
|
||||
if (tone === "blocked") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]";
|
||||
if (tone === "warning") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
|
||||
return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
|
||||
}
|
||||
|
||||
export function AwoooPStatusChainPanel({
|
||||
chain,
|
||||
compact = false,
|
||||
@@ -251,6 +260,62 @@ export function AwoooPStatusChainPanel({
|
||||
const providerItem = sourceCorrelation?.providers?.[provider];
|
||||
return `${provider} ${providerItem?.direct_ref_total ?? 0}/${providerItem?.candidate_total ?? 0}/${providerItem?.applied_link_total ?? 0}`;
|
||||
}).join(" · ");
|
||||
const sourceProviderEntries = Object.entries(sourceCorrelation?.providers ?? {});
|
||||
const sourceProviderReadyTotal = sourceProviderEntries.filter(([, provider]) => (
|
||||
Boolean(provider.latest_event_at)
|
||||
|| Boolean(provider.latest_heartbeat_at)
|
||||
|| (provider.direct_ref_total ?? 0) > 0
|
||||
|| (provider.candidate_total ?? 0) > 0
|
||||
|| (provider.applied_link_total ?? 0) > 0
|
||||
)).length;
|
||||
const providerEventTotal = sourceCorrelation?.provider_event_total ?? 0;
|
||||
const directRefTotal = sourceCorrelation?.direct_ref_total ?? 0;
|
||||
const candidateTotal = sourceCorrelation?.candidate_total ?? 0;
|
||||
const appliedLinkTotal = sourceCorrelation?.applied_link_total ?? 0;
|
||||
const providerIngressReady = providerEventTotal > 0 || sourceProviderReadyTotal > 0;
|
||||
const sourceEvidenceFound = directRefTotal > 0 || candidateTotal > 0 || appliedLinkTotal > 0;
|
||||
const sourceLinkVerified = sourceVerificationStatus === "applied_link_verified"
|
||||
|| sourceVerificationStatus === "direct_ref_verified";
|
||||
const sourceVerificationBlocked = sourceVerificationStatus === "missing"
|
||||
|| sourceVerificationStatus === "fetch_failed"
|
||||
|| sourceVerificationStatus === "no_incident_context";
|
||||
const sourceEvidenceFlow = sourceCorrelation ? [
|
||||
{
|
||||
key: "provider-ingress",
|
||||
Icon: RadioTower,
|
||||
tone: (providerIngressReady ? "success" : "warning") as SourceFlowTone,
|
||||
label: t("source.flow.providerIngress"),
|
||||
status: providerIngressReady ? t("source.flow.status.ready") : t("source.flow.status.waiting"),
|
||||
detail: t("source.flow.providerDetail", {
|
||||
providerEvents: providerEventTotal,
|
||||
readyProviders: sourceProviderReadyTotal,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "source-evidence",
|
||||
Icon: Link2,
|
||||
tone: (appliedLinkTotal > 0 ? "success" : (sourceEvidenceFound ? "warning" : "blocked")) as SourceFlowTone,
|
||||
label: t("source.flow.sourceEvidence"),
|
||||
status: appliedLinkTotal > 0
|
||||
? t("source.flow.status.applied")
|
||||
: (sourceEvidenceFound ? t("source.flow.status.needsReview") : t("source.flow.status.waiting")),
|
||||
detail: t("source.directCandidateValue", {
|
||||
direct: directRefTotal,
|
||||
candidate: candidateTotal,
|
||||
applied: appliedLinkTotal,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "applied-verification",
|
||||
Icon: CheckCircle2,
|
||||
tone: (sourceLinkVerified ? "success" : (sourceVerificationBlocked ? "blocked" : "warning")) as SourceFlowTone,
|
||||
label: t("source.flow.appliedVerification"),
|
||||
status: sourceVerificationLabels[sourceVerificationStatus] ?? valueOrEmpty(sourceVerificationStatus, emptyLabel),
|
||||
detail: t("source.flow.verificationDetail", {
|
||||
latest: latestAppliedLabel,
|
||||
}),
|
||||
},
|
||||
] : [];
|
||||
|
||||
return (
|
||||
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
|
||||
@@ -324,43 +389,68 @@ export function AwoooPStatusChainPanel({
|
||||
</div>
|
||||
|
||||
{sourceCorrelation && (
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-5">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.status")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={sourceStatusLabels[sourceStatus] ?? sourceStatus}>
|
||||
{sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceCorrelation.status, emptyLabel)}
|
||||
</p>
|
||||
<div>
|
||||
<div className={cn("grid gap-px bg-[#e0ddd4]", compact ? "grid-cols-1" : "md:grid-cols-3")}>
|
||||
{sourceEvidenceFlow.map((item) => (
|
||||
<div key={item.key} className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center border",
|
||||
sourceFlowToneClass(item.tone)
|
||||
)}>
|
||||
<item.Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#141413]" title={item.status}>
|
||||
{item.status}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 truncate font-mono text-xs text-[#5f5b52]" title={item.detail}>
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.verification")}</p>
|
||||
<p
|
||||
className="mt-2 truncate font-mono text-sm text-[#141413]"
|
||||
title={sourceVerificationLabels[sourceVerificationStatus] ?? sourceVerificationStatus}
|
||||
>
|
||||
{sourceVerificationLabels[sourceVerificationStatus] ?? valueOrEmpty(sourceVerificationStatus, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.directCandidate")}</p>
|
||||
<p className="mt-2 font-mono text-sm text-[#141413]">
|
||||
{t("source.directCandidateValue", {
|
||||
direct: sourceCorrelation.direct_ref_total ?? 0,
|
||||
candidate: sourceCorrelation.candidate_total ?? 0,
|
||||
applied: sourceCorrelation.applied_link_total ?? 0,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.latestApplied")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={latestAppliedLabel}>
|
||||
{latestAppliedLabel}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.providers")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={sourceProviderSummary}>
|
||||
{sourceProviderSummary}
|
||||
</p>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-5">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.status")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={sourceStatusLabels[sourceStatus] ?? sourceStatus}>
|
||||
{sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceCorrelation.status, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.verification")}</p>
|
||||
<p
|
||||
className="mt-2 truncate font-mono text-sm text-[#141413]"
|
||||
title={sourceVerificationLabels[sourceVerificationStatus] ?? sourceVerificationStatus}
|
||||
>
|
||||
{sourceVerificationLabels[sourceVerificationStatus] ?? valueOrEmpty(sourceVerificationStatus, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.directCandidate")}</p>
|
||||
<p className="mt-2 font-mono text-sm text-[#141413]">
|
||||
{t("source.directCandidateValue", {
|
||||
direct: directRefTotal,
|
||||
candidate: candidateTotal,
|
||||
applied: appliedLinkTotal,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.latestApplied")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={latestAppliedLabel}>
|
||||
{latestAppliedLabel}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("source.providers")}</p>
|
||||
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={sourceProviderSummary}>
|
||||
{sourceProviderSummary}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user