perf: fix dashboard out of memory crashing from massive historical metrics payloads

This commit is contained in:
SaelixCode
2026-03-19 19:59:40 -04:00
parent eb58f302a1
commit 4e9777d47f
3 changed files with 37 additions and 10 deletions
+1
View File
@@ -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/`.
+14 -1
View File
@@ -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);
}
+22 -9
View File
@@ -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<string, { time: string; timestamp: number; cpu: number; ram: number }> = {};
const cores = systemStats?.cpu.cores || 1;