feat(awooop): surface telegram callback coverage
This commit is contained in:
@@ -102,11 +102,37 @@ class CallbackReplyItem(BaseModel):
|
||||
run_detail_href: str | None = None
|
||||
|
||||
|
||||
class CallbackReplyAuditSummary(BaseModel):
|
||||
schema_version: str
|
||||
project_id: str
|
||||
outbound_total: int
|
||||
outbound_source_envelope_total: int
|
||||
outbound_source_refs_total: int
|
||||
outbound_incident_ref_total: int
|
||||
outbound_failed_total: int
|
||||
callback_total: int
|
||||
callback_sent_total: int
|
||||
callback_fallback_total: int
|
||||
callback_rescue_total: int
|
||||
callback_failed_total: int
|
||||
callback_detail_total: int
|
||||
callback_history_total: int
|
||||
callback_snapshot_captured_total: int
|
||||
callback_snapshot_partial_total: int
|
||||
callback_snapshot_missing_total: int
|
||||
callback_incident_total: int
|
||||
snapshot_status: str
|
||||
next_action: str
|
||||
latest_outbound_at: datetime | None = None
|
||||
latest_callback_at: datetime | None = None
|
||||
|
||||
|
||||
class ListCallbackRepliesResponse(BaseModel):
|
||||
items: list[CallbackReplyItem]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
summary: CallbackReplyAuditSummary | None = None
|
||||
|
||||
|
||||
class CicdEventItem(BaseModel):
|
||||
|
||||
@@ -110,6 +110,9 @@ _KM_STALE_COMPLETION_CALLBACK_SCHEMA_VERSION = (
|
||||
"km_stale_owner_review_completion_callback_summary_v1"
|
||||
)
|
||||
_CALLBACK_EVIDENCE_CAPTURE_STATUS_SCHEMA_VERSION = "callback_evidence_capture_status_v1"
|
||||
_CALLBACK_REPLY_AUDIT_SUMMARY_SCHEMA_VERSION = (
|
||||
"telegram_callback_reply_audit_summary_v1"
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Tenants
|
||||
@@ -398,6 +401,10 @@ async def list_callback_replies(
|
||||
total = count_result.scalar_one()
|
||||
rows_result = await db.execute(list_sql, params)
|
||||
rows = list(rows_result.mappings().all())
|
||||
summary = await _fetch_callback_reply_audit_summary(
|
||||
db,
|
||||
project_id=project_id or "awoooi",
|
||||
)
|
||||
|
||||
items = [_callback_reply_event_item(row) for row in rows]
|
||||
status_chain_cache: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
@@ -454,6 +461,165 @@ async def list_callback_replies(
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
async def _fetch_callback_reply_audit_summary(
|
||||
db: Any,
|
||||
*,
|
||||
project_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Summarize Telegram outbound mirror and callback evidence capture coverage."""
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COUNT(*) AS outbound_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope <> '{}'::jsonb
|
||||
) AS outbound_source_envelope_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope ? 'source_refs'
|
||||
) AS outbound_source_refs_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(
|
||||
source_envelope #> '{source_refs,incident_ids}',
|
||||
'[]'::jsonb
|
||||
) <> '[]'::jsonb
|
||||
) AS outbound_incident_ref_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE send_status = 'failed'
|
||||
) AS outbound_failed_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope ? 'callback_reply'
|
||||
) AS callback_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope #>> '{callback_reply,status}'
|
||||
= 'callback_reply_sent'
|
||||
) AS callback_sent_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope #>> '{callback_reply,status}'
|
||||
= 'callback_reply_fallback_sent'
|
||||
) AS callback_fallback_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope #>> '{callback_reply,status}'
|
||||
= 'callback_reply_rescue_sent'
|
||||
) AS callback_rescue_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope #>> '{callback_reply,status}'
|
||||
= 'callback_reply_failed'
|
||||
) AS callback_failed_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE LOWER(source_envelope #>> '{callback_reply,action}')
|
||||
= 'detail'
|
||||
) AS callback_detail_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE LOWER(source_envelope #>> '{callback_reply,action}')
|
||||
= 'history'
|
||||
) AS callback_history_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope ? 'callback_reply'
|
||||
AND source_envelope ? 'awooop_status_chain'
|
||||
AND source_envelope ? 'km_stale_completion_summary'
|
||||
) AS callback_snapshot_captured_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope ? 'callback_reply'
|
||||
AND (
|
||||
source_envelope ? 'awooop_status_chain'
|
||||
OR source_envelope ? 'km_stale_completion_summary'
|
||||
)
|
||||
AND NOT (
|
||||
source_envelope ? 'awooop_status_chain'
|
||||
AND source_envelope ? 'km_stale_completion_summary'
|
||||
)
|
||||
) AS callback_snapshot_partial_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE source_envelope ? 'callback_reply'
|
||||
AND NOT (
|
||||
source_envelope ? 'awooop_status_chain'
|
||||
OR source_envelope ? 'km_stale_completion_summary'
|
||||
)
|
||||
) AS callback_snapshot_missing_total,
|
||||
COUNT(DISTINCT source_envelope #>> '{callback_reply,incident_id}')
|
||||
FILTER (
|
||||
WHERE source_envelope ? 'callback_reply'
|
||||
AND COALESCE(
|
||||
source_envelope #>> '{callback_reply,incident_id}',
|
||||
''
|
||||
) <> ''
|
||||
) AS callback_incident_total,
|
||||
MAX(COALESCE(sent_at, queued_at)) AS latest_outbound_at,
|
||||
MAX(COALESCE(sent_at, queued_at)) FILTER (
|
||||
WHERE source_envelope ? 'callback_reply'
|
||||
) AS latest_callback_at
|
||||
FROM awooop_outbound_message
|
||||
WHERE project_id = :project_id
|
||||
AND channel_type = 'telegram'
|
||||
"""),
|
||||
{"project_id": project_id},
|
||||
)
|
||||
return _callback_reply_audit_summary_from_row(
|
||||
result.mappings().one(),
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
|
||||
def _callback_reply_audit_summary_from_row(
|
||||
row: Mapping[str, Any],
|
||||
*,
|
||||
project_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Convert aggregate SQL row into the public callback evidence audit summary."""
|
||||
outbound_total = _safe_int(row.get("outbound_total"))
|
||||
callback_total = _safe_int(row.get("callback_total"))
|
||||
captured = _safe_int(row.get("callback_snapshot_captured_total"))
|
||||
partial = _safe_int(row.get("callback_snapshot_partial_total"))
|
||||
missing = _safe_int(row.get("callback_snapshot_missing_total"))
|
||||
outbound_incident_refs = _safe_int(row.get("outbound_incident_ref_total"))
|
||||
|
||||
if callback_total <= 0:
|
||||
snapshot_status = "no_callback"
|
||||
next_action = "press_telegram_detail_or_history"
|
||||
elif missing > 0:
|
||||
snapshot_status = "not_captured"
|
||||
next_action = "press_telegram_detail_or_history_after_rollout"
|
||||
elif partial > 0:
|
||||
snapshot_status = "partial"
|
||||
next_action = "press_telegram_detail_or_history_after_rollout"
|
||||
elif outbound_total > 0 and outbound_incident_refs == 0:
|
||||
snapshot_status = "captured"
|
||||
next_action = "review_outbound_source_refs"
|
||||
else:
|
||||
snapshot_status = "captured"
|
||||
next_action = "none"
|
||||
|
||||
return {
|
||||
"schema_version": _CALLBACK_REPLY_AUDIT_SUMMARY_SCHEMA_VERSION,
|
||||
"project_id": project_id,
|
||||
"outbound_total": outbound_total,
|
||||
"outbound_source_envelope_total": _safe_int(
|
||||
row.get("outbound_source_envelope_total")
|
||||
),
|
||||
"outbound_source_refs_total": _safe_int(
|
||||
row.get("outbound_source_refs_total")
|
||||
),
|
||||
"outbound_incident_ref_total": outbound_incident_refs,
|
||||
"outbound_failed_total": _safe_int(row.get("outbound_failed_total")),
|
||||
"callback_total": callback_total,
|
||||
"callback_sent_total": _safe_int(row.get("callback_sent_total")),
|
||||
"callback_fallback_total": _safe_int(row.get("callback_fallback_total")),
|
||||
"callback_rescue_total": _safe_int(row.get("callback_rescue_total")),
|
||||
"callback_failed_total": _safe_int(row.get("callback_failed_total")),
|
||||
"callback_detail_total": _safe_int(row.get("callback_detail_total")),
|
||||
"callback_history_total": _safe_int(row.get("callback_history_total")),
|
||||
"callback_snapshot_captured_total": captured,
|
||||
"callback_snapshot_partial_total": partial,
|
||||
"callback_snapshot_missing_total": missing,
|
||||
"callback_incident_total": _safe_int(row.get("callback_incident_total")),
|
||||
"snapshot_status": snapshot_status,
|
||||
"next_action": next_action,
|
||||
"latest_outbound_at": row.get("latest_outbound_at"),
|
||||
"latest_callback_at": row.get("latest_callback_at"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from src.services.platform_operator_service import (
|
||||
_ai_route_policy_order,
|
||||
_ai_route_repair_evidence_item,
|
||||
_build_awooop_status_chain,
|
||||
_callback_reply_audit_summary_from_row,
|
||||
_callback_reply_event_item,
|
||||
_callback_reply_summary_matches_status,
|
||||
_cicd_duration_seconds,
|
||||
@@ -658,6 +659,30 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"summary": {
|
||||
"schema_version": "telegram_callback_reply_audit_summary_v1",
|
||||
"project_id": "awoooi",
|
||||
"outbound_total": 120,
|
||||
"outbound_source_envelope_total": 118,
|
||||
"outbound_source_refs_total": 100,
|
||||
"outbound_incident_ref_total": 80,
|
||||
"outbound_failed_total": 1,
|
||||
"callback_total": 3,
|
||||
"callback_sent_total": 1,
|
||||
"callback_fallback_total": 1,
|
||||
"callback_rescue_total": 0,
|
||||
"callback_failed_total": 1,
|
||||
"callback_detail_total": 2,
|
||||
"callback_history_total": 1,
|
||||
"callback_snapshot_captured_total": 1,
|
||||
"callback_snapshot_partial_total": 1,
|
||||
"callback_snapshot_missing_total": 1,
|
||||
"callback_incident_total": 2,
|
||||
"snapshot_status": "not_captured",
|
||||
"next_action": "press_telegram_detail_or_history_after_rollout",
|
||||
"latest_outbound_at": datetime(2026, 5, 18, 7, 40, 0),
|
||||
"latest_callback_at": datetime(2026, 5, 18, 7, 31, 37),
|
||||
},
|
||||
})
|
||||
|
||||
dumped = response.model_dump(mode="json")
|
||||
@@ -676,6 +701,42 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
] == "Hermes"
|
||||
assert dumped["items"][0]["evidence_capture_status"]["status"] == "captured"
|
||||
assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi")
|
||||
assert dumped["summary"]["outbound_total"] == 120
|
||||
assert dumped["summary"]["callback_snapshot_missing_total"] == 1
|
||||
assert dumped["summary"]["snapshot_status"] == "not_captured"
|
||||
|
||||
|
||||
def test_callback_reply_audit_summary_marks_missing_snapshots() -> None:
|
||||
summary = _callback_reply_audit_summary_from_row(
|
||||
{
|
||||
"outbound_total": 5256,
|
||||
"outbound_source_envelope_total": 5256,
|
||||
"outbound_source_refs_total": 5000,
|
||||
"outbound_incident_ref_total": 3200,
|
||||
"outbound_failed_total": 0,
|
||||
"callback_total": 2,
|
||||
"callback_sent_total": 2,
|
||||
"callback_fallback_total": 0,
|
||||
"callback_rescue_total": 0,
|
||||
"callback_failed_total": 0,
|
||||
"callback_detail_total": 0,
|
||||
"callback_history_total": 2,
|
||||
"callback_snapshot_captured_total": 0,
|
||||
"callback_snapshot_partial_total": 0,
|
||||
"callback_snapshot_missing_total": 2,
|
||||
"callback_incident_total": 1,
|
||||
"latest_outbound_at": datetime(2026, 5, 25, 8, 42, 22),
|
||||
"latest_callback_at": datetime(2026, 5, 24, 14, 38, 4),
|
||||
},
|
||||
project_id="awoooi",
|
||||
)
|
||||
|
||||
assert summary["schema_version"] == "telegram_callback_reply_audit_summary_v1"
|
||||
assert summary["outbound_total"] == 5256
|
||||
assert summary["callback_total"] == 2
|
||||
assert summary["callback_snapshot_missing_total"] == 2
|
||||
assert summary["snapshot_status"] == "not_captured"
|
||||
assert summary["next_action"] == "press_telegram_detail_or_history_after_rollout"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2831,6 +2831,32 @@
|
||||
"total": "{count} items",
|
||||
"empty": "No callback reply evidence yet.",
|
||||
"error": "Callback evidence failed to load: {error}",
|
||||
"summary": {
|
||||
"outbound": "Outbound mirror",
|
||||
"outboundDetail": "source_refs {sourceRefs}; incident refs {incidentRefs}; coverage {coverage}",
|
||||
"callbacks": "Callback replies",
|
||||
"callbackDetail": "detail {detail} / history {history}; incidents {incidents}",
|
||||
"snapshots": "Evidence snapshots",
|
||||
"snapshotDetail": "captured {captured} / partial {partial} / missing {missing}; coverage {coverage}",
|
||||
"delivery": "Delivery failures",
|
||||
"deliveryDetail": "sent {sent}; fallback {fallback}; outbound failed {outboundFailed}",
|
||||
"next": "Next",
|
||||
"latest": "Latest callback: {time}",
|
||||
"statuses": {
|
||||
"captured": "Captured",
|
||||
"partial": "Partially captured",
|
||||
"not_captured": "Not captured",
|
||||
"no_callback": "No callback yet",
|
||||
"observed": "Recorded"
|
||||
},
|
||||
"nextActions": {
|
||||
"none": "No follow-up needed",
|
||||
"press_telegram_detail_or_history": "Press Telegram Detail / History once to create callback evidence",
|
||||
"press_telegram_detail_or_history_after_rollout": "Press Telegram Detail / History again to capture the new snapshot",
|
||||
"review_outbound_source_refs": "Review outbound source_refs gaps",
|
||||
"observed": "Wait for the next callback evidence"
|
||||
}
|
||||
},
|
||||
"action": "Action: {action}",
|
||||
"incident": "Incident: {incidentId}",
|
||||
"sendStatus": "Send status: {status}",
|
||||
|
||||
@@ -2832,6 +2832,32 @@
|
||||
"total": "{count} 筆",
|
||||
"empty": "目前尚無 callback reply evidence。",
|
||||
"error": "Callback evidence 載入失敗:{error}",
|
||||
"summary": {
|
||||
"outbound": "出站鏡像",
|
||||
"outboundDetail": "source_refs {sourceRefs};incident refs {incidentRefs};覆蓋 {coverage}",
|
||||
"callbacks": "Callback replies",
|
||||
"callbackDetail": "detail {detail} / history {history};Incident {incidents}",
|
||||
"snapshots": "Evidence snapshots",
|
||||
"snapshotDetail": "captured {captured} / partial {partial} / missing {missing};覆蓋 {coverage}",
|
||||
"delivery": "送達失敗",
|
||||
"deliveryDetail": "sent {sent};fallback {fallback};outbound failed {outboundFailed}",
|
||||
"next": "下一步",
|
||||
"latest": "最新 callback:{time}",
|
||||
"statuses": {
|
||||
"captured": "已捕捉",
|
||||
"partial": "部分捕捉",
|
||||
"not_captured": "未捕捉",
|
||||
"no_callback": "尚無 callback",
|
||||
"observed": "已記錄"
|
||||
},
|
||||
"nextActions": {
|
||||
"none": "不需補動作",
|
||||
"press_telegram_detail_or_history": "按一次 Telegram 詳情 / 歷史產生 callback evidence",
|
||||
"press_telegram_detail_or_history_after_rollout": "重新按 Telegram 詳情 / 歷史補新版 snapshot",
|
||||
"review_outbound_source_refs": "檢查 outbound source_refs 缺口",
|
||||
"observed": "等待下一次 callback evidence"
|
||||
}
|
||||
},
|
||||
"action": "動作:{action}",
|
||||
"incident": "Incident:{incidentId}",
|
||||
"sendStatus": "送訊狀態:{status}",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -20231,3 +20231,55 @@ production pod formatter smoke:
|
||||
- KM governance:約 84.5%。
|
||||
- AI Provider lane visibility:約 92.2%。
|
||||
- 完整 AI 自動化管理產品化:約 96.9%。
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-25 T183 — Run 監控補 Telegram Outbound / Callback Coverage Summary
|
||||
|
||||
**背景**:
|
||||
|
||||
- 使用者要求不只改善 Telegram 訊息文字,也要確認「所有告警訊息是否完整寫入 DB、是否能反查到 AwoooP / Sentry / SigNoz / MCP / PlayBook / KM 相關證據」。
|
||||
- production 查詢顯示 `awooop_outbound_message` 已有 `5256` 筆 Telegram outbound mirror;但 callback reply evidence 目前只有 `2` 筆,且是舊 rollout 前資料,`awooop_status_chain` / KM callback snapshot captured 皆為 `0`。
|
||||
- 既有 `/api/v1/platform/runs/callback-replies` 已能列出 callback reply event 與 live AwoooP status chain,但頁面缺少一眼可讀的 coverage summary,操作者仍難以判斷目前是「沒有資料」、「舊資料未捕捉 snapshot」,或「新流程已完整捕捉」。
|
||||
|
||||
**本輪修正**:
|
||||
|
||||
- `/api/v1/platform/runs/callback-replies` 回傳新增 `summary`:
|
||||
- Telegram outbound mirror total / source envelope total / source_refs total / incident refs total。
|
||||
- Callback reply total、detail/history 分布、sent/fallback/rescue/failed 分布。
|
||||
- Callback snapshot captured / partial / missing 統計。
|
||||
- `snapshot_status` 與 `next_action`,讓 UI 明確顯示下一步是補按 Telegram 詳情/歷史、檢查 source_refs,或不需處理。
|
||||
- Run 監控頁 `TG Callback Evidence` 區塊新增 coverage summary band:
|
||||
- 出站鏡像、Callback replies、Evidence snapshots、送達失敗、下一步。
|
||||
- 不新增 fake data;所有數據來自 `awooop_outbound_message.source_envelope` 的只讀聚合。
|
||||
- 新增 API response schema 與 unit tests,確保 summary 會被 Pydantic response 保留,並能標示 legacy callback snapshot missing。
|
||||
|
||||
**local validation(完成)**:
|
||||
|
||||
```text
|
||||
python3 -m py_compile apps/api/src/services/platform_operator_service.py apps/api/src/api/v1/platform/operator_runs.py apps/api/tests/test_awooop_operator_timeline_labels.py
|
||||
jq empty apps/web/messages/zh-TW.json apps/web/messages/en.json
|
||||
git diff --check
|
||||
PYTHONPATH=. DATABASE_URL='postgresql+asyncpg://test:test@localhost/test' /Users/ogt/.pyenv/shims/pytest tests/test_awooop_operator_timeline_labels.py -q
|
||||
51 passed in 1.19s
|
||||
pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-t183-tsconfig.tsbuildinfo
|
||||
pnpm --dir apps/web lint -- --file 'src/app/[locale]/awooop/runs/page.tsx'
|
||||
pass with pre-existing i18next/no-literal-string warnings in the same file
|
||||
NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build
|
||||
```
|
||||
|
||||
**目前整體進度**:
|
||||
|
||||
- AwoooP 告警可觀測鏈:約 99.52%。
|
||||
- 低風險自動修復閉環:約 95.8%。
|
||||
- 前端 AI 自動化管理介面同步:約 98.9%。
|
||||
- 首頁 KPI / 小龍蝦流程 truth alignment:約 96.5%。
|
||||
- Telegram 詳情 / 歷史可追溯:約 98.2%。
|
||||
- Telegram outbound / callback DB coverage 可視化:約 97.6%。
|
||||
- callback / DB replayability:約 97.3%。
|
||||
- MCP / 自建 MCP 可視化:約 95.0%。
|
||||
- Sentry / SigNoz source correlation:約 93.5%。
|
||||
- Ansible / PlayBook 可視化:約 92.5%。
|
||||
- KM governance:約 84.5%。
|
||||
- AI Provider lane visibility:約 92.2%。
|
||||
- 完整 AI 自動化管理產品化:約 97.1%。
|
||||
|
||||
Reference in New Issue
Block a user