feat(awooop): surface telegram callback coverage
All checks were successful
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m59s
CD Pipeline / post-deploy-checks (push) Successful in 1m20s

This commit is contained in:
Your Name
2026-05-25 16:56:28 +08:00
parent b7ee1f47ff
commit 449c4ac807
7 changed files with 524 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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