Commit Graph

166 Commits

Author SHA1 Message Date
SaelixCode 74964b0e26 fix(stats): throttle container stat WebSocket updates via ref buffer
Each container's onmessage handler was calling setContainerStats()
independently, causing React to schedule up to N separate reconciliation
passes per second (one per container). With 20 containers streaming Docker
stats at ~1 update/s, EditorLayout was re-rendering up to 20 times/s.

Fix: incoming stats are written into pendingStatsRef (no re-render cost),
then flushed to React state in one batched setContainerStats call every 1.5s.

Two bugs in the original proposal are addressed:
- rawBytesRef (never cleared) owns rx/tx tracking so net I/O rate is always
  accurate; avoids the stale containerStats closure that would have shown
  0 B/s after every flush cycle
- pending snapshot is captured and cleared BEFORE calling setState so the
  functional updater stays pure (no side-effects inside it)
- pendingStatsRef is cleared in the effect cleanup so stale entries from
  the previous stack don't briefly appear on stack switch
2026-03-20 12:14:09 -04:00
Anso 36cc14bac7 Merge branch 'develop' into fix/logs-view-oom-virtualization 2026-03-20 12:08:01 -04:00
Anso 9367abf8d3 Merge pull request #45 from AnsoCode/fix/create-stack-error-handling
fix(dashboard): surface server error messages in create-stack flow
2026-03-20 12:07:08 -04:00
SaelixCode 753b0c3539 fix(logs): use monotonic _id key to prevent O(n) DOM mutations on scroll
key={idx} with a shifting .slice(-300) window meant every new log arrival
caused React to update all 300 existing DOM nodes (index 1 became 0, 2
became 1, etc.), even though only one entry actually changed.

The proposed content-hash fix (timestamp + containerName + message.substring)
carries a real collision risk: chatty containers emitting identical lines at
the same millisecond produce duplicate keys, triggering undefined React
reconciliation behaviour.

Fix: add a _id: number field to LogEntry, stamped client-side with a
monotonic counter (logIdRef) at the point of ingestion — once in the SSE
onmessage handler and once in the polling setLogs path. React now tracks
data identity rather than position, so the slice window can shift without
touching the 299 unchanged DOM nodes.
2026-03-20 12:06:36 -04:00
SaelixCode 0db6c946e7 fix(logs): cap DOM rendering to 300 rows to prevent OOM crash
Playwright investigation revealed the Logs view (GlobalObservabilityView)
rendered 1,859+ log entries as real DOM nodes (9,616 total DOM nodes) with
no virtualization. Combined with a 5-second polling cycle replacing all
React elements each time and smooth-scroll animations stacking on every
update, the renderer process grew rapidly on a host running at 97% RAM
usage, crashing the browser tab with Out of Memory within minutes.

Fixes:
- Cap rendered DOM rows to MAX_DISPLAY_ROWS (300) via .slice(-300) so the
  browser only ever holds ~1,500 log-related DOM nodes regardless of how
  many entries are in state
- Add a truncation notice when log count exceeds the display cap
- Reduce SSE-mode in-memory log cap from 10,000 to MAX_LOG_ENTRIES (2,000)
- Switch auto-scroll from behavior:'smooth' to behavior:'instant' to stop
  stacking layout animations on every 5-second poll
- Reduce /api/logs/global response limit from 2,000 to 500 lines since the
  client renders at most 300 rows, making the extra payload wasteful
2026-03-20 11:59:24 -04:00
SaelixCode 3b2634f9dd fix(dashboard): surface server error messages in create-stack flow
- Read JSON error body from non-ok responses before throwing, so
  status-specific messages (409 already exists, 400 invalid name) reach
  the user instead of generic hardcoded strings
