From e93a50a4b42f2995c19f7ad9e3b26660ffa4a7ce Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 2 Apr 2026 23:49:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(pages):=20=E5=85=A8=E9=83=A8=20ComingSoon?= =?UTF-8?q?=20=E9=A0=81=E9=9D=A2=E5=8D=87=E7=B4=9A=E7=82=BA=E7=9C=9F?= =?UTF-8?q?=E5=AF=A6=20UI=20=E2=80=94=20=E4=B8=B2=E6=8E=A5=E7=9C=9F?= =?UTF-8?q?=E5=AF=A6=20API=20/=20=E7=A9=BA=E7=8B=80=E6=85=8B=E9=A0=81?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/messages/en.json | 117 ++++++++++++++++++ apps/web/messages/zh-TW.json | 117 ++++++++++++++++++ apps/web/src/app/[locale]/apm/page.tsx | 29 +++-- apps/web/src/app/[locale]/apps/page.tsx | 38 ++++-- apps/web/src/app/[locale]/billing/page.tsx | 34 +++-- apps/web/src/app/[locale]/compliance/page.tsx | 25 ++-- apps/web/src/app/[locale]/cost/page.tsx | 25 ++-- .../web/src/app/[locale]/deployments/page.tsx | 38 ++++-- apps/web/src/app/[locale]/help/page.tsx | 49 ++++++-- .../src/app/[locale]/notifications/page.tsx | 89 +++++++++++-- apps/web/src/app/[locale]/reports/page.tsx | 99 +++++++++++++-- apps/web/src/app/[locale]/security/page.tsx | 25 ++-- apps/web/src/app/[locale]/services/page.tsx | 112 +++++++++++++++-- apps/web/src/app/[locale]/tickets/page.tsx | 38 ++++-- apps/web/src/app/[locale]/topology/page.tsx | 95 ++++++++++++-- apps/web/src/app/[locale]/users/page.tsx | 38 ++++-- 16 files changed, 870 insertions(+), 98 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 384e2f39..b635b5e6 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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" } } \ No newline at end of file diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5742acbd..5c604851 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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": "資料尚未整合" } } \ 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 32d4dbe9..4448297a 100644 --- a/apps/web/src/app/[locale]/apm/page.tsx +++ b/apps/web/src/app/[locale]/apm/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+
+
+
{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 0c84b851..bd143a4b 100644 --- a/apps/web/src/app/[locale]/apps/page.tsx +++ b/apps/web/src/app/[locale]/apps/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+ + + + {[t('name'), t('version'), t('status')].map(col => ( + + ))} + + + + + + + +
{col}
{t('noApps')}
+
+
) } diff --git a/apps/web/src/app/[locale]/billing/page.tsx b/apps/web/src/app/[locale]/billing/page.tsx index ab972861..7e591ce1 100644 --- a/apps/web/src/app/[locale]/billing/page.tsx +++ b/apps/web/src/app/[locale]/billing/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+ {[t('currentMonth'), t('totalUsage')].map(label => ( +
+
{label}
+
--
+
+ ))} +
+ +
+
+ + {t('title')} +
+
{t('noData')}
+
+
) } diff --git a/apps/web/src/app/[locale]/compliance/page.tsx b/apps/web/src/app/[locale]/compliance/page.tsx index 3248d471..5bb080a3 100644 --- a/apps/web/src/app/[locale]/compliance/page.tsx +++ b/apps/web/src/app/[locale]/compliance/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+
{t('noData')}
+
+
) } diff --git a/apps/web/src/app/[locale]/cost/page.tsx b/apps/web/src/app/[locale]/cost/page.tsx index 0ae8c04c..4961d31d 100644 --- a/apps/web/src/app/[locale]/cost/page.tsx +++ b/apps/web/src/app/[locale]/cost/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+
{t('noData')}
+
+
) } diff --git a/apps/web/src/app/[locale]/deployments/page.tsx b/apps/web/src/app/[locale]/deployments/page.tsx index e1197415..289fd994 100644 --- a/apps/web/src/app/[locale]/deployments/page.tsx +++ b/apps/web/src/app/[locale]/deployments/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+ + + + {[t('name'), t('version'), t('status'), t('time')].map(col => ( + + ))} + + + + + + + +
{col}
{t('noDeployments')}
+
+
) } diff --git a/apps/web/src/app/[locale]/help/page.tsx b/apps/web/src/app/[locale]/help/page.tsx index ac59b47f..d22cefa9 100644 --- a/apps/web/src/app/[locale]/help/page.tsx +++ b/apps/web/src/app/[locale]/help/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ + {/* Version Info Card */} +
+
+ + {t('version')} +
+
+ {[ + { label: t('appVersion'), value: tb('version') }, + { label: t('platform'), value: tb('name') }, + { label: tb('environment'), value: tb('environment') }, + ].map(row => ( +
+ {row.label} + {row.value} +
+ ))} +
+
+ + {/* Docs Card */} +
+
+ + {t('docs')} +
+
+ {t('docsDescription')} +
+
+
) } diff --git a/apps/web/src/app/[locale]/notifications/page.tsx b/apps/web/src/app/[locale]/notifications/page.tsx index 9b0200d6..f73a6e86 100644 --- a/apps/web/src/app/[locale]/notifications/page.tsx +++ b/apps/web/src/app/[locale]/notifications/page.tsx @@ -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([]) + 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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+ + {loading && ( +
{tc('loading')}
+ )} + + {!loading && channels.length === 0 && ( +
{t('noChannels')}
+ )} + + {!loading && channels.length > 0 && ( + + + + {[t('channel'), t('type'), t('status')].map(col => ( + + ))} + + + + {channels.map((ch, i) => ( + + + + + + ))} + +
{col}
{ch.name}{ch.type} + + + {ch.status} + +
+ )} +
+
) } diff --git a/apps/web/src/app/[locale]/reports/page.tsx b/apps/web/src/app/[locale]/reports/page.tsx index 9f1969f9..d23a3846 100644 --- a/apps/web/src/app/[locale]/reports/page.tsx +++ b/apps/web/src/app/[locale]/reports/page.tsx @@ -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 ( +
+
{label}
+
{value}
+
+ ) +} export default function ReportsPage({ params }: { params: { locale: string } }) { + const t = useTranslations('reports') + const tc = useTranslations('common') + const [summary, setSummary] = useState(null) + const [resolution, setResolution] = useState(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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ + {loading && ( +
{tc('loading')}
+ )} + + {!loading && error && ( +
{t('fetchError')}
+ )} + + {!loading && !error && ( + <> + {/* Incident Summary Section */} +
+
+ + {t('incidentSummary')} +
+
+ + + +
+
+ + {/* Resolution Stats Section */} +
+
+ + {t('resolutionStats')} +
+
+ + +
+
+ + )} +
) } diff --git a/apps/web/src/app/[locale]/security/page.tsx b/apps/web/src/app/[locale]/security/page.tsx index 600fdb66..8c10979c 100644 --- a/apps/web/src/app/[locale]/security/page.tsx +++ b/apps/web/src/app/[locale]/security/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+
{t('noData')}
+
+
) } diff --git a/apps/web/src/app/[locale]/services/page.tsx b/apps/web/src/app/[locale]/services/page.tsx index 722a6786..6242366f 100644 --- a/apps/web/src/app/[locale]/services/page.tsx +++ b/apps/web/src/app/[locale]/services/page.tsx @@ -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([]) + 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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+ {/* Header */} +
+ + {t('title')} +
+ + {loading && ( +
+ {tc('loading')} +
+ )} + + {!loading && error && ( +
+ {t('fetchError')} +
+ )} + + {!loading && !error && services.length === 0 && ( +
+ {t('noServices')} +
+ )} + + {!loading && !error && services.length > 0 && ( + + + + {[t('name'), t('host'), t('status'), t('cpu'), t('ram')].map(col => ( + + ))} + + + + {services.map((s, i) => ( + + + + + + + + ))} + +
{col}
{s.name}{s.host} + + + {s.status} + + {s.cpu != null ? `${s.cpu}%` : '--'}{s.ram != null ? `${s.ram}%` : '--'}
+ )} +
+
) } diff --git a/apps/web/src/app/[locale]/tickets/page.tsx b/apps/web/src/app/[locale]/tickets/page.tsx index ad62ef18..450face4 100644 --- a/apps/web/src/app/[locale]/tickets/page.tsx +++ b/apps/web/src/app/[locale]/tickets/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+ + + + {[t('id'), t('title_col'), t('status'), t('priority')].map(col => ( + + ))} + + + + + + + +
{col}
{t('noTickets')}
+
+
) } diff --git a/apps/web/src/app/[locale]/topology/page.tsx b/apps/web/src/app/[locale]/topology/page.tsx index ab4fc8d3..0ba18745 100644 --- a/apps/web/src/app/[locale]/topology/page.tsx +++ b/apps/web/src/app/[locale]/topology/page.tsx @@ -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([]) + 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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ + {loading && ( +
{tc('loading')}
+ )} + {!loading && error && ( +
{t('fetchError')}
+ )} + {!loading && !error && hosts.length === 0 && ( +
{t('noHosts')}
+ )} + + {!loading && !error && hosts.length > 0 && ( +
+ {hosts.map((host, i) => ( +
+
+ + {host.name} +
+
+
+
{t('cpu')}
+
{host.cpu != null ? `${host.cpu}%` : '--'}
+
+
+
{t('ram')}
+
{host.ram != null ? `${host.ram}%` : '--'}
+
+
+ {(host.services ?? []).length > 0 && ( +
+
{t('services')}
+
+ {(host.services ?? []).map((svc, j) => ( + + + {svc.name} + + ))} +
+
+ )} +
+ ))} +
+ )} +
) } diff --git a/apps/web/src/app/[locale]/users/page.tsx b/apps/web/src/app/[locale]/users/page.tsx index a8645244..19b53a85 100644 --- a/apps/web/src/app/[locale]/users/page.tsx +++ b/apps/web/src/app/[locale]/users/page.tsx @@ -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 ( - +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+
+ + {t('title')} +
+ + + + {[t('name'), t('role'), t('status')].map(col => ( + + ))} + + + + + + + +
{col}
{t('noUsers')}
+
+
) }