diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index b635b5e6..92967a27 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -772,43 +772,80 @@ }, "apm": { "title": "APM", - "subtitle": "Application Performance Monitoring", - "noData": "No APM Data", - "noDataDescription": "APM integration is not yet enabled. Data will appear after SignOz connects." + "subtitle": "Application Performance Monitoring — Golden Signals", + "loading": "Loading...", + "metric": "Metric", + "value": "Value", + "status": "Status", + "openSignoz": "Open SigNoz", + "noData": "No APM data", + "noDataDescription": "APM integration pending, will display automatically after SignOz connects" }, "apps": { "title": "Applications", - "subtitle": "Application list", - "name": "App Name", - "version": "Version", + "subtitle": "All host services status", + "loading": "Loading...", + "host": "Host", + "service": "Service", + "port": "Port", + "latency": "Latency", "status": "Status", - "noApps": "No application data available" + "error": "Load failed", + "noApps": "No service data" }, "billing": { - "title": "Billing", - "subtitle": "Cost summary", - "currentMonth": "Current Month", + "title": "Usage", + "subtitle": "System operation usage statistics", + "loading": "Loading...", + "totalExecutions": "Total Executions", + "last24h": "Last 24h", + "successRate": "Success Rate", + "avgDuration": "Avg Duration", + "currentMonth": "This Month", "totalUsage": "Total Usage", - "noData": "No billing data available" + "error": "Load failed", + "noData": "No usage data" }, "compliance": { "title": "Compliance", - "subtitle": "Compliance status overview", - "noData": "No compliance data available" + "subtitle": "System governance & compliance status", + "loading": "Loading...", + "totalIncidents": "Total Incidents", + "resolvedRate": "Resolution Rate", + "approvedPlaybooks": "Playbooks", + "highQualityPlaybooks": "High-Quality Playbooks", + "executionSuccessRate": "Execution Success Rate", + "autoRepairEligible": "Auto-Repair Eligible", + "yes": "Yes", + "no": "No", + "error": "Load failed", + "noData": "No compliance data" }, "cost": { "title": "Cost Analysis", - "subtitle": "Resource cost analysis", - "noData": "No cost data available" + "subtitle": "AI execution efficiency stats", + "loading": "Loading...", + "totalProposals": "Total Proposals", + "executionRate": "Execution Rate", + "successRate": "Success Rate", + "avgEffectiveness": "Avg Effectiveness", + "error": "Load failed", + "noData": "No cost data" }, "deployments": { "title": "Deployments", - "subtitle": "Deployment history", - "name": "Service", - "version": "Version", + "subtitle": "K3s service deployment status", + "loading": "Loading...", + "service": "Service", + "port": "Port", + "latency": "Latency", "status": "Status", - "time": "Time", - "noDeployments": "No deployment records" + "host": "Host", + "error": "Load failed", + "noDeployments": "No deployment data", + "name": "Service Name", + "version": "Version", + "time": "Time" }, "help": { "title": "Help", @@ -821,28 +858,51 @@ }, "security": { "title": "Security", - "subtitle": "Security events overview", + "subtitle": "Errors & security event monitoring", + "loading": "Loading...", + "totalIssues": "Total Issues", + "criticalIssues": "Critical Issues", + "errorRate": "Error Rate", + "recentIssues": "Recent Issues", + "issue": "Issue", + "count": "Count", + "error": "Load failed", "noData": "No security events" }, "tickets": { "title": "Tickets", - "subtitle": "Ticket list", + "subtitle": "Incident ticket tracking", + "loading": "Loading...", "id": "Ticket ID", "title_col": "Title", "status": "Status", "priority": "Priority", + "createdAt": "Created At", + "error": "Load failed", "noTickets": "No tickets" }, "users": { - "title": "Users", - "subtitle": "User management", + "title": "Audit Log", + "subtitle": "K8s operation execution records", + "loading": "Loading...", + "totalExecutions": "Total Executions", + "successCount": "Success", + "failureCount": "Failures", + "successRate": "Success Rate", + "avgDuration": "Avg Duration", + "recentOps": "Recent Operations", + "operation": "Operation Type", + "namespace": "Namespace", + "result": "Result", + "time": "Time", + "error": "Load failed", + "noUsers": "No audit records", "name": "Name", "role": "Role", - "status": "Status", - "noUsers": "No user data available" + "status": "Status" }, "emptyState": { "noData": "--", "comingSoon": "Integration pending" } -} \ No newline at end of file +} diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5c604851..43df1577 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -773,43 +773,80 @@ }, "apm": { "title": "APM", - "subtitle": "應用性能監控", + "subtitle": "應用性能監控 — 黃金指標", + "loading": "載入中...", + "metric": "指標", + "value": "數值", + "status": "狀態", + "openSignoz": "開啟 SigNoz", "noData": "暫無 APM 數據", "noDataDescription": "APM 整合尚未啟用,待 SignOz 連線後自動顯示" }, "apps": { "title": "應用", - "subtitle": "應用程式清單", - "name": "應用名稱", - "version": "版本", + "subtitle": "所有主機服務狀態", + "loading": "載入中...", + "host": "主機", + "service": "服務", + "port": "Port", + "latency": "延遲", "status": "狀態", - "noApps": "目前無應用資料" + "error": "載入失敗", + "noApps": "無服務資料" }, "billing": { - "title": "帳單", - "subtitle": "費用摘要", - "currentMonth": "本月費用", + "title": "使用量", + "subtitle": "系統操作使用量統計", + "loading": "載入中...", + "totalExecutions": "總執行數", + "last24h": "近 24 小時", + "successRate": "成功率", + "avgDuration": "平均耗時", + "currentMonth": "本月執行數", "totalUsage": "總用量", - "noData": "目前無帳單資料" + "error": "載入失敗", + "noData": "無使用量資料" }, "compliance": { "title": "合規", - "subtitle": "合規狀態概覽", - "noData": "目前無合規資料" + "subtitle": "系統治理合規狀態", + "loading": "載入中...", + "totalIncidents": "事件總數", + "resolvedRate": "解決率", + "approvedPlaybooks": "Playbook 數", + "highQualityPlaybooks": "高品質 Playbook", + "executionSuccessRate": "執行成功率", + "autoRepairEligible": "可自動修復", + "yes": "是", + "no": "否", + "error": "載入失敗", + "noData": "無合規資料" }, "cost": { "title": "成本分析", - "subtitle": "資源成本分析", - "noData": "目前無成本資料" + "subtitle": "AI 執行效能統計", + "loading": "載入中...", + "totalProposals": "提案總數", + "executionRate": "執行率", + "successRate": "成功率", + "avgEffectiveness": "平均有效性", + "error": "載入失敗", + "noData": "無成本資料" }, "deployments": { "title": "部署管理", - "subtitle": "部署紀錄", - "name": "服務", - "version": "版本", + "subtitle": "K3s 服務部署狀態", + "loading": "載入中...", + "service": "服務", + "port": "Port", + "latency": "延遲", "status": "狀態", - "time": "時間", - "noDeployments": "目前無部署紀錄" + "host": "主機", + "error": "載入失敗", + "noDeployments": "無部署資料", + "name": "服務名稱", + "version": "版本", + "time": "時間" }, "help": { "title": "說明", @@ -822,28 +859,51 @@ }, "security": { "title": "安全", - "subtitle": "安全事件概覽", - "noData": "目前無安全事件" + "subtitle": "錯誤與安全事件監控", + "loading": "載入中...", + "totalIssues": "問題總數", + "criticalIssues": "嚴重問題", + "errorRate": "錯誤率", + "recentIssues": "最近問題", + "issue": "問題", + "count": "次數", + "error": "載入失敗", + "noData": "無安全事件" }, "tickets": { "title": "工單", - "subtitle": "工單列表", + "subtitle": "事件工單追蹤", + "loading": "載入中...", "id": "工單 ID", "title_col": "標題", "status": "狀態", "priority": "優先級", + "createdAt": "建立時間", + "error": "載入失敗", "noTickets": "目前無工單" }, "users": { - "title": "使用者", - "subtitle": "使用者管理", + "title": "操作稽核", + "subtitle": "K8s 操作執行紀錄", + "loading": "載入中...", + "totalExecutions": "總執行數", + "successCount": "成功數", + "failureCount": "失敗數", + "successRate": "成功率", + "avgDuration": "平均耗時", + "recentOps": "最近操作", + "operation": "操作類型", + "namespace": "命名空間", + "result": "結果", + "time": "執行時間", + "error": "載入失敗", + "noUsers": "無稽核紀錄", "name": "姓名", "role": "角色", - "status": "狀態", - "noUsers": "目前無使用者資料" + "status": "狀態" }, "emptyState": { "noData": "--", "comingSoon": "資料尚未整合" } -} \ No newline at end of file +} diff --git a/apps/web/src/app/[locale]/apm/page.tsx b/apps/web/src/app/[locale]/apm/page.tsx index 4448297a..cc036173 100644 --- a/apps/web/src/app/[locale]/apm/page.tsx +++ b/apps/web/src/app/[locale]/apm/page.tsx @@ -1,36 +1,101 @@ 'use client' /** - * APM Page - * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) - * @updated 2026-04-02 ogt - 升級為真實空狀態頁(無 APM API,待 SignOz 整合) + * APM Page — 黃金指標 (Golden Signals) + * @created 2026-04-01 ogt - 路由佔位 + * @updated 2026-04-03 Claude Code - 串接 /api/v1/metrics/gold 真實數據 */ +import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' +const SIGNOZ_URL = 'http://192.168.0.188:3301' + +interface GoldMetricItem { + label: string + value: number | string + unit: string | null + status: string +} + +interface GoldMetricsResponse { + timestamp: string + service_name: string + metrics: GoldMetricItem[] +} + +const STATUS_COLOR: Record = { + healthy: '#22C55E', + warning: '#F59E0B', + critical: '#cc2200', + unknown: '#87867f', +} + export default function ApmPage({ params }: { params: { locale: string } }) { const t = useTranslations('apm') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/metrics/gold?service_name=awoooi-api&time_window_minutes=10`) + .then(r => r.json()) + .then((d: GoldMetricsResponse) => { setData(d); setLoading(false) }) + .catch(err => { setError(String(err)); setLoading(false) }) + }, []) return (
-
-