- Apply defensive toast pattern: error?.message || error?.error || fallback
2026-03-20 09:57:01 -04:00
Anso 18314119b0 Merge pull request #44 from AnsoCode/fix/remote-node-hardening
fix(remote): harden WS stream lifecycle, auth precedence, and proxy error handling
2026-03-20 09:17:32 -04:00
SaelixCode abefd5e1f6 fix(remote): harden WS stream lifecycle, auth precedence, and proxy error handling
- Destroy Docker stats stream on WS close to prevent orphaned daemon polling
- Guard all ws.send() calls with readyState === OPEN check
- Add .catch() to unawaited streamStats/execContainer calls to prevent
  unhandled rejections crashing the process (Node >= 15)
- Close per-connection WebSocket.Server instances after handleUpgrade to
  prevent listener accumulation over many connections
- Invert auth token precedence to bearerToken || cookieToken in both
  authMiddleware and the WS upgrade handler so node-to-node Bearer tokens
  are never shadowed by a stale browser cookie
- Narrow proxyRes type in remoteNodeProxy error handler before calling
  .status() to avoid throwing on raw Socket (WS/TCP-level proxy errors)
2026-03-20 08:47:39 -04:00
SaelixCode 43436c15c1 Merge remote-tracking branch 'origin/main' into develop 2026-03-20 08:45:42 -04:00
Anso f115a48cfe Merge pull request #43 from AnsoCode/fix/remote-ws-stats-exec-openapp
fix(remote): strip cookie & nodeId from WS/HTTP proxy to remote nodes
2026-03-20 00:51:08 -04:00
SaelixCode 774190cbb8 fix(remote): strip cookie header and nodeId from WS/HTTP proxy to remote nodes
Two related auth/routing bugs broke Terminal logs, container stats, the
LogViewer SSE stream, and exec bash when a remote node was selected:

1. WebSocket proxy (upgrade handler) was forwarding the browser's
   `cookie` header to the remote Sencho instance.  The remote's
   `authMiddleware` evaluates `cookieToken || bearerToken`, so it
   picked the cookie first — signed with the *local* JWT secret — and
   returned 401 before even seeing the valid Bearer token.
   Fix: `delete req.headers['cookie']` before `wsProxyServer.ws()`,
   matching the `proxyReq.removeHeader('cookie')` already present in
   the HTTP proxy.

2. WebSocket proxy was also forwarding `?nodeId=<gatewayId>` in the
   URL.  The remote's `nodeContextMiddleware` rejected it with 404
   ("Node X not found") because gateway node IDs don't exist on the
   remote instance.
   Fix: strip `nodeId` from `req.url` before proxying, so the remote
   defaults cleanly to its own local node.

3. HTTP proxy (`remoteNodeProxy`) was forwarding `?nodeId=<gatewayId>`
   in `proxyReq.path` to the remote.  Affected EventSource endpoints
   like `/api/containers/:id/logs?nodeId=9` that pass nodeId as a
   query param rather than the `x-node-id` header.
   Fix: strip `nodeId` from `proxyReq.path` in `onProxyReq`.
2026-03-20 00:33:48 -04:00
SaelixCode 2ba44acde1 chore: ignore local Claude Code, Playwright MCP, and plans files 2026-03-19 23:16:17 -04:00
Anso dbf8ec8a2a Merge pull request #42 from AnsoCode/fix/remote-ws-stats-exec-openapp
fix(remote): repair stats, bash exec, and Open App for remote nodes
2026-03-19 22:48:23 -04:00
SaelixCode 94018d167f fix(remote): repair stats, bash exec, and Open App for remote nodes
- Generic WebSockets (stats, exec) now connect to /ws?nodeId= instead of
  the bare root so the upgrade handler detects the remote node and proxies
  the WS connection to the correct remote Sencho instance.
- Vite dev proxy gains ws:true on /api and a new /ws entry so WebSocket
  upgrades reach localhost:3000 during npm run dev.
- Backend WS message handler falls back to the default local node when the
  nodeId in a proxied message doesn't exist in the local DB.
- Open App button now extracts the hostname from the remote node's api_url
  instead of using window.location.hostname.
