Commit Graph

83 Commits

Author SHA1 Message Date
SaelixCode e190f3ad8a fix(alerts): overhaul alerts & notifications system for local and remote nodes
- StackAlertSheet: fetch agent status on open and show contextual banner
  (amber warning when no channels configured, green when active, blue info
  callout on remote nodes explaining remote-instance evaluation semantics)
- StackAlertSheet: surface actual server error message on addAlert failure
  instead of generic string; add console logging for all failure paths
- SettingsModal: expose Notifications tab for remote nodes so agents can be
  configured directly on any Sencho instance; section header shows node name
  and Remote badge with tooltip when proxying to a remote node
- SettingsModal: re-fetch agents/settings when active node changes
- SettingsModal: add hidden DialogTitle/DialogDescription to satisfy Radix
  UI accessibility requirements (eliminates console errors)
- backend POST /api/alerts: add Zod validation schema; rejects unknown
  metric/operator values, negative thresholds, and missing fields with 400
- EditorLayout WS: upgrade reconnect from flat 5s retry to exponential
  backoff (1s→2s→4s→8s→16s→30s max); onerror now logs the event; cleanup
  only closes OPEN sockets — CONNECTING sockets are closed in onopen after
  isMounted check, eliminating "closed before established" StrictMode error
2026-03-21 01:19:29 -04:00
SaelixCode 2e0f3e2711 security: harden terminal WebSocket endpoints against three attack vectors
- Reject node_proxy scoped JWT tokens with 403 on host-console and
  container exec (/ws) upgrades; machine-to-machine credentials must
  not open interactive shells
- Validate stackParam against path.resolve + startsWith(baseDir) to
  prevent directory traversal on the PTY cwd (Rule 9 pattern)
- Strip JWT_SECRET, AUTH_PASSWORD, AUTH_PASSWORD_HASH, DATABASE_URL
  from the environment passed to node-pty spawned shells
2026-03-21 00:12:16 -04:00
SaelixCode 0cb5fae947 feat(design): animated design system foundation with animate-ui and motion
Install motion + animate-ui, overhaul design tokens with brand cyan accent,
and replace CSS keyframe animations in Dialog, Tabs, Switch, and Tooltip
with spring-physics and blur-fade transitions via animate-ui Radix primitives.
2026-03-20 22:25:29 -04:00
SaelixCode a5ac3e4981 feat(notifications): replace polling with WebSocket push
Eliminates the 5-second setInterval polling loop for notifications in
EditorLayout. A persistent /ws/notifications WebSocket connection is
opened on mount; the backend pushes each alert the instant dispatchAlert
fires, with 5s auto-reconnect on close.

Key changes:
- NotificationService: injectable broadcaster callback (setBroadcaster)
- DatabaseService.addNotificationHistory: returns full NotificationHistory
  record (with id/is_read) instead of void
- index.ts: notificationSubscribers Set + /ws/notifications upgrade handler
  (JWT-verified, placed before remote proxy path)
- EditorLayout: polling removed, WS connect/reconnect replaces it
2026-03-20 20:47:51 -04:00
SaelixCode 322e717514 feat(settings): harden settings API and overhaul SettingsModal
Security:
- Strip auth credential keys (auth_username, auth_password_hash,
  auth_jwt_secret) from GET /api/settings response
- Add allowlist guard to POST /api/settings — rejects unknown or
  auth-namespace keys with a 400

Backend:
- Add PATCH /api/settings bulk endpoint with Zod schema validation
  (type coercion, range checks, URL format) and atomic SQLite transaction
- Add system_state table — moves last_janitor_alert_timestamp out of
  global_settings; adds getSystemState/setSystemState on DatabaseService
- Add metrics_retention_hours and log_retention_days configurable settings;
  MonitorService reads both dynamically each evaluation cycle
- Add cleanupOldNotifications(days) to DatabaseService, called each cycle

Frontend:
- Replace single isLoading flag with per-operation states
  (isSavingSystem, isSavingDeveloper, isSavingPassword, isSavingRegistry,
  isSavingAgent/isTestingAgent per agent type)
- Add skeleton loader that blocks interaction until fetchSettings resolves
- Explicit key-picking in fetchSettings — auth keys cannot enter state
- Unsaved-changes amber dot on System Limits and Developer sidebar items
- Separate saveSystemSettings / saveDeveloperSettings — no cross-tab clobber
- Developer tab gains Data Retention section (metrics hours, log days)
- All settings saves use new PATCH /api/settings endpoint
2026-03-20 19:57:34 -04:00
Anso 5c62a4a630 Merge branch 'develop' into feature/app-store-categories 2026-03-20 15:51:28 -04:00
SaelixCode 34cad76d45 feat(app-store): category filter bar and custom registry settings
- TemplateService: static LSIO_CATEGORY_MAP covers ~80 well-known apps
  across Automation, Downloaders, Media, Monitoring, Networking,
  Security, Development, Productivity, Utilities, and Other. Category
  lookup is O(1) and runs once at cache-fill time. Adds source field
  ('linuxserver' | 'custom') to Template for future per-source UX.
  Portainer v2 registries pass their native categories through unchanged.
  Adds clearCache() method wired to POST /api/templates/refresh-cache.

