diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx
index fc6d171..9be3e84 100644
--- a/web/src/components/Dashboard.jsx
+++ b/web/src/components/Dashboard.jsx
@@ -1,66 +1,105 @@
-import { useState, useEffect, useCallback } from 'react'
+import { useState, useEffect, useCallback, useRef } from 'react'
import { useI18n } from '../i18n'
-export default function Dashboard({ api }) {
+const MAX_POINTS = 30
+
+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 [tools, setTools] = useState([])
- const [systemInfo, setSystemInfo] = useState(null)
const [dashboardStatus, setDashboardStatus] = useState(null)
const [quota, setQuota] = useState(null)
const [recentCmds, setRecentCmds] = useState([])
const [processes, setProcesses] = useState([])
- const [updates, setUpdates] = useState([])
+ const [metrics, setMetrics] = useState(null)
+ const cpuRef = useRef([])
+ const memRef = useRef([])
+ const netRxRef = useRef([])
+ const netTxRef = useRef([])
const loadData = useCallback(async () => {
try {
- const [toolsData, systemData, dashData, quotaData, cmdData, procData, updatesData] = await Promise.all([
- api.getTools().catch(() => ({ tools: [] })),
- api.getSystem().catch(() => null),
+ 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.getUpdates().catch(() => ({ updates: [] })),
+ api.getSystemMetrics().catch(() => null),
])
- setTools(toolsData.tools || toolsData || [])
- setSystemInfo(systemData?.system || systemData)
setDashboardStatus(dashData)
setQuota(quotaData?.providers || [])
setRecentCmds(cmdData.commands || [])
setProcesses(procData.processes || [])
- setUpdates(updatesData.updates || updatesData || [])
+ 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() }, [loadData])
-
- const installedCount = tools.filter(t => t.installed || t.status === 'installed').length
- const sys = systemInfo || {}
+ 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 (
- {/* System */}
-
+ {/* CPU / RAM / Network Graphs */}
+
- {sys.os || sys.platform || 'System'} · {sys.arch || ''}
-
+ CPU
+ {metrics ? metrics.cpu_percent.toFixed(0) : '—'}%
-
- {tools.slice(0, 12).map((tool, i) => {
- const ok = tool.installed || tool.status === 'installed'
- return (
-
- {ok ? '●' : '○'} {tool.name}
-
- )
- })}
- {tools.length > 12 && +{tools.length - 12}}
+
+
+
+
+
+ RAM
+ {metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)} MB` : '—'}
+
+
+
+
+
+ Network
+ {metrics ? `↓${metrics.net_rx_kbs.toFixed(0)} ↑${metrics.net_tx_kbs.toFixed(0)} KB/s` : '—'}
+
+
+
{/* API Quota */}
@@ -127,7 +166,7 @@ export default function Dashboard({ api }) {
- {/* Status (MCP/LSP/Skills) */}
+ {/* Services */}
Services
@@ -158,24 +197,6 @@ export default function Dashboard({ api }) {
Loading...
)}
-
- {/* Updates */}
- {updates.length > 0 && (
-
-
- Updates Available
- {updates.length}
-
-
- {updates.slice(0, 5).map((u, i) => (
-
- {u.name}
- {u.current || '?'} → {u.latest || '?'}
-
- ))}
-
-
- )}
)
}
diff --git a/web/src/components/OnboardingWizard.jsx b/web/src/components/OnboardingWizard.jsx
index 09d8443..7d7e9df 100644
--- a/web/src/components/OnboardingWizard.jsx
+++ b/web/src/components/OnboardingWizard.jsx
@@ -100,16 +100,14 @@ export default function OnboardingWizard({ api, onComplete }) {
} else {
detected.push(...(await fallback()))
}
- const merged = [...new Set([...detected.map(n => n.toLowerCase()), ...BASE_EDITORS])]
- setEditorList(merged)
+ setEditorList([...new Set(detected.map(n => n.toLowerCase()))])
setScanMessage('')
} catch (err) {
try {
setScanMessage('Fallback: scan local...')
const data = await api.getEditors()
const detected = (data.editors || []).map(e => e.name)
- const merged = [...new Set([...detected, ...BASE_EDITORS])]
- setEditorList(merged)
+ setEditorList([...new Set(detected)])
} catch {}
setScanMessage('')
}
@@ -325,7 +323,7 @@ export default function OnboardingWizard({ api, onComplete }) {
Quel éditeur utilisez-vous ?
- {scanning ? 'Détection en cours...' : 'Sélectionnez votre éditeur ou tapez-en un autre ci-dessous.'}
+ {scanning ? 'Détection en cours...' : 'Sélectionnez votre éditeur.'}
{editorList.map(ed => (
@@ -338,14 +336,6 @@ export default function OnboardingWizard({ api, onComplete }) {
))}
-
setAnswers(a => ({ ...a, editor: e.target.value }))}
- autoFocus
- />
)}
diff --git a/web/src/styles/global.css b/web/src/styles/global.css
index 0213d00..82cabb0 100644
--- a/web/src/styles/global.css
+++ b/web/src/styles/global.css
@@ -169,6 +169,12 @@ input::placeholder { color: var(--text-disabled); }
color: var(--text-disabled);
}
.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 12px; }
+.statusbar-sudo {
+ font-size: 10px; font-weight: 700; font-family: var(--font-mono);
+ padding: 1px 6px; border-radius: 3px;
+ background: rgba(239, 68, 68, 0.15); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3);
+ text-transform: uppercase; letter-spacing: 0.5px;
+}
.statusbar-shortcut { display: inline-flex; align-items: center; gap: 4px; }
.statusbar-shortcut kbd {
display: inline-block; padding: 1px 5px; border-radius: 3px;
@@ -637,6 +643,14 @@ input::placeholder { color: var(--text-disabled); }
.dash-empty { font-size: 11px; color: var(--text-disabled); }
+/* Graph */
+.dash-graph-wrap { display: flex; flex-direction: column; gap: 2px; }
+.dash-graph-header { display: flex; justify-content: space-between; align-items: center; }
+.dash-graph-label { font-size: 9px; color: var(--text-disabled); text-transform: uppercase; }
+.dash-graph-value { font-size: 10px; font-family: var(--font-mono); font-weight: 600; }
+.dash-graph-svg { width: 100%; height: 32px; }
+.dash-graph-empty { font-size: 10px; color: var(--text-disabled); text-align: center; padding: 8px 0; }
+
/* Legacy dashboard kept for reference */
.dashboard-layout { display: flex; flex-direction: column; height: 100%; }
.dashboard-content { flex: 1; overflow-y: auto; }