feat(api): Phase 6.5 Statistics API + Y/n 按鈕修復

新增:
- /stats/incidents/summary - 事件總覽統計
- /stats/incidents/resolution - 解決時間 P50/P95
- /stats/ai-performance - AI 提案效能
- /stats/services/affected - 受影響服務排名

修復:
- Y/n 按鈕永久禁用問題 (decision.state=completed 但 incident 未解決)
- decision_manager.py: 只有當 incident 也已解決才返回已完成的 decision

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-24 09:50:03 +08:00
parent ab7ad09ed6
commit 765ee39a90
3 changed files with 447 additions and 20 deletions

View File

@@ -300,12 +300,23 @@ class DecisionManager:
# 1. 檢查現有 token
existing_token = await self._find_existing_token(incident.incident_id)
if existing_token and existing_token.state in (
DecisionState.READY,
DecisionState.EXECUTING,
DecisionState.COMPLETED,
):
return existing_token
if existing_token:
# READY 或 EXECUTING 狀態: 直接返回
if existing_token.state in (DecisionState.READY, DecisionState.EXECUTING):
return existing_token
# COMPLETED 狀態: 只有 incident 也已解決才返回,否則創建新 decision
# 修復: 避免 incident 未解決但 decision 已完成導致 Y/n 按鈕永久禁用
if existing_token.state == DecisionState.COMPLETED:
from src.models.incident import IncidentStatus
if incident.status in (IncidentStatus.RESOLVED, IncidentStatus.CLOSED):
return existing_token
# incident 仍在處理中,需要新的 decision
logger.info(
"decision_reset_for_active_incident",
token=existing_token.token,
incident_id=incident.incident_id,
incident_status=incident.status.value,
)
# 2. 建立新 token
token = DecisionToken(
@@ -539,12 +550,21 @@ class DecisionManager:
# 檢查現有 token
existing_token = await self._find_existing_token(incident.incident_id)
if existing_token and existing_token.state in (
DecisionState.READY,
DecisionState.EXECUTING,
DecisionState.COMPLETED,
):
return existing_token
if existing_token:
# READY 或 EXECUTING 狀態: 直接返回
if existing_token.state in (DecisionState.READY, DecisionState.EXECUTING):
return existing_token
# COMPLETED 狀態: 只有 incident 也已解決才返回
if existing_token.state == DecisionState.COMPLETED:
from src.models.incident import IncidentStatus
if incident.status in (IncidentStatus.RESOLVED, IncidentStatus.CLOSED):
return existing_token
logger.info(
"decision_reset_for_active_incident_consensus",
token=existing_token.token,
incident_id=incident.incident_id,
incident_status=incident.status.value,
)
# 建立新 token
token = DecisionToken(