diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 94e719fb0..e0b36d922 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -127,8 +127,8 @@ "boundaryTitle": "不可誤讀合約", "boundaryDetail": "此頁是監控與決策視圖,不是 runtime 授權;任何會改正式環境的動作仍需獨立批准與驗證。", "drilldownEyebrow": "08 / Drilldown", - "drilldownTitle": "細節分頁", - "drilldownDetail": "保留原本監控、APM、錯誤、應用與服務目錄,作為總圖後的細節查證入口。" + "drilldownTitle": "下鑽入口", + "drilldownDetail": "總覽頁只保留態勢、拓樸、訊號與缺口;監控、APM、錯誤、應用與服務目錄改用獨立入口查證。" }, "assetLedger": { "scopeLabel": "全域資產", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 94e719fb0..e0b36d922 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -127,8 +127,8 @@ "boundaryTitle": "不可誤讀合約", "boundaryDetail": "此頁是監控與決策視圖,不是 runtime 授權;任何會改正式環境的動作仍需獨立批准與驗證。", "drilldownEyebrow": "08 / Drilldown", - "drilldownTitle": "細節分頁", - "drilldownDetail": "保留原本監控、APM、錯誤、應用與服務目錄,作為總圖後的細節查證入口。" + "drilldownTitle": "下鑽入口", + "drilldownDetail": "總覽頁只保留態勢、拓樸、訊號與缺口;監控、APM、錯誤、應用與服務目錄改用獨立入口查證。" }, "assetLedger": { "scopeLabel": "全域資產", diff --git a/apps/web/src/app/[locale]/automation/page.tsx b/apps/web/src/app/[locale]/automation/page.tsx index 6ba0d3472..35380c517 100644 --- a/apps/web/src/app/[locale]/automation/page.tsx +++ b/apps/web/src/app/[locale]/automation/page.tsx @@ -3,30 +3,61 @@ /** * 自動化 (/automation) — Sprint 5 整合頁面 * 整合: 自動修復 + 神經指揮 + Drift 偵測 - * 全部使用 Panel 元件 (無雙重 AppLayout) + * 2026-06-26 IA reset: 不再用頁面級分頁承載主導航,改為入口卡片。 * 建立時間: 2026-04-08 (台北時區) * 更新時間: 2026-04-09 — 全部 Tab Panel 抽取完成 */ +import Link from 'next/link' import { useTranslations } from 'next-intl' +import { + Activity, + GitBranch, + Wrench, +} from 'lucide-react' import { AppLayout } from '@/components/layout' -import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' -import { AutoRepairPanel } from '@/components/panels/AutoRepairPanel' -import { NeuralCommandPanel } from '@/components/panels/NeuralCommandPanel' -import { DriftPanel } from '@/components/panels/DriftPanel' export default function AutomationPage({ params }: { params: { locale: string } }) { const t = useTranslations('nav') - const tabs: TabConfig[] = [ - { id: 'repair', label: t('autoRepair'), content: }, - { id: 'neural', label: t('neuralCommand'), content: }, - { id: 'drift', label: t('drift'), content: }, + const links = [ + { id: 'auto-repair', label: t('autoRepair'), href: `/${params.locale}/auto-repair`, Icon: Wrench }, + { id: 'neural-command', label: t('neuralCommand'), href: `/${params.locale}/neural-command`, Icon: Activity }, + { id: 'drift', label: t('drift'), href: `/${params.locale}/drift`, Icon: GitBranch }, ] return ( - +
+
+
+

+ {t('knowledgeAutomation')} +

+
+
+ {links.map(({ id, label, href, Icon }) => ( + +
+ + + + {href.replace(`/${params.locale}`, '')} + +
+
+ {label} +
+ + ))} +
+
+
) } diff --git a/apps/web/src/app/[locale]/awooop/layout.tsx b/apps/web/src/app/[locale]/awooop/layout.tsx index 999d00e40..5c4f75003 100644 --- a/apps/web/src/app/[locale]/awooop/layout.tsx +++ b/apps/web/src/app/[locale]/awooop/layout.tsx @@ -9,6 +9,7 @@ import { AppLayout } from "@/components/layout"; import { usePathname } from "@/i18n/routing"; +import { AWOOOP_WORKFLOW_ITEMS } from "@/lib/navigation/product-ia"; import { BrainCircuit, CalendarDays, @@ -18,38 +19,6 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; -// ============================================================================= -// 導航設定 -// ============================================================================= - -const navItems = [ - { - labelKey: "overview", - href: "/awooop" as const, - exact: true, - }, - { - labelKey: "workItems", - href: "/awooop/work-items" as const, - }, - { - labelKey: "runs", - href: "/awooop/runs" as const, - }, - { - labelKey: "approvals", - href: "/awooop/approvals" as const, - }, - { - labelKey: "contracts", - href: "/awooop/contracts" as const, - }, - { - labelKey: "tenants", - href: "/awooop/tenants" as const, - }, -]; - // ============================================================================= // AwoooP Console 佈局 // ============================================================================= @@ -63,10 +32,10 @@ export default function AwoooPLayout({ }) { const pathname = usePathname(); const t = useTranslations("awooop.shell"); - const activeItem = navItems.find((item) => ( + const activeItem = AWOOOP_WORKFLOW_ITEMS.find((item) => ( pathname === item.href || (!item.exact && pathname?.startsWith(item.href + "/")) - )) ?? navItems[0]; + )) ?? AWOOOP_WORKFLOW_ITEMS[0]; return ( diff --git a/apps/web/src/app/[locale]/governance/page.tsx b/apps/web/src/app/[locale]/governance/page.tsx index 489589670..6706a8b24 100644 --- a/apps/web/src/app/[locale]/governance/page.tsx +++ b/apps/web/src/app/[locale]/governance/page.tsx @@ -14,9 +14,15 @@ */ import { useTranslations } from 'next-intl' -import { ShieldCheck } from 'lucide-react' +import Link from 'next/link' +import { + Activity, + BrainCircuit, + ClipboardList, + Radar, + ShieldCheck, +} from 'lucide-react' import { AppLayout } from '@/components/layout' -import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge' import { GlassCard } from '@/components/ui/glass-card' import { SloTab } from './tabs/slo-tab' @@ -34,15 +40,15 @@ export default function GovernancePage({ }) { const t = useTranslations('governance') - const tabs: TabConfig[] = [ - { id: 'slo', label: t('tabs.slo'), content: }, - { id: 'events', label: t('tabs.events'), content: }, - { id: 'queue', label: t('tabs.queue'), content: }, - { id: 'agent-market', label: t('tabs.agentMarket'), content: }, - { id: 'automation-inventory', label: t('tabs.automationInventory'), content: }, + const governanceSections = [ + { id: 'slo', order: '01', label: t('tabs.slo'), content: , Icon: Radar }, + { id: 'events', order: '02', label: t('tabs.events'), content: , Icon: Activity }, + { id: 'queue', order: '03', label: t('tabs.queue'), content: , Icon: ClipboardList }, + { id: 'agent-market', order: '04', label: t('tabs.agentMarket'), content: , Icon: BrainCircuit }, + { id: 'automation-inventory', order: '05', label: t('tabs.automationInventory'), content: , Icon: ShieldCheck }, ] const requestedTab = Array.isArray(searchParams?.tab) ? searchParams?.tab[0] : searchParams?.tab - const defaultTab = tabs.some(tab => tab.id === requestedTab) ? requestedTab : 'slo' + const activeSection = governanceSections.find(section => section.id === requestedTab) ?? governanceSections[0] return ( @@ -58,7 +64,36 @@ export default function GovernancePage({ - +
+ {governanceSections.map((section) => { + const selected = section.id === activeSection.id + const Icon = section.Icon + return ( + +
+ + + + {section.order} + +
+
+ {section.label} +
+ + ) + })} +
+ + {activeSection.content}
) } diff --git a/apps/web/src/app/[locale]/observability/page.tsx b/apps/web/src/app/[locale]/observability/page.tsx index 4c2f74cb6..2655e3d3d 100644 --- a/apps/web/src/app/[locale]/observability/page.tsx +++ b/apps/web/src/app/[locale]/observability/page.tsx @@ -4,10 +4,11 @@ * 可觀測性 (/observability) * ======================== * 首屏整合只讀 AI Agent deployment layout、監控合約矩陣與服務健康缺口, - * 下方保留既有監控 / APM / 錯誤 / 應用 / 服務目錄分頁作為鑽取細節。 + * 下方保留既有監控 / APM / 錯誤 / 應用 / 服務目錄獨立入口作為鑽取細節。 */ import { useEffect, useMemo, useState, type ReactNode } from 'react' +import Link from 'next/link' import { useTranslations } from 'next-intl' import { Activity, @@ -33,12 +34,6 @@ import { type LucideIcon, } from 'lucide-react' import { AppLayout } from '@/components/layout' -import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' -import { MonitoringPanel } from '@/components/panels/MonitoringPanel' -import { ApmPanel } from '@/components/panels/ApmPanel' -import { ErrorsPanel } from '@/components/panels/ErrorsPanel' -import { AppsPanel } from '@/components/panels/AppsPanel' -import { ServicesPanel } from '@/components/panels/ServicesPanel' import { apiClient, type AiAgentAutomationInventorySnapshot, @@ -476,31 +471,36 @@ export default function ObservabilityPage({ params }: { params: { locale: string }, ] - const tabs: TabConfig[] = [ + const drilldownLinks = [ { id: 'monitoring', label: nav('monitoring'), - content: , + href: `/${params.locale}/monitoring`, + Icon: Activity, }, { id: 'apm', label: nav('apm'), - content: , + href: `/${params.locale}/apm`, + Icon: Radar, }, { id: 'errors', label: nav('errors'), - content: , + href: `/${params.locale}/errors`, + Icon: AlertTriangle, }, { id: 'apps', label: nav('apps'), - content: , + href: `/${params.locale}/apps`, + Icon: Boxes, }, { id: 'services', label: nav('services'), - content: , + href: `/${params.locale}/services`, + Icon: Server, }, ] @@ -765,7 +765,27 @@ export default function ObservabilityPage({ params }: { params: { locale: string title={t('sections.drilldownTitle')} detail={t('sections.drilldownDetail')} /> - +
+ {drilldownLinks.map(({ id, label, href, Icon }) => ( + +
+ + + + {href.replace(`/${params.locale}`, '')} + +
+
+ {label} +
+ + ))} +
diff --git a/apps/web/src/app/[locale]/operations/page.tsx b/apps/web/src/app/[locale]/operations/page.tsx index d104ccf17..098c0fdab 100644 --- a/apps/web/src/app/[locale]/operations/page.tsx +++ b/apps/web/src/app/[locale]/operations/page.tsx @@ -3,34 +3,67 @@ /** * 營運 (/operations) — Sprint 5 整合頁面 * 整合: 部署管理 + 工單 + 成本分析 + 行動日誌 + 計費 - * 全部使用 Panel 元件 (無雙重 AppLayout) + * 2026-06-26 IA reset: 不再用頁面級分頁承載主導航,改為入口卡片。 * 建立時間: 2026-04-08 (台北時區) * 更新時間: 2026-04-09 — 全部 Tab Panel 抽取完成 */ +import Link from 'next/link' import { useTranslations } from 'next-intl' +import { + BarChart3, + ClipboardList, + CreditCard, + FileText, + Rocket, +} from 'lucide-react' import { AppLayout } from '@/components/layout' -import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' -import { DeploymentsPanel } from '@/components/panels/DeploymentsPanel' -import { TicketsPanel } from '@/components/panels/TicketsPanel' -import { CostPanel } from '@/components/panels/CostPanel' -import { ActionLogsPanel } from '@/components/panels/ActionLogsPanel' -import { BillingPanel } from '@/components/panels/BillingPanel' export default function OperationsPage({ params }: { params: { locale: string } }) { const t = useTranslations('nav') - const tabs: TabConfig[] = [ - { id: 'deployments', label: t('deployments'), content: }, - { id: 'tickets', label: t('tickets'), content: }, - { id: 'cost', label: t('cost'), content: }, - { id: 'logs', label: t('actions'), content: }, - { id: 'billing', label: t('billing'), content: }, + const links = [ + { id: 'deployments', label: t('deployments'), href: `/${params.locale}/deployments`, Icon: Rocket }, + { id: 'tickets', label: t('tickets'), href: `/${params.locale}/tickets`, Icon: ClipboardList }, + { id: 'cost', label: t('cost'), href: `/${params.locale}/cost`, Icon: BarChart3 }, + { id: 'actions', label: t('actions'), href: `/${params.locale}/action-logs`, Icon: FileText }, + { id: 'billing', label: t('billing'), href: `/${params.locale}/billing`, Icon: CreditCard }, ] return ( - +
+
+
+
+

+ {t('operationsOverview')} +

+
+
+
+ {links.map(({ id, label, href, Icon }) => ( + +
+ + + + {href.replace(`/${params.locale}`, '')} + +
+
+ {label} +
+ + ))} +
+
+
) } diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 31394ec5c..f0db53a18 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -15,6 +15,7 @@ */ import React from 'react' +import Link from 'next/link' import { useTranslations } from 'next-intl' import { useState, useEffect } from 'react' import { useIncidents } from '@/hooks/useIncidents' @@ -23,7 +24,6 @@ import { useHosts, useDashboardStore, type Host } from '@/stores/dashboard.store import { IncidentCard } from '@/components/incident' import { OpenClawPanel } from '@/components/ai/openclaw-panel' import { AppLayout } from '@/components/layout' -import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' import { PulseSkeleton, CardSkeleton } from '@/components/shared/pulse-skeleton' import { DispositionMini } from '@/components/shared/disposition-mini' import { RecentActivity } from '@/components/shared/recent-activity' @@ -1730,17 +1730,25 @@ export default function Home({ params }: { params: { locale: string } }) { // 系統健康百分比 const systemHealthPct = totalServices > 0 ? Math.round((healthyServices / totalServices) * 100) : 0 - // Sprint 5: 4 Tab 配置 (人工批准 2026-04-08) + // IA reset: 首頁不再用頁面級分頁承載主導航,改為指令中心入口卡。 const alertsCount = incidents?.length ?? 0 - const tabs: TabConfig[] = [ - { id: 'overview', label: tDashboard('tabs.overview'), content: null }, - { id: 'alerts', label: tDashboard('tabs.alerts'), badge: alertsCount > 0 ? alertsCount : undefined, content: }, - { id: 'stream', label: tDashboard('tabs.stream'), content: }, - { id: 'disposition', label: tDashboard('tabs.disposition'), content: }, + const homeSections: Array<{ + id: string + order: string + label: string + href: string + badge?: number + content: React.ReactNode | null + }> = [ + { id: 'overview', order: '01', label: tDashboard('tabs.overview'), href: `/${locale}`, content: null }, + { id: 'alerts', order: '02', label: tDashboard('tabs.alerts'), href: `/${locale}?tab=alerts`, badge: alertsCount > 0 ? alertsCount : undefined, content: }, + { id: 'stream', order: '03', label: tDashboard('tabs.stream'), href: `/${locale}?tab=stream`, content: }, + { id: 'disposition', order: '04', label: tDashboard('tabs.disposition'), href: `/${locale}?tab=disposition`, content: }, ] - // Sprint 5: 從 URL 讀取當前 Tab + // 從 URL 讀取當前首頁區塊,保留舊 ?tab= deep link 相容。 const [activeTabId, setActiveTabId] = useState('overview') + const activeHomeSection = homeSections.find(section => section.id === activeTabId) ?? homeSections[0] const [infraView, setInfraView] = useState<'host' | 'topo'>('topo') const [selectedHost, setSelectedHost] = useState(null) const [compactViewport, setCompactViewport] = useState(false) @@ -1754,7 +1762,7 @@ export default function Home({ params }: { params: { locale: string } }) { } syncTab() window.addEventListener('popstate', syncTab) - // PageTabs router.push 後 URL 改變,用 hashchange + 定期低頻同步(1s)作為備援 + // Link navigation 後 URL 改變,用 hashchange + 定期低頻同步(1s)作為備援。 const fallback = setInterval(syncTab, 1000) return () => { window.removeEventListener('popstate', syncTab); clearInterval(fallback) } }, []) @@ -1995,15 +2003,71 @@ export default function Home({ params }: { params: { locale: string } }) { return ( - {/* Sprint 5: Tab Bar */} - ({ ...t, content: t.id === 'overview' ? <> : t.content }))} - defaultTab="overview" - syncWithUrl={true} - /> +
+
+ {homeSections.map(section => { + const active = section.id === activeHomeSection.id + return ( + +
+ + {section.order} + + {section.badge != null && ( + + {section.badge} + + )} +
+ + {section.label} + + + ) + })} +
+
+ + {activeHomeSection.id !== 'overview' && ( +
+ {activeHomeSection.content} +
+ )} {/* Tab 1 戰情總覽: 顯示現有首頁完整內容 */} - {activeTabId === 'overview' && ( + {activeHomeSection.id === 'overview' && (
}, - { id: "compliance", label: t("compliance"), content: }, + const drilldownLinks = [ + { id: "security", label: t("security"), href: `/${params.locale}/security`, icon: Activity }, + { id: "compliance", label: t("compliance"), href: `/${params.locale}/compliance`, icon: ClipboardCheck }, ]; return ( - +
+
+ {drilldownLinks.map((item) => { + const Icon = item.icon; + return ( + +
+ + + + {item.href.replace(`/${params.locale}`, "")} + +
+
+ {item.label} +
+ + ); + })} +
+
); } diff --git a/apps/web/src/components/command-palette/CommandPalette.tsx b/apps/web/src/components/command-palette/CommandPalette.tsx index 9a6aba46f..e710bd2f8 100644 --- a/apps/web/src/components/command-palette/CommandPalette.tsx +++ b/apps/web/src/components/command-palette/CommandPalette.tsx @@ -18,22 +18,15 @@ import { useTranslations } from 'next-intl' import { useRouter, usePathname } from 'next/navigation' import { useLocale } from 'next-intl' import { - Activity, - BookOpen, - BrainCircuit, - ClipboardList, - GitBranch, - Home, - Radar, Search, - Settings, - ShieldCheck, Terminal, - Wrench, - Zap, } from 'lucide-react' import { useTerminalStore } from '@/stores/terminal.store' import { Z_INDEX } from '@/lib/constants/z-index' +import { + PRODUCT_BOTTOM_NAV_ITEMS, + PRODUCT_NAV_SECTIONS, +} from '@/lib/navigation/product-ia' interface PaletteItem { id: string @@ -45,8 +38,18 @@ interface PaletteItem { secondary?: boolean } +const COMMAND_PALETTE_PRIMARY_NAV_IDS = new Set([ + 'command-center', + 'awooop-overview', + 'observability', + 'iwooos-security', + 'knowledge-automation', + 'operations', +]) + export function CommandPalette() { const t = useTranslations('commandPalette') + const tNav = useTranslations('nav') const router = useRouter() const locale = useLocale() const pathname = usePathname() @@ -63,6 +66,29 @@ export function CommandPalette() { setOpen(false) } + const navigationItems: PaletteItem[] = [ + ...PRODUCT_NAV_SECTIONS.flatMap(section => section.items), + ...PRODUCT_BOTTOM_NAV_ITEMS, + ].map((item) => { + const path = item.href === '/' ? '' : item.href + + return { + id: `nav-${item.id}`, + label: tNav(item.labelKey), + group: t('groupNav'), + icon: , + action: () => nav(path), + keywords: [ + item.id, + item.href, + item.labelKey, + ...(item.aliases ?? []), + ...(item.relatedPaths ?? []), + ], + secondary: !COMMAND_PALETTE_PRIMARY_NAV_IDS.has(item.id), + } + }) + const items: PaletteItem[] = [ // 快速動作 { @@ -73,146 +99,7 @@ export function CommandPalette() { action: () => { openTerminal(); setOpen(false) }, keywords: ['terminal', '終端', 'omni', 'cmd', '指令'], }, - // 導航 - { - id: 'home', - label: t('actionGoHome'), - group: t('groupNav'), - icon: , - action: () => nav(''), - keywords: ['home', '首頁', '指令中心', 'dashboard'], - }, - { - id: 'approvals', - label: t('actionGoApprovals'), - group: t('groupNav'), - icon: , - action: () => nav('/authorizations'), - keywords: ['授權', 'approve', '批准', 'authorization'], - secondary: true, - }, - { - id: 'observability', - label: t('actionGoObservability'), - group: t('groupNav'), - icon: , - action: () => nav('/observability'), - keywords: ['observability', '可觀測性', 'monitor', '監控'], - }, - { - id: 'awooop-home', - label: t('actionGoAwoooP'), - group: t('groupNav'), - icon: , - action: () => nav('/awooop'), - keywords: ['awooop', 'operator', 'console', '操作控制台', '總覽'], - }, - { - id: 'knowledge-automation', - label: t('actionGoKnowledgeAutomation'), - group: t('groupNav'), - icon: , - action: () => nav('/knowledge-base'), - keywords: ['knowledge', 'automation', '自動化', '知識', 'kb', 'playbook', 'runbook'], - }, - { - id: 'governance-security', - label: t('actionGoGovernanceSecurity'), - group: t('groupNav'), - icon: , - action: () => nav('/governance'), - keywords: ['governance', 'security', 'iwooos', 'code review', '治理', '安全', '資安', '審查'], - }, - { - id: 'awooop-work-items', - label: t('actionGoWorkItems'), - group: t('groupNav'), - icon: , - action: () => nav('/awooop/work-items'), - keywords: ['work items', '工作鏈路', '工作項', 'recurrence'], - secondary: true, - }, - { - id: 'awooop-runs', - label: t('actionGoRuns'), - group: t('groupNav'), - icon: , - action: () => nav('/awooop/runs'), - keywords: ['runs', 'run monitor', '執行監控', 'run 監控'], - secondary: true, - }, - { - id: 'awooop-approvals', - label: t('actionGoApprovalQueue'), - group: t('groupNav'), - icon: , - action: () => nav('/awooop/approvals'), - keywords: ['approval queue', '審批佇列', '人工閘門', 'approve'], - secondary: true, - }, - { - id: 'automation', - label: t('actionGoAutomation'), - group: t('groupNav'), - icon: , - action: () => nav('/automation'), - keywords: ['automation', '自動化', 'auto'], - secondary: true, - }, - { - id: 'operations', - label: t('actionGoOperations'), - group: t('groupNav'), - icon: , - action: () => nav('/operations'), - keywords: ['operations', '營運', 'ops'], - }, - { - id: 'iwooos', - label: t('actionGoIwooos'), - group: t('groupNav'), - icon: , - action: () => nav('/iwooos'), - keywords: [ - 'iwooos', - 'information security', - 'security', - '安全', - '安全合規', - 'compliance', - '合規', - '資安網', - '資安態勢', - ], - secondary: true, - }, - { - id: 'knowledge', - label: t('actionGoKnowledge'), - group: t('groupNav'), - icon: , - action: () => nav('/knowledge-base'), - keywords: ['knowledge', '知識', '殿堂', 'kb'], - secondary: true, - }, - { - id: 'settings', - label: t('actionGoSettings'), - group: t('groupNav'), - icon: , - action: () => nav('/settings'), - keywords: ['settings', '設定', 'config'], - secondary: true, - }, - { - id: 'terminal-page', - label: t('actionGoTerminal'), - group: t('groupNav'), - icon: , - action: () => nav('/terminal'), - keywords: ['terminal', '終端', 'shell'], - secondary: true, - }, + ...navigationItems, ] const filtered = query.trim() === '' diff --git a/apps/web/src/components/layout/page-tabs.tsx b/apps/web/src/components/layout/page-tabs.tsx deleted file mode 100644 index 626e237a4..000000000 --- a/apps/web/src/components/layout/page-tabs.tsx +++ /dev/null @@ -1,229 +0,0 @@ -'use client' - -/** - * PageTabs — 共用頁籤容器元件 - * ============================ - * Sprint 5: 多頁籤整合介面的核心元件 - * - * 功能: - * - Tab 切換 + URL query 同步 (?tab=alerts) - * - Badge 數字 (如告警數) - * - 瀏覽器後退回到上一個 Tab - * - React.lazy + Suspense 按需載入 - * - 骨架屏 Loading 狀態 - * - * 設計規範: - * - Tab Bar 高度: 36px - * - Active: 底部 2px accent (#d97757) + 文字加粗 - * - 字體: DM Mono 12px - * - 邊框: 0.5px solid #e0ddd4 - * - * 建立時間: 2026-04-08 (台北時區) - * 建立者: Claude Code (Sprint 5 Phase 1) - */ - -import { useState, useCallback, useMemo, useEffect, Suspense, type ReactNode } from 'react' -import type { Route } from 'next' -import { useSearchParams, useRouter, usePathname } from 'next/navigation' -import { useTranslations } from 'next-intl' - -// ============================================================================= -// 型別 -// ============================================================================= - -export interface TabConfig { - /** Tab ID (用於 URL query: ?tab=alerts) */ - id: string - /** 顯示名稱 */ - label: string - /** Badge 數字 (如告警數, 待審批數) */ - badge?: number - /** Tab 內容 (React 元件) */ - content: ReactNode -} - -export interface PageTabsProps { - /** Tab 配置清單 */ - tabs: TabConfig[] - /** 預設顯示的 Tab ID */ - defaultTab?: string - /** 是否同步 URL query (?tab=xxx) */ - syncWithUrl?: boolean -} - -// ============================================================================= -// 骨架屏 -// ============================================================================= - -function TabSkeleton() { - const t = useTranslations('dashboard') - return ( -
- -
- - - - - - - - - - - - - -
- - {t('loading')} - -
- ) -} - -// ============================================================================= -// 元件 -// ============================================================================= - -export function PageTabs({ tabs, defaultTab, syncWithUrl = true }: PageTabsProps) { - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - - // 從 URL 讀取當前 Tab,或使用預設值 - const urlTab = syncWithUrl ? searchParams.get('tab') : null - const initialTab = urlTab || defaultTab || tabs[0]?.id || '' - - const [activeTab, setActiveTab] = useState(initialTab) - const tabIds = tabs.map(t => t.id).join('|') - const firstTabId = tabs[0]?.id || '' - const knownTabIds = useMemo(() => tabIds.split('|'), [tabIds]) - const urlActiveTab = syncWithUrl && urlTab && knownTabIds.includes(urlTab) ? urlTab : null - const resolvedActiveTab = urlActiveTab || activeTab || defaultTab || firstTabId - - useEffect(() => { - const nextTab = (syncWithUrl ? urlTab : null) || defaultTab || firstTabId - if (!nextTab || !knownTabIds.includes(nextTab)) return - setActiveTab(prev => prev === nextTab ? prev : nextTab) - }, [defaultTab, firstTabId, knownTabIds, syncWithUrl, urlTab]) - - // 切換 Tab - const switchTab = useCallback((tabId: string) => { - setActiveTab(tabId) - if (syncWithUrl) { - const params = new URLSearchParams(searchParams.toString()) - if (tabId === (defaultTab || tabs[0]?.id)) { - params.delete('tab') - } else { - params.set('tab', tabId) - } - const query = params.toString() - const nextPath = `${pathname}${query ? `?${query}` : ''}` as Route - router.push(nextPath, { scroll: false }) - } - }, [syncWithUrl, searchParams, router, pathname, defaultTab, tabs]) - - // 找到目前的 Tab 內容 - const activeContent = useMemo(() => { - return tabs.find(t => t.id === resolvedActiveTab)?.content ?? tabs[0]?.content - }, [tabs, resolvedActiveTab]) - - return ( - <> - {/* Tab Bar */} -
- {tabs.map(tab => { - const isActive = tab.id === resolvedActiveTab - return ( - - ) - })} -
- - - {/* Tab 內容 */} - }> - {activeContent} - - - ) -} - -export default PageTabs diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx index 39587d757..2d4a55a98 100644 --- a/apps/web/src/components/layout/sidebar.tsx +++ b/apps/web/src/components/layout/sidebar.tsx @@ -26,26 +26,14 @@ import { usePathname } from 'next/navigation' import Link from 'next/link' import { cn } from '@/lib/utils' import { - BarChart3, - Bell, - BookOpen, - BrainCircuit, - Building2, ChevronLeft, ChevronRight, - ClipboardList, - FileText, - GitPullRequest, - HelpCircle, - LayoutDashboard, - Monitor, - Package, - PlayCircle, - Radar, - Settings, - ShieldCheck, - Terminal, } from 'lucide-react' +import { + PRODUCT_BOTTOM_NAV_ITEMS, + PRODUCT_NAV_SECTIONS, + type ProductNavItem, +} from '@/lib/navigation/product-ia' // Phase 8.0 #15: 改用 approval store SSE (移除 polling) import { useApprovalStore } from '@/stores/approval.store' @@ -61,22 +49,6 @@ interface SidebarProps { className?: string } -type NavItemConfig = { - id: string - href: string - labelKey: string - Icon: typeof LayoutDashboard - aliases?: string[] - relatedPaths?: string[] - exact?: boolean - badge?: boolean // 是否顯示動態徽章 -} - -type NavSection = { - sectionKey: string - items: NavItemConfig[] -} - // ============================================================ // 2026-06-26: Operator-first IA repair // ============================================================ @@ -84,129 +56,6 @@ type NavSection = { // 側欄放高頻任務與產品導航,頁面內不再重複用二層分頁藏入口。 // ============================================================ -const NAV_SECTIONS: NavSection[] = [ - { - sectionKey: 'workspaces', - items: [ - { id: 'command-center', href: '/', labelKey: 'commandCenter', Icon: LayoutDashboard }, - { - id: 'awooop-overview', - href: '/awooop', - labelKey: 'awooopHome', - Icon: BrainCircuit, - exact: true, - }, - ], - }, - { - sectionKey: 'queues', - items: [ - { - id: 'awooop-work-items', - href: '/awooop/work-items', - labelKey: 'workItems', - Icon: ClipboardList, - }, - { - id: 'awooop-runs', - href: '/awooop/runs', - labelKey: 'runMonitor', - Icon: PlayCircle, - }, - { - id: 'awooop-approvals', - href: '/awooop/approvals', - labelKey: 'approvalQueue', - Icon: ShieldCheck, - aliases: ['/authorizations'], - badge: true, - }, - { - id: 'awooop-contracts', - href: '/awooop/contracts', - labelKey: 'contracts', - Icon: FileText, - }, - { - id: 'awooop-tenants', - href: '/awooop/tenants', - labelKey: 'tenants', - Icon: Building2, - }, - ], - }, - { - sectionKey: 'monitoring', - items: [ - { - id: 'observability', - href: '/observability', - labelKey: 'observability', - Icon: Monitor, - aliases: ['/monitoring', '/apm', '/errors', '/apps', '/services', '/topology'], - }, - { - id: 'alerts', - href: '/alerts', - labelKey: 'alerts', - Icon: Bell, - aliases: ['/alert-operation-logs', '/notifications'], - }, - { - id: 'iwooos-security', - href: '/governance', - labelKey: 'iwooos', - Icon: ShieldCheck, - aliases: ['/security-compliance'], - relatedPaths: ['/iwooos', '/security', '/compliance'], - }, - { - id: 'code-review', - href: '/code-review', - labelKey: 'codeReview', - Icon: GitPullRequest, - }, - ], - }, - { - sectionKey: 'knowledge', - items: [ - { - id: 'knowledge-automation', - href: '/knowledge-base', - labelKey: 'knowledgeAutomation', - Icon: BookOpen, - aliases: ['/knowledge', '/automation', '/auto-repair', '/drift', '/neural-command'], - }, - { - id: 'reports', - href: '/reports', - labelKey: 'reports', - Icon: BarChart3, - }, - { - id: 'operations', - href: '/operations', - labelKey: 'operationsOverview', - Icon: Package, - aliases: ['/deployments', '/tickets', '/cost', '/billing', '/action-logs'], - }, - ], - }, - { - sectionKey: 'system', - items: [ - { id: 'terminal', href: '/terminal', labelKey: 'terminal', Icon: Terminal }, - { id: 'settings', href: '/settings', labelKey: 'settings', Icon: Settings }, - { id: 'classic', href: '/classic', labelKey: 'classicAICenter', Icon: Radar }, - ], - }, -] - -const BOTTOM_NAV_ITEMS: NavItemConfig[] = [ - { id: 'help', href: '/help', labelKey: 'help', Icon: HelpCircle }, -] - // ============================================================================= // Component // ============================================================================= @@ -244,7 +93,7 @@ export function Sidebar({ return pathname === fullHref || pathname.startsWith(fullHref + '/') } - const isActive = (item: NavItemConfig) => ( + const isActive = (item: ProductNavItem) => ( isRouteActive(item.href, item.exact) || item.aliases?.some(alias => isRouteActive(alias)) === true || item.relatedPaths?.some(path => isRouteActive(path)) === true @@ -270,7 +119,7 @@ export function Sidebar({ {/* 導航列表 - Operator workflow sections */}