feat(awooop): show source ref gap recency
All checks were successful
CD Pipeline / tests (push) Successful in 1m32s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 3m41s
CD Pipeline / post-deploy-checks (push) Successful in 2m13s

This commit is contained in:
Your Name
2026-05-25 20:12:19 +08:00
parent f30405997d
commit a8b7299d1c
7 changed files with 151 additions and 9 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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 27624h 22最後 05/25 18:59`
- `unknown 15424h 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%。