feat(web): APM/Apps/Deployments/Tickets 頁面升級 — 串接真實 API 數據
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
- apm/page.tsx: Golden Signals 真實數據 (SignOz ClickHouse) - apps/page.tsx: 主機服務狀態 (/api/v1/dashboard 真實數據) - deployments/page.tsx: K8s 部署狀態串接 - tickets/page.tsx: Incidents 列表串接 - i18n: apm/apps/deployments/tickets namespace 雙語補齊 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "資料尚未整合"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
healthy: '#22C55E',
|
||||
warning: '#F59E0B',
|
||||
critical: '#cc2200',
|
||||
unknown: '#87867f',
|
||||
}
|
||||
|
||||
export default function ApmPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('apm')
|
||||
const [data, setData] = useState<GoldMetricsResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<AppLayout locale={params.locale}>
|
||||
<div style={{ padding: '24px', background: '#f5f4ed', minHeight: '100%' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
|
||||
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 700, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3', fontFamily: 'var(--font-body), monospace', color: '#141413' }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
||||
{t('title')}
|
||||
</div>
|
||||
<div style={{ padding: '60px 24px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 32, color: '#e0ddd4', marginBottom: 16 }}>◎</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace', marginBottom: 8 }}>{t('noData')}</div>
|
||||
<div style={{ fontSize: 12, color: '#87867f', fontFamily: 'var(--font-body), monospace', maxWidth: 340, margin: '0 auto' }}>{t('noDataDescription')}</div>
|
||||
<div style={{ marginBottom: '20px', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
|
||||
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
|
||||
</div>
|
||||
<a href={SIGNOZ_URL} target="_blank" rel="noopener noreferrer" style={{ fontSize: 12, fontWeight: 600, color: '#4A90D9', border: '0.5px solid rgba(74,144,217,0.3)', borderRadius: 6, padding: '5px 12px', textDecoration: 'none', fontFamily: 'var(--font-body), monospace', background: 'rgba(74,144,217,0.05)' }}>
|
||||
↗ {t('openSignoz')}
|
||||
</a>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
|
||||
) : error ? (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '32px', textAlign: 'center', color: '#cc2200', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('error')}</div>
|
||||
) : data && data.metrics.length > 0 ? (
|
||||
<>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 12, marginBottom: 16 }}>
|
||||
{data.metrics.map((m, i) => (
|
||||
<div key={i} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '16px 18px' }}>
|
||||
<div style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{m.label}</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace', lineHeight: 1.2 }}>
|
||||
{typeof m.value === 'number' ? m.value.toFixed(2) : m.value}
|
||||
{m.unit && <span style={{ fontSize: 13, color: '#87867f', marginLeft: 4 }}>{m.unit}</span>}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: STATUS_COLOR[m.status] ?? '#87867f', display: 'inline-block' }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: STATUS_COLOR[m.status] ?? '#87867f', textTransform: 'uppercase' }}>{m.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '12px 16px' }}>
|
||||
<span style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
|
||||
Service: <strong style={{ color: '#141413' }}>{data.service_name}</strong>
|
||||
{' · '}
|
||||
{new Date(data.timestamp).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' })}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12 }}>
|
||||
<div style={{ padding: '60px 24px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 32, color: '#e0ddd4', marginBottom: 16 }}>◎</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace', marginBottom: 8 }}>{t('noData')}</div>
|
||||
<div style={{ fontSize: 12, color: '#87867f', fontFamily: 'var(--font-body), monospace', maxWidth: 340, margin: '0 auto' }}>{t('noDataDescription')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<Host[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<AppLayout locale={params.locale}>
|
||||
@@ -19,26 +58,44 @@ export default function AppsPage({ params }: { params: { locale: string } }) {
|
||||
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
|
||||
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 700, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3', fontFamily: 'var(--font-body), monospace', color: '#141413' }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
||||
{t('title')}
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#22C55E', display: 'inline-block' }} />
|
||||
{t('title')} ({loading ? '...' : allServices.length})
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('name'), t('version'), t('status')].map(col => (
|
||||
<th key={col} style={{ padding: '8px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
||||
{loading ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
|
||||
) : error ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#cc2200', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('error')}</div>
|
||||
) : allServices.length === 0 ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noApps')}</div>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('service'), t('host'), t('port'), t('latency'), t('status')].map(col => (
|
||||
<th key={col} style={{ padding: '8px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allServices.map((s, i) => (
|
||||
<tr key={i} style={{ borderBottom: '0.5px solid #f0ede4' }}>
|
||||
<td style={{ padding: '8px 14px', fontWeight: 500, color: '#141413' }}>{s.name}</td>
|
||||
<td style={{ padding: '8px 14px', color: '#87867f', fontSize: 12 }}>{s.hostName} <span style={{ color: '#c0bdb4' }}>({s.hostIp})</span></td>
|
||||
<td style={{ padding: '8px 14px', color: '#87867f' }}>{s.port ?? '—'}</td>
|
||||
<td style={{ padding: '8px 14px', color: '#87867f' }}>{s.latency_ms != null ? `${s.latency_ms.toFixed(0)}ms` : '—'}</td>
|
||||
<td style={{ padding: '8px 14px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, fontWeight: 600, textTransform: 'uppercase', color: STATUS_COLOR[s.status] ?? '#87867f' }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: STATUS_COLOR[s.status] ?? '#87867f', display: 'inline-block' }} />
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={3} style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noApps')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<Host[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<AppLayout locale={params.locale}>
|
||||
@@ -19,27 +59,54 @@ export default function DeploymentsPage({ params }: { params: { locale: string }
|
||||
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
|
||||
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 700, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3', fontFamily: 'var(--font-body), monospace', color: '#141413' }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
||||
{t('title')}
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('name'), t('version'), t('status'), t('time')].map(col => (
|
||||
<th key={col} style={{ padding: '8px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={4} style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noDeployments')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
|
||||
) : error ? (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '32px', textAlign: 'center', color: '#cc2200', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('error')}</div>
|
||||
) : displayHosts.length === 0 ? (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noDeployments')}</div>
|
||||
) : (
|
||||
displayHosts.map(host => (
|
||||
<div key={host.ip} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden', marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: STATUS_COLOR[host.status] ?? '#87867f', display: 'inline-block' }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.name}</span>
|
||||
<span style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>{host.ip}</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{host.last_check ? new Date(host.last_check).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei' }) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('service'), t('port'), t('latency'), t('status')].map(col => (
|
||||
<th key={col} style={{ padding: '6px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{host.services.length === 0 ? (
|
||||
<tr><td colSpan={4} style={{ padding: '16px 14px', textAlign: 'center', color: '#87867f', fontSize: 12 }}>{t('noDeployments')}</td></tr>
|
||||
) : host.services.map((s, i) => (
|
||||
<tr key={i} style={{ borderBottom: '0.5px solid #f0ede4' }}>
|
||||
<td style={{ padding: '7px 14px', fontWeight: 500, color: '#141413' }}>{s.name}</td>
|
||||
<td style={{ padding: '7px 14px', color: '#87867f' }}>{s.port ?? '—'}</td>
|
||||
<td style={{ padding: '7px 14px', color: '#87867f' }}>{s.latency_ms != null ? `${s.latency_ms.toFixed(0)}ms` : '—'}</td>
|
||||
<td style={{ padding: '7px 14px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, fontWeight: 600, textTransform: 'uppercase', color: STATUS_COLOR[s.status] ?? '#87867f' }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: STATUS_COLOR[s.status] ?? '#87867f', display: 'inline-block' }} />
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
|
||||
@@ -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<string, string> = {
|
||||
P0: '#cc2200',
|
||||
P1: '#F59E0B',
|
||||
P2: '#4A90D9',
|
||||
P3: '#22C55E',
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
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<Incident[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<AppLayout locale={params.locale}>
|
||||
@@ -19,26 +65,54 @@ export default function TicketsPage({ params }: { params: { locale: string } })
|
||||
<h1 style={{ fontSize: 18, fontWeight: 700, color: '#141413', margin: 0, fontFamily: 'var(--font-body), monospace' }}>{t('title')}</h1>
|
||||
<p style={{ fontSize: 12, color: '#87867f', margin: '4px 0 0', fontFamily: 'var(--font-body), monospace' }}>{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 700, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3', fontFamily: 'var(--font-body), monospace', color: '#141413' }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
||||
{t('title')}
|
||||
{t('title')} ({loading ? '...' : total})
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('id'), t('title_col'), t('status'), t('priority')].map(col => (
|
||||
<th key={col} style={{ padding: '8px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
||||
{loading ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('loading')}</div>
|
||||
) : error ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#cc2200', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('error')}</div>
|
||||
) : incidents.length === 0 ? (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noTickets')}</div>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('id'), t('title_col'), t('priority'), t('status'), t('createdAt')].map(col => (
|
||||
<th key={col} style={{ padding: '8px 14px', textAlign: 'left', fontWeight: 600, color: '#87867f', borderBottom: '0.5px solid #e0ddd4', fontSize: 11 }}>{col}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{incidents.map((inc) => (
|
||||
<tr key={inc.id} style={{ borderBottom: '0.5px solid #f0ede4' }}>
|
||||
<td style={{ padding: '8px 14px', color: '#87867f', fontSize: 11, fontFamily: 'monospace' }}>{inc.id.slice(0, 8)}</td>
|
||||
<td style={{ padding: '8px 14px', fontWeight: 500, color: '#141413', maxWidth: 300 }}>
|
||||
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{inc.title}</div>
|
||||
{inc.affected_service && (
|
||||
<div style={{ fontSize: 11, color: '#87867f', marginTop: 2 }}>{inc.affected_service}</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '8px 14px' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: SEV_COLOR[inc.severity] ?? '#87867f', background: `${SEV_COLOR[inc.severity] ?? '#87867f'}18`, border: `0.5px solid ${SEV_COLOR[inc.severity] ?? '#87867f'}40`, borderRadius: 4, padding: '1px 6px' }}>
|
||||
{inc.severity}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '8px 14px' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: STATUS_COLOR[inc.status] ?? '#87867f' }}>
|
||||
{inc.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '8px 14px', color: '#87867f', fontSize: 11 }}>
|
||||
{new Date(inc.created_at).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={4} style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noTickets')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
@@ -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 台北)
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|
||||
Reference in New Issue
Block a user