diff --git a/apps/env/20260130-000000/index.tsx b/apps/env/20260130-000000/index.tsx
index f494613..1d1347a 100644
--- a/apps/env/20260130-000000/index.tsx
+++ b/apps/env/20260130-000000/index.tsx
@@ -19,18 +19,6 @@ const Container = define('Container', {
color: theme('colors-text'),
})
-const Header = define('Header', {
- marginBottom: '20px',
- paddingBottom: '10px',
- borderBottom: `2px solid ${theme('colors-border')}`,
-})
-
-const Title = define('Title', {
- margin: 0,
- fontSize: '24px',
- fontWeight: 'bold',
-})
-
const EnvList = define('EnvList', {
listStyle: 'none',
padding: 0,
@@ -170,9 +158,6 @@ function Layout({ title, children }: LayoutProps) {
-
- Environment Variables
-
{children}
diff --git a/apps/stats/20260130-000000/.npmrc b/apps/stats/20260130-000000/.npmrc
new file mode 100644
index 0000000..6c57d5c
--- /dev/null
+++ b/apps/stats/20260130-000000/.npmrc
@@ -0,0 +1 @@
+registry=https://npm.nose.space
diff --git a/apps/stats/20260130-000000/bun.lock b/apps/stats/20260130-000000/bun.lock
new file mode 100644
index 0000000..aee70f1
--- /dev/null
+++ b/apps/stats/20260130-000000/bun.lock
@@ -0,0 +1,45 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "stats",
+ "dependencies": {
+ "@because/forge": "^0.0.1",
+ "@because/hype": "^0.0.2",
+ "@because/toes": "^0.0.5",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5.9.3",
+ },
+ },
+ },
+ "packages": {
+ "@because/forge": ["@because/forge@0.0.1", "https://npm.nose.space/@because/forge/-/forge-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-QS5CK51gcWma91i4uECWe4HPJeNHcE+Af4SQHOcfEovyzOEa7VOTAjei+jIWr2i+abGWqQCEC9wIuFgPgyr2Bg=="],
+
+ "@because/hype": ["@because/hype@0.0.2", "https://npm.nose.space/@because/hype/-/hype-0.0.2.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-fdKeII6USGC1loVVj+tPz086cKz+Bm+XozNee3NOnK4VP+q4yNPP2Fq1Yujw5xeDYE+ZvJn40gKwlngRvmX2hA=="],
+
+ "@because/toes": ["@because/toes@0.0.5", "https://npm.nose.space/@because/toes/-/toes-0.0.5.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", "commander": "^14.0.2", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.2" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-YM1VuR1sym7m7pFcaiqnjg6eJUyhJYUH2ROBb+xi+HEXajq46ZL8KDyyCtz7WiHTfrbxcEWGjqyj20a7UppcJg=="],
+
+ "@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
+
+ "@types/node": ["@types/node@25.2.0", "https://npm.nose.space/@types/node/-/node-25.2.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
+
+ "bun-types": ["bun-types@1.3.8", "https://npm.nose.space/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
+
+ "commander": ["commander@14.0.3", "https://npm.nose.space/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
+
+ "diff": ["diff@8.0.3", "https://npm.nose.space/diff/-/diff-8.0.3.tgz", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
+
+ "hono": ["hono@4.11.7", "https://npm.nose.space/hono/-/hono-4.11.7.tgz", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
+
+ "kleur": ["kleur@4.1.5", "https://npm.nose.space/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+ }
+}
diff --git a/apps/stats/20260130-000000/index.tsx b/apps/stats/20260130-000000/index.tsx
new file mode 100644
index 0000000..7747730
--- /dev/null
+++ b/apps/stats/20260130-000000/index.tsx
@@ -0,0 +1,738 @@
+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(
+
+
+
+
+ | State |
+ PID
+ CPU
+ MEM
+ RSS
+
+
+
+
+ |
+
+ {stats.state}
+
+ |
+ {stats.pid ?? '-'}
+ {formatPercent(stats.cpu)}
+ {formatPercent(stats.memory)}
+ {formatRss(stats.rss)}
+
+
+
+
+
+
+ CPU Usage
+
+
+
+ Collecting data...
+
+
+
+
+ Memory %
+
+
+
+ Collecting data...
+
+
+
+
+ RSS Memory
+
+
+
+
+
+
+
+
+