From eb8ef0dd4d0700e7b3d1d0294693e9c142127956 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 15:28:20 +0000 Subject: [PATCH 1/2] Rename stats to metrics and add data size metric Rename the "stats" CLI command, tool app, and all internal references to "metrics". Add file size tracking from each app's DATA_DIR as a new metric, shown in both the CLI table and web UI. https://claude.ai/code/session_013agP8J1cCfrWZkueZ33jQB --- CLAUDE.md | 2 +- .../{stats => metrics}/20260130-000000/.npmrc | 0 .../20260130-000000/bun.lock | 0 .../20260130-000000/index.tsx | 214 +++++++++++------- .../20260130-000000/package.json | 2 +- .../20260130-000000/tsconfig.json | 0 src/cli/commands/index.ts | 2 +- src/cli/commands/{stats.ts => metrics.ts} | 58 +++-- src/cli/setup.ts | 8 +- 9 files changed, 170 insertions(+), 116 deletions(-) rename apps/{stats => metrics}/20260130-000000/.npmrc (100%) rename apps/{stats => metrics}/20260130-000000/bun.lock (100%) rename apps/{stats => metrics}/20260130-000000/index.tsx (84%) rename apps/{stats => metrics}/20260130-000000/package.json (95%) rename apps/{stats => metrics}/20260130-000000/tsconfig.json (100%) rename src/cli/commands/{stats.ts => metrics.ts} (52%) diff --git a/CLAUDE.md b/CLAUDE.md index 457808e..4d6ac86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Client-side SPA rendered with `hono/jsx/dom`. No build step -- Bun serves `.tsx` CLI commands: - **Apps**: `list`, `info`, `new`, `get`, `open`, `rename`, `rm` -- **Lifecycle**: `start`, `stop`, `restart`, `logs`, `stats`, `cron` +- **Lifecycle**: `start`, `stop`, `restart`, `logs`, `metrics`, `cron` - **Sync**: `push`, `pull`, `status`, `diff`, `sync`, `clean`, `stash` - **Config**: `config`, `env`, `versions`, `history`, `rollback` diff --git a/apps/stats/20260130-000000/.npmrc b/apps/metrics/20260130-000000/.npmrc similarity index 100% rename from apps/stats/20260130-000000/.npmrc rename to apps/metrics/20260130-000000/.npmrc diff --git a/apps/stats/20260130-000000/bun.lock b/apps/metrics/20260130-000000/bun.lock similarity index 100% rename from apps/stats/20260130-000000/bun.lock rename to apps/metrics/20260130-000000/bun.lock diff --git a/apps/stats/20260130-000000/index.tsx b/apps/metrics/20260130-000000/index.tsx similarity index 84% rename from apps/stats/20260130-000000/index.tsx rename to apps/metrics/20260130-000000/index.tsx index 7747730..a43be5d 100644 --- a/apps/stats/20260130-000000/index.tsx +++ b/apps/metrics/20260130-000000/index.tsx @@ -1,15 +1,18 @@ import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme } from '@because/toes/tools' +import { existsSync, readdirSync, statSync } from 'fs' +import { join } from 'path' import type { Child } from 'hono/jsx' // ============================================================================ // Configuration // ============================================================================ -const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process stats +const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process metrics const HISTORY_MAX_SAMPLES = 60 // Keep 10 minutes of history at 10s intervals +const TOES_DIR = process.env.TOES_DIR! const TOES_URL = process.env.TOES_URL! // ============================================================================ @@ -24,6 +27,13 @@ interface App { tool?: boolean } +interface AppMetrics extends App { + cpu?: number + dataSize?: number + memory?: number + rss?: number +} + interface HistorySample { timestamp: number cpu: number @@ -31,58 +41,26 @@ interface HistorySample { rss: number } -interface ProcessStats { +interface ProcessMetrics { pid: number cpu: number memory: number rss: number } -interface AppStats extends App { - cpu?: number - memory?: number - rss?: number -} - // ============================================================================ -// Process Stats Collection +// Process Metrics Collection // ============================================================================ -const statsCache = new Map() const appHistory = new Map() // app name -> history +const metricsCache = new Map() -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 getHistory(appName: string): HistorySample[] { + return appHistory.get(appName) ?? [] } -function getProcessStats(pid: number): ProcessStats | undefined { - return statsCache.get(pid) +function getProcessMetrics(pid: number): ProcessMetrics | undefined { + return metricsCache.get(pid) } function recordHistory(appName: string, cpu: number, memory: number, rss: number): void { @@ -102,12 +80,38 @@ function recordHistory(appName: string, cpu: number, memory: number, rss: number appHistory.set(appName, history) } -function getHistory(appName: string): HistorySample[] { - return appHistory.get(appName) ?? [] +async function sampleProcessMetrics(): 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 + + metricsCache.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) { + metricsCache.set(pid, { pid, cpu, memory, rss }) + } + } + } + } catch (err) { + console.error('Failed to sample process metrics:', err) + } } async function sampleAndRecordHistory(): Promise { - await sampleProcessStats() + await sampleProcessMetrics() // Record history for all running apps try { @@ -117,9 +121,9 @@ async function sampleAndRecordHistory(): Promise { 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) + const metrics = getProcessMetrics(app.pid) + if (metrics) { + recordHistory(app.name, metrics.cpu, metrics.memory, metrics.rss) } } } @@ -132,6 +136,29 @@ async function sampleAndRecordHistory(): Promise { sampleAndRecordHistory() setInterval(sampleAndRecordHistory, SAMPLE_INTERVAL_MS) +// ============================================================================ +// Data Size +// ============================================================================ + +function getDataSize(appName: string): number { + const dataDir = join(TOES_DIR, appName) + if (!existsSync(dataDir)) return 0 + return dirSize(dataDir) +} + +function dirSize(dir: string): number { + let total = 0 + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name) + if (entry.isDirectory()) { + total += dirSize(path) + } else { + total += statSync(path).size + } + } + return total +} + // ============================================================================ // API Client // ============================================================================ @@ -146,30 +173,32 @@ async function fetchApps(): Promise { } } -async function getAppStats(): Promise { +async function getAppMetrics(): Promise { const apps = await fetchApps() return apps.map(app => { - const stats = app.pid ? getProcessStats(app.pid) : undefined + const metrics = app.pid ? getProcessMetrics(app.pid) : undefined return { ...app, - cpu: stats?.cpu, - memory: stats?.memory, - rss: stats?.rss, + cpu: metrics?.cpu, + dataSize: getDataSize(app.name), + memory: metrics?.memory, + rss: metrics?.rss, } }) } -async function getAppStatsByName(name: string): Promise { +async function getAppMetricsByName(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 + const metrics = app.pid ? getProcessMetrics(app.pid) : undefined return { ...app, - cpu: stats?.cpu, - memory: stats?.memory, - rss: stats?.rss, + cpu: metrics?.cpu, + dataSize: getDataSize(app.name), + memory: metrics?.memory, + rss: metrics?.rss, } } @@ -310,11 +339,12 @@ const NoDataMessage = define('NoDataMessage', { // 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 formatBytes(bytes?: number): string { + if (bytes === undefined) return '-' + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB` } function formatPercent(value?: number): string { @@ -322,6 +352,13 @@ function formatPercent(value?: number): string { return `${value.toFixed(1)}%` } +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 getStatusColor(state: string): string { switch (state) { case 'running': @@ -377,18 +414,18 @@ app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { })) // API endpoint for CLI -app.get('/api/stats', async c => { - const stats = await getAppStats() - return c.json(stats) +app.get('/api/metrics', async c => { + const metrics = await getAppMetrics() + return c.json(metrics) }) -app.get('/api/stats/:name', async c => { +app.get('/api/metrics/:name', async c => { const name = c.req.param('name') - const stats = await getAppStatsByName(name) - if (!stats) { + const metrics = await getAppMetricsByName(name) + if (!metrics) { return c.json({ error: 'App not found' }, 404) } - return c.json(stats) + return c.json(metrics) }) app.get('/api/history/:name', c => { @@ -403,18 +440,18 @@ app.get('/', async c => { // Single app view if (appName) { - const stats = await getAppStatsByName(appName) + const metrics = await getAppMetricsByName(appName) - if (!stats) { + if (!metrics) { return c.html( - + App not found: {appName} ) } return c.html( - + @@ -423,19 +460,21 @@ app.get('/', async c => { CPUMEMRSS + Data - {stats.pid ?? '-'} - {formatPercent(stats.cpu)} - {formatPercent(stats.memory)} - {formatRss(stats.rss)} + {metrics.pid ?? '-'} + {formatPercent(metrics.cpu)} + {formatPercent(metrics.memory)} + {formatRss(metrics.rss)} + {formatBytes(metrics.dataSize)}
- - {stats.state} + + {metrics.state}
@@ -674,29 +713,30 @@ app.get('/', async c => { } // All apps view - const stats = await getAppStats() + const metrics = await getAppMetrics() // Sort: running first, then by name - stats.sort((a, b) => { + 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 (stats.length === 0) { + if (metrics.length === 0) { return c.html( - + No apps found ) } - const running = stats.filter(s => s.state === 'running') + 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) return c.html( - + @@ -706,10 +746,11 @@ app.get('/', async c => { CPUMEMRSS + Data - {stats.map(s => ( + {metrics.map(s => ( ))}
{s.name} @@ -724,12 +765,13 @@ app.get('/', async c => { {formatPercent(s.cpu)} {formatPercent(s.memory)} {formatRss(s.rss)} + {formatBytes(s.dataSize)}
- {running.length} running · {formatPercent(totalCpu)} CPU · {formatRss(totalRss)} total + {running.length} running · {formatPercent(totalCpu)} CPU · {formatRss(totalRss)} RSS · {formatBytes(totalData)} data
) diff --git a/apps/stats/20260130-000000/package.json b/apps/metrics/20260130-000000/package.json similarity index 95% rename from apps/stats/20260130-000000/package.json rename to apps/metrics/20260130-000000/package.json index 73ae360..e7ad3c8 100644 --- a/apps/stats/20260130-000000/package.json +++ b/apps/metrics/20260130-000000/package.json @@ -1,5 +1,5 @@ { - "name": "stats", + "name": "metrics", "module": "index.tsx", "type": "module", "private": true, diff --git a/apps/stats/20260130-000000/tsconfig.json b/apps/metrics/20260130-000000/tsconfig.json similarity index 100% rename from apps/stats/20260130-000000/tsconfig.json rename to apps/metrics/20260130-000000/tsconfig.json diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index ee2e0c5..4a434d4 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -13,5 +13,5 @@ export { startApp, stopApp, } from './manage' -export { statsApp } from './stats' +export { metricsApp } from './metrics' export { cleanApp, diffApp, getApp, historyApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync' diff --git a/src/cli/commands/stats.ts b/src/cli/commands/metrics.ts similarity index 52% rename from src/cli/commands/stats.ts rename to src/cli/commands/metrics.ts index 646c075..4a3c76b 100644 --- a/src/cli/commands/stats.ts +++ b/src/cli/commands/metrics.ts @@ -2,7 +2,7 @@ import color from 'kleur' import { get } from '../http' import { resolveAppName } from '../name' -interface AppStats { +interface AppMetrics { name: string state: string port?: number @@ -10,9 +10,18 @@ interface AppStats { cpu?: number memory?: number rss?: number + dataSize?: number tool?: boolean } +function formatBytes(bytes?: number): string { + if (bytes === undefined) return '-' + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB` +} + function formatRss(kb?: number): string { if (kb === undefined) return '-' if (kb < 1024) return `${kb} KB` @@ -30,55 +39,56 @@ function pad(str: string, len: number, right = false): string { return str.padEnd(len) } -export async function statsApp(arg?: string) { - // If arg is provided, show stats for that app only +export async function metricsApp(arg?: string) { + // If arg is provided, show metrics for that app only if (arg) { const name = resolveAppName(arg) if (!name) return - const stats: AppStats | undefined = await get(`/api/tools/stats/api/stats/${name}`) - if (!stats) { + const metrics: AppMetrics | undefined = await get(`/api/tools/metrics/api/metrics/${name}`) + if (!metrics) { console.error(`App not found: ${name}`) return } - console.log(`${color.bold(stats.name)} ${stats.tool ? color.gray('[tool]') : ''}`) - console.log(` State: ${stats.state}`) - if (stats.pid) console.log(` PID: ${stats.pid}`) - if (stats.port) console.log(` Port: ${stats.port}`) - if (stats.cpu !== undefined) console.log(` CPU: ${formatPercent(stats.cpu)}`) - if (stats.memory !== undefined) console.log(` Memory: ${formatPercent(stats.memory)}`) - if (stats.rss !== undefined) console.log(` RSS: ${formatRss(stats.rss)}`) + console.log(`${color.bold(metrics.name)} ${metrics.tool ? color.gray('[tool]') : ''}`) + console.log(` State: ${metrics.state}`) + if (metrics.pid) console.log(` PID: ${metrics.pid}`) + if (metrics.port) console.log(` Port: ${metrics.port}`) + if (metrics.cpu !== undefined) console.log(` CPU: ${formatPercent(metrics.cpu)}`) + if (metrics.memory !== undefined) console.log(` Memory: ${formatPercent(metrics.memory)}`) + if (metrics.rss !== undefined) console.log(` RSS: ${formatRss(metrics.rss)}`) + console.log(` Data: ${formatBytes(metrics.dataSize)}`) return } - // Show stats for all apps - const stats: AppStats[] | undefined = await get('/api/tools/stats/api/stats') - if (!stats || stats.length === 0) { + // Show metrics for all apps + const metrics: AppMetrics[] | undefined = await get('/api/tools/metrics/api/metrics') + if (!metrics || metrics.length === 0) { console.log('No apps found') return } // Sort: running first, then by name - stats.sort((a, b) => { + 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) }) // Calculate column widths - const nameWidth = Math.max(4, ...stats.map(s => s.name.length + (s.tool ? 7 : 0))) - const stateWidth = Math.max(5, ...stats.map(s => s.state.length)) + const nameWidth = Math.max(4, ...metrics.map(s => s.name.length + (s.tool ? 7 : 0))) + const stateWidth = Math.max(5, ...metrics.map(s => s.state.length)) // Header console.log( color.gray( - `${pad('NAME', nameWidth)} ${pad('STATE', stateWidth)} ${pad('PID', 7, true)} ${pad('CPU', 7, true)} ${pad('MEM', 7, true)} ${pad('RSS', 10, true)}` + `${pad('NAME', nameWidth)} ${pad('STATE', stateWidth)} ${pad('PID', 7, true)} ${pad('CPU', 7, true)} ${pad('MEM', 7, true)} ${pad('RSS', 10, true)} ${pad('DATA', 10, true)}` ) ) // Rows - for (const s of stats) { + for (const s of metrics) { const name = s.tool ? `${s.name} ${color.gray('[tool]')}` : s.name const stateColor = s.state === 'running' ? color.green : s.state === 'invalid' ? color.red : color.gray const state = stateColor(s.state) @@ -87,17 +97,19 @@ export async function statsApp(arg?: string) { const cpu = formatPercent(s.cpu) const mem = formatPercent(s.memory) const rss = formatRss(s.rss) + const data = formatBytes(s.dataSize) console.log( - `${pad(name, nameWidth)} ${pad(state, stateWidth)} ${pad(pid, 7, true)} ${pad(cpu, 7, true)} ${pad(mem, 7, true)} ${pad(rss, 10, true)}` + `${pad(name, nameWidth)} ${pad(state, stateWidth)} ${pad(pid, 7, true)} ${pad(cpu, 7, true)} ${pad(mem, 7, true)} ${pad(rss, 10, true)} ${pad(data, 10, true)}` ) } // Summary - const running = stats.filter(s => s.state === 'running') + 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) console.log() - console.log(color.gray(`${running.length} running, ${formatPercent(totalCpu)} CPU, ${formatRss(totalRss)} total`)) + console.log(color.gray(`${running.length} running, ${formatPercent(totalCpu)} CPU, ${formatRss(totalRss)} RSS, ${formatBytes(totalData)} data`)) } diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 58d1e70..ead335d 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -32,7 +32,7 @@ import { stashListApp, stashPopApp, startApp, - statsApp, + metricsApp, statusApp, stopApp, syncApp, @@ -161,11 +161,11 @@ program .action(logApp) program - .command('stats') + .command('metrics') .helpGroup('Lifecycle:') - .description('Show CPU and memory stats for apps') + .description('Show CPU, memory, and disk metrics for apps') .argument('[name]', 'app name (uses current directory if omitted)') - .action(statsApp) + .action(metricsApp) const cron = program .command('cron') From 7a79133d784d2b2ce51d5ba04737dbf104ca6dbc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 16:20:42 +0000 Subject: [PATCH 2/2] Add DATA chart with daily X axis and show 2 charts per row Track data size history daily (up to 30 days) in memory, with a dedicated /api/data-history/:name endpoint. The Data Size chart uses day labels (e.g. "Feb 12") instead of minute-based timestamps. Charts are now displayed in a 2-column grid. https://claude.ai/code/session_013agP8J1cCfrWZkueZ33jQB --- apps/metrics/20260130-000000/index.tsx | 131 ++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/apps/metrics/20260130-000000/index.tsx b/apps/metrics/20260130-000000/index.tsx index a43be5d..152dbcb 100644 --- a/apps/metrics/20260130-000000/index.tsx +++ b/apps/metrics/20260130-000000/index.tsx @@ -9,6 +9,7 @@ import type { Child } from 'hono/jsx' // Configuration // ============================================================================ +const DATA_HISTORY_MAX_DAYS = 30 // Keep 30 days of data size history const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process metrics const HISTORY_MAX_SAMPLES = 60 // Keep 10 minutes of history at 10s intervals @@ -41,6 +42,11 @@ interface HistorySample { rss: number } +interface DataSample { + date: string + bytes: number +} + interface ProcessMetrics { pid: number cpu: number @@ -53,8 +59,13 @@ interface ProcessMetrics { // ============================================================================ const appHistory = new Map() // app name -> history +const dataHistory = new Map() // app name -> daily data size const metricsCache = new Map() +function getDataHistory(appName: string): DataSample[] { + return dataHistory.get(appName) ?? [] +} + function getHistory(appName: string): HistorySample[] { return appHistory.get(appName) ?? [] } @@ -110,15 +121,36 @@ async function sampleProcessMetrics(): Promise { } } +function recordDataSize(appName: string): void { + const today = new Date().toISOString().slice(0, 10) + const history = dataHistory.get(appName) ?? [] + const bytes = getDataSize(appName) + + // Update today's entry or add new one + const existing = history.find(s => s.date === today) + if (existing) { + existing.bytes = bytes + } else { + history.push({ date: today, bytes }) + } + + // Keep only the last N days + while (history.length > DATA_HISTORY_MAX_DAYS) { + history.shift() + } + + dataHistory.set(appName, history) +} + async function sampleAndRecordHistory(): Promise { await sampleProcessMetrics() - // 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[] + // Record process history for running apps for (const app of apps) { if (app.pid && app.state === 'running') { const metrics = getProcessMetrics(app.pid) @@ -127,6 +159,11 @@ async function sampleAndRecordHistory(): Promise { } } } + + // Record data size history for all apps (filesystem-based, not process-based) + for (const app of apps) { + recordDataSize(app.name) + } } catch { // Ignore errors } @@ -301,7 +338,7 @@ const EmptyState = define('EmptyState', { const ChartsContainer = define('ChartsContainer', { marginTop: '24px', display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', + gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px', }) @@ -428,6 +465,12 @@ app.get('/api/metrics/:name', async c => { return c.json(metrics) }) +app.get('/api/data-history/:name', c => { + const name = c.req.param('name') + const history = getDataHistory(name) + return c.json(history) +}) + app.get('/api/history/:name', c => { const name = c.req.param('name') const history = getHistory(name) @@ -507,6 +550,15 @@ app.get('/', async c => { + + Data Size + + + + + @@ -517,6 +569,7 @@ app.get('/', async c => { const textColor = isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'; const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; const cpuColor = 'rgb(59, 130, 246)'; + const dataColor = 'rgb(245, 158, 11)'; const memColor = 'rgb(168, 85, 247)'; const rssColor = 'rgb(34, 197, 94)'; @@ -557,18 +610,76 @@ app.get('/', async c => { } }; - let cpuChart, memChart, rssChart; + let cpuChart, dataChart, memChart, rssChart; function formatTime(ts) { const d = new Date(ts); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } + function formatBytes(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB'; + return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'; + } + + function formatDate(dateStr) { + const d = new Date(dateStr + 'T00:00:00'); + return d.toLocaleDateString([], { month: 'short', day: 'numeric' }); + } + function formatRss(kb) { if (kb < 1024) return kb + ' KB'; return (kb / 1024).toFixed(1) + ' MB'; } + function updateDataChart(history) { + if (history.length === 0) { + document.getElementById('dataChart').style.display = 'none'; + document.getElementById('dataNoData').style.display = 'flex'; + return; + } + + document.getElementById('dataChart').style.display = 'block'; + document.getElementById('dataNoData').style.display = 'none'; + + const labels = history.map(h => formatDate(h.date)); + const data = history.map(h => h.bytes / (1024 * 1024)); // Convert to MB + + if (!dataChart) { + dataChart = new Chart(document.getElementById('dataChart'), { + type: 'line', + data: { + labels: labels, + datasets: [{ + data: data, + borderColor: dataColor, + backgroundColor: dataColor.replace('rgb', 'rgba').replace(')', ', 0.1)'), + fill: true + }] + }, + options: { + ...commonOptions, + scales: { + ...commonOptions.scales, + y: { + ...commonOptions.scales.y, + ticks: { + ...commonOptions.scales.y.ticks, + callback: v => v.toFixed(1) + ' MB' + } + } + } + } + }); + } else { + dataChart.data.labels = labels; + dataChart.data.datasets[0].data = data; + dataChart.update('none'); + } + } + function updateCharts(history) { const labels = history.map(h => formatTime(h.timestamp)); const cpuData = history.map(h => h.cpu); @@ -689,6 +800,18 @@ app.get('/', async c => { } } + async function fetchDataHistory() { + try { + const res = await fetch('/api/data-history/' + encodeURIComponent(appName)); + if (res.ok) { + const history = await res.json(); + updateDataChart(history); + } + } catch (e) { + console.error('Failed to fetch data history:', e); + } + } + async function fetchHistory() { try { const res = await fetch('/api/history/' + encodeURIComponent(appName)); @@ -703,9 +826,11 @@ app.get('/', async c => { // Initial fetch fetchHistory(); + fetchDataHistory(); // Update every 10 seconds setInterval(fetchHistory, 10000); + setInterval(fetchDataHistory, 60000); // Data size changes slowly })(); `}} />