({ ...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 */}