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%。