All checks were successful
Beta Release / beta (push) Successful in 46s
- Rewrite dashboard from 4 tabs to single grid view with 5s auto-refresh - Add live CPU/RAM/Network SVG graphs with rolling 30-point history - Add backend /api/system/metrics reading /proc/stat, /proc/meminfo, /proc/net/dev - Add backend /api/providers/quota for MiniMax and Z.AI quota monitoring - Add backend /api/recent-commands reading bash/zsh history - Add backend /api/running-processes filtering editors/IDEs/languages - Add sudo/root indicator (⚡ ROOT) in footer when running as root - Remove duplicate Ctrl+1-4 shortcut from page-specific footer (keep only right side) - Add Ctrl+R shortcut on dashboard for metrics-only refresh - Make API key mandatory in onboarding, auto-scan editors via AI chat - Remove manual editor input, only show AI-detected editors - Bump version to 0.3.3 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
203 lines
8.3 KiB
JavaScript
203 lines
8.3 KiB
JavaScript
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
import { useI18n } from '../i18n'
|
|
|
|
const MAX_POINTS = 30
|
|
|
|
function MiniGraph({ data, max, color, label, unit }) {
|
|
if (!data || data.length < 2) return <div className="dash-graph-empty">collecting...</div>
|
|
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 (
|
|
<div className="dash-graph-wrap">
|
|
<div className="dash-graph-header">
|
|
<span className="dash-graph-label">{label}</span>
|
|
<span className="dash-graph-value" style={{ color }}>{last.toFixed(1)}{unit}</span>
|
|
</div>
|
|
<svg viewBox={`0 0 ${w} ${h}`} className="dash-graph-svg" preserveAspectRatio="none">
|
|
<polyline fill="none" stroke={color} strokeWidth="1.5" points={points} vectorEffect="non-scaling-stroke" />
|
|
</svg>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function Dashboard({ api, refreshRef }) {
|
|
const { t } = useI18n()
|
|
const [dashboardStatus, setDashboardStatus] = useState(null)
|
|
const [quota, setQuota] = useState(null)
|
|
const [recentCmds, setRecentCmds] = useState([])
|
|
const [processes, setProcesses] = useState([])
|
|
const [metrics, setMetrics] = useState(null)
|
|
const cpuRef = useRef([])
|
|
const memRef = useRef([])
|
|
const netRxRef = useRef([])
|
|
const netTxRef = useRef([])
|
|
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
const [dashData, quotaData, cmdData, procData, metricsData] = await Promise.all([
|
|
api.getDashboardStatus().catch(() => null),
|
|
api.getProvidersQuota().catch(() => null),
|
|
api.getRecentCommands().catch(() => ({ commands: [] })),
|
|
api.getRunningProcesses().catch(() => ({ processes: [] })),
|
|
api.getSystemMetrics().catch(() => null),
|
|
])
|
|
setDashboardStatus(dashData)
|
|
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
|
|
const iv = setInterval(loadData, 5000)
|
|
return () => clearInterval(iv)
|
|
}, [loadData, refreshRef])
|
|
|
|
const minimax = (quota || []).find(p => p.name === 'minimax')
|
|
const zai = (quota || []).find(p => p.name === 'zai')
|
|
|
|
return (
|
|
<div className="dash-grid">
|
|
{/* CPU / RAM / Network Graphs */}
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">CPU</span>
|
|
<span className="dash-count">{metrics ? metrics.cpu_percent.toFixed(0) : '—'}%</span>
|
|
</div>
|
|
<MiniGraph data={cpuRef.current} max={100} color="var(--accent)" label="CPU" unit="%" />
|
|
</div>
|
|
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">RAM</span>
|
|
<span className="dash-count">{metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)} MB` : '—'}</span>
|
|
</div>
|
|
<MiniGraph data={memRef.current} max={100} color="#a78bfa" label="RAM" unit="%" />
|
|
</div>
|
|
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">Network</span>
|
|
<span className="dash-count">{metrics ? `↓${metrics.net_rx_kbs.toFixed(0)} ↑${metrics.net_tx_kbs.toFixed(0)} KB/s` : '—'}</span>
|
|
</div>
|
|
<MiniGraph data={netRxRef.current} max={null} color="#34d399" label="RX" unit=" KB/s" />
|
|
<MiniGraph data={netTxRef.current} max={null} color="#f59e0b" label="TX" unit=" KB/s" />
|
|
</div>
|
|
|
|
{/* API Quota */}
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">API Quota</span>
|
|
</div>
|
|
<div className="dash-quota-list">
|
|
{minimax && minimax.data?.models?.map((m, i) => (
|
|
<div key={i} className="dash-quota-row">
|
|
<span className="dash-quota-name">{String(m.model).replace('MiniMax-', '')}</span>
|
|
<div className="dash-bar">
|
|
<div className="dash-bar-fill" style={{ width: `${Math.min(100, (m.used / m.total) * 100)}%` }} />
|
|
</div>
|
|
<span className="dash-quota-val">{m.remaining}/{m.total}</span>
|
|
</div>
|
|
))}
|
|
{minimax && minimax.data?.models?.length === 0 && (
|
|
<div className="dash-quota-row">
|
|
<span className="dash-quota-name">MiniMax</span>
|
|
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{minimax.error || 'no data'}</span>
|
|
</div>
|
|
)}
|
|
{zai && (
|
|
<div className="dash-quota-row">
|
|
<span className="dash-quota-name">Z.AI</span>
|
|
<span className="dash-quota-val">{zai.healthy ? '✓ active' : zai.error || '—'}</span>
|
|
</div>
|
|
)}
|
|
{!minimax && !zai && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Running Processes */}
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">Running Processes</span>
|
|
<span className="dash-count">{processes.length}</span>
|
|
</div>
|
|
<div className="dash-proc-list">
|
|
{processes.length === 0 && <span className="dash-empty">No relevant processes</span>}
|
|
{processes.slice(0, 8).map((p, i) => (
|
|
<div key={i} className="dash-proc-row">
|
|
<span className="dash-proc-name">{p.name}</span>
|
|
<span className="dash-proc-res">cpu {p.cpu}% · mem {p.mem}%</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Commands */}
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">Recent Commands</span>
|
|
</div>
|
|
<div className="dash-cmd-list">
|
|
{recentCmds.length === 0 && <span className="dash-empty">No history</span>}
|
|
{recentCmds.slice(0, 8).map((c, i) => (
|
|
<div key={i} className="dash-cmd-row" title={c.cmd}>
|
|
<span className="dash-cmd-shell">{c.shell}</span>
|
|
<span className="dash-cmd-text">{c.cmd.length > 45 ? c.cmd.slice(0, 42) + '...' : c.cmd}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Services */}
|
|
<div className="dash-card">
|
|
<div className="dash-card-head">
|
|
<span className="dash-label">Services</span>
|
|
</div>
|
|
{dashboardStatus ? (
|
|
<div className="dash-services">
|
|
<div className="dash-svc-row">
|
|
<span className="dash-svc-name">MCP</span>
|
|
<span className="dash-svc-val">{dashboardStatus.mcp?.healthy || 0}/{dashboardStatus.mcp?.total || 0} healthy</span>
|
|
</div>
|
|
<div className="dash-svc-row">
|
|
<span className="dash-svc-name">LSP</span>
|
|
<span className="dash-svc-val">{dashboardStatus.lsp?.installed || 0}/{dashboardStatus.lsp?.total || 0} installed</span>
|
|
</div>
|
|
<div className="dash-svc-row">
|
|
<span className="dash-svc-name">Skills</span>
|
|
<span className="dash-svc-val">{dashboardStatus.skills?.total || 0} deployed</span>
|
|
</div>
|
|
{(dashboardStatus.skills?.issues || []).length > 0 && (
|
|
<div className="dash-svc-issues">
|
|
{(dashboardStatus.skills.issues || []).slice(0, 3).map((issue, i) => (
|
|
<div key={i} className="dash-svc-issue">⚠ {issue}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span className="dash-empty">Loading...</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|