From 3f473c5c97d3d914df898dc8b590b04c838b85ff Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Thu, 19 Mar 2026 22:02:29 -0400 Subject: [PATCH] fix(backend): remove broken remote branch in /api/system/stats The remoteNodeProxy middleware (line 373) is already positioned before all API route definitions, so remote requests are correctly proxied to the target Sencho instance before any route handler executes. However, /api/system/stats had a dead remote branch that called NodeRegistry.getDocker() for remote nodes. This method throws by design ("remote nodes are not directly accessible") since no direct Docker TCP socket is used for remote nodes. The thrown error propagated through the catch block, returning a 500 to the frontend and causing system stats to show as loading/unavailable for remote nodes. Removed the dead branch. Remote requests for /api/system/stats are now correctly proxied, returning real CPU/RAM/disk data from the remote host. --- CHANGELOG.md | 1 + backend/src/index.ts | 28 ++-------------------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eed4f5f4..76212264 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:** Remote node system stats, container stats, logs, and exec returning errors — `remoteNodeProxy` middleware was already positioned before API route definitions, but `/api/system/stats` contained a dead remote branch that called `NodeRegistry.getDocker()` for remote nodes, which throws since remote nodes have no direct Docker socket access. Removed the broken branch; remote requests are correctly intercepted by the proxy middleware before reaching any route handler. - **Added:** Background image update checker — `ImageUpdateService` polls OCI-compliant registries (Docker Hub, GHCR, LSCR, etc.) every 6 hours using manifest digest comparison against local `RepoDigests`. Results cached in a new `stack_update_status` SQLite table. A pulsing blue dot badge appears in the stack list sidebar for stacks with available updates. Manual refresh available via `POST /api/image-updates/refresh` (rate-limited to once per 10 minutes). - **Fixed:** `AppStoreView` and `GlobalObservabilityView` using raw `fetch()` instead of `apiFetch()` — all calls now inject the `x-node-id` header so templates, deploys, stacks, and logs are correctly proxied to the active remote node. - **Fixed:** `HostConsole` WebSocket URL missing `?nodeId=` query parameter — the upgrade handler now receives the active node ID and routes the PTY session to the correct node. diff --git a/backend/src/index.ts b/backend/src/index.ts index 26a88aa2..082e560a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1139,35 +1139,11 @@ app.get('/api/logs/global/stream', async (req: Request, res: Response) => { // Get host system stats app.get('/api/system/stats', async (req: Request, res: Response) => { try { - const nodeId = req.nodeId ?? NodeRegistry.getInstance().getDefaultNodeId(); - const node = NodeRegistry.getInstance().getNode(nodeId); - const rxSec = Math.max(0, globalDockerNetwork.rxSec); const txSec = Math.max(0, globalDockerNetwork.txSec); - if (node && node.type === 'remote') { - // Remote node: use Docker daemon info for CPU/RAM - disk is not available via Docker API - const docker = NodeRegistry.getInstance().getDocker(nodeId); - const info = await docker.info(); - - res.json({ - cpu: { - usage: '0', - cores: info.NCPU ?? 0, - }, - memory: { - total: info.MemTotal ?? 0, - used: 0, - free: info.MemTotal ?? 0, - usagePercent: '0', - }, - disk: null, - network: { rxBytes: 0, txBytes: 0, rxSec, txSec }, - }); - return; - } - - // Local node: use systeminformation for accurate host metrics + // Remote node requests are intercepted and proxied by remoteNodeProxy before reaching here. + // This handler only runs for local nodes. const [currentLoad, mem, fsSize] = await Promise.all([ si.currentLoad(), si.mem(),