feat(pages): 全部 ComingSoon 頁面升級為真實 UI — 串接真實 API / 空狀態頁面
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m47s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 6m47s
- services/topology: 串接 /api/v1/dashboard,顯示服務清單表格與主機拓撲卡片 grid - notifications: 串接 /api/v1/notifications/channels,404 時顯示空列表 - reports: 串接 /api/v1/stats/incident-summary + /api/v1/stats/resolution-stats,顯示統計卡片 - apm: 乾淨空狀態頁(SignOz 待整合) - apps/tickets/users/deployments: 空列表表格結構 - billing/compliance/cost/security: 空狀態卡片結構 - help: 靜態系統版本資訊頁 - zh-TW.json + en.json: 新增所有頁面 i18n key(零 hardcode 字串) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -727,5 +727,122 @@
|
||||
"latency": "Latency",
|
||||
"uptime": "Uptime",
|
||||
"lastCheck": "Last Check"
|
||||
},
|
||||
"services": {
|
||||
"title": "Services",
|
||||
"subtitle": "All services across hosts",
|
||||
"name": "Service Name",
|
||||
"host": "Host",
|
||||
"status": "Status",
|
||||
"cpu": "CPU%",
|
||||
"ram": "RAM%",
|
||||
"noServices": "No service data available",
|
||||
"fetchError": "Failed to load services"
|
||||
},
|
||||
"topology": {
|
||||
"title": "Topology",
|
||||
"subtitle": "Host architecture view",
|
||||
"noHosts": "No host data available",
|
||||
"fetchError": "Failed to load host data",
|
||||
"services": "Services",
|
||||
"cpu": "CPU",
|
||||
"ram": "RAM"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"subtitle": "Notification channel settings",
|
||||
"channel": "Channel",
|
||||
"type": "Type",
|
||||
"status": "Status",
|
||||
"noChannels": "No notification channels",
|
||||
"fetchError": "Failed to load notification channels"
|
||||
},
|
||||
"reports": {
|
||||
"title": "Reports",
|
||||
"subtitle": "Incident statistics summary",
|
||||
"incidentSummary": "Incident Summary",
|
||||
"resolutionStats": "Resolution Statistics",
|
||||
"total": "Total",
|
||||
"resolved": "Resolved",
|
||||
"unresolved": "Unresolved",
|
||||
"avgResolutionTime": "Avg Resolution Time",
|
||||
"resolutionRate": "Resolution Rate",
|
||||
"fetchError": "Failed to load report data",
|
||||
"noData": "No statistics available"
|
||||
},
|
||||
"apm": {
|
||||
"title": "APM",
|
||||
"subtitle": "Application Performance Monitoring",
|
||||
"noData": "No APM Data",
|
||||
"noDataDescription": "APM integration is not yet enabled. Data will appear after SignOz connects."
|
||||
},
|
||||
"apps": {
|
||||
"title": "Applications",
|
||||
"subtitle": "Application list",
|
||||
"name": "App Name",
|
||||
"version": "Version",
|
||||
"status": "Status",
|
||||
"noApps": "No application data available"
|
||||
},
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"subtitle": "Cost summary",
|
||||
"currentMonth": "Current Month",
|
||||
"totalUsage": "Total Usage",
|
||||
"noData": "No billing data available"
|
||||
},
|
||||
"compliance": {
|
||||
"title": "Compliance",
|
||||
"subtitle": "Compliance status overview",
|
||||
"noData": "No compliance data available"
|
||||
},
|
||||
"cost": {
|
||||
"title": "Cost Analysis",
|
||||
"subtitle": "Resource cost analysis",
|
||||
"noData": "No cost data available"
|
||||
},
|
||||
"deployments": {
|
||||
"title": "Deployments",
|
||||
"subtitle": "Deployment history",
|
||||
"name": "Service",
|
||||
"version": "Version",
|
||||
"status": "Status",
|
||||
"time": "Time",
|
||||
"noDeployments": "No deployment records"
|
||||
},
|
||||
"help": {
|
||||
"title": "Help",
|
||||
"subtitle": "System information",
|
||||
"version": "Version Info",
|
||||
"appVersion": "Application Version",
|
||||
"platform": "Platform",
|
||||
"docs": "Documentation",
|
||||
"docsDescription": "Visit AWOOOI Docs for full documentation"
|
||||
},
|
||||
"security": {
|
||||
"title": "Security",
|
||||
"subtitle": "Security events overview",
|
||||
"noData": "No security events"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Tickets",
|
||||
"subtitle": "Ticket list",
|
||||
"id": "Ticket ID",
|
||||
"title_col": "Title",
|
||||
"status": "Status",
|
||||
"priority": "Priority",
|
||||
"noTickets": "No tickets"
|
||||
},
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"subtitle": "User management",
|
||||
"name": "Name",
|
||||
"role": "Role",
|
||||
"status": "Status",
|
||||
"noUsers": "No user data available"
|
||||
},
|
||||
"emptyState": {
|
||||
"noData": "--",
|
||||
"comingSoon": "Integration pending"
|
||||
}
|
||||
}
|
||||
@@ -728,5 +728,122 @@
|
||||
"latency": "延遲",
|
||||
"uptime": "可用率",
|
||||
"lastCheck": "最後檢查"
|
||||
},
|
||||
"services": {
|
||||
"title": "服務目錄",
|
||||
"subtitle": "所有主機上的服務清單",
|
||||
"name": "服務名稱",
|
||||
"host": "主機",
|
||||
"status": "狀態",
|
||||
"cpu": "CPU%",
|
||||
"ram": "RAM%",
|
||||
"noServices": "目前無服務資料",
|
||||
"fetchError": "無法取得服務清單"
|
||||
},
|
||||
"topology": {
|
||||
"title": "拓撲圖",
|
||||
"subtitle": "主機架構視圖",
|
||||
"noHosts": "目前無主機資料",
|
||||
"fetchError": "無法取得主機資料",
|
||||
"services": "服務",
|
||||
"cpu": "CPU",
|
||||
"ram": "RAM"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"subtitle": "通知頻道設定",
|
||||
"channel": "頻道",
|
||||
"type": "類型",
|
||||
"status": "狀態",
|
||||
"noChannels": "目前無通知頻道",
|
||||
"fetchError": "無法取得通知頻道"
|
||||
},
|
||||
"reports": {
|
||||
"title": "報表",
|
||||
"subtitle": "事件統計摘要",
|
||||
"incidentSummary": "事件摘要",
|
||||
"resolutionStats": "解決率統計",
|
||||
"total": "總計",
|
||||
"resolved": "已解決",
|
||||
"unresolved": "未解決",
|
||||
"avgResolutionTime": "平均解決時間",
|
||||
"resolutionRate": "解決率",
|
||||
"fetchError": "無法取得報表資料",
|
||||
"noData": "目前無統計資料"
|
||||
},
|
||||
"apm": {
|
||||
"title": "APM",
|
||||
"subtitle": "應用性能監控",
|
||||
"noData": "暫無 APM 數據",
|
||||
"noDataDescription": "APM 整合尚未啟用,待 SignOz 連線後自動顯示"
|
||||
},
|
||||
"apps": {
|
||||
"title": "應用",
|
||||
"subtitle": "應用程式清單",
|
||||
"name": "應用名稱",
|
||||
"version": "版本",
|
||||
"status": "狀態",
|
||||
"noApps": "目前無應用資料"
|
||||
},
|
||||
"billing": {
|
||||
"title": "帳單",
|
||||
"subtitle": "費用摘要",
|
||||
"currentMonth": "本月費用",
|
||||
"totalUsage": "總用量",
|
||||
"noData": "目前無帳單資料"
|
||||
},
|
||||
"compliance": {
|
||||
"title": "合規",
|
||||
"subtitle": "合規狀態概覽",
|
||||
"noData": "目前無合規資料"
|
||||
},
|
||||
"cost": {
|
||||
"title": "成本分析",
|
||||
"subtitle": "資源成本分析",
|
||||
"noData": "目前無成本資料"
|
||||
},
|
||||
"deployments": {
|
||||
"title": "部署管理",
|
||||
"subtitle": "部署紀錄",
|
||||
"name": "服務",
|
||||
"version": "版本",
|
||||
"status": "狀態",
|
||||
"time": "時間",
|
||||
"noDeployments": "目前無部署紀錄"
|
||||
},
|
||||
"help": {
|
||||
"title": "說明",
|
||||
"subtitle": "系統資訊與說明",
|
||||
"version": "版本資訊",
|
||||
"appVersion": "應用程式版本",
|
||||
"platform": "平台",
|
||||
"docs": "文件",
|
||||
"docsDescription": "查閱完整說明文件請造訪 AWOOOI Docs"
|
||||
},
|
||||
"security": {
|
||||
"title": "安全",
|
||||
"subtitle": "安全事件概覽",
|
||||
"noData": "目前無安全事件"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "工單",
|
||||
"subtitle": "工單列表",
|
||||
"id": "工單 ID",
|
||||
"title_col": "標題",
|
||||
"status": "狀態",
|
||||
"priority": "優先級",
|
||||
"noTickets": "目前無工單"
|
||||
},
|
||||
"users": {
|
||||
"title": "使用者",
|
||||
"subtitle": "使用者管理",
|
||||
"name": "姓名",
|
||||
"role": "角色",
|
||||
"status": "狀態",
|
||||
"noUsers": "目前無使用者資料"
|
||||
},
|
||||
"emptyState": {
|
||||
"noData": "--",
|
||||
"comingSoon": "資料尚未整合"
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,35 @@
|
||||
/**
|
||||
* APM Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空狀態頁(無 APM API,待 SignOz 整合)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { Activity } from 'lucide-react'
|
||||
|
||||
export default function ApmPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('apm')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={Activity}
|
||||
title="APM"
|
||||
description="應用性能監控,SignOz 整合中"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,44 @@
|
||||
/**
|
||||
* 應用 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空列表頁(無 apps API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { AppWindow } from 'lucide-react'
|
||||
|
||||
export default function AppsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('apps')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={AppWindow}
|
||||
title="應用"
|
||||
description="應用程式目錄"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,40 @@
|
||||
/**
|
||||
* 帳單 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空頁(無 billing API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { CreditCard } from 'lucide-react'
|
||||
|
||||
export default function BillingPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('billing')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={CreditCard}
|
||||
title="帳單"
|
||||
description="帳單與用量查詢"
|
||||
/>
|
||||
<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={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
{[t('currentMonth'), t('totalUsage')].map(label => (
|
||||
<div key={label} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '20px 24px', minWidth: 160 }}>
|
||||
<div style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace', marginBottom: 6 }}>{label}</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>--</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16, 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: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noData')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,31 @@
|
||||
/**
|
||||
* 合規 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空頁(無 compliance API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { ClipboardCheck } from 'lucide-react'
|
||||
|
||||
export default function CompliancePage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('compliance')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={ClipboardCheck}
|
||||
title="合規"
|
||||
description="合規檢查與稽核報告"
|
||||
/>
|
||||
<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: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noData')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,31 @@
|
||||
/**
|
||||
* 成本分析 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空頁(無 cost API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { DollarSign } from 'lucide-react'
|
||||
|
||||
export default function CostPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('cost')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={DollarSign}
|
||||
title="成本分析"
|
||||
description="雲端成本分析報表"
|
||||
/>
|
||||
<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: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noData')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,44 @@
|
||||
/**
|
||||
* 部署管理 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空列表頁(無 deployments API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { Package } from 'lucide-react'
|
||||
|
||||
export default function DeploymentsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('deployments')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={Package}
|
||||
title="部署管理"
|
||||
description="K8s 部署管理介面"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,55 @@
|
||||
/**
|
||||
* 說明文件 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為靜態系統版本資訊頁
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
|
||||
export default function HelpPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('help')
|
||||
const tb = useTranslations('brand')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={HelpCircle}
|
||||
title="說明文件"
|
||||
description="操作手冊與 FAQ"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* Version Info Card */}
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden', marginBottom: 16 }}>
|
||||
<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('version')}
|
||||
</div>
|
||||
<div style={{ padding: '16px 14px', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{[
|
||||
{ label: t('appVersion'), value: tb('version') },
|
||||
{ label: t('platform'), value: tb('name') },
|
||||
{ label: tb('environment'), value: tb('environment') },
|
||||
].map(row => (
|
||||
<div key={row.label} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '0.5px solid #f0eee7' }}>
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>{row.label}</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#141413' }}>{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Docs Card */}
|
||||
<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('docs')}
|
||||
</div>
|
||||
<div style={{ padding: '16px 14px', fontSize: 13, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>
|
||||
{t('docsDescription')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,95 @@
|
||||
/**
|
||||
* 通知設定 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實 UI,串接 /api/v1/notifications/channels
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { BellRing } from 'lucide-react'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
interface Channel {
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
|
||||
const statusColor = (s: string) => {
|
||||
if (s === 'active' || s === 'enabled') return '#4caf50'
|
||||
if (s === 'warning') return '#ff9800'
|
||||
if (s === 'error' || s === 'disabled') return '#f44336'
|
||||
return '#87867f'
|
||||
}
|
||||
|
||||
export default function NotificationsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('notifications')
|
||||
const tc = useTranslations('common')
|
||||
const [channels, setChannels] = useState<Channel[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetch(`${API_BASE}/api/v1/notifications/channels`)
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('not found')
|
||||
return r.json()
|
||||
})
|
||||
.then(data => { setChannels(Array.isArray(data) ? data : (data?.channels ?? [])) })
|
||||
.catch(() => { setChannels([]) })
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={BellRing}
|
||||
title="通知設定"
|
||||
description="通知偏好與訂閱設定"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{loading && (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{tc('loading')}</div>
|
||||
)}
|
||||
|
||||
{!loading && channels.length === 0 && (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noChannels')}</div>
|
||||
)}
|
||||
|
||||
{!loading && channels.length > 0 && (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('channel'), t('type'), 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>
|
||||
{channels.map((ch, i) => (
|
||||
<tr key={i} style={{ borderBottom: '0.5px solid #e0ddd4' }}>
|
||||
<td style={{ padding: '10px 14px', color: '#141413', fontWeight: 500 }}>{ch.name}</td>
|
||||
<td style={{ padding: '10px 14px', color: '#87867f' }}>{ch.type}</td>
|
||||
<td style={{ padding: '10px 14px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor(ch.status), display: 'inline-block' }} />
|
||||
<span style={{ color: '#141413' }}>{ch.status}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,105 @@
|
||||
/**
|
||||
* 報表 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實 UI,串接 /api/v1/stats/incident-summary + /api/v1/stats/resolution-stats
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
interface IncidentSummary {
|
||||
total?: number
|
||||
resolved?: number
|
||||
unresolved?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ResolutionStats {
|
||||
resolutionRate?: number
|
||||
avgResolutionTime?: string | number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: string | number }) {
|
||||
return (
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '20px 24px', minWidth: 140 }}>
|
||||
<div style={{ fontSize: 11, color: '#87867f', fontFamily: 'var(--font-body), monospace', marginBottom: 6 }}>{label}</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ReportsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('reports')
|
||||
const tc = useTranslations('common')
|
||||
const [summary, setSummary] = useState<IncidentSummary | null>(null)
|
||||
const [resolution, setResolution] = useState<ResolutionStats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setError(false)
|
||||
Promise.all([
|
||||
fetch(`${API_BASE}/api/v1/stats/incident-summary`).then(r => r.json()).catch(() => null),
|
||||
fetch(`${API_BASE}/api/v1/stats/resolution-stats`).then(r => r.json()).catch(() => null),
|
||||
])
|
||||
.then(([s, r]) => {
|
||||
setSummary(s)
|
||||
setResolution(r)
|
||||
})
|
||||
.catch(() => setError(true))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={FileText}
|
||||
title="報表"
|
||||
description="週期性報告系統"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{tc('loading')}</div>
|
||||
)}
|
||||
|
||||
{!loading && error && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#f44336', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('fetchError')}</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
{/* Incident Summary Section */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{t('incidentSummary')}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
|
||||
<StatCard label={t('total')} value={summary?.total ?? '--'} />
|
||||
<StatCard label={t('resolved')} value={summary?.resolved ?? '--'} />
|
||||
<StatCard label={t('unresolved')} value={summary?.unresolved ?? '--'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resolution Stats Section */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', display: 'inline-block' }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{t('resolutionStats')}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
|
||||
<StatCard label={t('resolutionRate')} value={resolution?.resolutionRate != null ? `${resolution.resolutionRate}%` : '--'} />
|
||||
<StatCard label={t('avgResolutionTime')} value={resolution?.avgResolutionTime ?? '--'} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,31 @@
|
||||
/**
|
||||
* 安全 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空頁(無 security API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { Shield } from 'lucide-react'
|
||||
|
||||
export default function SecurityPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('security')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={Shield}
|
||||
title="安全"
|
||||
description="安全事件與威脅偵測"
|
||||
/>
|
||||
<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: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noData')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,118 @@
|
||||
/**
|
||||
* 服務目錄 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實 UI,串接 /api/v1/dashboard
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { Server } from 'lucide-react'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
interface ServiceItem {
|
||||
name: string
|
||||
host: string
|
||||
status: string
|
||||
cpu?: number
|
||||
ram?: number
|
||||
}
|
||||
|
||||
const statusColor = (s: string) => {
|
||||
if (s === 'healthy' || s === 'running') return '#4caf50'
|
||||
if (s === 'warning') return '#ff9800'
|
||||
if (s === 'critical' || s === 'error') return '#f44336'
|
||||
return '#87867f'
|
||||
}
|
||||
|
||||
export default function ServicesPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('services')
|
||||
const tc = useTranslations('common')
|
||||
const [services, setServices] = useState<ServiceItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setError(false)
|
||||
fetch(`${API_BASE}/api/v1/dashboard`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const hosts: { name: string; services?: { name: string; status: string; cpu?: number; ram?: number }[] }[] = data?.hosts ?? []
|
||||
const list: ServiceItem[] = []
|
||||
hosts.forEach(h => {
|
||||
(h.services ?? []).forEach(s => {
|
||||
list.push({ name: s.name, host: h.name, status: s.status, cpu: s.cpu, ram: s.ram })
|
||||
})
|
||||
})
|
||||
setServices(list)
|
||||
})
|
||||
.catch(() => setError(true))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={Server}
|
||||
title="服務目錄"
|
||||
description="服務登錄中心"
|
||||
/>
|
||||
<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' }}>
|
||||
{/* Header */}
|
||||
<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>
|
||||
|
||||
{loading && (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
{tc('loading')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && error && (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#f44336', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
{t('fetchError')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && services.length === 0 && (
|
||||
<div style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
{t('noServices')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && services.length > 0 && (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('name'), t('host'), t('status'), t('cpu'), t('ram')].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>
|
||||
{services.map((s, i) => (
|
||||
<tr key={i} style={{ borderBottom: '0.5px solid #e0ddd4' }}>
|
||||
<td style={{ padding: '10px 14px', color: '#141413', fontWeight: 500 }}>{s.name}</td>
|
||||
<td style={{ padding: '10px 14px', color: '#87867f' }}>{s.host}</td>
|
||||
<td style={{ padding: '10px 14px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor(s.status), display: 'inline-block' }} />
|
||||
<span style={{ color: '#141413' }}>{s.status}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', color: '#141413' }}>{s.cpu != null ? `${s.cpu}%` : '--'}</td>
|
||||
<td style={{ padding: '10px 14px', color: '#141413' }}>{s.ram != null ? `${s.ram}%` : '--'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,44 @@
|
||||
/**
|
||||
* 工單 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空列表頁(無 tickets API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { Ticket } from 'lucide-react'
|
||||
|
||||
export default function TicketsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('tickets')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={Ticket}
|
||||
title="工單"
|
||||
description="工單系統整合中"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,101 @@
|
||||
/**
|
||||
* 拓撲圖 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實 UI,串接 /api/v1/dashboard hosts
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { GitBranch } from 'lucide-react'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
interface HostItem {
|
||||
name: string
|
||||
status: string
|
||||
cpu?: number
|
||||
ram?: number
|
||||
services?: { name: string; status: string }[]
|
||||
}
|
||||
|
||||
const statusColor = (s: string) => {
|
||||
if (s === 'healthy' || s === 'running') return '#4caf50'
|
||||
if (s === 'warning') return '#ff9800'
|
||||
if (s === 'critical' || s === 'error') return '#f44336'
|
||||
return '#87867f'
|
||||
}
|
||||
|
||||
export default function TopologyPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('topology')
|
||||
const tc = useTranslations('common')
|
||||
const [hosts, setHosts] = useState<HostItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setError(false)
|
||||
fetch(`${API_BASE}/api/v1/dashboard`)
|
||||
.then(r => r.json())
|
||||
.then(data => { setHosts(data?.hosts ?? []) })
|
||||
.catch(() => setError(true))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={GitBranch}
|
||||
title="拓撲圖"
|
||||
description="服務依賴拓撲,開發中"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{tc('loading')}</div>
|
||||
)}
|
||||
{!loading && error && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#f44336', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('fetchError')}</div>
|
||||
)}
|
||||
{!loading && !error && hosts.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noHosts')}</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && hosts.length > 0 && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 16 }}>
|
||||
{hosts.map((host, i) => (
|
||||
<div key={i} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4', background: '#faf9f3' }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor(host.status), display: 'inline-block' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.name}</span>
|
||||
</div>
|
||||
<div style={{ padding: '12px 14px', display: 'flex', gap: 16 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>{t('cpu')}</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.cpu != null ? `${host.cpu}%` : '--'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace' }}>{t('ram')}</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: '#141413', fontFamily: 'var(--font-body), monospace' }}>{host.ram != null ? `${host.ram}%` : '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{(host.services ?? []).length > 0 && (
|
||||
<div style={{ padding: '0 14px 12px' }}>
|
||||
<div style={{ fontSize: 10, color: '#87867f', fontFamily: 'var(--font-body), monospace', marginBottom: 6 }}>{t('services')}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{(host.services ?? []).map((svc, j) => (
|
||||
<span key={j} style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, border: '0.5px solid #e0ddd4', color: '#141413', background: '#faf9f3', fontFamily: 'var(--font-body), monospace' }}>
|
||||
<span style={{ display: 'inline-block', width: 5, height: 5, borderRadius: '50%', background: statusColor(svc.status), marginRight: 4, verticalAlign: 'middle' }} />
|
||||
{svc.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,44 @@
|
||||
/**
|
||||
* 使用者管理 Page
|
||||
* @created 2026-04-01 ogt - 路由佔位 (awaiting implementation)
|
||||
* @updated 2026-04-02 ogt - 升級為真實空列表頁(無 users API)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { ComingSoon } from '@/components/layout'
|
||||
import { Users } from 'lucide-react'
|
||||
|
||||
export default function UsersPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('users')
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<ComingSoon
|
||||
icon={Users}
|
||||
title="使用者管理"
|
||||
description="RBAC 使用者與角色管理"
|
||||
/>
|
||||
<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>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#faf9f3' }}>
|
||||
{[t('name'), t('role'), 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>
|
||||
<tr>
|
||||
<td colSpan={3} style={{ padding: '32px', textAlign: 'center', color: '#87867f', fontFamily: 'var(--font-body), monospace', fontSize: 13 }}>{t('noUsers')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user