2026-03-19 22:45:00 -04:00
Anso a467c2faea Merge pull request #41 from AnsoCode/feature/fix-remote-proxy-order
Fix Remote Node System Stats: Remove Broken getDocker() Branch
2026-03-19 22:08:16 -04:00
SaelixCode 3f473c5c97 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.
2026-03-19 22:02:29 -04:00
Anso ef5621e484 Merge pull request #40 from AnsoCode/feature/image-update-checker
feat(system): background image update checker with stack badges
2026-03-19 21:37:30 -04:00
SaelixCode d64d23fc50 feat(system): background image update checker with stack badges
Adds an ImageUpdateService that queries OCI registry manifest endpoints
every 6 hours to compare remote digests against local RepoDigests.
Results are cached in a new stack_update_status SQLite table. A pulsing
blue dot badge appears in the stack sidebar for stacks with updates
available. Manual refresh available via POST /api/image-updates/refresh
with a 10-minute rate limit.
2026-03-19 21:28:48 -04:00
Anso b7748b4d17 Merge pull request #39 from AnsoCode/feature/remote-context
feat(ui): Phase 57 - Remote Context Navigation
2026-03-19 20:45:34 -04:00
SaelixCode 04c770c198 feat(ui): Phase 57 - remote context UX (Option A) + network layer fixes
Task 1 - Network layer fixes:
- AppStoreView: replace raw fetch() with apiFetch() on /templates and
  /templates/deploy so x-node-id header is injected for proxy routing
- GlobalObservabilityView: replace raw fetch() with apiFetch() on
  /stacks and /logs/global
- HostConsole: append ?nodeId= to WebSocket URL so the upgrade handler
  correctly routes the PTY session to the active remote node

Task 2 - Scoped context two-tier UX (Option A / Portainer-style):
- EditorLayout: add a context pill in the top header bar showing the
  active node name (pulsing blue for remote, green for local)
- HostConsole: header now reads "Host Console - [Node Name]"
- ResourcesView: title now reads "Resources Hub - [Node Name]" for remote
- GlobalObservabilityView: floating node badge in top-left corner for remote
- AppStoreView: deploy sheet shows "Deploying to: [Node Name]" badge
- SettingsModal: remote nodes only see System Limits + Developer tabs;
  global-only tabs (Account, Appearance, Notifications, Nodes) are hidden;
  header subtitle shows remote node name
2026-03-19 20:39:57 -04:00
Anso e027a94492 Merge pull request #38 from AnsoCode/fix/dashboard-oom-leak
perf: fix dashboard out of memory crash on remote nodes
2026-03-19 20:04:37 -04:00
SaelixCode 4e9777d47f perf: fix dashboard out of memory crashing from massive historical metrics payloads 2026-03-19 19:59:40 -04:00
Anso eb58f302a1 Merge pull request #37 from AnsoCode/fix/node-context-hydration
fix(frontend): sync NodeContext with localStorage on initial load
2026-03-19 19:23:13 -04:00
SaelixCode f400959a4b fix(frontend): sync NodeContext with localStorage on initial load 2026-03-19 18:36:27 -04:00
Anso 39e63bea8e Merge pull request #36 from AnsoCode/fix/remote-proxy-cookie-strip-and-middleware-loop
fix: strip browser cookie from proxy requests; fix nodeContextMiddleware self-heal loop
2026-03-19 18:14:27 -04:00
SaelixCode 7b2f28f505 fix: proxy forwards browser cookie to remote causing 401; fix nodeContextMiddleware loop
Two backend fixes:

1. proxyReq was forwarding the browser's sencho_token cookie to the remote
   Sencho. The remote's authMiddleware uses cookieToken || bearerToken, so
   the invalid local-signed cookie was verified before the valid Bearer token,
   returning 401 on every proxied API call. Strip the cookie header in
   proxyReq so remote auth uses only the Bearer token.

2. nodeContextMiddleware blocked /api/nodes when x-node-id referenced a
   deleted node, trapping the frontend in an unrecoverable 404 loop (it
   could not fetch the nodes list to discover the node was gone). Exempt
   /api/nodes alongside /api/auth/ so the app can always self-heal.
