diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index ca447870..274bcd8d 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -58,7 +58,8 @@
"users": "Users",
"notifications": "Notifications",
"billing": "Billing",
- "help": "Help"
+ "help": "Help",
+ "drift": "Drift Detection"
},
"locale": {
"switch": "Switch Language",
@@ -924,5 +925,36 @@
"emptyState": {
"noData": "--",
"comingSoon": "Integration pending"
+ },
+ "drift": {
+ "title": "Config Drift Detection",
+ "subtitle": "GitOps Guardian — Detects drift between K8s actual state and Git YAML",
+ "scan": "Scan Now",
+ "scanning": "Scanning...",
+ "noReports": "No drift reports yet",
+ "noReportsHint": "CronJob scans hourly automatically, or click \"Scan Now\" to trigger manually",
+ "noDrift": "No Drift",
+ "reportId": "Report ID",
+ "scannedAt": "Scanned At",
+ "namespace": "Namespace",
+ "triggeredBy": "Triggered By",
+ "highCount": "High",
+ "mediumCount": "Medium",
+ "infoCount": "Info",
+ "status": "Status",
+ "driftLevel": {
+ "high": "High",
+ "medium": "Medium",
+ "info": "Info"
+ },
+ "interpretation": "Nemotron Intent Analysis",
+ "noInterpretation": "No analysis needed (no drift)",
+ "rollback": "Rollback to Git",
+ "adopt": "Adopt Change",
+ "rollbackConfirm": "Rollback this resource to Git state?",
+ "adoptConfirm": "Adopt this change and update Git?",
+ "pending": "Pending",
+ "resolved": "Resolved",
+ "ignored": "Ignored"
}
-}
\ No newline at end of file
+}
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 7a6e0a49..eac763e5 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -58,7 +58,8 @@
"users": "使用者",
"notifications": "通知",
"billing": "帳單",
- "help": "說明"
+ "help": "說明",
+ "drift": "漂移偵測"
},
"locale": {
"switch": "切換語系",
@@ -925,5 +926,36 @@
"emptyState": {
"noData": "--",
"comingSoon": "資料尚未整合"
+ },
+ "drift": {
+ "title": "配置漂移偵測",
+ "subtitle": "GitOps 守門員 — 偵測 K8s 實際狀態 vs Git YAML 的漂移",
+ "scan": "立即掃描",
+ "scanning": "掃描中...",
+ "noReports": "目前無漂移報告",
+ "noReportsHint": "CronJob 每小時自動掃描,或點擊「立即掃描」手動觸發",
+ "noDrift": "無漂移",
+ "reportId": "報告 ID",
+ "scannedAt": "掃描時間",
+ "namespace": "Namespace",
+ "triggeredBy": "觸發來源",
+ "highCount": "高",
+ "mediumCount": "中",
+ "infoCount": "低",
+ "status": "狀態",
+ "driftLevel": {
+ "high": "高",
+ "medium": "中",
+ "info": "低"
+ },
+ "interpretation": "Nemotron 意圖分析",
+ "noInterpretation": "無需分析(無漂移)",
+ "rollback": "覆蓋回 Git",
+ "adopt": "承認變更",
+ "rollbackConfirm": "確定要將此資源覆蓋回 Git 狀態嗎?",
+ "adoptConfirm": "確定要將此變更承認並更新至 Git 嗎?",
+ "pending": "待處理",
+ "resolved": "已解決",
+ "ignored": "已忽略"
}
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/app/[locale]/drift/page.tsx b/apps/web/src/app/[locale]/drift/page.tsx
new file mode 100644
index 00000000..23989dff
--- /dev/null
+++ b/apps/web/src/app/[locale]/drift/page.tsx
@@ -0,0 +1,324 @@
+'use client'
+
+/**
+ * Config Drift Detection Page - 配置漂移偵測
+ * =============================================
+ * Phase 25 P2: GitOps 守門員
+ * 偵測 K8s 實際狀態 vs Git YAML 漂移
+ *
+ * API: /api/v1/drift/scan, /api/v1/drift/reports
+ * CronJob: drift-scanner (每小時自動)
+ *
+ * 建立時間: 2026-04-04 (台北時區)
+ * 建立者: Claude Code (Phase 25 P2)
+ * 關聯設計: docs/superpowers/specs/2026-04-04-nemotron-active-defense-design.md 方向三
+ */
+
+import { useState, useEffect, useCallback } from 'react'
+import { useTranslations } from 'next-intl'
+import { AppLayout } from '@/components/layout'
+import { cn } from '@/lib/utils'
+import {
+ Diff, RefreshCw, AlertTriangle, CheckCircle2,
+ Clock, Terminal, GitMerge, Info,
+} from 'lucide-react'
+
+// =============================================================================
+// Types
+// =============================================================================
+
+interface DriftReport {
+ report_id: string
+ scanned_at: string
+ namespace: string
+ triggered_by: string
+ high_count: number
+ medium_count: number
+ info_count: number
+ interpretation: string | null
+ status: 'pending' | 'resolved' | 'ignored'
+ created_at: string
+ resolved_at: string | null
+}
+
+interface ScanResult {
+ report_id: string
+ summary: string
+ high_count: number
+ medium_count: number
+ info_count: number
+ has_critical_drift: boolean
+ interpretation: string | null
+}
+
+// =============================================================================
+// Helpers
+// =============================================================================
+
+const getApiBase = () => {
+ if (typeof window === 'undefined') return ''
+ return process.env.NEXT_PUBLIC_API_URL ?? ''
+}
+
+const fmtTime = (iso: string) => {
+ try {
+ return new Date(iso).toLocaleString('zh-TW', {
+ year: 'numeric', month: '2-digit', day: '2-digit',
+ hour: '2-digit', minute: '2-digit',
+ })
+ } catch {
+ return iso
+ }
+}
+
+// =============================================================================
+// Sub-components
+// =============================================================================
+
+function DriftLevelBadge({ high, medium, info, t }: {
+ high: number; medium: number; info: number
+ t: (k: string) => string
+}) {
+ if (high === 0 && medium === 0 && info === 0) {
+ return (
+
+
+ {t('subtitle')} +
++ {scanResult.interpretation} +
+ )} +{t('noReports')}
+{t('noReportsHint')}
+
+ {report.report_id.slice(0, 8)}
+
+ + {report.interpretation} +
+ )} + {/* Metadata */} +