Some checks failed
Code Review / ai-code-review (push) Successful in 48s
run-migration / migrate (push) Failing after 45s
CD Pipeline / tests (push) Successful in 3m46s
Type Sync Check / check-type-sync (push) Successful in 2m8s
CD Pipeline / build-and-deploy (push) Failing after 31m14s
CD Pipeline / post-deploy-checks (push) Has been skipped
【十二人專家團隊全景掃描 + 並行四軌實施】
統帥質疑「有讓 12-agent 一起協作嗎」後,依照團隊規則完成全鏈路交付:
onboarder + critic + db-expert + debugger + frontend-designer 並行掃描,
找到 6 大 Gap,再由 fullstack-engineer × 4、refactor-specialist 協作落地。
【Track C — trust_drift 雙寫整併】
兩條獨立寫 event_type=trust_drift 路徑互不呼叫,下游 consumer 拿到雙份資料
無法判定 source-of-truth。整併保留 governance_agent.check_trust_drift(功能
更全:auto-deprecate + Telegram + PG),TrustDriftDetector 降為純統計 lib,
W-6 watchdog 改呼叫 governance_agent。新增 TestSinglePgWritePerDriftScenario
驗證同一 drift 場景只觸發一次 PG 寫入。
變更:
- apps/api/src/services/trust_drift_detector.py(lib only,不再寫 PG)
- apps/api/tests/test_trust_drift_watchdog.py(W-6 改 mock governance_agent)
【Track D — governance_remediation_dispatch 派遣表】
ai_governance_events 是不可變 Event Sourcing,不能塞執行狀態。新建派遣表
作為投影層:1 event → 0..N dispatches,狀態可變、可重試、可審計。
- PgEnum 5 種 event_type + 7 階段狀態機(pending → dispatched → executing →
succeeded/failed/cancelled/skipped)
- 失敗重試 INSERT 新 row(不改舊 row 的 status,保留審計痕跡)
- Partial unique index ux_grd_one_active_per_event 強制「同事件唯一活躍」
- 4 個複合 index 支援 worker poll、去重查詢、觀測面板
- FK 對應 ai_governance_events / playbooks / incidents / approval_records
全部 SET NULL(avoid cascade lock,但 governance_event 用 RESTRICT)
變更:
- apps/api/src/db/models.py(GovernanceRemediationDispatch ORM class)
- apps/api/migrations/governance_remediation_dispatch_2026-05-03.sql
- apps/api/src/repositories/governance_remediation_dispatch_repo.py
(6 個 async 函式 + 3 個自訂例外:DispatchAlreadyActive /
InvalidStatusTransition / DispatchNotFound)
- apps/api/src/models/governance_dispatch.py(DecisionContextV1 等 4 schema)
- apps/api/tests/test_governance_remediation_dispatch.py(29 tests)
【Track B — /governance 頁面】
後端 PR1 三個 endpoint + 前端 PR2-5 完整三 Tab。
PR1 後端:
- GET /api/v1/ai/governance/events(events_tab,含 event_type/severity/
狀態/時間範圍篩選 + 分頁)
- GET /api/v1/ai/governance/queue(queue_tab,含 graceful fallback:
dispatch 表不存在時回 table_pending=True 不拋 500)
- GET /api/v1/ai/governance/summary(slo_tab 30d 違反時序圖)
- severity 映射規則寫死(critic 建議未來移 settings)
PR2-5 前端:
- /governance 路由 + AppLayout + Compliance Badge 橫幅 + PageTabs
- SLO Tab:3 KPI 卡片(Syne 28px + StatusOrb + 7d sparkline)+
30d 違反 stacked BarChart
- Events Tab:篩選列 + 表格 + inline 展開行(JSON / 修復建議 / 派遣記錄)
- Queue Tab:HITL 待辦卡片 + 信任度進度條 + 批准/拒絕按鈕(本 PR console.log)
- Sidebar 加入「AI 治理」入口(ShieldCheck icon)
- i18n 雙語完整(governance namespace + nav.governance)
- 7 個新元件:slo-kpi-card / slo-violation-chart / events-table /
events-filter-bar / event-detail-drawer / queue-item-card / queue-history-tabs
變更:
- apps/api/src/api/v1/ai_governance.py(router)
- apps/api/src/services/governance_query_service.py
- apps/api/src/models/governance.py(Pydantic V2 schemas)
- apps/api/tests/test_ai_governance_endpoints.py(21 tests)
- apps/web/src/app/[locale]/governance/(page + 3 tabs)
- apps/web/src/components/governance/(7 元件)
- apps/web/messages/{zh-TW,en}.json(governance namespace)
- apps/web/src/components/layout/sidebar.tsx(+1 行)
- apps/api/src/main.py(router include)
【Track A — GovernanceDispatcher 決策融合】
把治理事件接到 remediation 執行器,走北極星方向決策融合(LLM × Playbook trust
× MCP),符合「禁寫死規則」鐵律。
- 設計鐵律:DecisionFusionAdapter 是新增 wrapper,**不修改任何 Tier 3 檔**
(decision_manager / learning_service / trust_engine),只 consume 既有 API
- 三維融合公式:confidence = 0.4×llm + 0.3×playbook_trust + 0.3×mcp_consistency
(權重加 TODO 標明未來由 AI 自學調整)
- 三分支決策路徑:
confidence ≥ 0.85 → auto_dispatch(status=dispatched)
0.65 ≤ confidence < 0.85 → pending_approval(HITL)
confidence < 0.65 → skip + log
- decision_context JSONB 完整記錄三維輸入快照(給未來 fine-tune 用)
- poll 30s 掃 unresolved 事件,仿 governance loop 模式
- 重複事件擋去重(呼叫 get_active_for_event)
變更:
- apps/api/src/services/governance_dispatcher.py
- apps/api/src/services/decision_fusion_adapter.py
- apps/api/tests/test_governance_dispatcher.py(14 tests)
- apps/api/src/main.py(lifespan task 接 run_governance_dispatcher_loop)
【驗證】
1836 個 unit test 全過(29 skipped 為既有 PG integration env 問題)
【調度教訓 — 已記入 memory】
- vuln-verifier 應在 fullstack-engineer **之前**跑(避免並行讀到已修代碼誤判)
- critic 雙輪審查不可省(第二輪抓到 NaN sentinel + Prom rule 連鎖)
- 北極星「禁寫死規則」搭配 decision-fusion 確實實施
【未動 Tier 3 — 已驗證】
git diff 確認本 commit 完全沒改 decision_manager.py / learning_service.py /
trust_engine.py,只新增 wrapper service consume 既有 API。
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* Sidebar - Phase 7.0 極簡五柱導航
|
||
* =================================
|
||
* Nothing.tech 視覺憲法:
|
||
* - 純白背景 (bg-white)
|
||
* - 極細右邊框 (border-r-[0.5px] border-neutral-200)
|
||
* - 無陰影
|
||
* - 單色圖示
|
||
*
|
||
* 5 大核心樞紐:
|
||
* 1. 全局戰情室 (/)
|
||
* 2. 授權中心 (/authorizations) - 含動態徽章
|
||
* 3. 行動日誌 (/action-logs)
|
||
* 4. 知識殿堂 (/knowledge-base)
|
||
* 5. 系統設定 (/settings)
|
||
*
|
||
* Phase 19: 使用 Z_INDEX.SIDEBAR (40)
|
||
* @see lib/constants/z-index.ts
|
||
*/
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { Z_INDEX } from '@/lib/constants/z-index'
|
||
// Phase 8.0 #15: SSE 驅動的待簽核數量
|
||
import { useTranslations } from 'next-intl'
|
||
import { usePathname } from 'next/navigation'
|
||
import Link from 'next/link'
|
||
import { cn } from '@/lib/utils'
|
||
import {
|
||
LayoutDashboard, ShieldCheck, Bell, Monitor, Activity,
|
||
Bug, GitBranch, Shield, ClipboardCheck,
|
||
Wrench, Package, Ticket, DollarSign, Zap, FileText,
|
||
BookOpen, Terminal, AppWindow, Server,
|
||
Users, BellRing, CreditCard, HelpCircle, Settings,
|
||
ChevronLeft, ChevronRight, Diff, BrainCircuit,
|
||
} from 'lucide-react'
|
||
// Phase 8.0 #15: 改用 approval store SSE (移除 polling)
|
||
import { useApprovalStore } from '@/stores/approval.store'
|
||
|
||
// =============================================================================
|
||
// Types
|
||
// =============================================================================
|
||
|
||
interface SidebarProps {
|
||
locale: string
|
||
collapsed?: boolean
|
||
onToggle?: () => void
|
||
className?: string
|
||
}
|
||
|
||
type NavItemConfig = {
|
||
id: string
|
||
href: string
|
||
labelKey: string
|
||
Icon: typeof LayoutDashboard
|
||
badge?: boolean // 是否顯示動態徽章
|
||
}
|
||
|
||
type NavSection = {
|
||
sectionKey: string
|
||
sectionLabel: string
|
||
items: NavItemConfig[]
|
||
}
|
||
|
||
// ============================================================
|
||
// Sprint 5: 精簡導航 6+2+經典(統帥批准 2026-04-08)
|
||
// ============================================================
|
||
// 整合對照:
|
||
// 指令中心 / → 整合: 儀表板 + 授權 + 告警 + 報表 (4 Tab)
|
||
// 可觀測性 /observability → 整合: 監控 + APM + 錯誤 + 應用 + 服務 (5 Tab) [暫指 /monitoring]
|
||
// 自動化 /automation → 整合: 自動修復 + 神經指揮 + Drift (3 Tab) [暫指 /auto-repair]
|
||
// 營運 /operations → 整合: 部署 + 工單 + 成本 + 行動日誌 + 計費 (5 Tab) [暫指 /deployments]
|
||
// 安全合規 /security-compliance → 整合: 安全 + 合規 (2 Tab) [暫指 /security]
|
||
// 知識 /knowledge → 知識庫 [暫指 /knowledge-base]
|
||
// ============================================================
|
||
|
||
const NAV_SECTIONS: NavSection[] = [
|
||
{
|
||
sectionKey: 'main',
|
||
sectionLabel: '',
|
||
items: [
|
||
{ id: 'command-center', href: '/', labelKey: 'commandCenter', Icon: LayoutDashboard },
|
||
{ id: 'observability', href: '/observability', labelKey: 'observability', Icon: Monitor },
|
||
{ id: 'automation', href: '/automation', labelKey: 'automation', Icon: Wrench },
|
||
{ id: 'operations', href: '/operations', labelKey: 'operations', Icon: Package },
|
||
{ id: 'security-compliance', href: '/security-compliance', labelKey: 'securityCompliance',Icon: Shield },
|
||
{ id: 'knowledge', href: '/knowledge', labelKey: 'knowledge', Icon: BookOpen },
|
||
{ id: 'governance', href: '/governance', labelKey: 'governance', Icon: ShieldCheck },
|
||
],
|
||
},
|
||
{
|
||
// Legacy: 經典 AI 中心 (統帥指示保留)
|
||
sectionKey: 'legacy',
|
||
sectionLabel: 'legacy',
|
||
items: [
|
||
{ id: 'classic', href: '/classic', labelKey: 'classicAICenter', Icon: LayoutDashboard },
|
||
],
|
||
},
|
||
]
|
||
|
||
const BOTTOM_NAV_ITEMS: NavItemConfig[] = [
|
||
{ id: 'terminal', href: '/terminal', labelKey: 'terminal', Icon: Terminal },
|
||
{ id: 'settings', href: '/settings', labelKey: 'settings', Icon: Settings },
|
||
]
|
||
|
||
// =============================================================================
|
||
// Component
|
||
// =============================================================================
|
||
|
||
export function Sidebar({
|
||
locale,
|
||
collapsed = false,
|
||
onToggle,
|
||
className,
|
||
}: SidebarProps) {
|
||
const t = useTranslations('nav')
|
||
const tBrand = useTranslations('brand')
|
||
const tSection = useTranslations('navSection')
|
||
const tSidebar = useTranslations('sidebar')
|
||
const pathname = usePathname()
|
||
|
||
// Phase 8.0 #15: 改用 SSE 驅動的 pending count (移除 30s polling)
|
||
// 防止 SSR hydration mismatch: 只在 client 顯示 badge
|
||
const [mounted, setMounted] = useState(false)
|
||
const pendingApprovals = useApprovalStore(state => state.pendingApprovals)
|
||
const pendingCount = pendingApprovals.length
|
||
|
||
useEffect(() => {
|
||
setMounted(true)
|
||
}, [])
|
||
|
||
const isActive = (href: string) => {
|
||
const fullHref = `/${locale}${href === '/' ? '' : href}`
|
||
if (href === '/') {
|
||
return pathname === `/${locale}` || pathname === `/${locale}/`
|
||
}
|
||
return pathname === fullHref || pathname.startsWith(fullHref + '/')
|
||
}
|
||
|
||
return (
|
||
<aside
|
||
className={cn(
|
||
'fixed left-0 flex flex-col',
|
||
className
|
||
)}
|
||
style={{
|
||
top: 68,
|
||
bottom: 0,
|
||
zIndex: Z_INDEX.SIDEBAR,
|
||
background: '#faf9f3',
|
||
borderRight: '0.5px solid #e0ddd4',
|
||
width: collapsed ? 64 : 224,
|
||
transition: 'width 0.3s ease',
|
||
}}
|
||
>
|
||
|
||
{/* 導航列表 - AI中心 v6 4分區 */}
|
||
<nav className="flex-1 py-2 overflow-y-auto">
|
||
{/* 4 分區菜單 */}
|
||
{NAV_SECTIONS.map(section => (
|
||
<div key={section.sectionKey} style={{ marginBottom: 4 }}>
|
||
{/* 分區標題 — main 不顯示,legacy 顯示分隔線+標題 */}
|
||
{!collapsed && section.sectionLabel && (
|
||
<div style={{
|
||
fontSize: 10,
|
||
color: '#b0ad9f',
|
||
letterSpacing: '1.5px',
|
||
textTransform: 'uppercase' as const,
|
||
padding: '12px 12px 3px',
|
||
fontFamily: 'var(--font-body), monospace',
|
||
borderTop: '0.5px solid #e0ddd4',
|
||
marginTop: 4,
|
||
}}>
|
||
{section.sectionLabel}
|
||
</div>
|
||
)}
|
||
{section.items.map(item => {
|
||
const active = isActive(item.href)
|
||
const count = item.badge && mounted ? pendingCount : 0
|
||
return (
|
||
<Link
|
||
key={item.id}
|
||
href={`/${locale}${item.href === '/' ? '' : item.href}`}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: collapsed ? 0 : 8,
|
||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||
padding: collapsed ? '8px 0' : '6px 12px',
|
||
fontSize: 14,
|
||
color: active ? '#141413' : '#87867f',
|
||
fontWeight: active ? 600 : 400,
|
||
borderRight: active ? '2px solid #d97757' : '2px solid transparent',
|
||
background: active ? 'rgba(217,119,87,0.08)' : 'transparent',
|
||
textDecoration: 'none',
|
||
transition: 'all 0.15s',
|
||
position: 'relative' as const,
|
||
}}
|
||
>
|
||
<item.Icon size={15} />
|
||
{!collapsed && <span>{t(item.labelKey)}</span>}
|
||
{item.badge && count > 0 && (
|
||
<span style={{
|
||
marginLeft: 'auto',
|
||
fontSize: 12,
|
||
background: '#d97757',
|
||
color: 'white',
|
||
borderRadius: 10,
|
||
padding: '1px 5px',
|
||
fontWeight: 700,
|
||
}}>
|
||
{count > 99 ? '99+' : count}
|
||
</span>
|
||
)}
|
||
</Link>
|
||
)
|
||
})}
|
||
</div>
|
||
))}
|
||
|
||
{/* Separator */}
|
||
<div style={{ borderTop: '0.5px solid #e0ddd4', margin: '6px 0' }} />
|
||
|
||
{/* 底部菜單 */}
|
||
{BOTTOM_NAV_ITEMS.map(item => {
|
||
const active = isActive(item.href)
|
||
return (
|
||
<Link
|
||
key={item.id}
|
||
href={`/${locale}${item.href === '/' ? '' : item.href}`}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: collapsed ? 0 : 8,
|
||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||
padding: collapsed ? '7px 0' : '5px 12px',
|
||
fontSize: 13,
|
||
color: active ? '#141413' : '#87867f',
|
||
fontWeight: active ? 600 : 400,
|
||
borderRight: active ? '2px solid #d97757' : '2px solid transparent',
|
||
background: active ? 'rgba(217,119,87,0.08)' : 'transparent',
|
||
textDecoration: 'none',
|
||
transition: 'all 0.15s',
|
||
}}
|
||
>
|
||
<item.Icon size={15} />
|
||
{!collapsed && <span>{t(item.labelKey)}</span>}
|
||
</Link>
|
||
)
|
||
})}
|
||
</nav>
|
||
|
||
{/* 底部區域 - 版本號 */}
|
||
<div style={{
|
||
borderTop: '0.5px solid #e0ddd4',
|
||
padding: collapsed ? '8px 0' : '8px 12px',
|
||
textAlign: collapsed ? 'center' : 'left',
|
||
}}>
|
||
{!collapsed && (
|
||
<span style={{ fontSize: 10, color: '#b0ad9f', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||
v1.0.0
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 折疊按鈕 */}
|
||
<button
|
||
onClick={onToggle}
|
||
style={{
|
||
position: 'absolute',
|
||
right: -12,
|
||
top: 80,
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: '50%',
|
||
background: '#faf9f3',
|
||
border: '0.5px solid #e0ddd4',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.15s',
|
||
}}
|
||
aria-label={collapsed ? tSidebar('expand') : tSidebar('collapse')}
|
||
>
|
||
{collapsed ? (
|
||
<ChevronRight className="w-3 h-3 text-neutral-400" />
|
||
) : (
|
||
<ChevronLeft className="w-3 h-3 text-neutral-400" />
|
||
)}
|
||
</button>
|
||
</aside>
|
||
)
|
||
}
|