From cdf60bb8c8e28a6a163ec86c40daf6932de20e25 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 1 Apr 2026 22:59:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(pages):=20=E8=A3=9C=E5=85=A8=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=BC=BA=E5=A4=B1=E8=B7=AF=E7=94=B1=E9=A0=81=E9=9D=A2?= =?UTF-8?q?=20(15=20=E5=80=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 實作頁面: - /monitoring: 黃金指標 + 主機狀態 + 服務健康表格 - /alerts: P0-P3 分組 + 嚴重度統計 (已送) - /terminal: OmniTerminal 路由 (已送) - /authorizations: LiveApprovalPanel (已送) 佔位頁面 (ComingSoon): - /apm /topology /security /compliance - /auto-repair /deployments /tickets /cost /reports - /apps /services /users /notifications /billing /help 新增 ComingSoon 元件 (極簡動點動畫) Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/[locale]/apm/page.tsx | 22 ++ apps/web/src/app/[locale]/apps/page.tsx | 22 ++ .../web/src/app/[locale]/auto-repair/page.tsx | 22 ++ apps/web/src/app/[locale]/billing/page.tsx | 22 ++ apps/web/src/app/[locale]/compliance/page.tsx | 22 ++ apps/web/src/app/[locale]/cost/page.tsx | 22 ++ .../web/src/app/[locale]/deployments/page.tsx | 22 ++ apps/web/src/app/[locale]/help/page.tsx | 22 ++ apps/web/src/app/[locale]/monitoring/page.tsx | 265 ++++++++++++++++++ .../src/app/[locale]/notifications/page.tsx | 22 ++ apps/web/src/app/[locale]/reports/page.tsx | 22 ++ apps/web/src/app/[locale]/security/page.tsx | 22 ++ apps/web/src/app/[locale]/services/page.tsx | 22 ++ apps/web/src/app/[locale]/tickets/page.tsx | 22 ++ apps/web/src/app/[locale]/topology/page.tsx | 22 ++ apps/web/src/app/[locale]/users/page.tsx | 22 ++ .../web/src/components/layout/coming-soon.tsx | 52 ++++ apps/web/src/components/layout/index.ts | 1 + 18 files changed, 648 insertions(+) create mode 100644 apps/web/src/app/[locale]/apm/page.tsx create mode 100644 apps/web/src/app/[locale]/apps/page.tsx create mode 100644 apps/web/src/app/[locale]/auto-repair/page.tsx create mode 100644 apps/web/src/app/[locale]/billing/page.tsx create mode 100644 apps/web/src/app/[locale]/compliance/page.tsx create mode 100644 apps/web/src/app/[locale]/cost/page.tsx create mode 100644 apps/web/src/app/[locale]/deployments/page.tsx create mode 100644 apps/web/src/app/[locale]/help/page.tsx create mode 100644 apps/web/src/app/[locale]/monitoring/page.tsx create mode 100644 apps/web/src/app/[locale]/notifications/page.tsx create mode 100644 apps/web/src/app/[locale]/reports/page.tsx create mode 100644 apps/web/src/app/[locale]/security/page.tsx create mode 100644 apps/web/src/app/[locale]/services/page.tsx create mode 100644 apps/web/src/app/[locale]/tickets/page.tsx create mode 100644 apps/web/src/app/[locale]/topology/page.tsx create mode 100644 apps/web/src/app/[locale]/users/page.tsx create mode 100644 apps/web/src/components/layout/coming-soon.tsx diff --git a/apps/web/src/app/[locale]/apm/page.tsx b/apps/web/src/app/[locale]/apm/page.tsx new file mode 100644 index 000000000..32d4dbe9a --- /dev/null +++ b/apps/web/src/app/[locale]/apm/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * APM Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Activity } from 'lucide-react' + +export default function ApmPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/apps/page.tsx b/apps/web/src/app/[locale]/apps/page.tsx new file mode 100644 index 000000000..0c84b851d --- /dev/null +++ b/apps/web/src/app/[locale]/apps/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 應用 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { AppWindow } from 'lucide-react' + +export default function AppsPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/auto-repair/page.tsx b/apps/web/src/app/[locale]/auto-repair/page.tsx new file mode 100644 index 000000000..159902588 --- /dev/null +++ b/apps/web/src/app/[locale]/auto-repair/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 自動修復 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Wrench } from 'lucide-react' + +export default function AutoRepairPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/billing/page.tsx b/apps/web/src/app/[locale]/billing/page.tsx new file mode 100644 index 000000000..ab9728614 --- /dev/null +++ b/apps/web/src/app/[locale]/billing/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 帳單 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { CreditCard } from 'lucide-react' + +export default function BillingPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/compliance/page.tsx b/apps/web/src/app/[locale]/compliance/page.tsx new file mode 100644 index 000000000..3248d4715 --- /dev/null +++ b/apps/web/src/app/[locale]/compliance/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 合規 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { ClipboardCheck } from 'lucide-react' + +export default function CompliancePage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/cost/page.tsx b/apps/web/src/app/[locale]/cost/page.tsx new file mode 100644 index 000000000..0ae8c04c7 --- /dev/null +++ b/apps/web/src/app/[locale]/cost/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 成本分析 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { DollarSign } from 'lucide-react' + +export default function CostPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/deployments/page.tsx b/apps/web/src/app/[locale]/deployments/page.tsx new file mode 100644 index 000000000..e11974155 --- /dev/null +++ b/apps/web/src/app/[locale]/deployments/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 部署管理 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Package } from 'lucide-react' + +export default function DeploymentsPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/help/page.tsx b/apps/web/src/app/[locale]/help/page.tsx new file mode 100644 index 000000000..ac59b47fe --- /dev/null +++ b/apps/web/src/app/[locale]/help/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 說明文件 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { HelpCircle } from 'lucide-react' + +export default function HelpPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/monitoring/page.tsx b/apps/web/src/app/[locale]/monitoring/page.tsx new file mode 100644 index 000000000..2f2b60328 --- /dev/null +++ b/apps/web/src/app/[locale]/monitoring/page.tsx @@ -0,0 +1,265 @@ +'use client' + +/** + * Monitoring Page - 服務監控 + * =========================== + * 黃金指標 (Gold Metrics) + 四主機狀態 + 服務健康 + * 資料來源: + * GET /api/v1/metrics/gold + * GET /api/v1/dashboard/hosts + * GET /api/v1/dashboard (SSE 快照) + * + * @created 2026-04-01 ogt + */ + +import { useState, useEffect, useCallback, useRef } from 'react' +import { useTranslations } from 'next-intl' +import { AppLayout } from '@/components/layout' +import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics' +import { GlobalPulseChart } from '@/components/charts/global-pulse-chart' +import { HostGrid } from '@/components/infra/host-grid' +import { cn } from '@/lib/utils' +import { + Monitor, RefreshCw, AlertCircle, + CheckCircle2, XCircle, Minus, +} from 'lucide-react' + +// ============================================================================= +// Types +// ============================================================================= + +interface ServiceHealth { + name: string + status: 'healthy' | 'warning' | 'critical' | 'unknown' + latency_ms: number | null + uptime_pct: number | null + last_checked: string +} + +interface DashboardResponse { + healthy_count: number + warning_count: number + critical_count: number + total_count: number + services: ServiceHealth[] + timestamp: string +} + +// ============================================================================= +// Helpers +// ============================================================================= + +const getApiBaseUrl = () => { + if (typeof window === 'undefined') return '' + const url = process.env.NEXT_PUBLIC_API_URL + if (!url) console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL') // eslint-disable-line no-console + return url ?? '' +} + +const STATUS_ICON = { + healthy: , + warning: , + critical: , + unknown: , +} + +const STATUS_BADGE = { + healthy: 'bg-status-healthy/10 text-status-healthy border-status-healthy/20', + warning: 'bg-status-warning/10 text-status-warning border-status-warning/20', + critical: 'bg-status-critical/10 text-status-critical border-status-critical/20', + unknown: 'bg-nothing-gray-100 text-nothing-gray-500 border-nothing-gray-200', +} + +// ============================================================================= +// Sub-component: Health Summary +// ============================================================================= + +function HealthSummary({ data }: { data: DashboardResponse }) { + const total = data.total_count || 1 + return ( +
+ {[ + { label: '正常', count: data.healthy_count, color: 'text-status-healthy', bg: 'bg-status-healthy/10' }, + { label: '警告', count: data.warning_count, color: 'text-status-warning', bg: 'bg-status-warning/10' }, + { label: '異常', count: data.critical_count, color: 'text-status-critical', bg: 'bg-status-critical/10' }, + ].map(item => ( +
+
+ {item.count} +
+
+ {item.label} · {Math.round(item.count / total * 100)}% +
+
+ ))} +
+ ) +} + +// ============================================================================= +// Page +// ============================================================================= + +export default function MonitoringPage({ params }: { params: { locale: string } }) { + const t = useTranslations() + + const { metrics, isLoading: metricsLoading } = useGlobalPulseMetrics({ + pollInterval: 30000, + enablePolling: true, + }) + + const [dashboard, setDashboard] = useState(null) + const [dashLoading, setDashLoading] = useState(true) + const [dashError, setDashError] = useState(null) + const abortRef = useRef(null) + + const fetchDashboard = useCallback(async () => { + const base = getApiBaseUrl() + if (!base) return + + abortRef.current?.abort() + const ctrl = new AbortController() + abortRef.current = ctrl + + setDashLoading(true) + setDashError(null) + try { + const res = await fetch(`${base}/api/v1/dashboard`, { signal: ctrl.signal }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + setDashboard(await res.json()) + } catch (e) { + if (e instanceof Error && e.name === 'AbortError') return + setDashError(e instanceof Error ? e.message : 'Unknown error') + } finally { + setDashLoading(false) + } + }, []) + + useEffect(() => { + fetchDashboard() + const id = setInterval(fetchDashboard, 30000) + return () => { + clearInterval(id) + abortRef.current?.abort() + } + }, [fetchDashboard]) + + const isLoading = metricsLoading || dashLoading + + return ( + + {/* Header */} +
+
+

