diff --git a/CHANGELOG.md b/CHANGELOG.md index 54fc65c5..655fc69c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Fixed:** A massive memory leak (browser Out of Memory crash) by throttling historical metrics polling down to 60s and downsampling SQLite metrics payload sizes by 12x. - **Fixed:** A bug where the active node UI dropdown would desync from the actual API requests on initial page load by properly hydrating state from localStorage. - **Fixed:** Remote node proxy forwarding the browser's `sencho_token` cookie to the remote Sencho instance — the remote's `authMiddleware` evaluates `cookieToken || bearerToken` and the cookie (signed with the local JWT secret) was validated before the valid Bearer token, causing 401 on all proxied API calls. Fixed by stripping the `cookie` header in `proxyReq` so only the Bearer token is used for remote authentication. - **Fixed:** `nodeContextMiddleware` blocking `/api/nodes` when `x-node-id` references a deleted/non-existent node — the nodes list endpoint must always succeed so the frontend can re-sync a stale node ID in localStorage; exempted alongside `/api/auth/`. diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts index eb9810ca..46b73177 100644 --- a/backend/src/services/DatabaseService.ts +++ b/backend/src/services/DatabaseService.ts @@ -324,7 +324,20 @@ export class DatabaseService { public getContainerMetrics(hoursLookback = 24): any[] { const cutoff = Date.now() - (hoursLookback * 60 * 60 * 1000); - const stmt = this.db.prepare('SELECT * FROM container_metrics WHERE timestamp >= ? ORDER BY timestamp ASC'); + const stmt = this.db.prepare(` + SELECT + container_id, + stack_name, + AVG(cpu_percent) as cpu_percent, + AVG(memory_mb) as memory_mb, + MAX(net_rx_mb) as net_rx_mb, + MAX(net_tx_mb) as net_tx_mb, + (timestamp / 60000) * 60000 as timestamp + FROM container_metrics + WHERE timestamp >= ? + GROUP BY container_id, stack_name, (timestamp / 60000) + ORDER BY timestamp ASC + `); return stmt.all(cutoff); } diff --git a/frontend/src/components/HomeDashboard.tsx b/frontend/src/components/HomeDashboard.tsx index 915f2ba7..355eae46 100644 --- a/frontend/src/components/HomeDashboard.tsx +++ b/frontend/src/components/HomeDashboard.tsx @@ -82,20 +82,15 @@ export default function HomeDashboard() { return () => clearInterval(interval); }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps - // Fetch system stats and historical metrics - re-runs when active node changes + // Fetch system stats (CPU/RAM/Disk/Network) - 5s polling, re-runs on node switch useEffect(() => { setSystemStats(null); - setMetrics([]); const fetchSystemStats = async () => { try { - const [sysRes, metricsRes] = await Promise.all([ - apiFetch('/system/stats'), - apiFetch('/metrics/historical') - ]); - if (sysRes.ok) setSystemStats(await sysRes.json()); - if (metricsRes.ok) setMetrics(await metricsRes.json()); + const res = await apiFetch('/system/stats'); + if (res.ok) setSystemStats(await res.json()); } catch (error) { - console.error('Failed to fetch system stats or metrics:', error); + console.error('Failed to fetch system stats:', error); } }; fetchSystemStats(); @@ -103,6 +98,24 @@ export default function HomeDashboard() { return () => clearInterval(interval); }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps + // Fetch historical metrics - intentionally slow 60s poll to prevent OOM. + // The backend now returns 1-minute buckets (down from raw 5s points), so + // re-fetching more often than 60s would return the same data anyway. + useEffect(() => { + setMetrics([]); + const fetchMetrics = async () => { + try { + const res = await apiFetch('/metrics/historical'); + if (res.ok) setMetrics(await res.json()); + } catch (error) { + console.error('Failed to fetch historical metrics:', error); + } + }; + fetchMetrics(); + const interval = setInterval(fetchMetrics, 60000); + return () => clearInterval(interval); + }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps + const chartData = useMemo(() => { const buckets: Record = {}; const cores = systemStats?.cpu.cores || 1;