diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py index 3d16adc2..28eaf318 100644 --- a/apps/api/src/api/v1/platform/operator_runs.py +++ b/apps/api/src/api/v1/platform/operator_runs.py @@ -105,6 +105,9 @@ class CallbackReplyItem(BaseModel): class OutboundReplyMarkupGapPrefix(BaseModel): prefix: str total: int + recent_24h_total: int = 0 + first_sent_at: datetime | None = None + last_sent_at: datetime | None = None class CallbackReplyAuditSummary(BaseModel): diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index d8dbd730..6dc24775 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -501,7 +501,10 @@ async def _fetch_callback_reply_audit_summary( SELECT jsonb_agg( jsonb_build_object( 'prefix', prefix, - 'total', total + 'total', total, + 'recent_24h_total', recent_24h_total, + 'first_sent_at', first_sent_at, + 'last_sent_at', last_sent_at ) ORDER BY total DESC, prefix ASC ) @@ -515,7 +518,13 @@ async def _fetch_callback_reply_audit_summary( ), 'unknown' ) AS prefix, - COUNT(*) AS total + COUNT(*) AS total, + COUNT(*) FILTER ( + WHERE COALESCE(sent_at, queued_at) + >= NOW() - INTERVAL '24 hours' + ) AS recent_24h_total, + MIN(COALESCE(sent_at, queued_at)) AS first_sent_at, + MAX(COALESCE(sent_at, queued_at)) AS last_sent_at FROM awooop_outbound_message WHERE project_id = :project_id AND channel_type = 'telegram' @@ -693,6 +702,9 @@ def _reply_markup_gap_prefixes_from_value(value: Any) -> list[dict[str, Any]]: prefixes.append({ "prefix": prefix[:80], "total": _safe_int(item.get("total")), + "recent_24h_total": _safe_int(item.get("recent_24h_total")), + "first_sent_at": item.get("first_sent_at"), + "last_sent_at": item.get("last_sent_at"), }) if len(prefixes) >= 5: break diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index 957f52a5..d919d835 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -670,8 +670,18 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None: "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}, + { + "prefix": "silence", + "total": 3, + "recent_24h_total": 0, + "last_sent_at": datetime(2026, 5, 18, 7, 40, 0), + }, + { + "prefix": "drift_view", + "total": 1, + "recent_24h_total": 1, + "last_sent_at": datetime(2026, 5, 18, 8, 15, 0), + }, ], "outbound_failed_total": 1, "callback_total": 3, @@ -716,6 +726,9 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None: ][0] == { "prefix": "silence", "total": 3, + "recent_24h_total": 0, + "first_sent_at": None, + "last_sent_at": "2026-05-18T07:40:00", } assert dumped["summary"]["callback_snapshot_missing_total"] == 1 assert dumped["summary"]["snapshot_status"] == "partial" @@ -740,8 +753,18 @@ def test_callback_reply_audit_summary_marks_missing_snapshots() -> None: "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}, + { + "prefix": "silence", + "total": 8, + "recent_24h_total": 0, + "last_sent_at": datetime(2026, 5, 18, 7, 40, 0), + }, + { + "prefix": "drift_view", + "total": 4, + "recent_24h_total": 2, + "last_sent_at": datetime(2026, 5, 25, 8, 42, 22), + }, ], "outbound_failed_total": 0, "callback_total": 2, @@ -767,6 +790,12 @@ def test_callback_reply_audit_summary_marks_missing_snapshots() -> None: assert summary["callback_snapshot_missing_total"] == 2 assert summary["snapshot_status"] == "not_captured" assert summary["next_action"] == "press_telegram_detail_or_history_after_rollout" + assert summary["outbound_reply_markup_missing_incident_ref_top_prefixes"][0][ + "recent_24h_total" + ] == 0 + assert summary["outbound_reply_markup_missing_incident_ref_top_prefixes"][1][ + "last_sent_at" + ] == datetime(2026, 5, 25, 8, 42, 22) def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() -> None: @@ -779,8 +808,18 @@ def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() -> "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}, + { + "prefix": "silence", + "total": 275, + "recent_24h_total": 0, + "last_sent_at": datetime(2026, 5, 25, 10, 59, 49), + }, + { + "prefix": "drift_view", + "total": 144, + "recent_24h_total": 0, + "last_sent_at": datetime(2026, 5, 18, 18, 14, 27), + }, ], "outbound_failed_total": 0, "callback_total": 3, @@ -804,6 +843,9 @@ def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() -> assert summary["callback_snapshot_missing_total"] == 2 assert summary["snapshot_status"] == "partial" assert summary["next_action"] == "review_legacy_callback_snapshot_gap" + assert summary["outbound_reply_markup_missing_incident_ref_top_prefixes"][0][ + "last_sent_at" + ] == datetime(2026, 5, 25, 10, 59, 49) @pytest.mark.asyncio diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 448aa511..4a6694e7 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -2836,6 +2836,7 @@ "outboundDetail": "source_refs {sourceRefs}; incident refs {incidentRefs}; coverage {coverage}", "outboundReplyMarkupDetail": "reply_markup {replyMarkup}; button missing incident refs {missingIncidentRefs}", "outboundReplyMarkupTopPrefixes": "Gap top prefixes: {prefixes}", + "outboundReplyMarkupTopPrefixItem": "{prefix} {total} (24h {recent}, last {last})", "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 904c0eca..3cf9956a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -2837,6 +2837,7 @@ "outboundDetail": "source_refs {sourceRefs};incident refs {incidentRefs};覆蓋 {coverage}", "outboundReplyMarkupDetail": "reply_markup {replyMarkup};按鈕缺 incident refs {missingIncidentRefs}", "outboundReplyMarkupTopPrefixes": "缺口 top prefixes:{prefixes}", + "outboundReplyMarkupTopPrefixItem": "{prefix} {total}(24h {recent},最後 {last})", "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 20e3848f..b208a51b 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -150,6 +150,9 @@ interface CallbackReplyAuditSummary { outbound_reply_markup_missing_incident_ref_top_prefixes?: Array<{ prefix?: string | null; total?: number | null; + recent_24h_total?: number | null; + first_sent_at?: string | null; + last_sent_at?: string | null; }>; outbound_failed_total?: number; callback_total?: number; @@ -1984,7 +1987,22 @@ function CallbackReplyAuditSummaryPanel({ summary.outbound_reply_markup_missing_incident_ref_top_prefixes ?? [] ) .slice(0, 3) - .map((item) => `${item.prefix || "--"} ${item.total ?? 0}`) + .map((item) => { + const lastSeen = item.last_sent_at + ? new Date(item.last_sent_at).toLocaleString("zh-TW", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) + : "--"; + return t("outboundReplyMarkupTopPrefixItem", { + prefix: item.prefix || "--", + total: item.total ?? 0, + recent: item.recent_24h_total ?? 0, + last: lastSeen, + }); + }) .join(" / ") || "--"; const snapshotCoverage = formatCoveragePercent( summary.callback_snapshot_captured_total ?? 0, diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index a91f46f2..cdcbd4c6 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -20804,3 +20804,68 @@ GET /api/v1/health: - KM governance:約 84.6%。 - AI Provider lane visibility:約 92.2%。 - 完整 AI 自動化管理產品化:約 97.55%。 + +--- + +## 2026-05-25 T189 — Source Refs Gap Recency Breakdown + +**背景**: + +- T188 已把 `按鈕缺 incident refs 682` 拆出 top prefixes。 +- 但 operator 仍無法判斷這些缺口是 legacy 積欠,還是仍在最近持續重複新增。 +- 這直接對應統帥要求:Telegram / 前端必須能看出告警是否一直重複發生,以及目前跑到哪個階段。 + +**本輪修正**: + +- `/api/v1/platform/runs/callback-replies` 的 + `outbound_reply_markup_missing_incident_ref_top_prefixes` 每個 item 新增: + - `recent_24h_total` + - `first_sent_at` + - `last_sent_at` +- Run 監控 `TG Callback Evidence` 的 top prefixes 顯示改為: + - `silence 276(24h 22,最後 05/25 18:59)` + - `unknown 154(24h 0,最後 05/19 02:23)` +- 這讓 operator 可以直接分辨: + - `24h = 0`:舊資料缺口,不是正在洗版。 + - `24h > 0`:仍需追來源 template / gateway。 + +**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 +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.37s +pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-t189-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 + silence = 276, recent_24h_total = 22, last_sent_at = 2026-05-25T10:59:49Z + unknown = 154, recent_24h_total = 0, last_sent_at = 2026-05-18T18:23:04Z + drift_view = 144, recent_24h_total = 0, last_sent_at = 2026-05-18T18:14:27Z + ai_advisory_handled = 52, recent_24h_total = 23, last_sent_at = 2026-05-25T12:07:17Z + approve = 50, recent_24h_total = 0, last_sent_at = 2026-05-13T04:00:04Z +``` + +**目前整體進度**: + +- AwoooP 告警可觀測鏈:約 99.64%。 +- 低風險自動修復閉環:約 95.8%。 +- 前端 AI 自動化管理介面同步:約 99.35%。 +- 首頁 KPI / 小龍蝦流程 truth alignment:約 96.5%。 +- Telegram 詳情 / 歷史可追溯:約 99.0%。 +- Telegram outbound / callback DB coverage 可視化:約 99.35%。 +- callback / DB replayability:約 98.5%。 +- MCP / 自建 MCP 可視化:約 95.1%。 +- Sentry / SigNoz source correlation:約 94.45%。 +- Ansible / PlayBook 可視化:約 92.6%。 +- KM governance:約 84.6%。 +- AI Provider lane visibility:約 92.2%。 +- 完整 AI 自動化管理產品化:約 97.6%。