- AppStoreView: category pill bar (All + sorted dynamic categories)
  rendered between search and grid; active pill fills on click; clicking
  a category badge on a card also sets the active filter; app count
  updates reactively; filter composed with existing search query.

- SettingsModal: new App Store section (local-node only) exposes a
  custom Portainer v2 JSON URL input with Save & Refresh (saves setting
  then busts the 24h template cache) and Reset to Default.
2026-03-20 15:19:32 -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 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 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 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
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
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
SaelixCode 4e9777d47f perf: fix dashboard out of memory crashing from massive historical metrics payloads 2026-03-19 19:59:40 -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
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
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
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
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 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 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
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
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
SaelixCode f1f8e34da5 fix: harden telemetry parsing and null node fallbacks 2026-03-18 20:31:57 -04:00
SaelixCode 4bd80e29bf fix: harden docker api validation, handle sftp errors, and fix node manager ui 2026-03-18 14:13:07 -04:00
SaelixCode 69e86a0a37 fix: Resolve DatabaseService SQL syntax error and add concrete IFileAdapter implementations 2026-03-18 13:13:56 -04:00
SaelixCode 8c51198468 feat: Remote Nodes Wiring & SSH Adapters 2026-03-18 12:55:30 -04:00
SaelixCode d2c5b2de67 fix: cast req.params.id as string to resolve TS2345 type errors 2026-03-18 11:53:41 -04:00
SaelixCode 02e1ebe1b6 feat: Remote Nodes Foundation (Strategy B) - Add nodes table with auto-seeded default local node in DatabaseService - Create NodeRegistry service for multi-instance Docker daemon connections - Add 6 Node management API endpoints (CRUD + test connection) - Create NodeManager component with table UI and connection testing - Add NodeContext for frontend-wide active node state management - Add node switcher dropdown to sidebar (visible when >1 node) - Add Nodes tab to Settings Hub 2026-03-18 11:45:04 -04:00
SaelixCode b7e6b5a21c fix: replace naive log level detection with robust 3-tier regex classification engine
Eliminates massive false-positive error misclassifications caused by Docker
containers writing standard INFO logs to STDERR. The new hierarchy:
1. INFO/DEBUG/TRACE indicators (overrides STDERR default)
2. WARN/WARNING indicators
3. ERROR/FATAL/CRIT/CRITICAL/PANIC + Exception: indicators

Supports common log formats: level=info, [INFO], and bare INFO tokens.
Applied to both /api/logs/global and /api/logs/global/stream endpoints.
2026-03-10 14:50:51 -04:00
SaelixCode 448a64a10d feat: implement enterprise sse global logs and developer mode 2026-03-09 14:02:10 -04:00
SaelixCode b2674080c4 fix: implement smart auto-scroll and definitive stack filtering in global logs 2026-03-09 12:21:12 -04:00
SaelixCode 9af0f85749 fix: refine log level parsing and implement bottom auto-scroll 2026-03-06 23:07:17 -05:00
SaelixCode 8203dd6a14 fix: tty parsing, timezone mapping, and floating action bar for global logs 2026-03-06 22:52:36 -05:00
SaelixCode 29b10150b4 fix: remediate observability dashboard and global logs parsing 2026-03-06 22:24:42 -05:00
SaelixCode a4a5365da1 feat: implement centralized logging and historical metrics dashboard 2026-03-06 21:05:23 -05:00
SaelixCode b765403dcf feat: implement real-time container log streaming via SSE 2026-03-06 15:41:06 -05:00
SaelixCode b97952567d feat: implement pre-deploy collision checks and universal two-stage teardown 2026-03-06 14:45:37 -05:00
SaelixCode fb3a28834e fix: implement two-stage teardown for reliable atomic rollbacks 2026-03-06 10:18:47 -05:00
SaelixCode 953049a45d feat: implement smart error parser and post-deploy health probe 2026-03-05 19:59:35 -05:00
Anso 269d4f8d5c Merge branch 'develop' into feature/app-store-fixes 2026-03-05 19:22:53 -05:00
SaelixCode 69408257d2 fix: implement atomic deployment rollbacks and custom scrollbar UI 2026-03-05 18:55:03 -05:00
SaelixCode f2fbca17b7 feat: implement dynamic volumes, custom env vars, and timezone detection 2026-03-05 14:55:04 -05:00
SaelixCode 4aa4bf1b80 fix(backend,frontend): correct docker socket connection on windows and fix api proxy in vite config 2026-03-05 14:49:24 -05:00
SaelixCode f9e8874f6c feat: integrate official lsio api and rich template metadata 2026-03-05 12:37:59 -05:00
SaelixCode 7d59400114 fix: update lsio template registry url to valid endpoint 2026-03-05 11:00:00 -05:00
SaelixCode 536a714d9b feat: add dynamic template registry and smart volume path sanitizer 2026-03-05 10:15:54 -05:00
SaelixCode 1676dc22df feat: implement app templates storefront and deployment engine 2026-03-04 14:26:07 -05:00
SaelixCode b3521a078b feat: add Docker API endpoints for managing images, volumes, and networks; implement Resources view in editor 2026-03-04 11:17:16 -05:00
SaelixCode b45553c927 feat: refactor authentication handling and migrate config to database 2026-03-04 09:05:03 -05:00