From c644cfe99339eee8a2104d0d9051024d22e7d7b9 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Mon, 25 May 2026 19:49:48 +0800
Subject: [PATCH] feat(awooop): show source ref gap prefixes
---
apps/api/src/api/v1/platform/operator_runs.py | 8 +++
.../src/services/platform_operator_service.py | 57 ++++++++++++++++++
.../test_awooop_operator_timeline_labels.py | 18 ++++++
apps/web/messages/en.json | 1 +
apps/web/messages/zh-TW.json | 1 +
.../web/src/app/[locale]/awooop/runs/page.tsx | 15 +++++
docs/LOGBOOK.md | 60 +++++++++++++++++++
7 files changed, 160 insertions(+)
diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py
index 475653b1..3d16adc2 100644
--- a/apps/api/src/api/v1/platform/operator_runs.py
+++ b/apps/api/src/api/v1/platform/operator_runs.py
@@ -102,6 +102,11 @@ class CallbackReplyItem(BaseModel):
run_detail_href: str | None = None
+class OutboundReplyMarkupGapPrefix(BaseModel):
+ prefix: str
+ total: int
+
+
class CallbackReplyAuditSummary(BaseModel):
schema_version: str
project_id: str
@@ -111,6 +116,9 @@ class CallbackReplyAuditSummary(BaseModel):
outbound_incident_ref_total: int
outbound_reply_markup_total: int = 0
outbound_reply_markup_missing_incident_ref_total: int = 0
+ outbound_reply_markup_missing_incident_ref_top_prefixes: list[
+ OutboundReplyMarkupGapPrefix
+ ] = Field(default_factory=list)
outbound_failed_total: int
callback_total: int
callback_sent_total: int
diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py
index ad952ae1..d8dbd730 100644
--- a/apps/api/src/services/platform_operator_service.py
+++ b/apps/api/src/services/platform_operator_service.py
@@ -497,6 +497,39 @@ async def _fetch_callback_reply_audit_summary(
'[]'::jsonb
) = '[]'::jsonb
) AS outbound_reply_markup_missing_incident_ref_total,
+ COALESCE((
+ SELECT jsonb_agg(
+ jsonb_build_object(
+ 'prefix', prefix,
+ 'total', total
+ )
+ ORDER BY total DESC, prefix ASC
+ )
+ FROM (
+ SELECT
+ COALESCE(
+ NULLIF(
+ source_envelope #>>
+ '{reply_markup,buttons,0,callback_prefix}',
+ ''
+ ),
+ 'unknown'
+ ) AS prefix,
+ COUNT(*) AS total
+ FROM awooop_outbound_message
+ WHERE project_id = :project_id
+ AND channel_type = 'telegram'
+ AND source_envelope #>> '{reply_markup,present}' = 'true'
+ AND COALESCE(
+ source_envelope #> '{source_refs,incident_ids}',
+ '[]'::jsonb
+ ) = '[]'::jsonb
+ GROUP BY 1
+ ORDER BY total DESC, prefix ASC
+ LIMIT 5
+ ) missing_prefixes
+ ), '[]'::jsonb)
+ AS outbound_reply_markup_missing_incident_ref_top_prefixes,
COUNT(*) FILTER (
WHERE send_status = 'failed'
) AS outbound_failed_total,
@@ -586,6 +619,9 @@ def _callback_reply_audit_summary_from_row(
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"))
+ top_missing_prefixes = _reply_markup_gap_prefixes_from_value(
+ row.get("outbound_reply_markup_missing_incident_ref_top_prefixes")
+ )
if callback_total <= 0:
snapshot_status = "no_callback"
@@ -623,6 +659,9 @@ def _callback_reply_audit_summary_from_row(
"outbound_reply_markup_missing_incident_ref_total": _safe_int(
row.get("outbound_reply_markup_missing_incident_ref_total")
),
+ "outbound_reply_markup_missing_incident_ref_top_prefixes": (
+ top_missing_prefixes
+ ),
"outbound_failed_total": _safe_int(row.get("outbound_failed_total")),
"callback_total": callback_total,
"callback_sent_total": _safe_int(row.get("callback_sent_total")),
@@ -642,6 +681,24 @@ def _callback_reply_audit_summary_from_row(
}
+def _reply_markup_gap_prefixes_from_value(value: Any) -> list[dict[str, Any]]:
+ if not isinstance(value, list):
+ return []
+
+ prefixes: list[dict[str, Any]] = []
+ for item in value:
+ if not isinstance(item, Mapping):
+ continue
+ prefix = str(item.get("prefix") or "unknown").strip() or "unknown"
+ prefixes.append({
+ "prefix": prefix[:80],
+ "total": _safe_int(item.get("total")),
+ })
+ if len(prefixes) >= 5:
+ break
+ return prefixes
+
+
async def _fetch_km_stale_completion_summary_for_incident(
*,
project_id: str,
diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py
index 8e1089fa..957f52a5 100644
--- a/apps/api/tests/test_awooop_operator_timeline_labels.py
+++ b/apps/api/tests/test_awooop_operator_timeline_labels.py
@@ -669,6 +669,10 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
"outbound_incident_ref_total": 80,
"outbound_reply_markup_total": 30,
"outbound_reply_markup_missing_incident_ref_total": 4,
+ "outbound_reply_markup_missing_incident_ref_top_prefixes": [
+ {"prefix": "silence", "total": 3},
+ {"prefix": "drift_view", "total": 1},
+ ],
"outbound_failed_total": 1,
"callback_total": 3,
"callback_sent_total": 1,
@@ -707,6 +711,12 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
assert dumped["summary"]["outbound_total"] == 120
assert dumped["summary"]["outbound_reply_markup_total"] == 30
assert dumped["summary"]["outbound_reply_markup_missing_incident_ref_total"] == 4
+ assert dumped["summary"][
+ "outbound_reply_markup_missing_incident_ref_top_prefixes"
+ ][0] == {
+ "prefix": "silence",
+ "total": 3,
+ }
assert dumped["summary"]["callback_snapshot_missing_total"] == 1
assert dumped["summary"]["snapshot_status"] == "partial"
@@ -729,6 +739,10 @@ def test_callback_reply_audit_summary_marks_missing_snapshots() -> None:
"outbound_incident_ref_total": 3200,
"outbound_reply_markup_total": 100,
"outbound_reply_markup_missing_incident_ref_total": 12,
+ "outbound_reply_markup_missing_incident_ref_top_prefixes": [
+ {"prefix": "silence", "total": 8},
+ {"prefix": "drift_view", "total": 4},
+ ],
"outbound_failed_total": 0,
"callback_total": 2,
"callback_sent_total": 2,
@@ -764,6 +778,10 @@ def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() ->
"outbound_incident_ref_total": 920,
"outbound_reply_markup_total": 1322,
"outbound_reply_markup_missing_incident_ref_total": 684,
+ "outbound_reply_markup_missing_incident_ref_top_prefixes": [
+ {"prefix": "silence", "total": 275},
+ {"prefix": "drift_view", "total": 144},
+ ],
"outbound_failed_total": 0,
"callback_total": 3,
"callback_sent_total": 3,
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index fcfbf756..448aa511 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -2835,6 +2835,7 @@
"outbound": "Outbound mirror",
"outboundDetail": "source_refs {sourceRefs}; incident refs {incidentRefs}; coverage {coverage}",
"outboundReplyMarkupDetail": "reply_markup {replyMarkup}; button missing incident refs {missingIncidentRefs}",
+ "outboundReplyMarkupTopPrefixes": "Gap top prefixes: {prefixes}",
"callbacks": "Callback replies",
"callbackDetail": "detail {detail} / history {history}; incidents {incidents}",
"snapshots": "Evidence snapshots",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 68b0e442..904c0eca 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -2836,6 +2836,7 @@
"outbound": "出站鏡像",
"outboundDetail": "source_refs {sourceRefs};incident refs {incidentRefs};覆蓋 {coverage}",
"outboundReplyMarkupDetail": "reply_markup {replyMarkup};按鈕缺 incident refs {missingIncidentRefs}",
+ "outboundReplyMarkupTopPrefixes": "缺口 top prefixes:{prefixes}",
"callbacks": "Callback replies",
"callbackDetail": "detail {detail} / history {history};Incident {incidents}",
"snapshots": "Evidence snapshots",
diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx
index 6a06d9ce..20e3848f 100644
--- a/apps/web/src/app/[locale]/awooop/runs/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx
@@ -147,6 +147,10 @@ interface CallbackReplyAuditSummary {
outbound_incident_ref_total?: number;
outbound_reply_markup_total?: number;
outbound_reply_markup_missing_incident_ref_total?: number;
+ outbound_reply_markup_missing_incident_ref_top_prefixes?: Array<{
+ prefix?: string | null;
+ total?: number | null;
+ }>;
outbound_failed_total?: number;
callback_total?: number;
callback_sent_total?: number;
@@ -1976,6 +1980,12 @@ function CallbackReplyAuditSummaryPanel({
summary.outbound_incident_ref_total ?? 0,
outboundTotal
);
+ const topMissingPrefixes = (
+ summary.outbound_reply_markup_missing_incident_ref_top_prefixes ?? []
+ )
+ .slice(0, 3)
+ .map((item) => `${item.prefix || "--"} ${item.total ?? 0}`)
+ .join(" / ") || "--";
const snapshotCoverage = formatCoveragePercent(
summary.callback_snapshot_captured_total ?? 0,
callbackTotal
@@ -2017,6 +2027,11 @@ function CallbackReplyAuditSummaryPanel({
summary.outbound_reply_markup_missing_incident_ref_total ?? 0,
})}
+
+ {t("outboundReplyMarkupTopPrefixes", {
+ prefixes: topMissingPrefixes,
+ })}
+
{t("callbacks")}
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md
index 78dec334..76de213d 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -20704,3 +20704,63 @@ GET /api/v1/health:
- KM governance:約 84.6%。
- AI Provider lane visibility:約 92.2%。
- 完整 AI 自動化管理產品化:約 97.45%。
+
+---
+
+## 2026-05-25 T188 — Source Refs Gap Top Prefix Breakdown
+
+**背景**:
+
+- T187 已把 Telegram outbound button gap 顯示為 `按鈕缺 incident refs 682`。
+- 但 operator 仍不知道缺口集中在哪一類 Telegram 卡片或按鈕族群,無法判斷下一步應修哪個 template / gateway。
+
+**本輪修正**:
+
+- `/api/v1/platform/runs/callback-replies` summary 新增:
+ - `outbound_reply_markup_missing_incident_ref_top_prefixes`
+- Run 監控 `TG Callback Evidence` 的 Outbound mirror 卡新增:
+ - `缺口 top prefixes:silence 276 / unknown 154 / drift_view 144`
+- 這讓 source matching 的下一輪修補可以直接對準 `silence`、`drift_view`、`approve` 等 button prefix,而不是只看總數猜原因。
+
+**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
+ 53 passed in 1.02s
+pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-t188-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
+ pass; Sentry global-error / instrumentation-client warnings are pre-existing
+```
+
+**production read-only preflight(完成)**:
+
+```text
+kubectl exec -n awoooi-prod deploy/awoooi-api -- production DB read-only SQL
+ top prefixes:
+ silence = 276
+ unknown = 154
+ drift_view = 144
+ ai_advisory_handled = 51
+ approve = 50
+```
+
+**目前整體進度**:
+
+- AwoooP 告警可觀測鏈:約 99.62%。
+- 低風險自動修復閉環:約 95.8%。
+- 前端 AI 自動化管理介面同步:約 99.25%。
+- 首頁 KPI / 小龍蝦流程 truth alignment:約 96.5%。
+- Telegram 詳情 / 歷史可追溯:約 99.0%。
+- Telegram outbound / callback DB coverage 可視化:約 99.25%。
+- callback / DB replayability:約 98.5%。
+- MCP / 自建 MCP 可視化:約 95.1%。
+- Sentry / SigNoz source correlation:約 94.2%。
+- Ansible / PlayBook 可視化:約 92.6%。
+- KM governance:約 84.6%。
+- AI Provider lane visibility:約 92.2%。
+- 完整 AI 自動化管理產品化:約 97.5%。