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:
OG T
2026-04-08 18:07:59 +08:00
parent 39499c6be3
commit 11ff517406
3 changed files with 944 additions and 0 deletions

View File

@@ -14,8 +14,10 @@
"@awoooi/shared-types": "workspace:^", "@awoooi/shared-types": "workspace:^",
"@sentry/nextjs": "^10.45.0", "@sentry/nextjs": "^10.45.0",
"@tanstack/react-query": "^5.17.0", "@tanstack/react-query": "^5.17.0",
"@xyflow/react": "^12.10.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"elkjs": "^0.11.1",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"next": "14.1.0", "next": "14.1.0",
"next-intl": "^4.8.3", "next-intl": "^4.8.3",

View 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 APIfallback 到 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 有橘色 ↑Nextra 有折線
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 PanelAI + 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
View File

@@ -38,12 +38,18 @@ importers:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.17.0 specifier: ^5.17.0
version: 5.91.2(react@18.3.1) 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: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
clsx: clsx:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.1 version: 2.1.1
elkjs:
specifier: ^0.11.1
version: 0.11.1
lucide-react: lucide-react:
specifier: ^0.577.0 specifier: ^0.577.0
version: 0.577.0(react@18.3.1) version: 0.577.0(react@18.3.1)
@@ -1462,6 +1468,9 @@ packages:
'@types/d3-color@3.1.3': '@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-ease@3.0.2': '@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
@@ -1474,6 +1483,9 @@ packages:
'@types/d3-scale@4.0.9': '@types/d3-scale@4.0.9':
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} 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': '@types/d3-shape@3.1.8':
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
@@ -1483,6 +1495,12 @@ packages:
'@types/d3-timer@3.0.2': '@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} 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': '@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
@@ -1802,6 +1820,15 @@ packages:
'@xtuc/long@4.2.2': '@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 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: acorn-import-attributes@1.9.5:
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
peerDependencies: peerDependencies:
@@ -2093,6 +2120,9 @@ packages:
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} 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: client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@@ -2157,6 +2187,14 @@ packages:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'} 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: d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2177,6 +2215,10 @@ packages:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-shape@3.2.0: d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2193,6 +2235,16 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'} 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: damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -2302,6 +2354,9 @@ packages:
electron-to-chromium@1.5.321: electron-to-chromium@1.5.321:
resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==}
elkjs@0.11.1:
resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -5610,6 +5665,10 @@ snapshots:
'@types/d3-color@3.1.3': {} '@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-ease@3.0.2': {}
'@types/d3-interpolate@3.0.4': '@types/d3-interpolate@3.0.4':
@@ -5622,6 +5681,8 @@ snapshots:
dependencies: dependencies:
'@types/d3-time': 3.0.4 '@types/d3-time': 3.0.4
'@types/d3-selection@3.0.11': {}
'@types/d3-shape@3.1.8': '@types/d3-shape@3.1.8':
dependencies: dependencies:
'@types/d3-path': 3.1.1 '@types/d3-path': 3.1.1
@@ -5630,6 +5691,15 @@ snapshots:
'@types/d3-timer@3.0.2': {} '@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': '@types/debug@4.1.13':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
@@ -5996,6 +6066,29 @@ snapshots:
'@xtuc/long@4.2.2': {} '@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): acorn-import-attributes@1.9.5(acorn@8.16.0):
dependencies: dependencies:
acorn: 8.16.0 acorn: 8.16.0
@@ -6302,6 +6395,8 @@ snapshots:
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
classcat@5.0.5: {}
client-only@0.0.1: {} client-only@0.0.1: {}
clsx@2.1.1: {} clsx@2.1.1: {}
@@ -6346,6 +6441,13 @@ snapshots:
d3-color@3.1.0: {} 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-ease@3.0.1: {}
d3-format@3.1.2: {} d3-format@3.1.2: {}
@@ -6364,6 +6466,8 @@ snapshots:
d3-time: 3.1.0 d3-time: 3.1.0
d3-time-format: 4.1.0 d3-time-format: 4.1.0
d3-selection@3.0.0: {}
d3-shape@3.2.0: d3-shape@3.2.0:
dependencies: dependencies:
d3-path: 3.1.0 d3-path: 3.1.0
@@ -6378,6 +6482,23 @@ snapshots:
d3-timer@3.0.1: {} 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: {} damerau-levenshtein@1.0.8: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
@@ -6491,6 +6612,8 @@ snapshots:
electron-to-chromium@1.5.321: {} electron-to-chromium@1.5.321: {}
elkjs@0.11.1: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}