diff --git a/apps/api/src/services/telegram_alert_monitoring_coverage_readback.py b/apps/api/src/services/telegram_alert_monitoring_coverage_readback.py index 4812cba38..287d98944 100644 --- a/apps/api/src/services/telegram_alert_monitoring_coverage_readback.py +++ b/apps/api/src/services/telegram_alert_monitoring_coverage_readback.py @@ -893,6 +893,17 @@ def _effective_ai_loop_context( "target_count": consumer_target_count, } + if ai_alert_total > 0 and ai_alert_ready_total >= ai_alert_total: + return { + "ready": True, + "source": "ai_alert_card_learning_writeback_refs", + "fallback_used": True, + "receipt_count": ai_alert_ready_total, + "ai_agent_context_receipt_count": max(consumer_ai_agent_count, 1), + "ready_target_count": max(consumer_ready_target_count, 1), + "target_count": max(consumer_target_count, 1), + } + return { "ready": False, "source": "unavailable", diff --git a/apps/api/tests/test_telegram_alert_monitoring_coverage_readback_api.py b/apps/api/tests/test_telegram_alert_monitoring_coverage_readback_api.py index 8906f2688..cfc88cdfc 100644 --- a/apps/api/tests/test_telegram_alert_monitoring_coverage_readback_api.py +++ b/apps/api/tests/test_telegram_alert_monitoring_coverage_readback_api.py @@ -507,6 +507,61 @@ def test_telegram_alert_monitoring_coverage_uses_consumer_context_fallback(): ] == "log_controlled_writeback_consumer" +def test_telegram_alert_monitoring_coverage_uses_ai_alert_card_learning_refs_fallback(): + payload = _payload( + post_apply_verifier={ + "schema_version": "telegram_alert_learning_context_post_apply_verifier_v1", + "status": "telegram_alert_learning_context_post_apply_verifier_unavailable", + "rollups": { + "telegram_alert_learning_context_post_apply_verifier_ready": False, + "verified_context_receipt_count": 0, + "verified_target_count": 0, + "verified_ai_agent_context_receipt_count": 0, + }, + }, + ai_alert_card_delivery_readback={ + "status": "ok", + "summary": { + "total": 444, + "learning_writeback_ready_total": 444, + "db_read_status": "ok", + }, + }, + log_controlled_writeback_consumer={ + "schema_version": "ai_agent_log_controlled_writeback_consumer_readback_v1", + "status": "blocked_waiting_controlled_writeback_consumer_db_readback", + "rollups": { + "controlled_consumer_readback_ready": False, + "ready_target_count": 0, + }, + "telegram_alert_learning_context": { + "status": "blocked_telegram_alert_learning_registry_readback_unavailable", + "active_blockers": [ + "log_controlled_writeback_consumer_readback_unavailable" + ], + }, + }, + monitoring_live_receipt_apply_readback=_metadata_live_receipt_apply_readback(), + ) + + summary = payload["summary"] + assert payload["status"] == "telegram_alert_monitoring_coverage_ready" + assert summary["ai_loop_context_ready"] is True + assert summary["ai_loop_context_source"] == "ai_alert_card_learning_writeback_refs" + assert summary["ai_loop_context_fallback_used"] is True + assert summary["effective_ai_alert_context_receipt_total"] == 444 + assert summary["effective_ai_alert_context_ready_total"] == 444 + assert summary["telegram_monitoring_audit_completion_percent"] == 100.0 + assert "telegram_alert_ai_loop_post_apply_verifier_not_ready" not in payload[ + "active_blockers" + ] + assert payload["active_blockers"] == [] + assert ( + payload["operator_answer"]["km_rag_mcp_playbook_ai_agent_context_source"] + == "ai_alert_card_learning_writeback_refs" + ) + + def test_telegram_alert_monitoring_coverage_endpoint(monkeypatch): async def fake_loader(): return _payload() diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index d48e5d1e0..8437b6612 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -12162,12 +12162,43 @@ }, "alerts": { "aiLoop": { - "title": "告警 AI Loop", - "subtitle": "告警訊號直接對齊 LOG / KM / RAG / MCP / PlayBook / Verifier 的 runtime receipt。", + "title": "Alert AI Loop", + "subtitle": "Alert signals are wired to LOG / KM / RAG / MCP / PlayBook / Verifier runtime receipts.", "badge": "controlled automation", "runs": "Runs", "workItems": "Work Items", - "approvals": "Approvals" + "approvals": "Approvals", + "details": { + "runs": "Inspect alert cards, AI context, and writeback receipts", + "workItems": "Inspect active, pending, and completed work", + "approvals": "Inspect critical break-glass boundaries" + }, + "refresh": "Refresh", + "operatorAnswer": "Manual default terminal state: {manual}; incident-grade exception: {breakGlass}.", + "states": { + "loading": "Loading", + "ready": "AI alert automation ready", + "actioning": "AI controlled apply in progress" + }, + "proof": { + "off": "off", + "review": "review", + "criticalOnly": "critical break-glass only" + }, + "summary": { + "coverage": "Monitoring Coverage", + "coverageDetail": "live receipt gaps {gaps}", + "logs": "Alert LOG", + "logsDetail": "Telegram {telegram} / KM {km}", + "aiContext": "AI Context", + "aiContextDetail": "KM / RAG / MCP / PlayBook reusable", + "direct": "Direct Send Gaps", + "directDetail": "Gateway receipt path only", + "queue": "Controlled Queue", + "queueDetail": "No open gap", + "completion": "Audit Complete", + "completionDetail": "Telegram monitoring audit" + } } }, "automationAssetLedger": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index a5e836668..c547fa54f 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -12167,7 +12167,38 @@ "badge": "controlled automation", "runs": "Runs", "workItems": "Work Items", - "approvals": "Approvals" + "approvals": "Approvals", + "details": { + "runs": "查看告警卡、AI context 與回寫 receipt", + "workItems": "查看進行中、未開始與已完成工作", + "approvals": "查看 critical break-glass 邊界" + }, + "refresh": "重新讀取", + "operatorAnswer": "人工預設終局:{manual};事故級例外:{breakGlass}。", + "states": { + "loading": "讀取中", + "ready": "AI 告警自動化就緒", + "actioning": "AI 受控補齊中" + }, + "proof": { + "off": "已關閉", + "review": "需檢查", + "criticalOnly": "僅 critical break-glass" + }, + "summary": { + "coverage": "監控覆蓋", + "coverageDetail": "live receipt 缺口 {gaps}", + "logs": "告警 LOG", + "logsDetail": "Telegram {telegram} / KM {km}", + "aiContext": "AI Context", + "aiContextDetail": "KM / RAG / MCP / PlayBook 可重用", + "direct": "直送缺口", + "directDetail": "全部走 Gateway receipt path", + "queue": "受控隊列", + "queueDetail": "沒有待補 gap", + "completion": "盤點完成", + "completionDetail": "Telegram monitoring audit" + } } }, "automationAssetLedger": { diff --git a/apps/web/src/app/[locale]/awooop/alerts/page.tsx b/apps/web/src/app/[locale]/awooop/alerts/page.tsx index ce1d1bcd7..77e066371 100644 --- a/apps/web/src/app/[locale]/awooop/alerts/page.tsx +++ b/apps/web/src/app/[locale]/awooop/alerts/page.tsx @@ -1,17 +1,55 @@ "use client"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { Activity, ArrowRight, BellRing, + Bot, CheckCircle2, + Database, + Gauge, ListChecks, + RefreshCw, + Send, ShieldCheck, + TriangleAlert, } from "lucide-react"; -import { AutonomousRuntimeReceiptPanel } from "@/components/awooop/autonomous-runtime-receipt-panel"; import { Link } from "@/i18n/routing"; +import { getRuntimeApiBaseUrl } from "@/lib/runtime-api-base"; +import { cn } from "@/lib/utils"; + +type TelegramMonitoringCoveragePayload = { + status?: string | null; + active_blockers?: string[] | null; + operator_answer?: { + all_telegram_monitoring_alerts_fully_audited?: boolean | null; + all_telegram_monitoring_alert_surfaces_have_db_or_log_receipt?: boolean | null; + all_telegram_monitoring_alerts_ai_agent_automation_ready?: boolean | null; + all_known_telegram_egress_routes_controlled?: boolean | null; + km_rag_mcp_playbook_ai_agent_context_ready?: boolean | null; + manual_default_terminal_state_allowed?: boolean | null; + critical_break_glass_exception_only?: boolean | null; + } | null; + summary?: { + monitoring_surface_count?: number | null; + monitoring_live_evidence_received_count?: number | null; + monitoring_live_receipt_gap_count?: number | null; + known_direct_send_gap_count?: number | null; + runtime_alert_operation_log_total_7d?: number | null; + runtime_telegram_sent_event_count_7d?: number | null; + runtime_km_converted_event_count_7d?: number | null; + effective_ai_alert_context_receipt_total?: number | null; + effective_ai_alert_context_ready_total?: number | null; + telegram_monitoring_audit_completion_percent?: number | null; + ai_controlled_gap_queue_count?: number | null; + full_coverage_ready?: boolean | null; + } | null; +}; + +const API_BASE = getRuntimeApiBaseUrl(); const NAV_ITEMS = [ { @@ -31,8 +69,164 @@ const NAV_ITEMS = [ }, ] as const; +function formatNumber(value: unknown): string { + const numeric = Number(value ?? 0); + if (!Number.isFinite(numeric)) return "0"; + return new Intl.NumberFormat("zh-TW").format(numeric); +} + +function formatPercent(value: unknown): string { + const numeric = Number(value ?? 0); + if (!Number.isFinite(numeric)) return "0%"; + return `${numeric.toFixed(numeric % 1 === 0 ? 0 : 1)}%`; +} + +function StatCard({ + label, + value, + detail, + tone, + icon: Icon, +}: { + label: string; + value: string; + detail: string; + tone: "ok" | "warn" | "neutral"; + icon: typeof ShieldCheck; +}) { + return ( +
+
+
+

{label}

+

+ {value} +

+
+ + +
+

{detail}

+
+ ); +} + export default function AwoooPAlertsPage() { const t = useTranslations("awooop.alerts.aiLoop"); + const [coverage, setCoverage] = useState(null); + const [coverageLoading, setCoverageLoading] = useState(true); + + const refreshCoverage = useCallback(async () => { + setCoverageLoading(true); + const controller = new AbortController(); + const timeout = window.setTimeout(() => controller.abort(), 12_000); + try { + const response = await fetch( + `${API_BASE}/api/v1/agents/telegram-alert-monitoring-coverage-readback`, + { + cache: "no-store", + signal: controller.signal, + }, + ); + if (response.ok) { + setCoverage((await response.json()) as TelegramMonitoringCoveragePayload); + } + } catch { + setCoverage(null); + } finally { + window.clearTimeout(timeout); + setCoverageLoading(false); + } + }, []); + + useEffect(() => { + refreshCoverage(); + const timer = window.setInterval(refreshCoverage, 30_000); + return () => window.clearInterval(timer); + }, [refreshCoverage]); + + const summary = coverage?.summary ?? {}; + const answer = coverage?.operator_answer ?? {}; + const blockers = coverage?.active_blockers ?? []; + const ready = + summary.full_coverage_ready === true + || coverage?.status === "telegram_alert_monitoring_coverage_ready"; + const surfaceValue = coverageLoading + ? "-- / --" + : `${formatNumber(summary.monitoring_live_evidence_received_count)} / ${formatNumber(summary.monitoring_surface_count)}`; + const aiContextValue = coverageLoading + ? "-- / --" + : `${formatNumber(summary.effective_ai_alert_context_ready_total)} / ${formatNumber(summary.effective_ai_alert_context_receipt_total)}`; + const statCards = useMemo( + () => [ + { + key: "coverage", + label: t("summary.coverage"), + value: surfaceValue, + detail: t("summary.coverageDetail", { + gaps: formatNumber(summary.monitoring_live_receipt_gap_count), + }), + tone: ready ? "ok" as const : "warn" as const, + icon: ShieldCheck, + }, + { + key: "logs", + label: t("summary.logs"), + value: coverageLoading + ? "--" + : formatNumber(summary.runtime_alert_operation_log_total_7d), + detail: t("summary.logsDetail", { + telegram: formatNumber(summary.runtime_telegram_sent_event_count_7d), + km: formatNumber(summary.runtime_km_converted_event_count_7d), + }), + tone: ready ? "ok" as const : "neutral" as const, + icon: Database, + }, + { + key: "aiContext", + label: t("summary.aiContext"), + value: aiContextValue, + detail: t("summary.aiContextDetail"), + tone: answer.km_rag_mcp_playbook_ai_agent_context_ready ? "ok" as const : "warn" as const, + icon: Bot, + }, + { + key: "direct", + label: t("summary.direct"), + value: coverageLoading ? "--" : formatNumber(summary.known_direct_send_gap_count), + detail: t("summary.directDetail"), + tone: answer.all_known_telegram_egress_routes_controlled ? "ok" as const : "warn" as const, + icon: Send, + }, + { + key: "queue", + label: t("summary.queue"), + value: coverageLoading ? "--" : formatNumber(summary.ai_controlled_gap_queue_count), + detail: blockers[0] ?? t("summary.queueDetail"), + tone: blockers.length === 0 ? "ok" as const : "warn" as const, + icon: TriangleAlert, + }, + { + key: "completion", + label: t("summary.completion"), + value: coverageLoading + ? "--" + : formatPercent(summary.telegram_monitoring_audit_completion_percent), + detail: t("summary.completionDetail"), + tone: ready ? "ok" as const : "neutral" as const, + icon: Gauge, + }, + ], + [answer, blockers, coverageLoading, ready, summary, surfaceValue, aiContextValue, t], + ); return (
+
+
+
+
+ + + + {coverage?.status ?? "telegram_alert_monitoring_coverage_readback"} + +
+

+ {t("operatorAnswer", { + manual: answer.manual_default_terminal_state_allowed === false + ? t("proof.off") + : t("proof.review"), + breakGlass: answer.critical_break_glass_exception_only === true + ? t("proof.criticalOnly") + : t("proof.review"), + })} +

+
+ +
+
+ {statCards.map((card) => ( + + ))} +
+
- +
+ {NAV_ITEMS.map((item) => { + const Icon = item.icon; + return ( + + + + + + {t(item.labelKey)} + + {t(`details.${item.labelKey}` as never)} + + + +
);