feat(dashboard): single-view grid with live CPU/RAM/Net graphs, API quota, processes, and sudo indicator
All checks were successful
Beta Release / beta (push) Successful in 46s
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>
This commit is contained in:
@@ -23,6 +23,7 @@ func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
"name": version.Name,
|
"name": version.Name,
|
||||||
"version": version.Version,
|
"version": version.Version,
|
||||||
"author": version.Author,
|
"author": version.Author,
|
||||||
|
"sudo": os.Geteuid() == 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,3 +610,111 @@ func (s *Server) handleRunningProcesses(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
writeJSON(w, map[string]interface{}{"processes": procs})
|
writeJSON(w, map[string]interface{}{"processes": procs})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sysMetrics struct {
|
||||||
|
CPUPercent float64 `json:"cpu_percent"`
|
||||||
|
MemPercent float64 `json:"mem_percent"`
|
||||||
|
MemUsedMB float64 `json:"mem_used_mb"`
|
||||||
|
MemTotalMB float64 `json:"mem_total_mb"`
|
||||||
|
NetRxKBs float64 `json:"net_rx_kbs"`
|
||||||
|
NetTxKBs float64 `json:"net_tx_kbs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastCPU [2]float64
|
||||||
|
lastNet [2]float64
|
||||||
|
lastNetTs time.Time
|
||||||
|
lastCPUSet bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) handleSystemMetrics(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m := sysMetrics{}
|
||||||
|
|
||||||
|
// CPU from /proc/stat
|
||||||
|
if data, err := os.ReadFile("/proc/stat"); err == nil {
|
||||||
|
line := strings.Split(string(data), "\n")[0]
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 5 {
|
||||||
|
var idle, total float64
|
||||||
|
for i := 1; i < len(fields) && i <= 4; i++ {
|
||||||
|
var v float64
|
||||||
|
fmt.Sscanf(fields[i], "%f", &v)
|
||||||
|
total += v
|
||||||
|
if i == 4 {
|
||||||
|
idle = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastCPUSet {
|
||||||
|
dIdle := idle - lastCPU[0]
|
||||||
|
dTotal := total - lastCPU[1]
|
||||||
|
if dTotal > 0 {
|
||||||
|
m.CPUPercent = (1 - dIdle/dTotal) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastCPU = [2]float64{idle, total}
|
||||||
|
lastCPUSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory from /proc/meminfo
|
||||||
|
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||||||
|
var memTotal, memAvailable float64
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var v float64
|
||||||
|
fmt.Sscanf(fields[1], "%f", &v)
|
||||||
|
switch fields[0] {
|
||||||
|
case "MemTotal:":
|
||||||
|
memTotal = v
|
||||||
|
case "MemAvailable:":
|
||||||
|
memAvailable = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if memTotal > 0 {
|
||||||
|
m.MemTotalMB = memTotal / 1024
|
||||||
|
m.MemUsedMB = (memTotal - memAvailable) / 1024
|
||||||
|
m.MemPercent = (memTotal - memAvailable) / memTotal * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network from /proc/net/dev
|
||||||
|
if data, err := os.ReadFile("/proc/net/dev"); err == nil {
|
||||||
|
var rxBytes, txBytes float64
|
||||||
|
for _, line := range strings.Split(string(data), "\n")[2:] {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 10 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
iface := strings.TrimSuffix(fields[0], ":")
|
||||||
|
if iface == "lo" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var rx, tx float64
|
||||||
|
fmt.Sscanf(fields[1], "%f", &rx)
|
||||||
|
fmt.Sscanf(fields[9], "%f", &tx)
|
||||||
|
rxBytes += rx
|
||||||
|
txBytes += tx
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if !lastNetTs.IsZero() {
|
||||||
|
elapsed := now.Sub(lastNetTs).Seconds()
|
||||||
|
if elapsed > 0 {
|
||||||
|
m.NetRxKBs = (rxBytes - lastNet[0]) / 1024 / elapsed
|
||||||
|
m.NetTxKBs = (txBytes - lastNet[1]) / 1024 / elapsed
|
||||||
|
if m.NetRxKBs < 0 {
|
||||||
|
m.NetRxKBs = 0
|
||||||
|
}
|
||||||
|
if m.NetTxKBs < 0 {
|
||||||
|
m.NetTxKBs = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastNet = [2]float64{rxBytes, txBytes}
|
||||||
|
lastNetTs = now
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, m)
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ func (s *Server) routes() {
|
|||||||
s.mux.HandleFunc("/api/providers/quota", s.handleProvidersQuota)
|
s.mux.HandleFunc("/api/providers/quota", s.handleProvidersQuota)
|
||||||
s.mux.HandleFunc("/api/recent-commands", s.handleRecentCommands)
|
s.mux.HandleFunc("/api/recent-commands", s.handleRecentCommands)
|
||||||
s.mux.HandleFunc("/api/running-processes", s.handleRunningProcesses)
|
s.mux.HandleFunc("/api/running-processes", s.handleRunningProcesses)
|
||||||
|
s.mux.HandleFunc("/api/system/metrics", s.handleSystemMetrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "muyue"
|
Name = "muyue"
|
||||||
Version = "0.3.2"
|
Version = "0.3.3"
|
||||||
Author = "La Légion de Muyue"
|
Author = "La Légion de Muyue"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const api = {
|
|||||||
getProvidersQuota: () => request('/providers/quota'),
|
getProvidersQuota: () => request('/providers/quota'),
|
||||||
getRecentCommands: () => request('/recent-commands'),
|
getRecentCommands: () => request('/recent-commands'),
|
||||||
getRunningProcesses: () => request('/running-processes'),
|
getRunningProcesses: () => request('/running-processes'),
|
||||||
|
getSystemMetrics: () => request('/system/metrics'),
|
||||||
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
|
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
|
||||||
saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }),
|
saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }),
|
||||||
saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }),
|
saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||||
import { LayoutDashboard, Sparkles, Terminal, Settings } from 'lucide-react'
|
import { LayoutDashboard, Sparkles, Terminal, Settings } from 'lucide-react'
|
||||||
import api from '../api/client'
|
import api from '../api/client'
|
||||||
import { getTheme, applyTheme } from '../themes'
|
import { getTheme, applyTheme } from '../themes'
|
||||||
@@ -13,6 +13,9 @@ export default function App() {
|
|||||||
const [activeTab, setActiveTab] = useState('dash')
|
const [activeTab, setActiveTab] = useState('dash')
|
||||||
const [info, setInfo] = useState({})
|
const [info, setInfo] = useState({})
|
||||||
const [clock, setClock] = useState(new Date())
|
const [clock, setClock] = useState(new Date())
|
||||||
|
const [isSudo, setIsSudo] = useState(false)
|
||||||
|
const [dashRefreshKey, setDashRefreshKey] = useState(0)
|
||||||
|
const dashRefreshRef = useRef(null)
|
||||||
const [updates, setUpdates] = useState([])
|
const [updates, setUpdates] = useState([])
|
||||||
const [tools, setTools] = useState([])
|
const [tools, setTools] = useState([])
|
||||||
const [config, setConfig] = useState(null)
|
const [config, setConfig] = useState(null)
|
||||||
@@ -27,7 +30,7 @@ export default function App() {
|
|||||||
], [t])
|
], [t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getInfo().then(setInfo).catch(() => {})
|
api.getInfo().then(d => { setInfo(d); setIsSudo(!!d.sudo) }).catch(() => {})
|
||||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||||
api.getConfig().then(d => {
|
api.getConfig().then(d => {
|
||||||
@@ -60,6 +63,11 @@ export default function App() {
|
|||||||
if (map[e.code]) {
|
if (map[e.code]) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setActiveTab(map[e.code])
|
setActiveTab(map[e.code])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && e.code === 'KeyR') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (dashRefreshRef.current) dashRefreshRef.current()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', onKey)
|
window.addEventListener('keydown', onKey)
|
||||||
@@ -72,27 +80,21 @@ export default function App() {
|
|||||||
const installed = tools.filter(tool => tool.installed).length
|
const installed = tools.filter(tool => tool.installed).length
|
||||||
|
|
||||||
const WINDOW_SHORTCUTS = useMemo(() => ({
|
const WINDOW_SHORTCUTS = useMemo(() => ({
|
||||||
dash: [
|
dash: [],
|
||||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
||||||
],
|
|
||||||
studio: [
|
studio: [
|
||||||
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
|
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
|
||||||
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
||||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
||||||
],
|
],
|
||||||
shell: [
|
shell: [
|
||||||
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
||||||
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
||||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
||||||
],
|
|
||||||
config: [
|
|
||||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
|
||||||
],
|
],
|
||||||
|
config: [],
|
||||||
}), [layout, t])
|
}), [layout, t])
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'dash': return <Dashboard api={api} />
|
case 'dash': return <Dashboard api={api} refreshRef={dashRefreshRef} />
|
||||||
case 'studio': return <Studio api={api} />
|
case 'studio': return <Studio api={api} />
|
||||||
case 'shell': return <Shell api={api} />
|
case 'shell': return <Shell api={api} />
|
||||||
case 'config': return <Config api={api} />
|
case 'config': return <Config api={api} />
|
||||||
@@ -147,6 +149,12 @@ export default function App() {
|
|||||||
|
|
||||||
<footer className="statusbar">
|
<footer className="statusbar">
|
||||||
<div className="statusbar-left">
|
<div className="statusbar-left">
|
||||||
|
{isSudo && <span className="statusbar-sudo">⚡ ROOT</span>}
|
||||||
|
{activeTab === 'dash' && (
|
||||||
|
<span className="statusbar-shortcut">
|
||||||
|
<kbd>{layout.keys.ctrl}+R</kbd> refresh
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<FooterShortcuts shortcuts={WINDOW_SHORTCUTS[activeTab] || []} />
|
<FooterShortcuts shortcuts={WINDOW_SHORTCUTS[activeTab] || []} />
|
||||||
</div>
|
</div>
|
||||||
<div className="statusbar-right">
|
<div className="statusbar-right">
|
||||||
|
|||||||
@@ -1,66 +1,105 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { useI18n } from '../i18n'
|
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 <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 { t } = useI18n()
|
||||||
const [tools, setTools] = useState([])
|
|
||||||
const [systemInfo, setSystemInfo] = useState(null)
|
|
||||||
const [dashboardStatus, setDashboardStatus] = useState(null)
|
const [dashboardStatus, setDashboardStatus] = useState(null)
|
||||||
const [quota, setQuota] = useState(null)
|
const [quota, setQuota] = useState(null)
|
||||||
const [recentCmds, setRecentCmds] = useState([])
|
const [recentCmds, setRecentCmds] = useState([])
|
||||||
const [processes, setProcesses] = 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 () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [toolsData, systemData, dashData, quotaData, cmdData, procData, updatesData] = await Promise.all([
|
const [dashData, quotaData, cmdData, procData, metricsData] = await Promise.all([
|
||||||
api.getTools().catch(() => ({ tools: [] })),
|
|
||||||
api.getSystem().catch(() => null),
|
|
||||||
api.getDashboardStatus().catch(() => null),
|
api.getDashboardStatus().catch(() => null),
|
||||||
api.getProvidersQuota().catch(() => null),
|
api.getProvidersQuota().catch(() => null),
|
||||||
api.getRecentCommands().catch(() => ({ commands: [] })),
|
api.getRecentCommands().catch(() => ({ commands: [] })),
|
||||||
api.getRunningProcesses().catch(() => ({ processes: [] })),
|
api.getRunningProcesses().catch(() => ({ processes: [] })),
|
||||||
api.getUpdates().catch(() => ({ updates: [] })),
|
api.getSystemMetrics().catch(() => null),
|
||||||
])
|
])
|
||||||
setTools(toolsData.tools || toolsData || [])
|
|
||||||
setSystemInfo(systemData?.system || systemData)
|
|
||||||
setDashboardStatus(dashData)
|
setDashboardStatus(dashData)
|
||||||
setQuota(quotaData?.providers || [])
|
setQuota(quotaData?.providers || [])
|
||||||
setRecentCmds(cmdData.commands || [])
|
setRecentCmds(cmdData.commands || [])
|
||||||
setProcesses(procData.processes || [])
|
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) {
|
} catch (err) {
|
||||||
console.error('Dashboard load error:', err)
|
console.error('Dashboard load error:', err)
|
||||||
}
|
}
|
||||||
}, [api])
|
}, [api])
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [loadData])
|
useEffect(() => {
|
||||||
|
loadData()
|
||||||
const installedCount = tools.filter(t => t.installed || t.status === 'installed').length
|
if (refreshRef) refreshRef.current = loadData
|
||||||
const sys = systemInfo || {}
|
const iv = setInterval(loadData, 5000)
|
||||||
|
return () => clearInterval(iv)
|
||||||
|
}, [loadData, refreshRef])
|
||||||
|
|
||||||
const minimax = (quota || []).find(p => p.name === 'minimax')
|
const minimax = (quota || []).find(p => p.name === 'minimax')
|
||||||
const zai = (quota || []).find(p => p.name === 'zai')
|
const zai = (quota || []).find(p => p.name === 'zai')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dash-grid">
|
<div className="dash-grid">
|
||||||
{/* System */}
|
{/* CPU / RAM / Network Graphs */}
|
||||||
<div className="dash-card dash-span-2">
|
<div className="dash-card">
|
||||||
<div className="dash-card-head">
|
<div className="dash-card-head">
|
||||||
<span className="dash-label">{sys.os || sys.platform || 'System'} · {sys.arch || ''}</span>
|
<span className="dash-label">CPU</span>
|
||||||
<button className="sm ghost" onClick={() => api.runScan().then(loadData)}>↻ Rescan</button>
|
<span className="dash-count">{metrics ? metrics.cpu_percent.toFixed(0) : '—'}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="dash-tools-row">
|
<MiniGraph data={cpuRef.current} max={100} color="var(--accent)" label="CPU" unit="%" />
|
||||||
{tools.slice(0, 12).map((tool, i) => {
|
</div>
|
||||||
const ok = tool.installed || tool.status === 'installed'
|
|
||||||
return (
|
<div className="dash-card">
|
||||||
<span key={tool.name || i} className={`dash-tool-tag ${ok ? 'ok' : 'missing'}`}>
|
<div className="dash-card-head">
|
||||||
{ok ? '●' : '○'} {tool.name}
|
<span className="dash-label">RAM</span>
|
||||||
</span>
|
<span className="dash-count">{metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)} MB` : '—'}</span>
|
||||||
)
|
|
||||||
})}
|
|
||||||
{tools.length > 12 && <span className="dash-tool-tag">+{tools.length - 12}</span>}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* API Quota */}
|
{/* API Quota */}
|
||||||
@@ -127,7 +166,7 @@ export default function Dashboard({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status (MCP/LSP/Skills) */}
|
{/* Services */}
|
||||||
<div className="dash-card">
|
<div className="dash-card">
|
||||||
<div className="dash-card-head">
|
<div className="dash-card-head">
|
||||||
<span className="dash-label">Services</span>
|
<span className="dash-label">Services</span>
|
||||||
@@ -158,24 +197,6 @@ export default function Dashboard({ api }) {
|
|||||||
<span className="dash-empty">Loading...</span>
|
<span className="dash-empty">Loading...</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Updates */}
|
|
||||||
{updates.length > 0 && (
|
|
||||||
<div className="dash-card dash-span-2">
|
|
||||||
<div className="dash-card-head">
|
|
||||||
<span className="dash-label">Updates Available</span>
|
|
||||||
<span className="dash-count warn">{updates.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="dash-updates-list">
|
|
||||||
{updates.slice(0, 5).map((u, i) => (
|
|
||||||
<div key={u.name || i} className="dash-update-row">
|
|
||||||
<span className="dash-update-name">{u.name}</span>
|
|
||||||
<span className="dash-update-ver">{u.current || '?'} → {u.latest || '?'}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,16 +100,14 @@ export default function OnboardingWizard({ api, onComplete }) {
|
|||||||
} else {
|
} else {
|
||||||
detected.push(...(await fallback()))
|
detected.push(...(await fallback()))
|
||||||
}
|
}
|
||||||
const merged = [...new Set([...detected.map(n => n.toLowerCase()), ...BASE_EDITORS])]
|
setEditorList([...new Set(detected.map(n => n.toLowerCase()))])
|
||||||
setEditorList(merged)
|
|
||||||
setScanMessage('')
|
setScanMessage('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
try {
|
try {
|
||||||
setScanMessage('Fallback: scan local...')
|
setScanMessage('Fallback: scan local...')
|
||||||
const data = await api.getEditors()
|
const data = await api.getEditors()
|
||||||
const detected = (data.editors || []).map(e => e.name)
|
const detected = (data.editors || []).map(e => e.name)
|
||||||
const merged = [...new Set([...detected, ...BASE_EDITORS])]
|
setEditorList([...new Set(detected)])
|
||||||
setEditorList(merged)
|
|
||||||
} catch {}
|
} catch {}
|
||||||
setScanMessage('')
|
setScanMessage('')
|
||||||
}
|
}
|
||||||
@@ -325,7 +323,7 @@ export default function OnboardingWizard({ api, onComplete }) {
|
|||||||
<div className="onboarding-step">
|
<div className="onboarding-step">
|
||||||
<div className="onboarding-title">Quel éditeur utilisez-vous ?</div>
|
<div className="onboarding-title">Quel éditeur utilisez-vous ?</div>
|
||||||
<div className="onboarding-desc">
|
<div className="onboarding-desc">
|
||||||
{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.'}
|
||||||
</div>
|
</div>
|
||||||
<div className="onboarding-chips">
|
<div className="onboarding-chips">
|
||||||
{editorList.map(ed => (
|
{editorList.map(ed => (
|
||||||
@@ -338,14 +336,6 @@ export default function OnboardingWizard({ api, onComplete }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
className="onboarding-input"
|
|
||||||
style={{ marginTop: 12 }}
|
|
||||||
placeholder="Autre éditeur..."
|
|
||||||
value={answers.editor}
|
|
||||||
onChange={e => setAnswers(a => ({ ...a, editor: e.target.value }))}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
color: var(--text-disabled);
|
color: var(--text-disabled);
|
||||||
}
|
}
|
||||||
.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 12px; }
|
.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 { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
.statusbar-shortcut kbd {
|
.statusbar-shortcut kbd {
|
||||||
display: inline-block; padding: 1px 5px; border-radius: 3px;
|
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); }
|
.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 */
|
/* Legacy dashboard kept for reference */
|
||||||
.dashboard-layout { display: flex; flex-direction: column; height: 100%; }
|
.dashboard-layout { display: flex; flex-direction: column; height: 100%; }
|
||||||
.dashboard-content { flex: 1; overflow-y: auto; }
|
.dashboard-content { flex: 1; overflow-y: auto; }
|
||||||
|
|||||||
Reference in New Issue
Block a user