diff --git a/apps/env/20260130-000000/index.tsx b/apps/env/20260130-000000/index.tsx index 945da19..fa74ff0 100644 --- a/apps/env/20260130-000000/index.tsx +++ b/apps/env/20260130-000000/index.tsx @@ -300,9 +300,35 @@ app.get('/', async c => { const appName = c.req.query('app') if (!appName) { + // Dashboard view: global env vars only + const globalVars = parseEnvFile(GLOBAL_ENV_PATH) + return c.html( - - Please specify an app name with ?app=<name> + + {globalVars.length === 0 ? ( + No global environment variables + ) : ( + + {globalVars.map(v => ( + + {v.key} + {'••••••••'} + + +
+ Delete +
+
+
+ ))} +
+ )} +
+ + + +
+ Global vars are available to all apps. Changes take effect on next app restart.
) } @@ -437,7 +463,6 @@ app.post('/delete', async c => { app.post('/set-global', async c => { const appName = c.req.query('app') - if (!appName) return c.text('Missing app', 400) const body = await c.req.parseBody() const key = String(body.key).trim().toUpperCase() @@ -455,17 +480,17 @@ app.post('/set-global', async c => { } writeEnvFile(GLOBAL_ENV_PATH, vars) - return c.redirect(`/?app=${appName}&tab=global`) + return c.redirect(appName ? `/?app=${appName}&tab=global` : '/') }) app.post('/delete-global', async c => { const appName = c.req.query('app') const key = c.req.query('key') - if (!appName || !key) return c.text('Missing app or key', 400) + if (!key) return c.text('Missing key', 400) const vars = parseEnvFile(GLOBAL_ENV_PATH).filter(v => v.key !== key) writeEnvFile(GLOBAL_ENV_PATH, vars) - return c.redirect(`/?app=${appName}&tab=global`) + return c.redirect(appName ? `/?app=${appName}&tab=global` : '/') }) export default app.defaults diff --git a/apps/env/20260130-000000/package.json b/apps/env/20260130-000000/package.json index 6d5864e..cac9217 100644 --- a/apps/env/20260130-000000/package.json +++ b/apps/env/20260130-000000/package.json @@ -10,7 +10,8 @@ }, "toes": { "tool": ".env", - "icon": "🔑" + "icon": "🔑", + "dashboard": true }, "devDependencies": { "@types/bun": "latest" diff --git a/apps/metrics/20260130-000000/index.tsx b/apps/metrics/20260130-000000/index.tsx index 4850500..7282486 100644 --- a/apps/metrics/20260130-000000/index.tsx +++ b/apps/metrics/20260130-000000/index.tsx @@ -55,6 +55,12 @@ interface ProcessMetrics { rss: number } +interface SystemMetrics { + cpu: number + ram: { used: number, total: number, percent: number } + disk: { used: number, total: number, percent: number } +} + // ============================================================================ // Process Metrics Collection // ============================================================================ @@ -402,6 +408,40 @@ const ChartWrapper = define('ChartWrapper', { height: '150px', }) +const GaugeLabel = define('GaugeLabel', { + fontSize: '13px', + fontWeight: 600, + color: theme('colors-textMuted'), + textTransform: 'uppercase', + letterSpacing: '0.5px', +}) + +const GaugeValueText = define('GaugeValueText', { + textAlign: 'center', + fontSize: '20px', + fontWeight: 'bold', + marginTop: '-4px', + color: theme('colors-text'), +}) + +const GaugesCard = define('GaugesCard', { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '8px', + background: theme('colors-bgElement'), + border: `1px solid ${theme('colors-border')}`, + borderRadius: theme('radius-md'), + padding: '24px', +}) + +const GaugesGrid = define('GaugesGrid', { + display: 'flex', + justifyContent: 'center', + gap: '40px', + padding: '20px 0', +}) + const NoDataMessage = define('NoDataMessage', { display: 'flex', alignItems: 'center', @@ -435,6 +475,14 @@ function formatRss(kb?: number): string { return `${(kb / 1024 / 1024).toFixed(2)} GB` } +async function fetchSystemMetrics(): Promise { + try { + const res = await fetch(`${TOES_URL}/api/system/metrics`) + if (res.ok) return await res.json() as SystemMetrics + } catch {} + return { cpu: 0, ram: { used: 0, total: 0, percent: 0 }, disk: { used: 0, total: 0, percent: 0 } } +} + function getStatusColor(state: string): string { switch (state) { case 'running': @@ -477,6 +525,107 @@ function Layout({ title, children }: LayoutProps) { ) } +// ============================================================================ +// Gauge Rendering +// ============================================================================ + +const G_SEGMENTS = 19 +const G_START = -225 +const G_SWEEP = 270 +const G_CX = 60 +const G_CY = 60 +const G_R = 44 +const G_GAP = 3 +const G_SW = 8 +const G_NL = 38 + +const gToRad = (deg: number) => (deg * Math.PI) / 180 + +const gSegColor = (i: number): string => { + const t = i / (G_SEGMENTS - 1) + if (t < 0.4) return '#4caf50' + if (t < 0.6) return '#8bc34a' + if (t < 0.75) return '#ffc107' + if (t < 0.9) return '#ff9800' + return '#f44336' +} + +function renderGauge(value: number, id: string) { + const segSweep = G_SWEEP / G_SEGMENTS + const active = Math.round((value / 100) * G_SEGMENTS) + const innerR = G_R - G_SW / 2 + const outerR = G_R + G_SW / 2 + + const segments = [] + for (let i = 0; i < G_SEGMENTS; i++) { + const s = G_START + i * segSweep + G_GAP / 2 + const e = G_START + (i + 1) * segSweep - G_GAP / 2 + const x1 = G_CX + outerR * Math.cos(gToRad(s)) + const y1 = G_CY + outerR * Math.sin(gToRad(s)) + const x2 = G_CX + outerR * Math.cos(gToRad(e)) + const y2 = G_CY + outerR * Math.sin(gToRad(e)) + const x3 = G_CX + innerR * Math.cos(gToRad(e)) + const y3 = G_CY + innerR * Math.sin(gToRad(e)) + const x4 = G_CX + innerR * Math.cos(gToRad(s)) + const y4 = G_CY + innerR * Math.sin(gToRad(s)) + segments.push( + + ) + } + + const angle = G_START + (value / 100) * G_SWEEP + const nx = G_CX + G_NL * Math.cos(gToRad(angle)) + const ny = G_CY + G_NL * Math.sin(gToRad(angle)) + const pa = angle + 90 + const bw = 3 + const bx1 = G_CX + bw * Math.cos(gToRad(pa)) + const by1 = G_CY + bw * Math.sin(gToRad(pa)) + const bx2 = G_CX - bw * Math.cos(gToRad(pa)) + const by2 = G_CY - bw * Math.sin(gToRad(pa)) + + return ( + + {id} + + {segments} + + + + {value}% + + ) +} + +const gaugeScript = ` +(function() { + var S=19,ST=-225,SW=270,CX=60,CY=60,R=44,W=8,NL=38,GAP=3; + var iR=R-W/2, oR=R+W/2; + function rad(d){return d*Math.PI/180} + function sc(i){var t=i/(S-1);return t<.4?'#4caf50':t<.6?'#8bc34a':t<.75?'#ffc107':t<.9?'#ff9800':'#f44336'} + function upd(id,v){ + var svg=document.getElementById('gauge-'+id);if(!svg)return; + var a=Math.round((v/100)*S); + svg.querySelectorAll('[data-segment]').forEach(function(p,i){p.setAttribute('fill',i { return c.json(history) }) +app.get('/api/system', async c => { + const metrics = await fetchSystemMetrics() + return c.json(metrics) +}) + app.get('/api/history/:name', c => { const name = c.req.param('name') const history = getHistory(name) @@ -956,67 +1110,17 @@ app.get('/', async c => { ) } - // All apps view - const metrics = await getAppMetrics() - - // Sort: running first, then by name - metrics.sort((a, b) => { - if (a.state === 'running' && b.state !== 'running') return -1 - if (a.state !== 'running' && b.state === 'running') return 1 - return a.name.localeCompare(b.name) - }) - - if (metrics.length === 0) { - return c.html( - - No apps found - - ) - } - - const running = metrics.filter(s => s.state === 'running') - const totalCpu = running.reduce((sum, s) => sum + (s.cpu ?? 0), 0) - const totalRss = running.reduce((sum, s) => sum + (s.rss ?? 0), 0) - const totalData = metrics.reduce((sum, s) => sum + (s.dataSize ?? 0), 0) + // Dashboard view: system metrics gauges + const sys = await fetchSystemMetrics() return c.html( - - - - - - PID - CPU - MEM - RSS - Data - - - - {metrics.map(s => ( - - - - {s.pid ?? '-'} - {formatPercent(s.cpu)} - {formatPercent(s.memory)} - {formatRss(s.rss)} - {formatBytes(s.dataSize)} - - ))} - -
NameState
- {s.name} - {s.tool && [tool]} - - - {s.state} - -
- - {running.length} running · {formatPercent(totalCpu)} CPU · {formatRss(totalRss)} RSS · {formatBytes(totalData)} data - + + {renderGauge(sys.cpu, 'cpu')} + {renderGauge(sys.ram.percent, 'ram')} + {renderGauge(sys.disk.percent, 'disk')} + +