2026-03-19 18:10:48 -04:00
Anso efd3d7bd7f Merge pull request #35 from AnsoCode/fix/remote-proxy-api-path-prefix
fix: remote proxy strips /api prefix — remote Sencho returns SPA HTML instead of JSON
2026-03-19 17:01:57 -04:00
SaelixCode a26c255e7c fix: remote proxy strips /api prefix causing remote Sencho to return SPA HTML
app.use('/api/', ...) causes Express to strip the '/api/' prefix from
req.url before http-proxy-middleware sees it, so the proxy was forwarding
'/stats' to the remote instead of '/api/stats'. The remote Sencho found
no matching route, fell through to its SPA catch-all, and returned
index.html (200 text/html) — explaining the SyntaxError on res.json().

Added pathRewrite: (path) => '/api' + path to restore the full path.
The connection test passed because testRemoteConnection calls the remote
directly via axios with the full URL, bypassing the proxy entirely.
2026-03-19 16:59:27 -04:00
Anso 497a48c2ed Merge pull request #34 from AnsoCode/fix/node-switch-dashboard-refresh
fix: dashboard cards and stacks list do not update on remote node switch
2026-03-19 16:44:27 -04:00
SaelixCode ee9311ad03 fix: dashboard cards and stacks list do not update on remote node switch
- HomeDashboard: both polling useEffects now depend on activeNode?.id so
  intervals restart and stale local-node data is cleared immediately on
  node switch; added res.ok guard before calling res.json() to prevent
  error objects being written into stats/systemStats state
- EditorLayout: refreshStacks now guards res.ok before parsing the JSON
  body and iterates a typed fileList instead of the raw response value,
  preventing SyntaxError/TypeError when proxy returns a non-JSON response
  for an unreachable remote node
2026-03-19 16:43:07 -04:00
Anso ebec4a5943 Merge pull request #33 from AnsoCode/fix/distributed-api-polish
fix: Distributed API UI & Metrics Polish
2026-03-19 16:11:52 -04:00
SaelixCode eb0c0263c7 fix: Distributed API UI & metrics polish + DEP0060 suppression
Add Node modal — type selector & state reset:
- Restored a Local/Remote <Select> dropdown in renderFormFields so users can
  explicitly choose the node type instead of it defaulting silently to 'remote'.
- Switching type clears api_url and api_token so no stale remote credentials
  carry over if a user switches from Remote to Local mid-form.
- Replaced the static "Add Remote Node" title with a dynamic one that reflects
  the currently selected type ("Add Local Node" / "Add Remote Node").
- onOpenChange now resets formData to defaultFormData whenever the dialog
  opens, preventing stale values from a previous session leaking in.

Remote connection details — real metrics:
- testRemoteConnection previously returned hard-coded '-' for containers,
  images, and cpus after a successful auth/check ping.
- Now fires three parallel requests (Promise.allSettled) after auth passes:
    /api/stats          → containers total + running count
    /api/system/stats   → cpu.cores
    /api/system/images  → image list length
- Each field falls back to '-' gracefully if an endpoint is unavailable,
  so a slow or older remote instance never breaks the connection test.

DEP0060 util._extend suppression:
- http-proxy@1.18.1 calls util._extend when createProxyServer() is first
  invoked at runtime (NOT at import time). A process.emitWarning override
  placed before the proxy instantiations intercepts only DEP0060 without
  suppressing any other warnings. No package version changes needed.

