From bb03c9fe2d75e5fec1bf18afc405d93af440d602 Mon Sep 17 00:00:00 2001 From: Augustin Date: Thu, 23 Apr 2026 19:55:10 +0200 Subject: [PATCH] feat(dashboard): add background graphs to cards and improve layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/src/components/Dashboard.jsx | 203 +++++++++++++++++-------------- web/src/styles/global.css | 11 ++ 2 files changed, 122 insertions(+), 92 deletions(-) diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx index 9be3e84..ce2ad94 100644 --- a/web/src/components/Dashboard.jsx +++ b/web/src/components/Dashboard.jsx @@ -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 ( + + + + + + + + + + + ) +} + function MiniGraph({ data, max, color, label, unit }) { if (!data || data.length < 2) return
collecting...
const m = max || Math.max(...data, 1) @@ -21,6 +47,13 @@ function MiniGraph({ data, max, color, label, unit }) { {last.toFixed(1)}{unit} + + + + + + + @@ -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,80 +105,99 @@ 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 (
- {/* CPU / RAM / Network Graphs */} -
-
- CPU - {metrics ? metrics.cpu_percent.toFixed(0) : 'โ€”'}% + {/* CPU */} +
+ +
+
+ CPU + {metrics ? metrics.cpu_percent.toFixed(0) : 'โ€”'}% +
+
-
-
-
- RAM - {metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)} MB` : 'โ€”'} + {/* RAM */} +
+ +
+
+ RAM + {metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)}` : 'โ€”'} +
+
-
-
-
- Network - {metrics ? `โ†“${metrics.net_rx_kbs.toFixed(0)} โ†‘${metrics.net_tx_kbs.toFixed(0)} KB/s` : 'โ€”'} + {/* 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-', '')} -
-
+
+ 0 ? [totalQuotaUsed / totalQuotaMax * 100, ...(cpuRef.current.length > 0 ? [] : [0])] : []} max={100} color="#f472b6" /> +
+
+ API Quota +
+
+ {minimax && minimax.data?.models?.map((m, i) => ( +
+ {String(m.model).replace('MiniMax-', '')} +
+
+
+ {m.remaining}/{m.total}
- {m.remaining}/{m.total} -
- ))} - {minimax && minimax.data?.models?.length === 0 && ( -
- MiniMax - {minimax.error || 'no data'} -
- )} - {zai && ( -
- Z.AI - {zai.healthy ? 'โœ“ active' : zai.error || 'โ€”'} -
- )} - {!minimax && !zai && No providers} + ))} + {minimax && minimax.data?.models?.length === 0 && ( +
+ MiniMax + {minimax.error || 'no data'} +
+ )} + {zai && ( +
+ Z.AI + {zai.healthy ? 'โœ“ active' : zai.error || 'โ€”'} +
+ )} + {!minimax && !zai && No providers} +
{/* Running Processes */} -
-
- Running Processes - {processes.length} -
-
- {processes.length === 0 && No relevant processes} - {processes.slice(0, 8).map((p, i) => ( -
- {p.name} - cpu {p.cpu}% ยท mem {p.mem}% -
- ))} +
+ +
+
+ Processes + {processes.length} +
+
+ {processes.length === 0 && No relevant processes} + {processes.slice(0, 6).map((p, i) => ( +
+ {p.name} + cpu {p.cpu}% ยท mem {p.mem}% +
+ ))} +
@@ -165,38 +216,6 @@ export default function Dashboard({ api, refreshRef }) { ))}
- - {/* Services */} -
-
- Services -
- {dashboardStatus ? ( -
-
- MCP - {dashboardStatus.mcp?.healthy || 0}/{dashboardStatus.mcp?.total || 0} healthy -
-
- LSP - {dashboardStatus.lsp?.installed || 0}/{dashboardStatus.lsp?.total || 0} installed -
-
- Skills - {dashboardStatus.skills?.total || 0} deployed -
- {(dashboardStatus.skills?.issues || []).length > 0 && ( -
- {(dashboardStatus.skills.issues || []).slice(0, 3).map((issue, i) => ( -
โš  {issue}
- ))} -
- )} -
- ) : ( - Loading... - )} -
) } diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 82cabb0..94113c5 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -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;