From 11ff51740613c2ad568dc13a56c9ab4cfd8e1062 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 8 Apr 2026 18:07:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20Sprint=205=20Phase=200=20=E2=80=94?= =?UTF-8?q?=20=E5=AE=89=E8=A3=9D=20React=20Flow=20+=20elkjs=20+=20?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E7=B6=93=E5=85=B8=E9=A6=96=E9=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/web/package.json | 2 + apps/web/src/app/[locale]/classic/page.tsx | 819 +++++++++++++++++++++ pnpm-lock.yaml | 123 ++++ 3 files changed, 944 insertions(+) create mode 100644 apps/web/src/app/[locale]/classic/page.tsx diff --git a/apps/web/package.json b/apps/web/package.json index fa426a4b..87219531 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/src/app/[locale]/classic/page.tsx b/apps/web/src/app/[locale]/classic/page.tsx new file mode 100644 index 00000000..095e0b0d --- /dev/null +++ b/apps/web/src/app/[locale]/classic/page.tsx @@ -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 ( + + + + ) +} + +// ============================================================================= +// 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 = { + Grafana: '#F59E0B', + Prometheus: '#E85530', + Sentry: '#7B52BF', + Langfuse: '#0077CC', + SigNoz: '#4A90D9', + Gitea: '#22C55E', +} + +// 圖示 emoji +const TOOL_EMOJI: Record = { + Grafana: '📊', + Prometheus: '🔥', + Sentry: '🔭', + Langfuse: '🧪', + SigNoz: '🔭', + Gitea: '🐙', +} + +function MonitoringTools() { + const tDash = useTranslations('dashboard') + const tCommon = useTranslations('common') + const [tools, setTools] = useState([]) + 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 ( +
{tCommon('loading')}
+ ) + if (tools.length === 0) return ( +
{tDash('connectionError')}
+ ) + + return ( +
+ {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 ( + + {/* 左側彩色條 */} + + ) +} + +// ============================================================================= +// Main Page +// ============================================================================= + +// ============================================================================= +// Static Host Service Catalog +// 定義每台主機完整服務清單(API 只回傳部分,此處補全靜態資訊) +// ============================================================================= + +const HOST_CATALOG: Record = { + '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 ? ( +
+ {p0Count > 0 && ( + P0×{p0Count} + )} + {p2Count > 0 && ( + P2×{p2Count} + )} +
+ ) : undefined, + valueColor: incidentCount > 0 ? '#d97757' : undefined, + }, + { + label: tDashboard('serviceHealth'), + value: totalServices > 0 ? `${healthyServices}/${totalServices}` : '--', + valueColor: '#22C55E', + // 固定4條,按比例顯示健康數 + extra: totalServices > 0 ? ( +
+ {Array.from({ length: 4 }).map((_, idx) => ( + + ))} +
+ ) : 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 ? ( + + ) : undefined, + }, + { + label: tDashboard('autoRemediationRate'), + value: autoRemediationRate, + trend: autoRemediationPct > 0 ? { text: `↑${autoRemediationPct > 5 ? 5 : autoRemediationPct}%`, color: '#22C55E' } : undefined, + extra: ( +
+
+
+ ), + }, + { + label: tDashboard('mttrAvg'), + value: mttrAvg, + trend: mttrTrend, + extra: mttrSparkValues ? ( + + ) : undefined, + }, + { + label: tDashboard('podHealth'), + value: podHealthStr, + sub: podAllRunning + ? tDashboard('allRunning') + : totalServices > 0 ? `${totalServices - healthyServices} down` : undefined, + valueColor: podAllRunning ? '#22C55E' : totalServices > 0 ? '#cc2200' : undefined, + }, + ] + + return ( + + {/* fullBleed: AppLayout 不加 p-6,直接填滿 header 以下空間 */} +
+ + {/* ── Metrics Strip ─────────────────────────────────────────────────── */} +
+ {/* Chibi 龍蝦游泳列 */} +
+ +
+
+ + + + + + + + + + + + + +
+
+
+ {/* Metrics Row — figma-v2 完整複製 */} +
+ {metrics.map((m, i) => ( + +
+ {/* Label — figma: font-size:11px */} + + {m.label} + + {/* Value row — figma: height:32px,值 + trend 箭頭同行 */} +
+ + {String(m.value)} + + {m.trend && ( + {m.trend.text} + )} +
+ {/* Extra row — figma: height:20px margin-top:4px */} +
+ {m.extra ? m.extra : m.badge ? ( + + {m.badge.text} + + ) : m.sub ? ( + {m.sub} + ) : null} +
+
+ {/* Divider — figma: 獨立元素 width:0.5px height:36px */} + {i < metrics.length - 1 && ( +
+ )} + + ))} +
+
+ + {/* ── 主體 2 欄 ─────────────────────────────────────────────────────── */} +
+ + {/* ── Feed:活躍事件 (flex:1) ───────────────────────────────────── */} +
+
+
+ + {tDashboard('activeIncidents')} + + {(incidents?.length ?? 0) > 0 && ( + + {incidents?.length} + + )} +
+ +
+ {isIncidentsLoading ? ( +
+ {tCommon('loading')} +
+ ) : incidentsError ? ( +
{incidentsError}
+ ) : (incidents?.length ?? 0) === 0 ? ( +
+
+ {tDashboard('stable')} · 0 {tDashboard('activeIncidents')} +
+ ) : ( +
+ {incidents?.map((incident) => ( + + ))} +
+ )} +
+
+ + {/* ── Right Panel:AI + Infra (width:530px) ────────────────────── */} +
+ + {/* WoooClaw + Reasoning Stream */} +
+
+
+ {tDashboard('openclawEngine')} +
+ 0 ? 'analyzing' : 'patrolling'} + /> +
+ + {/* 基礎架構 Grid */} +
+
+
+ {tDashboard('infrastructure')} +
+ { + 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 + })()} /> +
+ + {/* 監控工具 — figma-v2 style: 左彩色條 + 可點擊 + meta行 */} +
+
+
+ {tDashboard('monitoringTools')} +
+ +
+ +
+
+
+ + ) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12c9e2ef..add30382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}