feat(awooop): surface telegram callback coverage
All checks were successful
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m59s
CD Pipeline / post-deploy-checks (push) Successful in 1m20s

This commit is contained in:
Your Name
2026-05-25 16:56:28 +08:00
parent b7ee1f47ff
commit 449c4ac807
7 changed files with 524 additions and 0 deletions

View File

@@ -138,6 +138,31 @@ interface CallbackEvidenceCaptureStatus {
event_at?: string | null;
}
interface CallbackReplyAuditSummary {
schema_version?: string;
project_id?: string;
outbound_total?: number;
outbound_source_envelope_total?: number;
outbound_source_refs_total?: number;
outbound_incident_ref_total?: number;
outbound_failed_total?: number;
callback_total?: number;
callback_sent_total?: number;
callback_fallback_total?: number;
callback_rescue_total?: number;
callback_failed_total?: number;
callback_detail_total?: number;
callback_history_total?: number;
callback_snapshot_captured_total?: number;
callback_snapshot_partial_total?: number;
callback_snapshot_missing_total?: number;
callback_incident_total?: number;
snapshot_status?: CallbackEvidenceCaptureState | "no_callback" | string | null;
next_action?: string | null;
latest_outbound_at?: string | null;
latest_callback_at?: string | null;
}
interface Run {
run_id: string;
project_id: string;
@@ -405,6 +430,7 @@ interface CallbackRepliesResponse {
total: number;
page: number;
per_page: number;
summary?: CallbackReplyAuditSummary | null;
}
interface AiRoutePolicyItem {
@@ -1908,13 +1934,148 @@ function CallbackAwoooPStatusChainSnapshot({
);
}
function formatCoveragePercent(value: number, total: number) {
if (total <= 0) return "0%";
return `${Math.round((value / total) * 100)}%`;
}
function normalizeCallbackAuditSnapshotStatus(statusValue?: string | null) {
if (
statusValue === "captured" ||
statusValue === "partial" ||
statusValue === "not_captured" ||
statusValue === "no_callback"
) {
return statusValue;
}
return "observed";
}
function CallbackReplyAuditSummaryPanel({
summary,
}: {
summary?: CallbackReplyAuditSummary | null;
}) {
const t = useTranslations("awooop.callbackReply.events.summary");
if (!summary) return null;
const outboundTotal = summary.outbound_total ?? 0;
const callbackTotal = summary.callback_total ?? 0;
const snapshotStatus = normalizeCallbackAuditSnapshotStatus(summary.snapshot_status);
const nextActionRaw = summary.next_action ?? "observed";
const nextActionKey = (
nextActionRaw === "none" ||
nextActionRaw === "press_telegram_detail_or_history" ||
nextActionRaw === "press_telegram_detail_or_history_after_rollout" ||
nextActionRaw === "review_outbound_source_refs"
) ? nextActionRaw : "observed";
const sourceRefCoverage = formatCoveragePercent(
summary.outbound_incident_ref_total ?? 0,
outboundTotal
);
const snapshotCoverage = formatCoveragePercent(
summary.callback_snapshot_captured_total ?? 0,
callbackTotal
);
const latestCallback = summary.latest_callback_at
? new Date(summary.latest_callback_at).toLocaleString("zh-TW", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "--";
const snapshotClass = {
captured: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
partial: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
not_captured: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
no_callback: "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]",
observed: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
}[snapshotStatus];
return (
<div className="grid gap-px border-b border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-5">
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("outbound")}</p>
<p className="mt-2 font-mono text-xl font-semibold text-[#141413]">
{outboundTotal}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("outboundDetail", {
sourceRefs: summary.outbound_source_refs_total ?? 0,
incidentRefs: summary.outbound_incident_ref_total ?? 0,
coverage: sourceRefCoverage,
})}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("callbacks")}</p>
<p className="mt-2 font-mono text-xl font-semibold text-[#141413]">
{callbackTotal}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("callbackDetail", {
detail: summary.callback_detail_total ?? 0,
history: summary.callback_history_total ?? 0,
incidents: summary.callback_incident_total ?? 0,
})}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("snapshots")}</p>
<span
className={cn(
"mt-2 inline-flex border px-2 py-0.5 text-xs font-semibold",
snapshotClass
)}
>
{t(`statuses.${snapshotStatus}` as never)}
</span>
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">
{t("snapshotDetail", {
captured: summary.callback_snapshot_captured_total ?? 0,
partial: summary.callback_snapshot_partial_total ?? 0,
missing: summary.callback_snapshot_missing_total ?? 0,
coverage: snapshotCoverage,
})}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("delivery")}</p>
<p className="mt-2 font-mono text-xl font-semibold text-[#141413]">
{summary.callback_failed_total ?? 0}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("deliveryDetail", {
sent: summary.callback_sent_total ?? 0,
fallback: (summary.callback_fallback_total ?? 0)
+ (summary.callback_rescue_total ?? 0),
outboundFailed: summary.outbound_failed_total ?? 0,
})}
</p>
</div>
<div className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{t("next")}</p>
<p className="mt-2 text-xs font-semibold leading-5 text-[#141413]">
{t(`nextActions.${nextActionKey}` as never)}
</p>
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
{t("latest", { time: latestCallback })}
</p>
</div>
</div>
);
}
function CallbackReplyEvidencePanel({
events,
total,
summary,
error,
}: {
events: CallbackReplyEvent[];
total: number;
summary?: CallbackReplyAuditSummary | null;
error: string | null;
}) {
const t = useTranslations("awooop.callbackReply.events");
@@ -1935,6 +2096,8 @@ function CallbackReplyEvidencePanel({
</span>
</div>
<CallbackReplyAuditSummaryPanel summary={summary} />
{error ? (
<div className="px-4 py-4 text-sm text-[#9f2f25]">
{t("error", { error })}
@@ -2440,6 +2603,7 @@ export default function RunsPage() {
const [eventRecurrenceError, setEventRecurrenceError] = useState<string | null>(null);
const [callbackEvents, setCallbackEvents] = useState<CallbackReplyEvent[]>([]);
const [callbackEventsTotal, setCallbackEventsTotal] = useState(0);
const [callbackAuditSummary, setCallbackAuditSummary] = useState<CallbackReplyAuditSummary | null>(null);
const [callbackEventsError, setCallbackEventsError] = useState<string | null>(null);
const [aiRouteStatus, setAiRouteStatus] = useState<AiRouteStatusResponse | null>(null);
const [aiRouteStatusError, setAiRouteStatusError] = useState<string | null>(null);
@@ -2568,10 +2732,12 @@ export default function RunsPage() {
const callbackData: CallbackRepliesResponse = await callbackRes.json();
setCallbackEvents(Array.isArray(callbackData.items) ? callbackData.items : []);
setCallbackEventsTotal(callbackData.total ?? 0);
setCallbackAuditSummary(callbackData.summary ?? null);
setCallbackEventsError(null);
} else {
setCallbackEvents([]);
setCallbackEventsTotal(0);
setCallbackAuditSummary(null);
setCallbackEventsError(`HTTP ${callbackRes.status}`);
}
@@ -2817,6 +2983,7 @@ export default function RunsPage() {
<CallbackReplyEvidencePanel
events={callbackEvents}
total={callbackEventsTotal}
summary={callbackAuditSummary}
error={callbackEventsError}
/>