fix(phase25): 首席架構師 Review R2 修正 (I1/I2/I3/I4/C3/M1)

I1: auto_repair_service — 失敗分支 anti_pattern task 補齊 _pending_tasks GC 防護
C3: drift_remediator — _kubectl_apply() 實作 resource_key 範圍過濾(修復虛設參數 bug)
M1: drift_remediator — _git_push() 標記 DISABLED,防止誤啟用
I2: drift.py — Telegram 通知移除失效的 adopt() 端點連結
I3: drift/page.tsx — handleScan POST body namespace→namespaces(對齊後端 DriftScanRequest)
I4: drift/page.tsx — 移除硬編碼英文字串,改用 t('loading')/t('highCount')/t('mediumCount')
i18n: zh-TW.json + en.json 補齊 drift.loading key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-05 00:22:38 +08:00
parent 4bc4757fdc
commit 4912c7f307
6 changed files with 47 additions and 36 deletions

View File

@@ -182,7 +182,7 @@ async def _analyze_and_notify(report: DriftReport) -> None:
f"<b>漂移詳情</b>:\n{diff_summary}\n\n"
f"Report ID: <code>{report.report_id}</code>\n"
f"POST /api/v1/drift/reports/{report.report_id}/rollback — 覆蓋回 Git\n"
f"POST /api/v1/drift/reports/{report.report_id}/adopt — 承認變更"
f"adopt 端點暫停開放,待 ADR-057 實作後啟用)"
)
except Exception as e:
import structlog

View File