+ + {t('nav.monitoring')} +

+

+ {t('alerts.autoRefresh', { seconds: 30 })} +

+
+ +
+ + {/* Error */} + {dashError && ( +
+ + {dashError} +
+ )} + + {/* Health Summary */} + {dashboard && } + + {/* Gold Metrics */} +
+

+ 黃金指標 (Gold Metrics) +

+ {metricsLoading && metrics.length === 0 ? ( +
+ +
+ ) : ( + + )} +
+ + {/* Host Grid */} +
+

+ 主機狀態 (Four-Host Architecture) +

+ +
+ + {/* Service Table */} + {dashboard && dashboard.services && dashboard.services.length > 0 && ( +
+

+ 服務清單 +

+
+ + + + {['服務名稱', '狀態', '延遲', '可用率', '最後檢查'].map(h => ( + + ))} + + + + {dashboard.services.map((svc, i) => ( + + + + + + + + ))} + +
+ {h} +
+ {svc.name} + + + {STATUS_ICON[svc.status]} + {svc.status} + + + {svc.latency_ms != null ? `${svc.latency_ms}ms` : '--'} + + {svc.uptime_pct != null ? `${svc.uptime_pct.toFixed(2)}%` : '--'} + + {svc.last_checked + ? new Date(svc.last_checked).toLocaleTimeString() + : '--'} +
+
+
+ )} +
+ ) +} diff --git a/apps/web/src/app/[locale]/notifications/page.tsx b/apps/web/src/app/[locale]/notifications/page.tsx new file mode 100644 index 000000000..9b0200d6e --- /dev/null +++ b/apps/web/src/app/[locale]/notifications/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 通知設定 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { BellRing } from 'lucide-react' + +export default function NotificationsPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/reports/page.tsx b/apps/web/src/app/[locale]/reports/page.tsx new file mode 100644 index 000000000..9f1969f98 --- /dev/null +++ b/apps/web/src/app/[locale]/reports/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 報表 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { FileText } from 'lucide-react' + +export default function ReportsPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/security/page.tsx b/apps/web/src/app/[locale]/security/page.tsx new file mode 100644 index 000000000..600fdb666 --- /dev/null +++ b/apps/web/src/app/[locale]/security/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 安全 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Shield } from 'lucide-react' + +export default function SecurityPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/services/page.tsx b/apps/web/src/app/[locale]/services/page.tsx new file mode 100644 index 000000000..722a6786c --- /dev/null +++ b/apps/web/src/app/[locale]/services/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 服務目錄 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Server } from 'lucide-react' + +export default function ServicesPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/tickets/page.tsx b/apps/web/src/app/[locale]/tickets/page.tsx new file mode 100644 index 000000000..ad62ef18e --- /dev/null +++ b/apps/web/src/app/[locale]/tickets/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 工單 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Ticket } from 'lucide-react' + +export default function TicketsPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/topology/page.tsx b/apps/web/src/app/[locale]/topology/page.tsx new file mode 100644 index 000000000..ab4fc8d3e --- /dev/null +++ b/apps/web/src/app/[locale]/topology/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 拓撲圖 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { GitBranch } from 'lucide-react' + +export default function TopologyPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/app/[locale]/users/page.tsx b/apps/web/src/app/[locale]/users/page.tsx new file mode 100644 index 000000000..a86452449 --- /dev/null +++ b/apps/web/src/app/[locale]/users/page.tsx @@ -0,0 +1,22 @@ +'use client' + +/** + * 使用者管理 Page + * @created 2026-04-01 ogt - 路由佔位 (awaiting implementation) + */ + +import { AppLayout } from '@/components/layout' +import { ComingSoon } from '@/components/layout' +import { Users } from 'lucide-react' + +export default function UsersPage({ params }: { params: { locale: string } }) { + return ( + + + + ) +} diff --git a/apps/web/src/components/layout/coming-soon.tsx b/apps/web/src/components/layout/coming-soon.tsx new file mode 100644 index 000000000..f8c26fdac --- /dev/null +++ b/apps/web/src/components/layout/coming-soon.tsx @@ -0,0 +1,52 @@ +'use client' + +/** + * ComingSoon - 統一的「建設中」佔位元件 + * ====================================== + * 用於尚未實作的路由頁面 + * Nothing.tech 極簡風格 + * + * @created 2026-04-01 ogt + */ + +import { type LucideIcon } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface ComingSoonProps { + icon?: LucideIcon + title: string + description?: string + className?: string +} + +export function ComingSoon({ icon: Icon, title, description, className }: ComingSoonProps) { + return ( +
+ {Icon && ( +
+ +
+ )} +

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ {[0, 1, 2].map(i => ( +
+ ))} +
+
+ ) +} diff --git a/apps/web/src/components/layout/index.ts b/apps/web/src/components/layout/index.ts index cd3f87521..56018d367 100644 --- a/apps/web/src/components/layout/index.ts +++ b/apps/web/src/components/layout/index.ts @@ -7,3 +7,4 @@ export { Sidebar } from './sidebar' export { Header } from './header' export { AppLayout } from './app-layout' +export { ComingSoon } from './coming-soon'