diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index 5b73575a..95c5cfeb 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -978,6 +978,12 @@ class WeeklyReportMessage:
# 成本
ai_cost_week: float = 0.0
ai_tokens_week: int = 0
+ # 2026-04-07 Claude Code: Sprint 4 F1 — 處置分佈
+ disposition_auto: int = 0
+ disposition_human: int = 0
+ disposition_manual: int = 0
+ disposition_cold_start: int = 0
+ disposition_total: int = 0
def format(self) -> str:
"""格式化為 Telegram HTML"""
@@ -1015,10 +1021,24 @@ class WeeklyReportMessage:
f"━━━━━━━━━━━━━━━━━━━\n"
f"💰 AI 成本\n"
f"├ 費用: ${self.ai_cost_week:.2f}\n"
- f"└ Tokens: {self.ai_tokens_week:,}"
+ f"└ Tokens: {self.ai_tokens_week:,}\n"
)
- return message[:900]
+ # Sprint 4 F1: 處置分佈(有資料才加)
+ if self.disposition_total > 0:
+ auto_total = self.disposition_auto + self.disposition_cold_start
+ auto_rate = int(auto_total / self.disposition_total * 100) if self.disposition_total > 0 else 0
+ message += (
+ f"━━━━━━━━━━━━━━━━━━━\n"
+ f"📋 處置分佈\n"
+ f"├ 🤖 自動修復: {self.disposition_auto}\n"
+ f"├ ❄️ 冷啟動信任: {self.disposition_cold_start}\n"
+ f"├ 👤 人工審核: {self.disposition_human}\n"
+ f"├ 🔧 手動處理: {self.disposition_manual}\n"
+ f"└ 自動化率: {auto_rate}%"
+ )
+
+ return message[:1200]
@dataclass
diff --git a/apps/api/src/services/weekly_report_service.py b/apps/api/src/services/weekly_report_service.py
index 57eefd78..d9d59ccb 100644
--- a/apps/api/src/services/weekly_report_service.py
+++ b/apps/api/src/services/weekly_report_service.py
@@ -178,6 +178,20 @@ class WeeklyReportService:
pod_restarts = k3s_status.pod_restart_48h if k3s_status else 0
hpa_events = 0 # 需要從 Prometheus 取得 HPA 事件
+ # 2026-04-07 Claude Code: Sprint 4 F1 — 取得處置分佈
+ disp_auto = disp_human = disp_manual = disp_cold = disp_total = 0
+ try:
+ from src.services.anomaly_counter import get_anomaly_counter
+ counter = get_anomaly_counter()
+ disp_summary, _ = await counter.get_all_disposition_stats()
+ disp_auto = disp_summary.get("auto_repair", 0)
+ disp_human = disp_summary.get("human_approved", 0)
+ disp_manual = disp_summary.get("manual_resolved", 0)
+ disp_cold = disp_summary.get("cold_start_trust", 0)
+ disp_total = disp_summary.get("total", 0)
+ except Exception as _disp_e:
+ logger.warning("weekly_report_disposition_failed", error=str(_disp_e))
+
# 組裝週報
report = WeeklyReportMessage(
week_range=week_range,
@@ -197,6 +211,11 @@ class WeeklyReportService:
deploy_count=deploys,
ai_cost_week=0.0, # 需要從 AI 成本追蹤取得
ai_tokens_week=0, # 需要從 AI 成本追蹤取得
+ disposition_auto=disp_auto,
+ disposition_human=disp_human,
+ disposition_manual=disp_manual,
+ disposition_cold_start=disp_cold,
+ disposition_total=disp_total,
)
logger.info(
diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx
index f0a8dc86..20b0dcb4 100644
--- a/apps/web/src/app/[locale]/page.tsx
+++ b/apps/web/src/app/[locale]/page.tsx
@@ -365,8 +365,22 @@ export default function Home({ params }: { params: { locale: string } }) {
// P0 count
const p0Count = incidents?.filter(i => i.severity === 'P0').length ?? 0
- // 自動處置率
+ // 2026-04-07 Claude Code: Sprint 4 E2 — 從 disposition API 取得真實自動化率
+ const [dispositionRate, setDispositionRate] = useState<{ auto_rate: number; total: number } | null>(null)
+ useEffect(() => {
+ fetch(`${API_BASE}/api/v1/stats/disposition`)
+ .then(r => r.json())
+ .then(d => {
+ if (d?.summary) setDispositionRate({ auto_rate: d.summary.auto_rate, total: d.summary.total })
+ })
+ .catch(() => {})
+ }, [])
+
+ // 自動處置率 — 優先使用 disposition API,fallback 到 incidents 推算
const autoRemediationRate = (() => {
+ if (dispositionRate && dispositionRate.total > 0) {
+ return `${Math.round(dispositionRate.auto_rate * 100)}%`
+ }
if (!incidents?.length) return '--'
const resolved = incidents.filter(i => i.status === 'resolved' || i.status === 'closed').length
return `${((resolved / incidents.length) * 100).toFixed(0)}%`
@@ -374,6 +388,9 @@ export default function Home({ params }: { params: { locale: string } }) {
// 自動處置率數值 (for progress bar)
const autoRemediationPct = (() => {
+ if (dispositionRate && dispositionRate.total > 0) {
+ return Math.round(dispositionRate.auto_rate * 100)
+ }
if (!incidents?.length) return 0
const resolved = incidents.filter(i => i.status === 'resolved' || i.status === 'closed').length
return Math.round((resolved / incidents.length) * 100)