import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme } from '@because/toes/tools' import type { Child } from 'hono/jsx' // ============================================================================ // Configuration // ============================================================================ const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process stats const HISTORY_MAX_SAMPLES = 60 // Keep 10 minutes of history at 10s intervals const TOES_URL = process.env.TOES_URL! // ============================================================================ // Types // ============================================================================ interface App { name: string state: string port?: number pid?: number tool?: boolean } interface HistorySample { timestamp: number cpu: number memory: number rss: number } interface ProcessStats { pid: number cpu: number memory: number rss: number } interface AppStats extends App { cpu?: number memory?: number rss?: number } // ============================================================================ // Process Stats Collection // ============================================================================ const statsCache = new Map() const appHistory = new Map() // app name -> history async function sampleProcessStats(): Promise { try { const proc = Bun.spawn(['ps', '-eo', 'pid,pcpu,pmem,rss'], { stdout: 'pipe', stderr: 'ignore', }) const text = await new Response(proc.stdout).text() const lines = text.trim().split('\n').slice(1) // Skip header statsCache.clear() for (const line of lines) { const parts = line.trim().split(/\s+/) if (parts.length >= 4) { const pid = parseInt(parts[0]!, 10) const cpu = parseFloat(parts[1]!) const memory = parseFloat(parts[2]!) const rss = parseInt(parts[3]!, 10) // KB if (!isNaN(pid) && pid > 0) { statsCache.set(pid, { pid, cpu, memory, rss }) } } } } catch (err) { console.error('Failed to sample process stats:', err) } } function getProcessStats(pid: number): ProcessStats | undefined { return statsCache.get(pid) } function recordHistory(appName: string, cpu: number, memory: number, rss: number): void { const history = appHistory.get(appName) ?? [] history.push({ timestamp: Date.now(), cpu, memory, rss, }) // Keep only the last N samples while (history.length > HISTORY_MAX_SAMPLES) { history.shift() } appHistory.set(appName, history) } function getHistory(appName: string): HistorySample[] { return appHistory.get(appName) ?? [] } async function sampleAndRecordHistory(): Promise { await sampleProcessStats() // Record history for all running apps try { const res = await fetch(`${TOES_URL}/api/apps`) if (!res.ok) return const apps = await res.json() as App[] for (const app of apps) { if (app.pid && app.state === 'running') { const stats = getProcessStats(app.pid) if (stats) { recordHistory(app.name, stats.cpu, stats.memory, stats.rss) } } } } catch { // Ignore errors } } // Start sampling on module load sampleAndRecordHistory() setInterval(sampleAndRecordHistory, SAMPLE_INTERVAL_MS) // ============================================================================ // API Client // ============================================================================ async function fetchApps(): Promise { try { const res = await fetch(`${TOES_URL}/api/apps`) if (!res.ok) return [] return await res.json() as App[] } catch { return [] } } async function getAppStats(): Promise { const apps = await fetchApps() return apps.map(app => { const stats = app.pid ? getProcessStats(app.pid) : undefined return { ...app, cpu: stats?.cpu, memory: stats?.memory, rss: stats?.rss, } }) } async function getAppStatsByName(name: string): Promise { const apps = await fetchApps() const app = apps.find(a => a.name === name) if (!app) return undefined const stats = app.pid ? getProcessStats(app.pid) : undefined return { ...app, cpu: stats?.cpu, memory: stats?.memory, rss: stats?.rss, } } // ============================================================================ // Styled Components // ============================================================================ const Container = define('Container', { fontFamily: theme('fonts-sans'), padding: '20px', paddingTop: 0, maxWidth: '900px', margin: '0 auto', color: theme('colors-text'), }) const Table = define('Table', { base: 'table', width: '100%', borderCollapse: 'collapse', fontSize: '14px', fontFamily: theme('fonts-mono'), }) const Th = define('Th', { base: 'th', textAlign: 'left', padding: '10px 12px', borderBottom: `2px solid ${theme('colors-border')}`, color: theme('colors-textMuted'), fontSize: '12px', fontWeight: 'normal', textTransform: 'uppercase', letterSpacing: '0.5px', }) const ThRight = define('ThRight', { base: 'th', textAlign: 'right', padding: '10px 12px', borderBottom: `2px solid ${theme('colors-border')}`, color: theme('colors-textMuted'), fontSize: '12px', fontWeight: 'normal', textTransform: 'uppercase', letterSpacing: '0.5px', }) const Tr = define('Tr', { base: 'tr', states: { ':hover': { backgroundColor: theme('colors-bgHover'), }, }, }) const Td = define('Td', { base: 'td', padding: '10px 12px', borderBottom: `1px solid ${theme('colors-border')}`, }) const TdRight = define('TdRight', { base: 'td', padding: '10px 12px', borderBottom: `1px solid ${theme('colors-border')}`, textAlign: 'right', }) const StatusBadge = define('StatusBadge', { base: 'span', fontSize: '12px', padding: '2px 6px', borderRadius: theme('radius-md'), }) const ToolBadge = define('ToolBadge', { base: 'span', fontSize: '11px', marginLeft: '6px', color: theme('colors-textMuted'), }) const Summary = define('Summary', { marginTop: '20px', padding: '12px 15px', backgroundColor: theme('colors-bgElement'), borderRadius: theme('radius-md'), fontSize: '13px', color: theme('colors-textMuted'), }) const EmptyState = define('EmptyState', { padding: '40px 20px', textAlign: 'center', color: theme('colors-textMuted'), }) const ChartsContainer = define('ChartsContainer', { marginTop: '24px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '20px', }) const ChartCard = define('ChartCard', { backgroundColor: theme('colors-bgElement'), borderRadius: theme('radius-md'), padding: '16px', border: `1px solid ${theme('colors-border')}`, }) const ChartTitle = define('ChartTitle', { margin: '0 0 12px 0', fontSize: '13px', fontWeight: 600, color: theme('colors-textMuted'), textTransform: 'uppercase', letterSpacing: '0.5px', }) const ChartWrapper = define('ChartWrapper', { position: 'relative', height: '150px', }) const NoDataMessage = define('NoDataMessage', { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: theme('colors-textMuted'), fontSize: '13px', }) // ============================================================================ // Helpers // ============================================================================ function formatRss(kb?: number): string { if (kb === undefined) return '-' if (kb < 1024) return `${kb} KB` if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB` return `${(kb / 1024 / 1024).toFixed(2)} GB` } function formatPercent(value?: number): string { if (value === undefined) return '-' return `${value.toFixed(1)}%` } function getStatusColor(state: string): string { switch (state) { case 'running': return theme('colors-statusRunning') case 'stopped': case 'stopping': return theme('colors-statusStopped') case 'invalid': return theme('colors-error') default: return theme('colors-textMuted') } } // ============================================================================ // Layout // ============================================================================ interface LayoutProps { title: string children: Child } function Layout({ title, children }: LayoutProps) { return ( {title} {children} ) } // ============================================================================ // App // ============================================================================ const app = new Hype({ prettyHTML: false }) app.get('/ok', c => c.text('ok')) app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) // API endpoint for CLI app.get('/api/stats', async c => { const stats = await getAppStats() return c.json(stats) }) app.get('/api/stats/:name', async c => { const name = c.req.param('name') const stats = await getAppStatsByName(name) if (!stats) { return c.json({ error: 'App not found' }, 404) } return c.json(stats) }) app.get('/api/history/:name', c => { const name = c.req.param('name') const history = getHistory(name) return c.json(history) }) // Web UI app.get('/', async c => { const appName = c.req.query('app') // Single app view if (appName) { const stats = await getAppStatsByName(appName) if (!stats) { return c.html( App not found: {appName} ) } return c.html( PIDCPUMEMRSS{stats.pid ?? '-'}{formatPercent(stats.cpu)}{formatPercent(stats.memory)}{formatRss(stats.rss)}
State
{stats.state}
CPU Usage Memory % RSS Memory