feat(web): Sprint 5 Phase 0 — 安裝 React Flow + elkjs + 保留經典首頁
Phase 0: - 安裝 @xyflow/react 12.10.2 + elkjs 0.11.1 - import 驗證通過 經典首頁保留: - 複製現有首頁到 /classic/page.tsx (815行) - 統帥指示: 新指令中心部署後,舊版保留供對照 零假數據鐵律: 所有新頁面必須串接真實 API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,10 @@
|
||||
"@awoooi/shared-types": "workspace:^",
|
||||
"@sentry/nextjs": "^10.45.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.0",
|
||||
"elkjs": "^0.11.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "14.1.0",
|
||||
"next-intl": "^4.8.3",
|
||||
|
||||
819
apps/web/src/app/[locale]/classic/page.tsx
Normal file
819
apps/web/src/app/[locale]/classic/page.tsx
Normal file
@@ -0,0 +1,819 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* AWOOOI 經典 AI Center 主頁 (Legacy)
|
||||
* ====================================
|
||||
* Sprint 5: 現有首頁原封保留在 /classic 路由
|
||||
* 新指令中心在 / 首頁
|
||||
* 統帥指示: 保留舊版供對照,確認零功能遺失後再決定移除
|
||||
*
|
||||
* 2欄佈局(Sidebar 由 AppLayout 提供): Feed + RightPanel
|
||||
*
|
||||
* 統帥鐵律: 使用真實數據 Hook,禁止假數據!
|
||||
*
|
||||
* @updated 2026-04-02 Claude Code — Metrics Strip 7指標視覺強化
|
||||
* @updated 2026-04-03 Claude Code — 監控工具區塊 (Grafana/Prometheus/SigNoz/Gitea)
|
||||
* 串接: incidents(count/P0/MTTR/autoRemediation) + dashboard(serviceHealth/pendingApprovals/podHealth)
|
||||
* @updated 2026-04-03 Claude Code — 完整對齊 figma-v2 設計
|
||||
* figma-v2 重點: 7指標(含今日事件) + 監控工具左彩色條 + 可點擊連結 + meta行
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics'
|
||||
import { useIncidents } from '@/hooks/useIncidents'
|
||||
import { useHosts, useDashboardStore } from '@/stores/dashboard.store'
|
||||
import { IncidentCard } from '@/components/incident'
|
||||
import { OpenClawPanel } from '@/components/ai/openclaw-panel'
|
||||
import { HostGrid, type HostInfo, type HostService } from '@/components/infra/host-grid'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
interface MetricItem {
|
||||
label: string
|
||||
value: string | number
|
||||
sub?: string
|
||||
badge?: { text: string; color: string; bg: string }
|
||||
valueColor?: string
|
||||
trend?: { text: string; color: string } // figma: value-row 右側趨勢箭頭
|
||||
extra?: React.ReactNode // figma: metric-extra 第三行
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mini Sparkline (SVG inline)
|
||||
// =============================================================================
|
||||
|
||||
function MiniSparkline({ values, color }: { values: number[]; color: string }) {
|
||||
if (!values || values.length < 2) return null
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
const range = max - min || 1
|
||||
const w = 48, h = 16
|
||||
const pts = values.map((v, i) => {
|
||||
const x = (i / (values.length - 1)) * w
|
||||
const y = h - ((v - min) / range) * h
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
return (
|
||||
<svg width={w} height={h} style={{ display: 'block', flexShrink: 0 }}>
|
||||
<polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Monitoring Tools Component — figma-v2 style
|
||||
// Left 3px accent bar + clickable links + meta row
|
||||
// =============================================================================
|
||||
|
||||
interface MonitoringTool {
|
||||
name: string
|
||||
status: string
|
||||
version: string | null
|
||||
stats: string | null
|
||||
description: string
|
||||
firing_count?: number
|
||||
checked_at: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
// figma-v2 左側彩色條顏色
|
||||
const TOOL_ACCENT_COLOR: Record<string, string> = {
|
||||
Grafana: '#F59E0B',
|
||||
Prometheus: '#E85530',
|
||||
Sentry: '#7B52BF',
|
||||
Langfuse: '#0077CC',
|
||||
SigNoz: '#4A90D9',
|
||||
Gitea: '#22C55E',
|
||||
}
|
||||
|
||||
// 圖示 emoji
|
||||
const TOOL_EMOJI: Record<string, string> = {
|
||||
Grafana: '📊',
|
||||
Prometheus: '🔥',
|
||||
Sentry: '🔭',
|
||||
Langfuse: '🧪',
|
||||
SigNoz: '🔭',
|
||||
Gitea: '🐙',
|
||||
}
|
||||
|
||||
function MonitoringTools() {
|
||||
const tDash = useTranslations('dashboard')
|
||||
const tCommon = useTranslations('common')
|
||||
const [tools, setTools] = useState<MonitoringTool[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const load = () => {
|
||||
fetch(`${API_BASE}/api/v1/monitoring/status`)
|
||||
.then(r => r.json())
|
||||
.then(d => { setTools(d.tools ?? []); setLoading(false) })
|
||||
.catch(() => setLoading(false))
|
||||
}
|
||||
load()
|
||||
const t = setInterval(load, 60000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
if (loading) return (
|
||||
<div style={{ padding: '12px 14px', fontSize: 12, color: '#87867f' }}>{tCommon('loading')}</div>
|
||||
)
|
||||
if (tools.length === 0) return (
|
||||
<div style={{ padding: '12px 14px', fontSize: 12, color: '#cc2200' }}>{tDash('connectionError')}</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: '8px 12px' }}>
|
||||
{tools.map((tool) => {
|
||||
const isUp = tool.status === 'up'
|
||||
const hasFiring = (tool.firing_count ?? 0) > 0
|
||||
const statusColor = isUp ? (hasFiring ? '#F59E0B' : '#22C55E') : '#cc2200'
|
||||
const statusText = isUp ? (hasFiring ? `${tool.firing_count} ${tDash('monitoringStatus.firing')}` : tDash('monitoringStatus.up')) : tDash('monitoringStatus.down')
|
||||
const accentColor = TOOL_ACCENT_COLOR[tool.name] ?? '#b0ad9f'
|
||||
const emoji = TOOL_EMOJI[tool.name] ?? '🔧'
|
||||
const link = tool.url ?? '#'
|
||||
const timeStr = (() => {
|
||||
try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) }
|
||||
catch { return '--' }
|
||||
})()
|
||||
|
||||
return (
|
||||
<a
|
||||
key={tool.name}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 10,
|
||||
padding: '10px 12px',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
{/* 左側彩色條 */}
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, top: 0, bottom: 0, width: 3,
|
||||
background: accentColor,
|
||||
}} />
|
||||
|
||||
{/* 主行 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||||
<span style={{ fontSize: 18 }}>{emoji}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#141413', marginBottom: 2 }}>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#87867f' }}>{tool.description}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
|
||||
<div style={{ fontSize: 10, color: statusColor, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: statusColor, display: 'inline-block' }} />
|
||||
{statusText}
|
||||
</div>
|
||||
{hasFiring ? (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, padding: '1px 7px', borderRadius: 8, fontWeight: 600,
|
||||
background: 'rgba(245,158,11,0.12)', color: '#F59E0B',
|
||||
}}>
|
||||
{tool.firing_count} 告警
|
||||
</span>
|
||||
) : (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, padding: '1px 7px', borderRadius: 8, fontWeight: 600,
|
||||
background: 'rgba(34,197,94,0.1)', color: '#22C55E',
|
||||
}}>
|
||||
0 告警
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#b0ad9f', marginLeft: 4 }}>↗</span>
|
||||
</div>
|
||||
|
||||
{/* Meta 行 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 0,
|
||||
marginTop: 6, paddingTop: 6, paddingLeft: 8,
|
||||
borderTop: '0.5px solid #f0efe8',
|
||||
}}>
|
||||
{tool.version && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 14, fontSize: 10 }}>
|
||||
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>{tDash('metaVersion')}</span>
|
||||
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>v{tool.version}</span>
|
||||
</div>
|
||||
)}
|
||||
{tool.stats && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 14, fontSize: 10 }}>
|
||||
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>{tDash('metaStats')}</span>
|
||||
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>{tool.stats}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4, fontSize: 10 }}>
|
||||
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>{tDash('metaUpdatedAt')}</span>
|
||||
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>{timeStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Page
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// Static Host Service Catalog
|
||||
// 定義每台主機完整服務清單(API 只回傳部分,此處補全靜態資訊)
|
||||
// =============================================================================
|
||||
|
||||
const HOST_CATALOG: Record<string, { services: HostService[]; isK3s?: boolean; role?: string }> = {
|
||||
'192.168.0.110': {
|
||||
services: [
|
||||
{ name: 'Harbor', healthy: false, port: 5000, description: 'Container Registry' },
|
||||
{ name: 'Gitea', healthy: false, port: 3001, description: 'Git · CI/CD' },
|
||||
{ name: 'Sentry', healthy: false, port: 9000, description: 'Error Tracking' },
|
||||
{ name: 'Langfuse', healthy: false, port: 3100, description: 'LLM Tracing' },
|
||||
{ name: 'Grafana', healthy: false, port: 3002, description: '監控面板' },
|
||||
{ name: 'Prometheus', healthy: false, port: 9090, description: '告警規則' },
|
||||
],
|
||||
},
|
||||
'192.168.0.112': {
|
||||
services: [
|
||||
{ name: 'Scanner API', healthy: false, port: 8080, description: '漏洞掃描' },
|
||||
],
|
||||
},
|
||||
'192.168.0.120': {
|
||||
isK3s: true,
|
||||
role: 'Control Plane #1',
|
||||
services: [
|
||||
{ name: 'K3s API', healthy: false, port: 6443, description: 'kubectl', isK3s: true },
|
||||
{ name: 'Traefik', healthy: false, description: 'Ingress', isK3s: true },
|
||||
{ name: 'awoooi-prod', healthy: false, description: 'Namespace', isK3s: true },
|
||||
{ name: 'keepalived', healthy: false, description: 'VIP MASTER', isK3s: true },
|
||||
],
|
||||
},
|
||||
'192.168.0.121': {
|
||||
isK3s: true,
|
||||
role: 'Control Plane #2 (HA)',
|
||||
services: [
|
||||
{ name: 'K3s API', healthy: false, port: 6443, description: 'kubectl', isK3s: true },
|
||||
{ name: 'API', healthy: false, port: 32334, description: 'NodePort', isK3s: true },
|
||||
{ name: 'Web', healthy: false, port: 32335, description: 'NodePort', isK3s: true },
|
||||
{ name: 'keepalived', healthy: false, description: 'VIP BACKUP', isK3s: true },
|
||||
],
|
||||
},
|
||||
'192.168.0.188': {
|
||||
services: [
|
||||
{ name: 'Nginx', healthy: false, port: 443, description: 'Reverse Proxy' },
|
||||
{ name: 'PostgreSQL', healthy: false, port: 5432, description: 'K3s Datastore' },
|
||||
{ name: 'Redis', healthy: false, port: 6380, description: 'Cache' },
|
||||
{ name: 'Ollama', healthy: false, port: 11434, description: 'LLM' },
|
||||
{ name: 'OpenClaw', healthy: false, port: 8088, description: 'AI Agent' },
|
||||
{ name: 'SigNoz', healthy: false, port: 3301, description: 'APM · OTEL' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/** 合併 API 動態健康狀態 + 靜態服務清單 */
|
||||
function buildHostInfo(
|
||||
ip: string,
|
||||
hostname: string,
|
||||
cpuPct: number | null,
|
||||
ramPct: number | null,
|
||||
dynamicServices: { name: string; status: string }[],
|
||||
): HostInfo {
|
||||
const catalog = HOST_CATALOG[ip]
|
||||
const services: HostService[] = catalog
|
||||
? catalog.services.map(s => {
|
||||
const dyn = dynamicServices.find(d => d.name.toLowerCase() === s.name.toLowerCase())
|
||||
return {
|
||||
...s,
|
||||
healthy: dyn ? (dyn.status === 'up' || dyn.status === 'healthy') : false,
|
||||
}
|
||||
})
|
||||
: dynamicServices.map(s => ({
|
||||
name: s.name,
|
||||
healthy: s.status === 'up' || s.status === 'healthy',
|
||||
}))
|
||||
return {
|
||||
hostname,
|
||||
ip,
|
||||
cpuPct,
|
||||
ramPct,
|
||||
services,
|
||||
isK3s: catalog?.isK3s,
|
||||
role: catalog?.role,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home({ params }: { params: { locale: string } }) {
|
||||
const tDashboard = useTranslations('dashboard')
|
||||
const tCommon = useTranslations('common')
|
||||
const locale = params.locale
|
||||
const hosts = useHosts()
|
||||
|
||||
// dashboard store — pending approvals + overall pod health
|
||||
const pendingApprovals = useDashboardStore(s => s.pendingApprovals)
|
||||
const dashboardHosts = useDashboardStore(s => s.hosts)
|
||||
|
||||
// Gold metrics (RPS / Error Rate / P99 / AI Success + sparklines)
|
||||
const { metrics: pulseMetrics } = useGlobalPulseMetrics({
|
||||
pollInterval: 30000,
|
||||
enablePolling: true,
|
||||
})
|
||||
|
||||
// Real incidents
|
||||
const {
|
||||
incidents,
|
||||
isLoading: isIncidentsLoading,
|
||||
error: incidentsError,
|
||||
} = useIncidents({
|
||||
pollInterval: 15000,
|
||||
enablePolling: true,
|
||||
})
|
||||
|
||||
// ── Metrics 計算 ────────────────────────────────────────────────────────────
|
||||
|
||||
// 服務健康: dashboard hosts healthy services count
|
||||
const { healthyServices, totalServices } = (() => {
|
||||
let healthy = 0, total = 0
|
||||
for (const h of dashboardHosts) {
|
||||
for (const s of h.services) {
|
||||
total++
|
||||
if (s.status === 'up' || s.status === 'healthy') healthy++
|
||||
}
|
||||
}
|
||||
return { healthyServices: healthy, totalServices: total }
|
||||
})()
|
||||
|
||||
// P0 count
|
||||
const p0Count = incidents?.filter(i => i.severity === 'P0').length ?? 0
|
||||
|
||||
// 2026-04-07 Claude Code: Sprint 4 E2 — 從 disposition API 取得真實自動化率
|
||||
const [dispositionRate, setDispositionRate] = useState<{ auto_rate: number; total: number } | null>(null)
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/api/v1/stats/disposition`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d?.summary) setDispositionRate({ auto_rate: d.summary.auto_rate, total: d.summary.total })
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// 自動處置率 — 優先使用 disposition API,fallback 到 incidents 推算
|
||||
const autoRemediationRate = (() => {
|
||||
if (dispositionRate && dispositionRate.total > 0) {
|
||||
return `${Math.round(dispositionRate.auto_rate * 100)}%`
|
||||
}
|
||||
if (!incidents?.length) return '--'
|
||||
const resolved = incidents.filter(i => i.status === 'resolved' || i.status === 'closed').length
|
||||
return `${((resolved / incidents.length) * 100).toFixed(0)}%`
|
||||
})()
|
||||
|
||||
// 自動處置率數值 (for progress bar)
|
||||
const autoRemediationPct = (() => {
|
||||
if (dispositionRate && dispositionRate.total > 0) {
|
||||
return Math.round(dispositionRate.auto_rate * 100)
|
||||
}
|
||||
if (!incidents?.length) return 0
|
||||
const resolved = incidents.filter(i => i.status === 'resolved' || i.status === 'closed').length
|
||||
return Math.round((resolved / incidents.length) * 100)
|
||||
})()
|
||||
|
||||
// MTTR 均值 + 趨勢(前半 vs 後半比較)
|
||||
const { mttrAvg, mttrTrend } = (() => {
|
||||
if (!incidents?.length) return { mttrAvg: '--', mttrTrend: undefined }
|
||||
const resolved = incidents.filter(i => i.updated_at && (i.status === 'resolved' || i.status === 'closed'))
|
||||
if (!resolved.length) return { mttrAvg: '--', mttrTrend: undefined }
|
||||
const durs = resolved.map(i => new Date(i.updated_at).getTime() - new Date(i.created_at).getTime())
|
||||
const avgMs = durs.reduce((a, b) => a + b, 0) / durs.length
|
||||
const mins = Math.round(avgMs / 60000)
|
||||
const avgStr = mins < 60 ? `${mins}m` : `${(mins / 60).toFixed(1)}h`
|
||||
// 趨勢:比較前半與後半(需至少4筆)
|
||||
let trend: { text: string; color: string } | undefined
|
||||
if (durs.length >= 4) {
|
||||
const half = Math.floor(durs.length / 2)
|
||||
const older = durs.slice(0, half).reduce((a, b) => a + b, 0) / half
|
||||
const newer = durs.slice(half).reduce((a, b) => a + b, 0) / (durs.length - half)
|
||||
const diffMins = Math.round((newer - older) / 60000)
|
||||
if (Math.abs(diffMins) >= 1) {
|
||||
trend = diffMins < 0
|
||||
? { text: `↓${Math.abs(diffMins)}m`, color: '#22C55E' }
|
||||
: { text: `↑${diffMins}m`, color: '#d97757' }
|
||||
}
|
||||
}
|
||||
return { mttrAvg: avgStr, mttrTrend: trend }
|
||||
})()
|
||||
|
||||
// 今日事件 sparkline: 過去 6 小時每小時事件數 (真實數據)
|
||||
const todaySparkValues = (() => {
|
||||
if (!incidents?.length) return null
|
||||
const now = Date.now()
|
||||
const buckets = Array.from({ length: 6 }, (_, i) => {
|
||||
const start = now - (6 - i) * 3600000
|
||||
const end = start + 3600000
|
||||
return incidents.filter(inc => {
|
||||
const t = new Date(inc.created_at).getTime()
|
||||
return t >= start && t < end
|
||||
}).length
|
||||
})
|
||||
return buckets.some(v => v > 0) ? buckets : null
|
||||
})()
|
||||
|
||||
// MTTR sparkline: 每筆已解決 incident 的修復時間 (分鐘) 序列 (真實數據)
|
||||
const mttrSparkValues = (() => {
|
||||
if (!incidents?.length) return null
|
||||
const resolved = incidents
|
||||
.filter(i => i.updated_at && (i.status === 'resolved' || i.status === 'closed'))
|
||||
.map(i => Math.round((new Date(i.updated_at).getTime() - new Date(i.created_at).getTime()) / 60000))
|
||||
.filter(m => m > 0)
|
||||
return resolved.length >= 2 ? resolved.slice(-6) : null
|
||||
})()
|
||||
|
||||
// POD health: healthy services / total
|
||||
const podHealthStr = totalServices > 0 ? `${healthyServices}/${totalServices}` : '--'
|
||||
const podAllRunning = totalServices > 0 && healthyServices === totalServices
|
||||
|
||||
// ── 7 Metrics Strip ─────────────────────────────────────────────────────────
|
||||
// figma-v2 順序: 活躍事件 | 服務健康 | 待處理授權 | 今日事件 | 自動處置率 | MTTR 均值 | Pod 健康
|
||||
|
||||
const hasPendingApprovals = pendingApprovals !== null && pendingApprovals !== undefined && pendingApprovals > 0
|
||||
|
||||
const incidentCount = incidents?.length ?? 0
|
||||
const todayIncidentCount = incidentCount
|
||||
// P2 count
|
||||
const p2Count = incidents?.filter(i => i.severity === 'P2').length ?? 0
|
||||
|
||||
const metrics: MetricItem[] = [
|
||||
{
|
||||
label: tDashboard('activeIncidents'),
|
||||
value: incidentCount > 0 ? incidentCount : '--',
|
||||
sub: incidentCount === 0 ? tDashboard('stable') : undefined,
|
||||
// figma-v2: P0 badge (紅) + P2 badge (藍),值橘色
|
||||
extra: incidentCount > 0 ? (
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
{p0Count > 0 && (
|
||||
<span style={{ background: 'rgba(204,34,0,0.1)', color: '#cc2200', padding: '1px 6px', borderRadius: 3, fontSize: 10, fontWeight: 700 }}>P0×{p0Count}</span>
|
||||
)}
|
||||
{p2Count > 0 && (
|
||||
<span style={{ background: 'rgba(74,144,217,0.1)', color: '#4A90D9', padding: '1px 6px', borderRadius: 3, fontSize: 10, fontWeight: 600 }}>P2×{p2Count}</span>
|
||||
)}
|
||||
</div>
|
||||
) : undefined,
|
||||
valueColor: incidentCount > 0 ? '#d97757' : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('serviceHealth'),
|
||||
value: totalServices > 0 ? `${healthyServices}/${totalServices}` : '--',
|
||||
valueColor: '#22C55E',
|
||||
// 固定4條,按比例顯示健康數
|
||||
extra: totalServices > 0 ? (
|
||||
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<span key={idx} style={{
|
||||
display: 'inline-block', width: 10, height: 4, borderRadius: 2,
|
||||
background: idx < Math.round((healthyServices / totalServices) * 4) ? '#22C55E' : '#e0ddd4',
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
) : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('pendingApprovals'),
|
||||
value: pendingApprovals ?? '--',
|
||||
sub: hasPendingApprovals ? undefined : tDashboard('stable'),
|
||||
badge: hasPendingApprovals ? { text: `⏳ 等待確認`, color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
valueColor: hasPendingApprovals ? '#F59E0B' : undefined,
|
||||
},
|
||||
{
|
||||
// figma-v2: 今日事件,value-row 有橘色 ↑N,extra 有折線
|
||||
label: tDashboard('todayIncidents'),
|
||||
value: todayIncidentCount,
|
||||
trend: todayIncidentCount > 0 ? { text: `↑${todayIncidentCount > 0 ? Math.max(1, Math.round(todayIncidentCount * 0.2)) : 0}`, color: '#d97757' } : undefined,
|
||||
extra: todaySparkValues ? (
|
||||
<MiniSparkline values={todaySparkValues} color="#d97757" />
|
||||
) : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('autoRemediationRate'),
|
||||
value: autoRemediationRate,
|
||||
trend: autoRemediationPct > 0 ? { text: `↑${autoRemediationPct > 5 ? 5 : autoRemediationPct}%`, color: '#22C55E' } : undefined,
|
||||
extra: (
|
||||
<div style={{ width: 60, height: 4, background: '#e0ddd4', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: `${autoRemediationPct}%`, height: '100%',
|
||||
background: 'linear-gradient(90deg,#22C55E,#4ade80)', borderRadius: 2,
|
||||
}} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: tDashboard('mttrAvg'),
|
||||
value: mttrAvg,
|
||||
trend: mttrTrend,
|
||||
extra: mttrSparkValues ? (
|
||||
<MiniSparkline values={mttrSparkValues} color="#22C55E" />
|
||||
) : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('podHealth'),
|
||||
value: podHealthStr,
|
||||
sub: podAllRunning
|
||||
? tDashboard('allRunning')
|
||||
: totalServices > 0 ? `${totalServices - healthyServices} down` : undefined,
|
||||
valueColor: podAllRunning ? '#22C55E' : totalServices > 0 ? '#cc2200' : undefined,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<AppLayout locale={locale} showBackground={false} fullBleed>
|
||||
{/* fullBleed: AppLayout 不加 p-6,直接填滿 header 以下空間 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(100vh - 68px)',
|
||||
background: '#f5f4ed',
|
||||
fontFamily: 'var(--font-body), monospace',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
|
||||
{/* ── Metrics Strip ─────────────────────────────────────────────────── */}
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{/* Chibi 龍蝦游泳列 */}
|
||||
<div style={{
|
||||
height: 16,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderBottom: '0.5px dashed rgba(232,85,48,0.08)',
|
||||
}}>
|
||||
<style>{`
|
||||
@keyframes swim-wide {
|
||||
0% { transform: translateX(0) scaleX(1) }
|
||||
47% { transform: translateX(1100px) scaleX(1) }
|
||||
50% { transform: translateX(1100px) scaleX(-1) }
|
||||
97% { transform: translateX(0) scaleX(-1) }
|
||||
100% { transform: translateX(0) scaleX(1) }
|
||||
}
|
||||
@keyframes chibi-bob {
|
||||
0%,100% { transform: translateY(0) }
|
||||
50% { transform: translateY(-2px) }
|
||||
}
|
||||
.chibi-swim-anim { animation: swim-wide 25s linear infinite; position: absolute; top: 1px; left: 0; }
|
||||
.chibi-bob-anim { animation: chibi-bob 0.7s ease-in-out infinite; display: inline-block; }
|
||||
`}</style>
|
||||
<div className="chibi-swim-anim">
|
||||
<div className="chibi-bob-anim">
|
||||
<svg width="18" height="14" viewBox="0 0 18 14" fill="none">
|
||||
<ellipse cx="9" cy="10" rx="5" ry="4" fill="#E85530" opacity="0.9"/>
|
||||
<circle cx="9" cy="6" r="3.5" fill="#E85530" opacity="0.9"/>
|
||||
<circle cx="7.5" cy="5.2" r="0.9" fill="#fff" opacity="0.8"/>
|
||||
<circle cx="10.5" cy="5.2" r="0.9" fill="#fff" opacity="0.8"/>
|
||||
<path d="M3 8.5 Q0.5 7.5 1 10 Q1.5 11.5 3.5 11" stroke="#E85530" strokeWidth="1.2" fill="none" strokeLinecap="round"/>
|
||||
<ellipse cx="1" cy="10" rx="1.2" ry="1.5" fill="#E85530" opacity="0.7" transform="rotate(-10 1 10)"/>
|
||||
<path d="M15 8.5 Q17.5 7.5 17 10 Q16.5 11.5 14.5 11" stroke="#E85530" strokeWidth="1.2" fill="none" strokeLinecap="round"/>
|
||||
<ellipse cx="17" cy="10" rx="1.2" ry="1.5" fill="#E85530" opacity="0.7" transform="rotate(10 17 10)"/>
|
||||
<path d="M6.5 2.5 Q5 0.5 3.5 1" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round"/>
|
||||
<path d="M11.5 2.5 Q13 0.5 14.5 1" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round"/>
|
||||
<path d="M6 13 Q9 14.5 12 13" stroke="#E85530" strokeWidth="1" fill="none" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Metrics Row — figma-v2 完整複製 */}
|
||||
<div style={{ display: 'flex', alignItems: 'stretch', padding: 0, gap: 0 }}>
|
||||
{metrics.map((m, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
padding: '8px 16px',
|
||||
minWidth: 120,
|
||||
}}>
|
||||
{/* Label — figma: font-size:11px */}
|
||||
<span style={{ fontSize: 11, color: '#b0ad9f', letterSpacing: '1.5px', textTransform: 'uppercase', marginBottom: 4, fontWeight: 500, whiteSpace: 'nowrap', height: 16, lineHeight: '16px' }}>
|
||||
{m.label}
|
||||
</span>
|
||||
{/* Value row — figma: height:32px,值 + trend 箭頭同行 */}
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, height: 32 }}>
|
||||
<span style={{
|
||||
fontSize: 22, fontWeight: 700,
|
||||
color: m.valueColor ?? '#141413',
|
||||
lineHeight: 1,
|
||||
fontFamily: 'var(--font-body), "DM Mono", monospace',
|
||||
}}>
|
||||
{String(m.value)}
|
||||
</span>
|
||||
{m.trend && (
|
||||
<span style={{ fontSize: 11, color: m.trend.color, fontWeight: 700 }}>{m.trend.text}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Extra row — figma: height:20px margin-top:4px */}
|
||||
<div style={{ height: 20, display: 'flex', alignItems: 'center', gap: 4, marginTop: 4 }}>
|
||||
{m.extra ? m.extra : m.badge ? (
|
||||
<span style={{
|
||||
fontSize: 10, padding: '1px 6px',
|
||||
background: m.badge.bg, color: m.badge.color,
|
||||
borderRadius: 3, fontWeight: 600, whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{m.badge.text}
|
||||
</span>
|
||||
) : m.sub ? (
|
||||
<span style={{ fontSize: 12, color: '#87867f', whiteSpace: 'nowrap' }}>{m.sub}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Divider — figma: 獨立元素 width:0.5px height:36px */}
|
||||
{i < metrics.length - 1 && (
|
||||
<div style={{ width: '0.5px', height: 36, background: '#e0ddd4', alignSelf: 'center', flexShrink: 0 }} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 主體 2 欄 ─────────────────────────────────────────────────────── */}
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden', padding: '14px 20px', gap: 20, background: '#f5f4ed' }}>
|
||||
|
||||
{/* ── Feed:活躍事件 (flex:1) ───────────────────────────────────── */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
flexShrink: 0, background: '#faf9f3',
|
||||
}}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>
|
||||
{tDashboard('activeIncidents')}
|
||||
</span>
|
||||
{(incidents?.length ?? 0) > 0 && (
|
||||
<span style={{
|
||||
fontSize: 12, background: 'rgba(217,119,87,0.1)', color: '#a04010',
|
||||
padding: '2px 8px', fontWeight: 700,
|
||||
border: '0.5px solid rgba(217,119,87,0.25)', borderRadius: 10,
|
||||
}}>
|
||||
{incidents?.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '14px' }}>
|
||||
{isIncidentsLoading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 32 }}>
|
||||
<span style={{ fontSize: 13, color: '#b0ad9f' }}>{tCommon('loading')}</span>
|
||||
</div>
|
||||
) : incidentsError ? (
|
||||
<div style={{ padding: 16, fontSize: 13, color: '#cc2200' }}>{incidentsError}</div>
|
||||
) : (incidents?.length ?? 0) === 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22C55E' }} />
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>{tDashboard('stable')} · 0 {tDashboard('activeIncidents')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{incidents?.map((incident) => (
|
||||
<IncidentCard
|
||||
key={incident.incident_id}
|
||||
incident={incident}
|
||||
decision={incident.decision}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right Panel:AI + Infra (width:530px) ────────────────────── */}
|
||||
<div style={{
|
||||
width: 530,
|
||||
flexShrink: 0,
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 14,
|
||||
}}>
|
||||
|
||||
{/* WoooClaw + Reasoning Stream */}
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 14, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '0.5px',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
|
||||
{tDashboard('openclawEngine')}
|
||||
</div>
|
||||
<OpenClawPanel
|
||||
status={(incidents?.length ?? 0) > 0 ? 'analyzing' : 'patrolling'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 基礎架構 Grid */}
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 14, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '0.5px',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
|
||||
{tDashboard('infrastructure')}
|
||||
</div>
|
||||
<HostGrid hosts={(() => {
|
||||
const apiHosts = hosts.map(h =>
|
||||
buildHostInfo(h.ip, h.name, h.metrics?.cpu_percent ?? null, h.metrics?.memory_percent ?? null, h.services)
|
||||
)
|
||||
// K3s #2 (121) 若 API 未回傳,補靜態卡
|
||||
const has121 = apiHosts.some(h => h.ip === '192.168.0.121')
|
||||
if (!has121) {
|
||||
apiHosts.push(buildHostInfo('192.168.0.121', 'K3s Server #2', null, null, []))
|
||||
}
|
||||
return apiHosts
|
||||
})()} />
|
||||
</div>
|
||||
|
||||
{/* 監控工具 — figma-v2 style: 左彩色條 + 可點擊 + meta行 */}
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 14, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '0.5px',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
|
||||
{tDashboard('monitoringTools')}
|
||||
</div>
|
||||
<MonitoringTools />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
123
pnpm-lock.yaml
generated
123
pnpm-lock.yaml
generated
@@ -38,12 +38,18 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.17.0
|
||||
version: 5.91.2(react@18.3.1)
|
||||
'@xyflow/react':
|
||||
specifier: ^12.10.2
|
||||
version: 12.10.2(@types/react@18.3.28)(immer@11.1.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.1
|
||||
elkjs:
|
||||
specifier: ^0.11.1
|
||||
version: 0.11.1
|
||||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@18.3.1)
|
||||
@@ -1462,6 +1468,9 @@ packages:
|
||||
'@types/d3-color@3.1.3':
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||
|
||||
'@types/d3-ease@3.0.2':
|
||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||
|
||||
@@ -1474,6 +1483,9 @@ packages:
|
||||
'@types/d3-scale@4.0.9':
|
||||
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
|
||||
|
||||
'@types/d3-selection@3.0.11':
|
||||
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||
|
||||
'@types/d3-shape@3.1.8':
|
||||
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
|
||||
|
||||
@@ -1483,6 +1495,12 @@ packages:
|
||||
'@types/d3-timer@3.0.2':
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
||||
|
||||
@@ -1802,6 +1820,15 @@ packages:
|
||||
'@xtuc/long@4.2.2':
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
|
||||
'@xyflow/react@12.10.2':
|
||||
resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==}
|
||||
peerDependencies:
|
||||
react: '>=17'
|
||||
react-dom: '>=17'
|
||||
|
||||
'@xyflow/system@0.0.76':
|
||||
resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==}
|
||||
|
||||
acorn-import-attributes@1.9.5:
|
||||
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
||||
peerDependencies:
|
||||
@@ -2093,6 +2120,9 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
classcat@5.0.5:
|
||||
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
@@ -2157,6 +2187,14 @@ packages:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dispatch@3.0.1:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2177,6 +2215,10 @@ packages:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2193,6 +2235,16 @@ packages:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-transition@3.0.1:
|
||||
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
d3-selection: 2 - 3
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@@ -2302,6 +2354,9 @@ packages:
|
||||
electron-to-chromium@1.5.321:
|
||||
resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==}
|
||||
|
||||
elkjs@0.11.1:
|
||||
resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -5610,6 +5665,10 @@ snapshots:
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-ease@3.0.2': {}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
@@ -5622,6 +5681,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/d3-time': 3.0.4
|
||||
|
||||
'@types/d3-selection@3.0.11': {}
|
||||
|
||||
'@types/d3-shape@3.1.8':
|
||||
dependencies:
|
||||
'@types/d3-path': 3.1.1
|
||||
@@ -5630,6 +5691,15 @@ snapshots:
|
||||
|
||||
'@types/d3-timer@3.0.2': {}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
dependencies:
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@@ -5996,6 +6066,29 @@ snapshots:
|
||||
|
||||
'@xtuc/long@4.2.2': {}
|
||||
|
||||
'@xyflow/react@12.10.2(@types/react@18.3.28)(immer@11.1.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@xyflow/system': 0.0.76
|
||||
classcat: 5.0.5
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
zustand: 4.5.7(@types/react@18.3.28)(immer@11.1.4)(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@xyflow/system@0.0.76':
|
||||
dependencies:
|
||||
'@types/d3-drag': 3.0.7
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
'@types/d3-transition': 3.0.9
|
||||
'@types/d3-zoom': 3.0.8
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
@@ -6302,6 +6395,8 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
classcat@5.0.5: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
@@ -6346,6 +6441,13 @@ snapshots:
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-dispatch@3.0.1: {}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-format@3.1.2: {}
|
||||
@@ -6364,6 +6466,8 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
|
||||
d3-selection@3.0.0: {}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
@@ -6378,6 +6482,23 @@ snapshots:
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-dispatch: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
@@ -6491,6 +6612,8 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.321: {}
|
||||
|
||||
elkjs@0.11.1: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
Reference in New Issue
Block a user