feat: terminal sudo blocking, token tracking, mermaid & consumption UI
All checks were successful
Beta Release / beta (push) Successful in 1m3s

- Block sudo/doas commands when not running as root
- Add real token counting from API responses
- Track and display consumption by provider/day
- Add Mermaid diagram rendering in Shell and Studio
- Add copy-to-clipboard buttons for code blocks
- Support tables in AI message rendering
- Update system prompt with context (date, time, root status)

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-26 12:43:15 +02:00
parent 0830e64ae6
commit cb3d35756a
21 changed files with 2166 additions and 208 deletions

View File

@@ -6,6 +6,12 @@ const MAX_POINTS = 30
const POLL_INTERVAL = 5000
const MAX_IDLE_POLLS = 3
function formatTokens(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
return String(n)
}
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)
@@ -37,9 +43,28 @@ function MiniGraph({ data, max, color, label, unit }) {
)
}
function BarChart({ data, max, color }) {
if (!data || data.length === 0) return null
const barW = 100 / 7
const m = max || Math.max(...data.map(d => d.tokens), 1)
return (
<svg viewBox="0 0 100 40" className="dash-graph-svg" preserveAspectRatio="none">
{data.map((d, i) => {
const h = Math.max(1, (d.tokens / m) * 36)
const x = i * barW + barW * 0.15
const w = barW * 0.7
return (
<rect key={i} x={x} y={40 - h} width={w} height={h} rx="1.5" fill={color} opacity={0.85} />
)
})}
</svg>
)
}
export default function Dashboard({ api, refreshRef }) {
const { t } = useI18n()
const [quota, setQuota] = useState(null)
const [consumption, setConsumption] = useState(null)
const [recentCmds, setRecentCmds] = useState([])
const [processes, setProcesses] = useState([])
const [metrics, setMetrics] = useState(null)
@@ -51,13 +76,15 @@ export default function Dashboard({ api, refreshRef }) {
const loadData = useCallback(async () => {
try {
const [quotaData, cmdData, procData, metricsData] = await Promise.all([
const [quotaData, consumData, cmdData, procData, metricsData] = await Promise.all([
api.getProvidersQuota().catch(() => null),
api.getProvidersConsumption().catch(() => null),
api.getRecentCommands().catch(() => ({ commands: [] })),
api.getRunningProcesses().catch(() => ({ processes: [] })),
api.getSystemMetrics().catch(() => null),
])
setQuota(quotaData?.providers || [])
setConsumption(consumData?.providers || {})
setRecentCmds(cmdData.commands || [])
setProcesses(procData.processes || [])
if (metricsData) {
@@ -91,7 +118,6 @@ export default function Dashboard({ api, refreshRef }) {
}, [loadData, refreshRef])
const minimax = (quota || []).find(p => p.name === 'minimax')
const mimo = (quota || []).find(p => p.name === 'mimo')
const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history', 'cat', 'echo', 'grep', 'export', 'alias', 'unalias', 'set', 'unset', 'source', '.', 'fg', 'bg', 'jobs', 'wait', 'true', 'false', 'yes', 'sleep', 'date', 'whoami', 'id', 'uname', 'hostname', 'uptime', 'df', 'free', 'top', 'htop', 'nano', 'vi', 'vim', 'less', 'more', 'tail', 'head', 'man', 'info', 'which', 'whereis', 'type', 'command', 'hash', 'builtin', 'help']
@@ -135,6 +161,12 @@ export default function Dashboard({ api, refreshRef }) {
})
})()
const providerEntries = consumption ? Object.entries(consumption) : []
const colors = ['var(--accent)', '#34d399', '#a78bfa', '#f59e0b', '#f472b6']
const maxDaily = providerEntries.length > 0
? Math.max(...providerEntries.map(([, p]) => Math.max(...(p.daily || []).map(d => d.tokens), 0)), 1)
: 1
return (
<div className="dash-grid">
{/* CPU */}
@@ -165,43 +197,36 @@ export default function Dashboard({ api, refreshRef }) {
<MiniGraph data={netTxRef.current} max={null} color="#f59e0b" label="TX" unit=" KB/s" />
</div>
{/* API Quota */}
{/* Consommation */}
<div className="dash-card">
<div className="dash-card-head">
<span className="dash-label">API Quota</span>
<span className="dash-label">Consommation</span>
<span className="dash-count">7j</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 className="dash-consumption-list">
{providerEntries.length === 0 && (
<span className="dash-empty">Aucune donnée</span>
)}
{providerEntries.map(([name, p], pi) => (
<div key={name} className="dash-consumption-provider">
<div className="dash-consumption-head">
<span className="dash-consumption-name" style={{ color: colors[pi % colors.length] }}>
{name.toUpperCase()}
</span>
<span className="dash-consumption-total">
{formatTokens(p.total_tokens)} tokens · {p.total_requests} req
</span>
</div>
<BarChart data={p.daily || []} max={maxDaily} color={colors[pi % colors.length]} />
<div className="dash-consumption-days">
{(p.daily || []).map((d, i) => (
<span key={i} className="dash-consumption-day">
{d.date.slice(5)} <strong>{formatTokens(d.tokens)}</strong>
</span>
))}
</div>
<span className="dash-quota-val">{m.used}/{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>
)}
{mimo && mimo.data?.models?.map((m, i) => (
<div key={i} className="dash-quota-row">
<span className="dash-quota-name">{String(m.model).replace('MiMo-', '')}</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.used}/{m.total}</span>
</div>
))}
{mimo && !mimo.data?.models?.length && (
<div className="dash-quota-row">
<span className="dash-quota-name">MiMo</span>
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{mimo.error || (mimo.healthy ? '✓ configured' : 'no key')}</span>
</div>
)}
{!minimax && !mimo && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
</div>
</div>