From 7a79133d784d2b2ce51d5ba04737dbf104ca6dbc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 16:20:42 +0000 Subject: [PATCH] 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 })(); `}} />