import { useState, useEffect, useCallback, useRef } from 'react' import { useI18n } from '../i18n' const MAX_POINTS = 30 const POLL_INTERVAL = 5000 const MAX_IDLE_POLLS = 3 function MiniGraph({ data, max, color, label, unit }) { if (!data || data.length < 2) return
collecting...
const m = max || Math.max(...data, 1) const w = 100 const h = 32 const points = data.map((v, i) => { const x = (i / (data.length - 1)) * w const y = h - (v / m) * h return `${x},${y}` }).join(' ') const last = data[data.length - 1] return (
{label} {last.toFixed(1)}{unit}
) } export default function Dashboard({ api, refreshRef }) { const { t } = useI18n() const [quota, setQuota] = useState(null) const [recentCmds, setRecentCmds] = useState([]) const [processes, setProcesses] = useState([]) const [metrics, setMetrics] = useState(null) const [copiedSet, setCopiedSet] = useState(new Set()) const cpuRef = useRef([]) const memRef = useRef([]) const netRxRef = useRef([]) const netTxRef = useRef([]) const loadData = useCallback(async () => { try { const [quotaData, cmdData, procData, metricsData] = await Promise.all([ api.getProvidersQuota().catch(() => null), api.getRecentCommands().catch(() => ({ commands: [] })), api.getRunningProcesses().catch(() => ({ processes: [] })), api.getSystemMetrics().catch(() => null), ]) setQuota(quotaData?.providers || []) setRecentCmds(cmdData.commands || []) setProcesses(procData.processes || []) if (metricsData) { setMetrics(metricsData) cpuRef.current = [...cpuRef.current, metricsData.cpu_percent].slice(-MAX_POINTS) memRef.current = [...memRef.current, metricsData.mem_percent].slice(-MAX_POINTS) netRxRef.current = [...netRxRef.current, metricsData.net_rx_kbs].slice(-MAX_POINTS) netTxRef.current = [...netTxRef.current, metricsData.net_tx_kbs].slice(-MAX_POINTS) } } catch (err) { console.error('Dashboard load error:', err) } }, [api]) useEffect(() => { loadData() if (refreshRef) refreshRef.current = loadData let active = true let idleTicks = 0 const iv = setInterval(() => { const hidden = document.querySelector('.dash-grid')?.closest('.tab-hidden') if (hidden) { idleTicks++ if (idleTicks >= MAX_IDLE_POLLS) return } else { idleTicks = 0 } if (active) loadData() }, POLL_INTERVAL) return () => { active = false; clearInterval(iv) } }, [loadData, refreshRef]) const minimax = (quota || []).find(p => p.name === 'minimax') const mimo = (quota || []).find(p => p.name === 'mimo') const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history', 'cat', 'echo', 'grep', 'export', 'alias', 'unalias', 'set', 'unset', 'source', '.', 'fg', 'bg', 'jobs', 'wait', 'true', 'false', 'yes', 'sleep', 'date', 'whoami', 'id', 'uname', 'hostname', 'uptime', 'df', 'free', 'top', 'htop', 'nano', 'vi', 'vim', 'less', 'more', 'tail', 'head', 'man', 'info', 'which', 'whereis', 'type', 'command', 'hash', 'builtin', 'help'] const topCmds = (() => { const counts = {} for (const c of recentCmds) { const base = c.cmd.split(/\s+/)[0] if (!base || base.length < 2 || EXCLUDE_CMDS.includes(base)) continue if (!/^[a-zA-Z@.\/]/.test(base)) continue counts[base] = (counts[base] || 0) + 1 } return Object.entries(counts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([cmd, count]) => ({ cmd, count })) })() const maxCount = topCmds.length > 0 ? topCmds[0].count : 1 const copyCmd = (cmd, key) => { navigator.clipboard.writeText(cmd) setCopiedSet(prev => new Set(prev).add(key)) setTimeout(() => setCopiedSet(prev => { const next = new Set(prev); next.delete(key); return next }), 1500) } const relativeTime = (ts) => { if (!ts) return '' const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000) if (diff < 60) return `${diff}s` if (diff < 3600) return `${Math.floor(diff / 60)}m` if (diff < 86400) return `${Math.floor(diff / 3600)}h` return `${Math.floor(diff / 86400)}d` } const recentUnique = (() => { const seen = new Set() return recentCmds.filter(c => { if (seen.has(c.cmd)) return false seen.add(c.cmd) return true }) })() return (
{/* CPU */}
CPU {metrics ? metrics.cpu_percent.toFixed(0) : '—'}%
{/* RAM */}
RAM {metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)}` : '—'}
{/* Network */}
Network {metrics ? `↓${metrics.net_rx_kbs.toFixed(0)} ↑${metrics.net_tx_kbs.toFixed(0)}` : '—'}
{/* API Quota */}
API Quota
{minimax && minimax.data?.models?.map((m, i) => (
{String(m.model).replace('MiniMax-', '')}
{m.used}/{m.total}
))} {minimax && minimax.data?.models?.length === 0 && (
MiniMax {minimax.error || 'no data'}
)} {mimo && mimo.data?.models?.map((m, i) => (
{String(m.model).replace('MiMo-', '')}
{m.used}/{m.total}
))} {mimo && !mimo.data?.models?.length && (
MiMo {mimo.error || (mimo.healthy ? '✓ configured' : 'no key')}
)} {!minimax && !mimo && No providers}
{/* Running Processes */}
Processes {processes.length}
{processes.length === 0 && No relevant processes} {processes.map((p, i) => (
{p.name} cpu {p.cpu}% · mem {p.mem}%
))}
{/* Recent Commands */}
Recent Commands {recentUnique.length}
{topCmds.length > 0 && (
Most used {topCmds.map((c, i) => (
copyCmd(c.cmd, `top-${i}`)} title={c.cmd}> {copiedSet.has(`top-${i}`) ? '✓ Copié' : c.cmd}
{c.count}×
))}
)}
{recentUnique.length === 0 && No history} {recentUnique.map((c, i) => (
copyCmd(c.cmd, `list-${i}`)} title={c.cmd + ' · click to copy'}>
{c.cmd.length > 38 ? c.cmd.slice(0, 35) + '...' : c.cmd} {relativeTime(c.ts)}
{copiedSet.has(`list-${i}`) ? '✓' : '⎘'}
))}
) }