import { allApps, APPS_DIR, onChange, TOES_DIR } from '$apps' import { onHostLog } from '../tui' import { Hype } from '@because/hype' import { cpus, platform, totalmem } from 'os' import { join } from 'path' import { existsSync, mkdirSync, readFileSync, statfsSync, writeFileSync } from 'fs' export interface AppMetrics { cpu: number mem: number disk: number } export interface SystemMetrics { cpu: number // 0-100 ram: { used: number, total: number, percent: number } disk: { used: number, total: number, percent: number } apps: Record } export interface WifiConfig { network: string password: string } export interface UnifiedLogLine { time: number app: string text: string } const router = Hype.router() // Unified log buffer const unifiedLogs: UnifiedLogLine[] = [] const MAX_UNIFIED_LOGS = 200 const unifiedLogListeners = new Set<(line: UnifiedLogLine) => void>() // Track last log counts per app for delta detection const lastLogCounts = new Map() // CPU tracking let lastCpuTimes: { idle: number, total: number } | null = null function getCpuUsage(): number { const cpuList = cpus() let idle = 0 let total = 0 for (const cpu of cpuList) { idle += cpu.times.idle total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq } if (!lastCpuTimes) { lastCpuTimes = { idle, total } return 0 } const idleDiff = idle - lastCpuTimes.idle const totalDiff = total - lastCpuTimes.total lastCpuTimes = { idle, total } if (totalDiff === 0) return 0 return Math.round((1 - idleDiff / totalDiff) * 100) } function getMemoryUsage(): { used: number, total: number, percent: number } { const total = totalmem() const apps = allApps().filter(a => a.proc?.pid) let used = 0 if (platform() === 'linux') { for (const app of apps) { try { const status = readFileSync(`/proc/${app.proc!.pid}/status`, 'utf-8') const match = status.match(/VmRSS:\s+(\d+)/) if (match) used += parseInt(match[1]!, 10) * 1024 } catch {} } } else { // macOS: batch ps call for all pids const pids = apps.map(a => a.proc!.pid).join(',') if (pids) { try { const result = Bun.spawnSync(['ps', '-o', 'rss=', '-p', pids]) const output = result.stdout.toString() for (const line of output.split('\n')) { const kb = parseInt(line.trim(), 10) if (kb) used += kb * 1024 } } catch {} } } return { used, total, percent: used > 0 ? Math.max(1, Math.round((used / total) * 100)) : 0, } } function getDiskUsage(): { used: number, total: number, percent: number } { try { const stats = statfsSync('/') const total = stats.blocks * stats.bsize const free = stats.bfree * stats.bsize const used = total - free return { used, total, percent: Math.round((used / total) * 100), } } catch { return { used: 0, total: 0, percent: 0 } } } // Per-app disk cache (updated every 30s) let _appDiskCache: Record = {} let _appDiskLastUpdate = 0 const DISK_CACHE_TTL = 30000 function getAppMetrics(): Record { const apps = allApps() const running = apps.filter(a => a.proc?.pid) const result: Record = {} // CPU + MEM via ps (works on both macOS and Linux) const pidToName = new Map() for (const app of running) { pidToName.set(app.proc!.pid, app.name) } if (pidToName.size > 0) { try { const pids = [...pidToName.keys()].join(',') const ps = Bun.spawnSync(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids]) for (const line of ps.stdout.toString().split('\n')) { const parts = line.trim().split(/\s+/) if (parts.length < 3) continue const pid = parseInt(parts[0]!, 10) const cpu = parseFloat(parts[1]!) || 0 const mem = (parseInt(parts[2]!, 10) || 0) * 1024 const name = pidToName.get(pid) if (name) result[name] = { cpu: Math.round(cpu), mem, disk: 0 } } } catch {} } // Disk usage per app (cached) const now = Date.now() if (now - _appDiskLastUpdate > DISK_CACHE_TTL) { _appDiskLastUpdate = now _appDiskCache = {} for (const app of apps) { try { const du = Bun.spawnSync(['du', '-sk', join(APPS_DIR, app.name)]) const kb = parseInt(du.stdout.toString().trim().split('\t')[0]!, 10) if (kb) _appDiskCache[app.name] = kb * 1024 } catch {} } } // Merge disk into results, fill in stopped apps for (const app of apps) { if (!result[app.name]) result[app.name] = { cpu: 0, mem: 0, disk: 0 } result[app.name]!.disk = _appDiskCache[app.name] ?? 0 } return result } // Get current system metrics router.get('/metrics', c => { const metrics: SystemMetrics = { cpu: getCpuUsage(), ram: getMemoryUsage(), disk: getDiskUsage(), apps: getAppMetrics(), } return c.json(metrics) }) // SSE stream for real-time metrics (updates every 2s) router.sse('/metrics/stream', (send) => { const sendMetrics = () => { const metrics: SystemMetrics = { cpu: getCpuUsage(), ram: getMemoryUsage(), disk: getDiskUsage(), apps: getAppMetrics(), } send(metrics) } // Initial send sendMetrics() // Update every 2 seconds const interval = setInterval(sendMetrics, 2000) return () => clearInterval(interval) }) // WiFi config const CONFIG_DIR = join(TOES_DIR, 'config') const WIFI_PATH = join(CONFIG_DIR, 'wifi.json') function readWifiConfig(): WifiConfig { try { if (existsSync(WIFI_PATH)) { return JSON.parse(readFileSync(WIFI_PATH, 'utf-8')) } } catch {} return { network: '', password: '' } } function writeWifiConfig(config: WifiConfig) { if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true }) writeFileSync(WIFI_PATH, JSON.stringify(config, null, 2)) } router.get('/wifi', c => { return c.json(readWifiConfig()) }) router.put('/wifi', async c => { const body = await c.req.json() const config: WifiConfig = { network: String(body.network ?? ''), password: String(body.password ?? ''), } writeWifiConfig(config) return c.json(config) }) // Get recent unified logs router.get('/logs', c => { const tail = c.req.query('tail') const count = tail ? parseInt(tail, 10) : MAX_UNIFIED_LOGS return c.json(unifiedLogs.slice(-count)) }) // Clear unified logs router.post('/logs/clear', c => { unifiedLogs.length = 0 return c.json({ ok: true }) }) // SSE stream for unified logs router.sse('/logs/stream', (send) => { // Send existing logs first for (const line of unifiedLogs.slice(-50)) { send(line) } // Subscribe to new logs const listener = (line: UnifiedLogLine) => send(line) unifiedLogListeners.add(listener) return () => unifiedLogListeners.delete(listener) }) // Collect logs from all apps function collectLogs() { const apps = allApps() for (const app of apps) { const logs = app.logs ?? [] const lastCount = lastLogCounts.get(app.name) ?? 0 // Get new logs since last check const newLogs = logs.slice(lastCount) lastLogCounts.set(app.name, logs.length) for (const log of newLogs) { const line: UnifiedLogLine = { time: log.time, app: app.name, text: log.text, } // Add to buffer unifiedLogs.push(line) if (unifiedLogs.length > MAX_UNIFIED_LOGS) { unifiedLogs.shift() } // Notify listeners for (const listener of unifiedLogListeners) { listener(line) } } } } function pushHostLog(text: string) { const line: UnifiedLogLine = { time: Date.now(), app: 'toes', text } unifiedLogs.push(line) if (unifiedLogs.length > MAX_UNIFIED_LOGS) unifiedLogs.shift() for (const listener of unifiedLogListeners) listener(line) } // Subscribe to app changes to collect logs onChange(collectLogs) // Subscribe to host-level log messages onHostLog(pushHostLog) export default router