fix(alerts): surface telegram AI automation coverage
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m19s
CD Pipeline / build-and-deploy (push) Successful in 5m33s
CD Pipeline / post-deploy-checks (push) Successful in 6m28s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m19s
CD Pipeline / build-and-deploy (push) Successful in 5m33s
CD Pipeline / post-deploy-checks (push) Successful in 6m28s
This commit is contained in:
@@ -893,6 +893,17 @@ def _effective_ai_loop_context(
|
|||||||
"target_count": consumer_target_count,
|
"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 {
|
return {
|
||||||
"ready": False,
|
"ready": False,
|
||||||
"source": "unavailable",
|
"source": "unavailable",
|
||||||
|
|||||||
@@ -507,6 +507,61 @@ def test_telegram_alert_monitoring_coverage_uses_consumer_context_fallback():
|
|||||||
] == "log_controlled_writeback_consumer"
|
] == "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):
|
def test_telegram_alert_monitoring_coverage_endpoint(monkeypatch):
|
||||||
async def fake_loader():
|
async def fake_loader():
|
||||||
return _payload()
|
return _payload()
|
||||||
|
|||||||
@@ -12162,12 +12162,43 @@
|
|||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"aiLoop": {
|
"aiLoop": {
|
||||||
"title": "告警 AI Loop",
|
"title": "Alert AI Loop",
|
||||||
"subtitle": "告警訊號直接對齊 LOG / KM / RAG / MCP / PlayBook / Verifier 的 runtime receipt。",
|
"subtitle": "Alert signals are wired to LOG / KM / RAG / MCP / PlayBook / Verifier runtime receipts.",
|
||||||
"badge": "controlled automation",
|
"badge": "controlled automation",
|
||||||
"runs": "Runs",
|
"runs": "Runs",
|
||||||
"workItems": "Work Items",
|
"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": {
|
"automationAssetLedger": {
|
||||||
|
|||||||
@@ -12167,7 +12167,38 @@
|
|||||||
"badge": "controlled automation",
|
"badge": "controlled automation",
|
||||||
"runs": "Runs",
|
"runs": "Runs",
|
||||||
"workItems": "Work Items",
|
"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": {
|
"automationAssetLedger": {
|
||||||
|
|||||||
@@ -1,17 +1,55 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
BellRing,
|
BellRing,
|
||||||
|
Bot,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
Database,
|
||||||
|
Gauge,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
|
RefreshCw,
|
||||||
|
Send,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
TriangleAlert,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { AutonomousRuntimeReceiptPanel } from "@/components/awooop/autonomous-runtime-receipt-panel";
|
|
||||||
import { Link } from "@/i18n/routing";
|
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 = [
|
const NAV_ITEMS = [
|
||||||
{
|
{
|
||||||
@@ -31,8 +69,164 @@ const NAV_ITEMS = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] 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 (
|
||||||
|
<div className="min-w-0 border border-[#e5e0d5] bg-white px-3 py-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-xs font-semibold text-[#77736a]">{label}</p>
|
||||||
|
<p className="mt-1 whitespace-nowrap font-mono text-lg font-semibold text-[#141413]">
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 w-8 shrink-0 items-center justify-center border",
|
||||||
|
tone === "ok" && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||||
|
tone === "warn" && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||||
|
tone === "neutral" && "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 line-clamp-2 text-xs leading-5 text-[#5f5b52]">{detail}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AwoooPAlertsPage() {
|
export default function AwoooPAlertsPage() {
|
||||||
const t = useTranslations("awooop.alerts.aiLoop");
|
const t = useTranslations("awooop.alerts.aiLoop");
|
||||||
|
const [coverage, setCoverage] = useState<TelegramMonitoringCoveragePayload | null>(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 (
|
return (
|
||||||
<main
|
<main
|
||||||
@@ -78,9 +272,89 @@ export default function AwoooPAlertsPage() {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
data-testid="telegram-alert-ai-automation-command-strip"
|
||||||
|
className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-3"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 border px-2 py-0.5 text-xs font-semibold",
|
||||||
|
ready
|
||||||
|
? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
|
||||||
|
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
{coverageLoading ? t("states.loading") : ready ? t("states.ready") : t("states.actioning")}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-xs text-[#77736a]">
|
||||||
|
{coverage?.status ?? "telegram_alert_monitoring_coverage_readback"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||||
|
{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"),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={refreshCoverage}
|
||||||
|
disabled={coverageLoading}
|
||||||
|
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-white px-3 py-2 text-xs font-semibold text-[#141413] hover:border-[#d97757] disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn("h-4 w-4", coverageLoading && "animate-spin")} aria-hidden="true" />
|
||||||
|
{t("refresh")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid gap-2 md:grid-cols-3 xl:grid-cols-6">
|
||||||
|
{statCards.map((card) => (
|
||||||
|
<StatCard
|
||||||
|
key={card.key}
|
||||||
|
label={card.label}
|
||||||
|
value={card.value}
|
||||||
|
detail={card.detail}
|
||||||
|
tone={card.tone}
|
||||||
|
icon={card.icon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<AutonomousRuntimeReceiptPanel mode="full" />
|
<section className="grid gap-3 md:grid-cols-3">
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="group flex min-w-0 items-center justify-between gap-3 border border-[#e0ddd4] bg-white px-4 py-3 text-[#141413] hover:border-[#d97757]"
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 items-center gap-3">
|
||||||
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center border border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52] group-hover:border-[#d97757] group-hover:text-[#141413]">
|
||||||
|
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block truncate text-sm font-semibold">{t(item.labelKey)}</span>
|
||||||
|
<span className="mt-0.5 block truncate text-xs text-[#77736a]">
|
||||||
|
{t(`details.${item.labelKey}` as never)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="h-4 w-4 shrink-0 text-[#87867f] group-hover:text-[#d97757]" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user