feat(dashboard): add background graphs to cards and improve layout
All checks were successful
Beta Release / beta (push) Successful in 39s

- Add BgGraph component for subtle background SVG graphs
- Add gradient fills to MiniGraph components
- Track process count over time
- Calculate total API quota usage
- Improve card styling with overlay content

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-23 19:55:10 +02:00
parent 79d082180c
commit bb03c9fe2d
2 changed files with 122 additions and 92 deletions

View File

@@ -3,6 +3,32 @@ import { useI18n } from '../i18n'
const MAX_POINTS = 30
function BgGraph({ data, max, color }) {
if (!data || data.length < 2) return null
const m = max || Math.max(...data, 1)
const w = 120
const h = 60
const points = data.map((v, i) => {
const x = (i / (data.length - 1)) * w
const y = h - (v / m) * h
return `${x},${y}`
})
const area = `${points.join(' ')} ${w},${h} 0,${h}`
const line = points.join(' ')
return (
<svg viewBox={`0 0 ${w} ${h}`} className="dash-bg-graph" preserveAspectRatio="none">
<defs>
<linearGradient id={`g-${color.replace('#','')}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.25" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
<polygon fill={`url(#g-${color.replace('#','')})`} points={area} />
<polyline fill="none" stroke={color} strokeWidth="1.5" points={line} vectorEffect="non-scaling-stroke" opacity="0.6" />
</svg>
)
}
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)
@@ -21,6 +47,13 @@ function MiniGraph({ data, max, color, label, unit }) {
<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">
<defs>
<linearGradient id={`fg-${color.replace('#','').replace('var(','').replace(')','')}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
<stop offset="100%" stopColor={color} stopOpacity="0.02" />
</linearGradient>
</defs>
<polygon fill={`url(#fg-${color.replace('#','').replace('var(','').replace(')','')})`} points={`${points} ${w},${h} 0,${h}`} />
<polyline fill="none" stroke={color} strokeWidth="1.5" points={points} vectorEffect="non-scaling-stroke" />
</svg>
</div>
@@ -29,7 +62,6 @@ function MiniGraph({ data, max, color, label, unit }) {
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([])
@@ -38,17 +70,16 @@ export default function Dashboard({ api, refreshRef }) {
const memRef = useRef([])
const netRxRef = useRef([])
const netTxRef = useRef([])
const procCountRef = useRef([])
const loadData = useCallback(async () => {
try {
const [dashData, quotaData, cmdData, procData, metricsData] = await Promise.all([
api.getDashboardStatus().catch(() => null),
const [quotaData, cmdData, procData, metricsData] = await Promise.all([
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 || [])
@@ -59,6 +90,7 @@ export default function Dashboard({ api, refreshRef }) {
netRxRef.current = [...netRxRef.current, metricsData.net_rx_kbs].slice(-MAX_POINTS)
netTxRef.current = [...netTxRef.current, metricsData.net_tx_kbs].slice(-MAX_POINTS)
}
procCountRef.current = [...procCountRef.current, procData.processes?.length || 0].slice(-MAX_POINTS)
} catch (err) {
console.error('Dashboard load error:', err)
}
@@ -73,37 +105,52 @@ export default function Dashboard({ api, refreshRef }) {
const minimax = (quota || []).find(p => p.name === 'minimax')
const zai = (quota || []).find(p => p.name === 'zai')
const totalQuotaUsed = minimax?.data?.models?.reduce((s, m) => s + (m.used || 0), 0) || 0
const totalQuotaMax = minimax?.data?.models?.reduce((s, m) => s + (m.total || 0), 0) || 1
return (
<div className="dash-grid">
{/* CPU / RAM / Network Graphs */}
<div className="dash-card">
{/* CPU */}
<div className="dash-card dash-card-graph">
<BgGraph data={cpuRef.current} max={100} color="#06b6d4" />
<div className="dash-card-content">
<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>
<div className="dash-card">
{/* RAM */}
<div className="dash-card dash-card-graph">
<BgGraph data={memRef.current} max={100} color="#a78bfa" />
<div className="dash-card-content">
<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>
<span className="dash-count">{metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)}` : '—'}</span>
</div>
<MiniGraph data={memRef.current} max={100} color="#a78bfa" label="RAM" unit="%" />
</div>
</div>
<div className="dash-card">
{/* Network */}
<div className="dash-card dash-card-graph">
<BgGraph data={netRxRef.current} max={null} color="#34d399" />
<div className="dash-card-content">
<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>
<span className="dash-count">{metrics ? `${metrics.net_rx_kbs.toFixed(0)}${metrics.net_tx_kbs.toFixed(0)}` : '—'}</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>
</div>
{/* API Quota */}
<div className="dash-card">
<div className="dash-card dash-card-graph">
<BgGraph data={totalQuotaMax > 0 ? [totalQuotaUsed / totalQuotaMax * 100, ...(cpuRef.current.length > 0 ? [] : [0])] : []} max={100} color="#f472b6" />
<div className="dash-card-content">
<div className="dash-card-head">
<span className="dash-label">API Quota</span>
</div>
@@ -132,16 +179,19 @@ export default function Dashboard({ api, refreshRef }) {
{!minimax && !zai && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
</div>
</div>
</div>
{/* Running Processes */}
<div className="dash-card">
<div className="dash-card dash-card-graph">
<BgGraph data={procCountRef.current} max={null} color="#fb923c" />
<div className="dash-card-content">
<div className="dash-card-head">
<span className="dash-label">Running Processes</span>
<span className="dash-label">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) => (
{processes.slice(0, 6).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>
@@ -149,6 +199,7 @@ export default function Dashboard({ api, refreshRef }) {
))}
</div>
</div>
</div>
{/* Recent Commands */}
<div className="dash-card">
@@ -165,38 +216,6 @@ export default function Dashboard({ api, refreshRef }) {
))}
</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>
)
}

View File

@@ -541,11 +541,22 @@ input::placeholder { color: var(--text-disabled); }
overflow: hidden;
}
.dash-card {
position: relative;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: 14px 16px;
display: flex; flex-direction: column; gap: 8px;
overflow: hidden;
}
.dash-card-graph { padding: 0; }
.dash-bg-graph {
position: absolute; inset: 0; width: 100%; height: 100%;
opacity: 0.35; pointer-events: none;
}
.dash-card-content {
position: relative; z-index: 1;
padding: 14px 16px;
display: flex; flex-direction: column; gap: 8px;
}
.dash-span-2 { grid-column: span 2; }
.dash-card-head {
display: flex; align-items: center; justify-content: space-between;