diff --git a/apps/api/src/api/v1/auto_repair.py b/apps/api/src/api/v1/auto_repair.py index 84a3aa6b..dd5bd1e3 100644 --- a/apps/api/src/api/v1/auto_repair.py +++ b/apps/api/src/api/v1/auto_repair.py @@ -13,7 +13,7 @@ Phase 8.2: API Router 實作 - 業務邏輯委託給 Service 層 """ -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field from src.services.auto_repair_service import ( @@ -197,3 +197,97 @@ async def get_auto_repair_stats() -> dict: "overall_success_rate": total_success / total_executions if total_executions > 0 else 0.0, "auto_repair_eligible": high_quality_count > 0, } + + +# ============================================================================= +# History Models & Endpoint +# 2026-04-06 Claude Code: Sprint 3 T_frontend — 修復歷史記錄 API +# ============================================================================= + + +class RepairHistoryItem(BaseModel): + """修復歷史記錄項目""" + + id: str + incident_id: str + playbook_id: str + playbook_name: str + action_type: str # "kubectl" | "ssh_command" | "manual" + uri_scheme: str # "kubectl://" | "openclaw://" | "ansible://" + command: str + status: str # "success" | "failed" | "pending_approval" | "running" + executed_at: str + duration_ms: int | None = None + error: str | None = None + rag_confidence: float | None = None + + +class RepairHistoryResponse(BaseModel): + """修復歷史記錄回應""" + + count: int + items: list[RepairHistoryItem] + + +@router.get("/history", response_model=RepairHistoryResponse) +async def get_repair_history( + limit: int = Query(20, ge=1, le=100), +) -> RepairHistoryResponse: + """ + 取得修復歷史記錄。 + 從 incidents (working memory) 推導,回傳有 auto_repair 活動的事件。 + 2026-04-06 Claude Code: Sprint 3 T_frontend + """ + try: + incident_service = get_incident_service() + all_incidents = await incident_service.get_active_incidents() + + items: list[RepairHistoryItem] = [] + for incident in all_incidents: + fs = incident.frequency_stats + if fs is None or fs.auto_repair_count == 0: + continue + + # 從 frequency_stats 推導修復狀態 + if fs.last_repair_success is True: + status = "success" + elif fs.last_repair_success is False: + status = "failed" + else: + status = "running" + + action = fs.last_repair_action or "kubectl rollout restart" + # 推導 action_type 和 uri_scheme + if action.startswith("kubectl"): + action_type = "kubectl" + uri_scheme = "kubectl://" + elif action.startswith("ssh") or action.startswith("ansible"): + action_type = "ssh_command" + uri_scheme = "ansible://" + else: + action_type = "manual" + uri_scheme = "openclaw://" + + items.append(RepairHistoryItem( + id=f"hist-{incident.incident_id}", + incident_id=incident.incident_id, + playbook_id="unknown", + playbook_name=action, + action_type=action_type, + uri_scheme=uri_scheme, + command=action, + status=status, + executed_at=incident.updated_at.isoformat(), + duration_ms=None, + error=None, + rag_confidence=None, + )) + + # 最多回傳 limit 筆,newest first (updated_at 已是活躍事件,先按 ID 截斷) + items = items[:limit] + + return RepairHistoryResponse(count=len(items), items=items) + + except Exception: + # 任何錯誤都回傳空列表,不中斷前端 + return RepairHistoryResponse(count=0, items=[]) diff --git a/apps/web/src/app/[locale]/neural-command/page.tsx b/apps/web/src/app/[locale]/neural-command/page.tsx index ce37a2dc..1633a05d 100644 --- a/apps/web/src/app/[locale]/neural-command/page.tsx +++ b/apps/web/src/app/[locale]/neural-command/page.tsx @@ -61,12 +61,15 @@ export default function NeuralCommandPage({ params }: { params: { locale: string const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) const [lastRefresh, setLastRefresh] = useState(new Date()) + const [pendingApprovals, setPendingApprovals] = useState(0) const fetchData = useCallback(async () => { try { - const [statsRes, pbRes] = await Promise.all([ + const [statsRes, pbRes, histRes, approvalsRes] = await Promise.all([ fetch('/api/v1/auto-repair/stats'), fetch('/api/v1/playbooks/'), + fetch('/api/v1/auto-repair/history?limit=20'), + fetch('/api/v1/approvals/pending'), ]) if (statsRes.ok) { @@ -77,6 +80,14 @@ export default function NeuralCommandPage({ params }: { params: { locale: string const data = await pbRes.json() setPlaybooks(data.items?.map((i: { playbook: PlaybookItem }) => i.playbook) ?? []) } + if (histRes.ok) { + const data = await histRes.json() + setHistory(data.items ?? []) + } + if (approvalsRes.ok) { + const data = await approvalsRes.json() + setPendingApprovals(data.count ?? 0) + } setLastRefresh(new Date()) } catch { @@ -93,7 +104,6 @@ export default function NeuralCommandPage({ params }: { params: { locale: string }, [fetchData]) const approvedPlaybooks = playbooks.filter(p => p.status === 'approved') - const pendingApprovals = 0 // TODO: fetch from /api/v1/approvals return (