Also includes linter/formatter normalisation across multiple files.
2026-03-19 16:06:47 -04:00
Anso 9e6f72111d Merge pull request #32 from AnsoCode/fix/distributed-api-refinement
fix: Distributed API Proxy & Auth Refinement
2026-03-19 15:35:47 -04:00
SaelixCode fddd855624 fix: Distributed API proxy memory leak, node switcher refresh, and copy button
Proxy memory leak (MaxListenersExceededWarning + DEP0060):
createProxyMiddleware was instantiated inside the request handler on every
single API call. Each new instance registered fresh 'close' listeners on the
HTTP server and re-ran the http-proxy util._extend deprecated path. After ~10
requests the MaxListeners threshold was breached. Fix: declare ONE global
remoteNodeProxy at startup using the router option to dynamically resolve the
target URL per request. Listeners are registered once. ECONNREFUSED errors are
caught in the on.error handler and returned as structured 502 JSON.

Node switcher "nothing happens":
EditorLayout had a single useEffect([], []) that called refreshStacks() once
on mount. Changing the active node updated NodeContext state and localStorage
but nothing re-triggered the stack list fetch. Fix: split into two effects —
notifications polling (no dependency) and a stack-refresh effect keyed on
activeNode?.id. When the node changes, stale editor/container/file state is
cleared and the stacks for the new node are fetched.

