feat(awooop): surface source dossier coverage
All checks were successful
Code Review / ai-code-review (push) Successful in 9s
CD Pipeline / tests (push) Successful in 1m8s
CD Pipeline / build-and-deploy (push) Successful in 3m52s
CD Pipeline / post-deploy-checks (push) Successful in 1m34s

This commit is contained in:
Your Name
2026-05-18 19:01:28 +08:00
parent fbde48438b
commit 213523c77d
6 changed files with 608 additions and 1 deletions

View File

@@ -141,6 +141,41 @@ interface RecentEventsResponse {
limit: number;
}
interface DossierCoverageSummary {
source_count: number;
source_envelope_total: number;
missing_source_envelope_total: number;
with_source_refs_total: number;
missing_source_refs_total: number;
duplicate_total: number;
redacted_total: number;
source_ref_total: number;
sentry_ref_total: number;
signoz_ref_total: number;
alert_ref_total: number;
latest_received_at?: string | null;
}
interface DossierCoverageProvider {
provider: string;
total: number;
duplicate_total: number;
redacted_total: number;
source_ref_total: number;
missing_source_refs_total: number;
sentry_ref_total: number;
signoz_ref_total: number;
alert_ref_total: number;
latest_received_at?: string | null;
}
interface DossierCoverageResponse {
project_id: string;
limit: number;
summary: DossierCoverageSummary;
providers: DossierCoverageProvider[];
}
interface CallbackReplyEvent {
message_id: string;
run_id: string;
@@ -700,6 +735,155 @@ function RunRow({ run }: { run: Run }) {
);
}
function SourceDossierCoveragePanel({
coverage,
error,
}: {
coverage: DossierCoverageResponse | null;
error: string | null;
}) {
const t = useTranslations("awooop.sourceDossierCoverage");
const summary = coverage?.summary;
const providers = coverage?.providers ?? [];
const latestAt = summary?.latest_received_at
? new Date(summary.latest_received_at).toLocaleTimeString("zh-TW", {
hour: "2-digit",
minute: "2-digit",
})
: "--";
const metrics = [
{
label: t("metrics.sources"),
value: summary?.source_count ?? 0,
detail: t("details.latest", { time: latestAt }),
className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
},
{
label: t("metrics.refs"),
value: summary?.source_ref_total ?? 0,
detail: t("details.withRefs", { count: summary?.with_source_refs_total ?? 0 }),
className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
},
{
label: t("metrics.missingRefs"),
value: summary?.missing_source_refs_total ?? 0,
detail: t("details.missingEnvelope", {
count: summary?.missing_source_envelope_total ?? 0,
}),
className: (summary?.missing_source_refs_total ?? 0) > 0
? "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"
: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
},
{
label: t("metrics.duplicates"),
value: summary?.duplicate_total ?? 0,
detail: t("details.redacted", { count: summary?.redacted_total ?? 0 }),
className: "border-[#d8d3c7] bg-white text-[#5f5b52]",
},
{
label: t("metrics.sentry"),
value: summary?.sentry_ref_total ?? 0,
detail: t("details.alertRefs", { count: summary?.alert_ref_total ?? 0 }),
className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
},
{
label: t("metrics.signoz"),
value: summary?.signoz_ref_total ?? 0,
detail: t("details.limit", { count: coverage?.limit ?? 0 }),
className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
},
];
return (
<section className="border border-[#e0ddd4] bg-white">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex items-center gap-2">
<ListChecks className="h-4 w-4 text-[#1f5b9b]" aria-hidden="true" />
<div>
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="text-xs text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
<span className="border border-[#9bb6d9] bg-[#eef5ff] px-2 py-0.5 text-xs font-semibold text-[#1f5b9b]">
{t("total", { count: summary?.source_count ?? 0 })}
</span>
</div>
{error ? (
<div className="px-4 py-4 text-sm text-[#9f2f25]">
{t("error", { error })}
</div>
) : (
<>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3 xl:grid-cols-6">
{metrics.map((item) => (
<div key={item.label} className="bg-white px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
{item.value}
</div>
</div>
<span
className={cn(
"flex h-8 w-8 items-center justify-center border",
item.className
)}
>
<SearchCheck className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">{item.detail}</p>
</div>
))}
</div>
{providers.length === 0 ? (
<div className="px-4 py-4 text-sm text-[#5f5b52]">
{t("empty")}
</div>
) : (
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2 xl:grid-cols-4">
{providers.slice(0, 4).map((provider) => {
const providerLatest = provider.latest_received_at
? new Date(provider.latest_received_at).toLocaleTimeString("zh-TW", {
hour: "2-digit",
minute: "2-digit",
})
: "--";
return (
<article key={provider.provider} className="bg-white px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate font-mono text-xs font-semibold text-[#141413]">
{provider.provider}
</p>
<p className="mt-1 text-xs text-[#77736a]">
{t("provider.latest", { time: providerLatest })}
</p>
</div>
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-xs text-[#5f5b52]">
{provider.total}
</span>
</div>
<div className="mt-3 grid grid-cols-2 gap-2 text-xs leading-5 text-[#5f5b52]">
<p>{t("provider.refs", { count: provider.source_ref_total })}</p>
<p>{t("provider.missing", { count: provider.missing_source_refs_total })}</p>
<p>{t("provider.redacted", { count: provider.redacted_total })}</p>
<p>{t("provider.duplicates", { count: provider.duplicate_total })}</p>
</div>
</article>
);
})}
</div>
)}
</>
)}
</section>
);
}
function GroupedAlertEventsPanel({ events }: { events: PlatformEvent[] }) {
return (
<section className="border border-[#e0ddd4] bg-white">
@@ -867,6 +1051,8 @@ export default function RunsPage() {
const tCallback = useTranslations("awooop.callbackReply");
const [runs, setRuns] = useState<Run[]>([]);
const [groupedEvents, setGroupedEvents] = useState<PlatformEvent[]>([]);
const [dossierCoverage, setDossierCoverage] = useState<DossierCoverageResponse | null>(null);
const [dossierCoverageError, setDossierCoverageError] = useState<string | null>(null);
const [callbackEvents, setCallbackEvents] = useState<CallbackReplyEvent[]>([]);
const [callbackEventsTotal, setCallbackEventsTotal] = useState(0);
const [callbackEventsError, setCallbackEventsError] = useState<string | null>(null);
@@ -949,6 +1135,21 @@ export default function RunsPage() {
setGroupedEvents(Array.isArray(eventsData.events) ? eventsData.events : []);
}
const dossierCoverageParams = new URLSearchParams();
dossierCoverageParams.set("limit", "100");
if (projectFilter) dossierCoverageParams.set("project_id", projectFilter);
const dossierCoverageRes = await fetch(
`${API_BASE}/api/v1/platform/events/dossier/coverage?${dossierCoverageParams.toString()}`
);
if (dossierCoverageRes.ok) {
const dossierCoverageData: DossierCoverageResponse = await dossierCoverageRes.json();
setDossierCoverage(dossierCoverageData);
setDossierCoverageError(null);
} else {
setDossierCoverage(null);
setDossierCoverageError(`HTTP ${dossierCoverageRes.status}`);
}
const callbackParams = new URLSearchParams();
callbackParams.set("per_page", "6");
if (projectFilter) callbackParams.set("project_id", projectFilter);
@@ -1161,6 +1362,11 @@ export default function RunsPage() {
})}
</section>
<SourceDossierCoveragePanel
coverage={dossierCoverage}
error={dossierCoverageError}
/>
<GroupedAlertEventsPanel events={groupedEvents} />
<CallbackReplyEvidencePanel