@@ -407,15 +407,18 @@ class AutoRepairService:
)
# 2026-04-04 Claude Code: Phase 25 P1 — 失敗修復後 fire-and-forget 生成 ANTI_PATTERN
# 2026-04-05 Claude Code: I1 修正 — 補齊 _pending_tasks GC 防護(對稱化)
try:
from src.services.runbook_generator import get_runbook_generator
import asyncio as _asyncio
symptoms = self._extract_symptoms(incident)
symptoms_hash = symptoms.compute_hash()
gen = get_runbook_generator()
import asyncio as _asyncio
_asyncio.create_task(
_ap_task = _asyncio.create_task(
gen.generate_anti_pattern(incident, playbook, fail_result, symptoms_hash)
)
self._pending_tasks.add(_ap_task)
_ap_task.add_done_callback(self._pending_tasks.discard)
except Exception as _ap_e:
logger.warning("anti_pattern_task_failed", error=str(_ap_e))

View File

@@ -166,9 +166,31 @@ class DriftRemediator:
# =========================================================================
def _kubectl_apply(self, namespace: str, resource_key: str | None) -> dict:
"""執行 kubectl apply同步"""
"""
執行 kubectl apply同步
2026-04-05 Claude Code: C3 修正 — resource_key 現在實際影響 apply 範圍
- resource_key=None: apply 整個 k8s/ 目錄
- resource_key="Deployment/api": 只 apply 匹配前綴的 YAML 檔
"""
try:
cmd = ["kubectl", "apply", "-f", self._k8s_dir, "-n", namespace, "--dry-run=none"]
if resource_key:
# 從 resource_key (e.g. "Deployment/api") 推斷檔名前綴
kind_lower = resource_key.split("/")[0].lower() if "/" in resource_key else resource_key.lower()
import pathlib
k8s_path = pathlib.Path(self._k8s_dir)
matched = list(k8s_path.glob(f"*{kind_lower}*.yaml")) + list(k8s_path.glob(f"*{kind_lower}*.yml"))
if matched:
target = str(matched[0])
logger.info("kubectl_apply_targeted", resource_key=resource_key, file=target)
else:
# 找不到匹配檔案fallback 整目錄但記錄警告
logger.warning("kubectl_apply_no_match_fallback", resource_key=resource_key, k8s_dir=self._k8s_dir)
target = self._k8s_dir
else:
target = self._k8s_dir
cmd = ["kubectl", "apply", "-f", target, "-n", namespace, "--dry-run=none"]
proc = subprocess.run(
cmd,
capture_output=True,
@@ -184,34 +206,18 @@ class DriftRemediator:
except Exception as e:
return {"success": False, "message": str(e)}
def _git_push(self, commit_msg: str) -> dict:
"""執行 git add + commit + push gitea同步"""
try:
# git add
subprocess.run(["git", "add", "-A"], check=True, timeout=10)
# git commit
subprocess.run(
["git", "commit", "-m", commit_msg],
check=True,
timeout=10,
)
# git push gitea main
proc = subprocess.run(
["git", "push", "gitea", "main"],
capture_output=True,
text=True,
timeout=30,
)
if proc.returncode == 0:
return {"success": True, "message": "已推送至 gitea main"}
else:
return {"success": False, "message": proc.stderr[:500]}
except subprocess.CalledProcessError as e:
return {"success": False, "message": f"git 操作失敗: {e}"}
except subprocess.TimeoutExpired:
return {"success": False, "message": "git push 超時"}
except Exception as e:
return {"success": False, "message": str(e)}
def _git_push(self, _commit_msg: str) -> dict:
"""
執行 git add + commit + push gitea同步
2026-04-05 Claude Code: M1 — DISABLED 標記,避免誤啟用
adopt() 端點已回傳 501此方法目前不可到達。
ADR-057 起草後改由 Gitea PR API 實作,屆時此方法整體移除。
"""
# DISABLED: adopt() 端點已返回 501此方法不應被呼叫
# 保留程式碼僅作歷史參考ADR-057 完成後刪除
return {"success": False, "message": "git_push DISABLED — 請參考 ADR-057"}
async def _notify_telegram(self, message: str) -> None:
"""推送通知到 Telegram"""

View File

@@ -934,6 +934,7 @@
"subtitle": "GitOps Guardian — Detects drift between K8s actual state and Git YAML",
"scan": "Scan Now",
"scanning": "Scanning...",
"loading": "Loading...",
"noReports": "No drift reports yet",
"noReportsHint": "CronJob scans hourly automatically, or click \"Scan Now\" to trigger manually",
"noDrift": "No Drift",

View File

@@ -935,6 +935,7 @@
"subtitle": "GitOps 守門員 — 偵測 K8s 實際狀態 vs Git YAML 的漂移",
"scan": "立即掃描",
"scanning": "掃描中...",
"loading": "載入中...",
"noReports": "目前無漂移報告",
"noReportsHint": "CronJob 每小時自動掃描,或點擊「立即掃描」手動觸發",
"noDrift": "無漂移",

View File

@@ -162,7 +162,7 @@ export default function DriftPage({ params }: { params: { locale: string } }) {
const res = await fetch(`${getApiBase()}/api/v1/drift/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ namespace: 'awoooi-prod', triggered_by: 'web_manual' }),
body: JSON.stringify({ namespaces: ['awoooi-prod'], triggered_by: 'web_manual' }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: ScanResult = await res.json()
@@ -241,7 +241,7 @@ export default function DriftPage({ params }: { params: { locale: string } }) {
{scanResult.summary}
{(scanResult.high_count > 0 || scanResult.medium_count > 0) && (
<span className="ml-2 text-neutral-500 font-normal">
High: {scanResult.high_count}, Medium: {scanResult.medium_count}
{t('highCount')} {scanResult.high_count}, {t('mediumCount')} {scanResult.medium_count}
</span>
)}
</div>
@@ -265,7 +265,7 @@ export default function DriftPage({ params }: { params: { locale: string } }) {
{loading && reports.length === 0 ? (
<div className="flex items-center justify-center h-32 text-neutral-400">
<RefreshCw size={16} className="animate-spin mr-2" />
<span className="text-[12px]">Loading...</span>
<span className="text-[12px]">{t('loading')}</span>
</div>
) : reports.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 text-neutral-400">