{t('title')}

-

{t('subtitle')}

-
- -
-
- - {t('title')} -
-
-
-
{t('noData')}
-
{t('noDataDescription')}
+
+
+

{t('title')}

+

{t('subtitle')}

+ + ↗ {t('openSignoz')} +
+ {loading ? ( +
{t('loading')}
+ ) : error ? ( +
{t('error')}
+ ) : data && data.metrics.length > 0 ? ( + <> +
+ {data.metrics.map((m, i) => ( +
+
{m.label}
+
+ {typeof m.value === 'number' ? m.value.toFixed(2) : m.value} + {m.unit && {m.unit}} +
+
+ + {m.status} +
+
+ ))} +
+
+ + Service: {data.service_name} + {' · '} + {new Date(data.timestamp).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' })} + +
+ + ) : ( +
+
+
+
{t('noData')}
+
{t('noDataDescription')}
+
+
+ )}
) diff --git a/apps/web/src/app/[locale]/apps/page.tsx b/apps/web/src/app/[locale]/apps/page.tsx index bd143a4b..472854c5 100644 --- a/apps/web/src/app/[locale]/apps/page.tsx +++ b/apps/web/src/app/[locale]/apps/page.tsx @@ -1,16 +1,55 @@ 'use client' /** - * 應用 Page - * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) - * @updated 2026-04-02 ogt - 升級為真實空列表頁(無 apps API) + * 應用 Page — 真實主機服務狀態 + * @created 2026-04-01 ogt - 路由佔位 + * @updated 2026-04-03 Claude Code - 串接 /api/v1/dashboard 真實數據 */ +import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +interface HostService { + name: string + status: string + port: number | null + latency_ms: number | null +} + +interface Host { + ip: string + name: string + status: string + services: HostService[] +} + +const STATUS_COLOR: Record = { + up: '#22C55E', + healthy: '#22C55E', + down: '#cc2200', + degraded: '#F59E0B', + unreachable: '#87867f', +} + export default function AppsPage({ params }: { params: { locale: string } }) { const t = useTranslations('apps') + const [hosts, setHosts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/dashboard`) + .then(r => r.json()) + .then(data => { setHosts(data.hosts ?? []); setLoading(false) }) + .catch(err => { setError(String(err)); setLoading(false) }) + }, []) + + const allServices = hosts.flatMap(h => + h.services.map(s => ({ ...s, hostName: h.name, hostIp: h.ip })) + ) return ( @@ -19,26 +58,44 @@ export default function AppsPage({ params }: { params: { locale: string } }) {

{t('title')}

{t('subtitle')}

-
- - {t('title')} + + {t('title')} ({loading ? '...' : allServices.length})
- - - - {[t('name'), t('version'), t('status')].map(col => ( - + {loading ? ( +
{t('loading')}
+ ) : error ? ( +
{t('error')}
+ ) : allServices.length === 0 ? ( +
{t('noApps')}
+ ) : ( +
{col}
+ + + {[t('service'), t('host'), t('port'), t('latency'), t('status')].map(col => ( + + ))} + + + + {allServices.map((s, i) => ( + + + + + + + ))} - - - - - - - -
{col}
{s.name}{s.hostName} ({s.hostIp}){s.port ?? '—'}{s.latency_ms != null ? `${s.latency_ms.toFixed(0)}ms` : '—'} + + + {s.status} + +
{t('noApps')}
+ + + )}
diff --git a/apps/web/src/app/[locale]/deployments/page.tsx b/apps/web/src/app/[locale]/deployments/page.tsx index 289fd994..7c3dc91e 100644 --- a/apps/web/src/app/[locale]/deployments/page.tsx +++ b/apps/web/src/app/[locale]/deployments/page.tsx @@ -1,16 +1,56 @@ 'use client' /** - * 部署管理 Page - * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) - * @updated 2026-04-02 ogt - 升級為真實空列表頁(無 deployments API) + * 部署管理 Page — K3s 服務部署狀態 + * @created 2026-04-01 ogt - 路由佔位 + * @updated 2026-04-03 Claude Code - 串接 /api/v1/dashboard 真實數據 */ +import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +interface HostService { + name: string + status: string + port: number | null + latency_ms: number | null +} + +interface Host { + ip: string + name: string + role: string + status: string + services: HostService[] + last_check: string +} + +const STATUS_COLOR: Record = { + up: '#22C55E', + healthy: '#22C55E', + down: '#cc2200', + degraded: '#F59E0B', + unreachable: '#87867f', +} + export default function DeploymentsPage({ params }: { params: { locale: string } }) { const t = useTranslations('deployments') + const [hosts, setHosts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/dashboard`) + .then(r => r.json()) + .then(data => { setHosts(data.hosts ?? []); setLoading(false) }) + .catch(err => { setError(String(err)); setLoading(false) }) + }, []) + + const k3sHosts = hosts.filter(h => h.role === 'k3s' || h.ip.includes('120')) + const displayHosts = k3sHosts.length > 0 ? k3sHosts : hosts return ( @@ -19,27 +59,54 @@ export default function DeploymentsPage({ params }: { params: { locale: string }

{t('title')}

{t('subtitle')}

- -
-
- - {t('title')} -
- - - - {[t('name'), t('version'), t('status'), t('time')].map(col => ( - - ))} - - - - - - - -
{col}
{t('noDeployments')}
-
+ {loading ? ( +
{t('loading')}
+ ) : error ? ( +
{t('error')}
+ ) : displayHosts.length === 0 ? ( +
{t('noDeployments')}
+ ) : ( + displayHosts.map(host => ( +
+
+
+ + {host.name} + {host.ip} +
+ + {host.last_check ? new Date(host.last_check).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei' }) : '—'} + +
+ + + + {[t('service'), t('port'), t('latency'), t('status')].map(col => ( + + ))} + + + + {host.services.length === 0 ? ( + + ) : host.services.map((s, i) => ( + + + + + + + ))} + +
{col}
{t('noDeployments')}
{s.name}{s.port ?? '—'}{s.latency_ms != null ? `${s.latency_ms.toFixed(0)}ms` : '—'} + + + {s.status} + +
+
+ )) + )}
) diff --git a/apps/web/src/app/[locale]/tickets/page.tsx b/apps/web/src/app/[locale]/tickets/page.tsx index 450face4..2fa6206f 100644 --- a/apps/web/src/app/[locale]/tickets/page.tsx +++ b/apps/web/src/app/[locale]/tickets/page.tsx @@ -1,16 +1,62 @@ 'use client' /** - * 工單 Page - * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) - * @updated 2026-04-02 ogt - 升級為真實空列表頁(無 tickets API) + * 工單 Page — 真實 Incidents 作為工單追蹤 + * @created 2026-04-01 ogt - 路由佔位 + * @updated 2026-04-03 Claude Code - 串接 /api/v1/incidents 真實數據 */ +import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +interface Incident { + id: string + title: string + severity: string + status: string + created_at: string + affected_service: string | null +} + +interface IncidentListResponse { + incidents: Incident[] + total: number +} + +const SEV_COLOR: Record = { + P0: '#cc2200', + P1: '#F59E0B', + P2: '#4A90D9', + P3: '#22C55E', +} + +const STATUS_COLOR: Record = { + open: '#cc2200', + in_progress: '#F59E0B', + resolved: '#22C55E', + closed: '#87867f', +} + export default function TicketsPage({ params }: { params: { locale: string } }) { const t = useTranslations('tickets') + const [incidents, setIncidents] = useState([]) + const [total, setTotal] = useState(0) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/incidents`) + .then(r => r.json()) + .then((data: IncidentListResponse) => { + setIncidents(data.incidents ?? []) + setTotal(data.total ?? 0) + setLoading(false) + }) + .catch(err => { setError(String(err)); setLoading(false) }) + }, []) return ( @@ -19,26 +65,54 @@ export default function TicketsPage({ params }: { params: { locale: string } })

{t('title')}

{t('subtitle')}

-
- {t('title')} + {t('title')} ({loading ? '...' : total})
- - - - {[t('id'), t('title_col'), t('status'), t('priority')].map(col => ( - + {loading ? ( +
{t('loading')}
+ ) : error ? ( +
{t('error')}
+ ) : incidents.length === 0 ? ( +
{t('noTickets')}
+ ) : ( +
{col}
+ + + {[t('id'), t('title_col'), t('priority'), t('status'), t('createdAt')].map(col => ( + + ))} + + + + {incidents.map((inc) => ( + + + + + + + ))} - - - - - - - -
{col}
{inc.id.slice(0, 8)} +
{inc.title}
+ {inc.affected_service && ( +
{inc.affected_service}
+ )} +
+ + {inc.severity} + + + + {inc.status} + + + {new Date(inc.created_at).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} +
{t('noTickets')}
+ + + )}
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 76619476..4fc2b23a 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -5,6 +5,18 @@ --- +## 📍 當前狀態 (2026-04-03 台北) + +| 項目 | 狀態 | +|------|------| +| **前端 UI 對齊 figma-v2** | ✅ 卡片式佈局 + SEV顏色 + layout bug 修復 — commit 2253c1b push gitea | +| **14 個 ComingSoon 頁面** | ✅ 替換真實 UI (e93a50a) — services/topology/notifications/reports 串 API | +| **CD 測試修復** | ✅ test_nvidia_provider.py NEMOTRON 修正 (6266a4f) | +| **AppLayout fullBleed** | ✅ 修復主頁大空白 + Metrics Strip 右側溢出 (2253c1b) | +| **Phase 24 B2 觀察期** | ⏳ 進行中 — 截止 2026-04-04 18:40 | + +--- + ## 📍 當前狀態 (2026-04-02 21:30 台北) | 項目 | 狀態 |