Copy button silently failing:
navigator.clipboard.writeText() throws DOMException in non-HTTPS / non-localhost
contexts (e.g. http://192.168.x.x). The uncaught async exception silently
swallowed the success toast and state update. Fix: wrapped in try/catch with
an execCommand('copy') textarea fallback and a final error toast if both fail.
2026-03-19 15:34:04 -04:00
Anso 45a642014f Merge pull request #31 from AnsoCode/fix/distributed-api-auth
fix: Distributed API Auth
2026-03-19 15:14:45 -04:00
SaelixCode 5932bced36 fix: Distributed API auth hardening — Bearer tokens and URL normalization
- Extend WS upgrade handler to accept Authorization: Bearer tokens as a
  fallback to cookie auth. Remote Sencho instances receive proxied WS
  connections carrying Bearer (no cookie), so the previous cookie-only
  check caused immediate 401 rejections for all proxied log/terminal streams.
- Log token validation failures in authMiddleware (was silently swallowed,
  violating no-empty-catch directive).
- Normalize api_url by stripping trailing slashes in testRemoteConnection,
  the HTTP proxy target, and the WS proxy target to prevent double-slash URLs.
2026-03-19 15:11:57 -04:00
SaelixCode 67c7078128 fix: stop infinite page reload caused by premature NodeProvider mount and 401 hard-redirect
- Move NodeProvider inside the authenticated branch in App.tsx so it only
  mounts after auth is confirmed; previously it mounted on boot causing
  refreshNodes to fire before any session existed
- Replace window.location.href='/' on 401 in apiFetch with a
  sencho-unauthorized custom event; AuthContext listens and transitions
  appStatus to notAuthenticated without a full browser reload
2026-03-19 13:57:48 -04:00
Anso ac5032d363 Merge pull request #30 from AnsoCode/fix/memory-leak-and-reload
fix: Memory Leak & Reload Loop
2026-03-19 13:30:29 -04:00
SaelixCode fd07374956 fix: memory leak in SSE log accumulation and infinite reload loop in NodeContext
- Cap GlobalObservabilityView SSE log array at 10,000 entries to prevent
  unbounded memory growth during long dev-mode sessions
- Break NodeContext infinite re-fetch loop by replacing the activeNode
  closure dependency in refreshNodes with a useRef read, making the
  callback stable and preventing the useEffect -> setState -> useCallback
  -> useEffect cycle
2026-03-19 13:28:21 -04:00
Anso 880919fb78 Merge pull request #29 from AnsoCode/fix/monitor-service-skip-remote-nodes
fix: Skip remote nodes in MonitorService
2026-03-19 12:43:27 -04:00
SaelixCode b48cf62e5b fix: skip remote nodes in MonitorService to prevent direct Docker access errors
Remote nodes in the Distributed API model are self-monitoring — each
remote Sencho instance runs its own MonitorService against its local
Docker socket. The main instance must not attempt direct DockerController
access for remote nodes, which caused fatal crashes on every 30s tick.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:41:34 -04:00
SaelixCode 67d43bb3c6 chore: untrack .claude/ settings from git 2026-03-19 12:24:43 -04:00
Anso e585313f22 Merge pull request #28 from AnsoCode/feature/distributed-api-pivot
refactor: Distributed API Pivot
2026-03-19 12:23:25 -04:00
SaelixCode c91c9ed7fd refactor: pivot from ssh proxy to distributed api model
- Delete SSHFileAdapter, IFileAdapter, LocalFileAdapter (SSH/SFTP stack)
- Remove ssh2, ssh2-sftp-client dependencies; add http-proxy-middleware, http-proxy
- NodeRegistry: remote nodes no longer use Dockerode TCP; new getProxyTarget() returns {apiUrl, apiToken}
- NodeRegistry.testConnection: remote nodes use HTTP GET /api/auth/check instead of docker.info()
- DatabaseService: Node interface swaps SSH/TLS fields for api_url + api_token; legacy columns preserved for DB compat
- FileSystemService: reverted to clean local-only fs.promises; adapter pattern fully removed
- ComposeService: executeRemote() and SSH log streaming deleted; local-only execution remains
- index.ts: add /api/auth/generate-node-token endpoint (long-lived JWT, scope:node_proxy)
- index.ts: authMiddleware now accepts Bearer token in addition to cookie (Sencho-to-Sencho auth)
- index.ts: remote HTTP proxy middleware intercepts all /api/ requests for remote nodes, strips x-node-id, injects Authorization header, proxies to api_url
- index.ts: WS upgrade handler proxies WebSocket connections for remote nodes via http-proxy wsProxyServer
- NodeManager.tsx: form reduced to Name, API URL, API Token; Generate Node Token button added inline
- NodeContext.tsx: Node interface updated to api_url/api_token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:20:32 -04:00
Anso 23730430d7 Merge pull request #27 from AnsoCode/feature/remote-nodes-security-polish
feat: Remote Nodes Security & Polish
2026-03-19 10:09:26 -04:00
SaelixCode 96b1105343 fix: add tls_ca, tls_cert, tls_key to frontend Node interface
NodeContext.tsx was missing the three TLS fields that were already
present on the backend Node type, causing TS2339 build errors in
NodeManager.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 10:08:27 -04:00
SaelixCode 2a37e114df feat: implement remote tls/ssh security, isolate system stats, and polish ux
- NodeRegistry: wire TLS ca/cert/key into Dockerode when present on node config
- index.ts: /api/system/stats now branches on node type — remote nodes use docker.info() for CPU/RAM, local keeps systeminformation; disk gracefully returns null for remote
- index.ts: POST /api/nodes now persists tls_ca, tls_cert, tls_key fields
- FileSystemService: throw clean error on missing/empty compose_dir instead of crashing path.join
- FileSystemService: guard getStacks() against falsy item.name entries
- SSHFileAdapter: filter undefined/non-string names from SFTP readdir before returning
- NodeManager: add SSH Authentication Type toggle (Password vs Private Key)
- NodeManager: add Enable TLS toggle with conditional CA/cert/key textarea fields
- NodeManager: auto-test connection immediately after node creation
- NodeManager: replace "Strategy B" copy with Docker TCP setup instructions
- NodeManager: add pr-8 to header and DialogHeader to prevent overlap with parent dialog X button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 09:40:03 -04:00
Anso 1fb0494e2e Merge pull request #26 from AnsoCode/fix/remote-nodes-remediation
fix: Remote Nodes Remediation — Port Routing, SSH Credentials & compose_dir
2026-03-18 21:07:07 -04:00
SaelixCode 26b8f62968 fix: separate Docker API port from SSH port, add SSH credential UI, fix compose_dir routing 2026-03-18 21:04:43 -04:00
Anso 792e977098 Merge pull request #25 from AnsoCode/fix/remote-nodes-telemetry-crashes
fix: Remote Nodes Telemetry Fixes
2026-03-18 20:33:51 -04:00