Files
sencho/CHANGELOG.md
T

19 KiB
Raw Blame History

Changelog

All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Fixed

  • fix(ci): docker-publish.yml was triggered by release: types: [published] (GitHub Release event) instead of push: tags: v* — pushing a git tag never fired the workflow. Changed trigger to push: tags: v* and updated enable conditions from github.event_name == 'release' to startsWith(github.ref, 'refs/tags/v') so any v* tag push automatically builds and publishes latest + semver tags to Docker Hub without requiring a manual GitHub Release.

Added

  • feat(ci): Automated versioning via release-please — on every push to main, the release-please workflow opens or updates a Release PR with a generated CHANGELOG entry and package.json version bump. Merging the Release PR creates the vX.Y.Z tag which triggers docker-publish.yml. Version bumps follow Conventional Commits: fix: → patch, feat: → minor, feat!: / BREAKING CHANGE: → major.

0.1.0 - 2026-03-24

Security

  • Fixed: Missing authMiddleware on GET /api/notifications, POST /api/notifications/read, DELETE /api/notifications/:id, DELETE /api/notifications, POST /api/notifications/test, and POST /api/system/console-token — any unauthenticated client could reach these endpoints.
  • Fixed: Remote node api_url accepted without validation — an attacker could set it to http://localhost:6379 to SSRF into internal services. Now validates: must be a well-formed http:// or https:// URL and the hostname may not be localhost, 127.x.x.x, [::1], or 0.0.0.0.
  • Fixed: env_file paths in compose.yaml were accepted without boundary checking — absolute paths like /etc/passwd could be read or written. All resolved env file paths are now validated to stay within the stack directory.
  • Fixed: Stack name validated in write routes but not GET routes — path-traversal names now return 400 on all routes.
  • Fixed: stackParam query parameter on /api/system/host-console now validated against path.resolve + startsWith(baseDir) to prevent directory traversal when setting the PTY working directory.
  • Fixed: HostTerminalService no longer forwards full process.env to spawned PTY shells — JWT_SECRET, AUTH_PASSWORD, AUTH_PASSWORD_HASH, and DATABASE_URL are stripped before the shell is spawned.
  • Fixed: Host Console and container exec WebSocket endpoints now reject node_proxy scoped JWT tokens with HTTP 403.
  • Fixed: GET /api/settings no longer leaks auth_username, auth_password_hash, or auth_jwt_secret to the frontend.
  • Fixed: POST /api/settings enforces a strict allowlist of writable keys — auth credential keys and unknown keys are rejected with a 400 error.
  • Added: Rate limiting on /api/auth/login and /api/auth/setup — 5 attempts per 15-minute window per IP, using express-rate-limit.
  • Added: helmet middleware for security response headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, etc.).
  • Changed: CORS is now restricted to FRONTEND_URL env var in production; development continues to allow any origin.

Added

Infrastructure & CI

  • linux/arm64 platform support in the Docker Hub publish workflow (Raspberry Pi 4/5, Oracle ARM VMs) — native modules (bcrypt, better-sqlite3, node-pty) cross-compiled via tonistiigi/xx to eliminate the SIGILL crash caused by Node.js v20 using ARMv8.1 LSE atomic instructions unsupported by GitHub Actions QEMU.
  • docker/setup-qemu-action@v3 step to docker-publish.yml — without it multi-platform builds hung indefinitely.
  • Automated Docker Hub CI/CD pipeline publishing dev and latest tags.
  • Automated documentation pipeline with Mintlify sync and screenshot refresh CI job.
  • HEALTHCHECK directive in Dockerfile — Docker polls /api/health every 30 s and restarts an unhealthy container.
  • GET /api/health public endpoint returning { status: "ok", uptime }.
  • docker-entrypoint.sh — runs as root, fixes $DATA_DIR volume ownership, then drops to the non-root sencho user via su-exec before starting Node. Eliminates SQLITE_READONLY crashes on host-mounted volumes.
  • Non-root sencho system user in Dockerfile; process no longer runs as root.
  • Graceful shutdown — backend listens for SIGTERM/SIGINT, drains HTTP connections, stops MonitorService and ImageUpdateService, and closes the SQLite connection before exiting.
  • Vitest backend test suite — 38 tests covering validation utilities, health endpoint, authentication flows, auth middleware enforcement, console-token security, and SSRF validation. Run with cd backend && npm test.
  • Playwright E2E test scaffolding (e2e/) — auth, stack management, and node management specs with shared login helper. Run with npm run test:e2e.
  • CI workflow runs Vitest unit tests and ESLint on every PR.
  • isValidStackName, isValidRemoteUrl, isPathWithinBase extracted to backend/src/utils/validation.ts for reuse and testability.

Multi-Node & Distributed API

  • Distributed API proxying using http-proxy-middleware for HTTP and WebSockets — replaces the SSH/SFTP architecture entirely (~500 lines removed).
  • Long-lived JWT generation for Sencho-to-Sencho API authentication (POST /api/auth/generate-node-token).
  • nodeContextMiddleware in Express to dynamically extract x-node-id headers and ?nodeId= query parameters for WebSocket upgrades.
  • NodeRegistry service managing multiple Docker daemon connections.
  • Node management API endpoints: list, get, create, update, delete, and test connection.
  • Two-tier scoped navigation UX — context pill in the top header always shows the active node name (pulsing blue for remote, green for local).
  • Remote-aware headers in HostConsole, ResourcesView, GlobalObservabilityView, and AppStoreView.
  • SettingsModal scopes its sidebar to the active node type — global-only tabs hidden when a remote node is active.
  • Cross-node notification aggregation — notification bell surfaces alerts from all connected remote nodes with dedicated real-time WebSocket connections per remote node.
  • Remote node host console and container exec WebSocket proxy — gateway exchanges node_proxy token for a short-lived console_session JWT (60 s TTL) before forwarding.
  • localOnly option on apiFetch — omits x-node-id so requests always route to the local node.

Application Features

  • App Store — LinuxServer.io API integration as default template registry with rich metadata (architectures, docs links, GitHub links), category filter, one-click deployment, atomic rollback on failure, custom Portainer v2 registry URL support, editable ports/volumes/environment variables, post-deploy health probe.
  • Resources Hub — Images, Volumes, and Networks tabs with Managed/External/Unused classification, Docker Disk Footprint stacked-bar widget, scoped prune operations (Sencho-only vs All Docker), managed/external filter toggles, and classification badges.
  • Global Observability — centralized dashboard tracking 24-hour historical metrics and aggregating global tail logs across all containers. Dozzle-style Action Bar with multi-select stack filtering, search, STDOUT/STDERR toggles, and Developer Mode SSE real-time streaming.
  • Background image update checker — polls OCI-compliant registries every 6 hours using manifest digest comparison; results cached in stack_update_status table; pulsing blue dot badge on stacks with available updates.
  • Real-time WebSocket notifications — replaces 5-second polling; NotificationService.setBroadcaster() pushes each new alert to all authenticated subscribers the moment it fires.
  • Live Container Logs viewer using SSE for real-time terminal output.
  • Animated design systemmotion package and animate-ui library; new brand cyan token; spring-based dialog/tooltip/tab animations; prefers-reduced-motion respected globally; Geist font via Google Fonts CDN.
  • Theme-aware sidebar logo — dark and light variants auto-switch based on active theme.
  • Auto theme option (light/dark/auto) with window.matchMedia listener.
  • PATCH /api/settings bulk-update endpoint — validates all values via Zod schema, persists atomically in a single SQLite transaction.
  • system_state SQLite table — separates runtime operational state from user-defined config in global_settings.
  • Configurable metrics_retention_hours (default: 24 h) and log_retention_days (default: 30 d) — MonitorService reads these dynamically each cycle.
  • Managed/unmanaged container count split in GET /api/stats — Home Dashboard "Active Containers" card shows "N managed · N external".
  • Two-Stage Teardown for stack deletion — docker compose down sweeps ghost networks before deployment files are deleted.
  • Custom Environment Variable injection tool in deployment UI.
  • ErrorBoundary component now wraps root <App /> in main.tsx.
  • Git Flow branching strategy and branch protection.

Fixed

Authentication & Proxy

  • Login loop caused by remote node auth failure — apiFetch now only fires sencho-unauthorized when the x-sencho-proxy: 1 header is absent (i.e., a genuine local session failure, not a remote node auth error).
  • authMiddleware and WS upgrade handler now evaluate bearerToken || cookieToken (Bearer first) — cookie no longer shadows a valid Bearer token on node-to-node proxy calls.
  • Remote node proxy stripping the /api path prefix — added pathRewrite: (path) => '/api' + path to restore the full path when forwarding to remote instances.
  • Remote node HTTP proxy body forwarding — replaced proxyReq.write(JSON.stringify(req.body)) (raced against http-proxy's process.nextTick(proxyReq.end)) with a conditional JSON body parser that skips express.json() for remote-targeted requests; the raw IncomingMessage stream is left unconsumed so http-proxy's req.pipe(proxyReq) forwards it intact.
  • Remote node proxy forwarding the browser's sencho_token cookie to the remote instance — stripped in proxyReq so only the Bearer token is used.
  • Remote WebSocket upgrades forwarding the browser cookie header — stripped before wsProxyServer.ws() so the remote's authMiddleware uses the Bearer token exclusively.
  • nodeContextMiddleware blocking /api/nodes when x-node-id references a deleted node — exempted alongside /api/auth/ so the frontend can re-sync a stale node ID.
  • Backend memory leak from createProxyMiddleware called inside the request handler on every API call — refactored to a single globally-instantiated proxy using the router option.
  • remoteNodeProxy error handler unsafely cast proxyRes to Response on WebSocket/TCP-level errors — type-narrowed before sending 502.

WebSocket & Streaming

  • Container stats WebSocket flooding React with up to 20+ setState calls per second — replaced with a ref-buffer + 1.5 s flush interval pattern.
  • streamStats Docker stats stream leaking after WebSocket client disconnect — ws.on('close') handler calls stats.destroy(); all ws.send() calls guarded with readyState === OPEN.
  • streamStats and execContainer called unawaited — unhandled promise rejections now chain .catch(), log the error, and close the WebSocket cleanly.
  • Per-connection WebSocket.Server instances for stack logs and host console never closed after upgrade — wss.close() called immediately after handleUpgrade.
  • WebSocket notification reconnect upgraded to exponential backoff (1 s → 30 s max) instead of flat 5-second retry; ws.onerror logs the event; cleanup guards against closing an already-closing socket.
  • Terminal logs and container stats WebSockets failing with "HTTP Authentication failed" on remote nodes — gateway's cookie header stripped before forwarding to remote; nodeId query param stripped from forwarded URL.
  • LogViewer returning 404 on remote nodes — nodeId query param stripped from proxyReq.path in onProxyReq.

UI & Frontend

  • Blank page on HTTP deployments (root cause — Helmet 8 default CSP upgrade-insecure-requests and HSTS) — upgradeInsecureRequests: null and strictTransportSecurity: false set explicitly.
  • COOP header console warning on HTTP deployments — crossOriginOpenerPolicy: false.
  • Inline script CSP violation from Vite module-preload polyfill — disabled via build.modulePreload.polyfill: false.
  • CSP workerSrc missing (Monaco editor workers) — added worker-src 'self' blob:.
  • CSP connectSrc implicit — added explicit connect-src 'self' ws: wss:.
  • Docker socket EACCES root:root edge case — entrypoint handles GID 0 in addition to the standard root:docker case.
  • Managed container count wrong when stacks launched from COMPOSE_DIR root — classification now uses com.docker.compose.project.working_dir.
  • Browser Out of Memory crash in GlobalObservabilityView — capped DOM rendering to last 300 entries, reduced SSE log cap to 2,000 entries, replaced key={idx} with monotonic _id counter.
  • HomeDashboard create-stack error handling — reads JSON error body before throwing; uses defensive toast pattern.
  • AlertDialogContent using asChild with motion.div wrapper crashing on delete-stack confirmation — replaced with CSS keyframe animations.
  • animate-ui auto-height.tsx importing WithAsChild without type keyword — crashed browser module loader.
  • animate-ui switch.tsx double-spreading Radix props onto motion.button DOM element.
  • "Always Local" badge tooltip crashing (getStrictContext) — replaced animate-ui tooltip with pure Radix primitives.
  • Cancel/Add Node buttons in NodeManager dialogs stuck together.
  • Resources/App Store/Logs menu buttons not toggling off on second click.
  • Monaco container height accumulation on tab switching — reset to 0×0 and force synchronous reflow before re-measuring.
  • AppStoreView and GlobalObservabilityView using raw fetch() instead of apiFetch() — all calls now inject x-node-id.
  • HostConsole WebSocket URL missing ?nodeId= query parameter.
  • "Open App" button opening http://localhost:{port} for remote node containers — resolves hostname from remote node's api_url.
  • Dashboard cards showing stale local-node data after switching to a remote node — polling effects now depend on activeNode?.id and clear state immediately on node change.
  • refreshStacks crashing with SyntaxError or TypeError when the remote proxy returns a non-JSON response — checks res.ok before calling res.json().
  • Four empty catch {} blocks in EditorLayout — now surface errors via toast.error().
  • StackAlertSheet not fetching notification agent status from the active node on open.
  • SettingsModal Notifications tab hidden when a remote node is active — now visible and configurable on remote nodes.
  • POST /api/alerts now validates the request body with a Zod schema — rejects unknown metric/operator values, negative thresholds, and missing fields with a structured 400.
  • WebSocket.Server replaced with named import WebSocketServer from ws to fix ESM/CJS interop.
  • NodeProvider mounted outside the auth gate — moved inside the authenticated branch so refreshNodes no longer fires before authentication.
  • Infinite re-fetch loop in NodeContextrefreshNodes useCallback no longer depends on activeNode state; replaced with useRef.
  • Infinite page reload loop — apiFetch replaced window.location.href = '/' with a sencho-unauthorized custom event.
  • API Token copy button failing silently on HTTP/non-localhost — added execCommand('copy') fallback.
  • E2E nodes tests permanently timing out because the Add Node submit button requires api_token to be non-empty.
  • ESLint CI step — replaced all any annotations with proper types, fixed unused catch variables.
  • [DEP0060] DeprecationWarning: util._extend from http-proxy@1.18.1 — suppressed at call site.
  • Global Logs false-positive error misclassifications — replaced naive regex with a robust 3-tier classification engine.
  • Memory leak in GlobalObservabilityView SSE mode — log array capped at 2,000 entries.
  • Historical metrics memory leak — polling throttled to 60 s; SQLite payload downsampled by 12×.
  • Active node UI dropdown desyncing from API requests on initial page load — state hydrated from localStorage.
  • MonitorService crash (Cannot read properties of undefined (reading 'cpu_usage')) during Docker container transition states.
  • Deleted node ghost API calls — 404 errors intercepted globally, forcing UI to resync to default node.
  • Horizontal UI overflow in Node Manager settings on smaller resolutions.
  • Docker API parsing bug where HTML string responses from misconfigured ports were counted as containers.

Changed

  • Architecture: Replaced SSH/SFTP remote node model with Distributed API proxy (HTTP/WebSocket) — remote nodes now only require an API URL and Bearer token. Node Manager UI vastly simplified.
  • Docs: Migrated Mintlify config from deprecated mint.json to docs.json v2 format; bootstrapped full user-facing documentation (configuration, stack management, editor, multi-node, alerts, dashboard, resources, app store, observability, settings reference, troubleshooting, backup & restore).
  • Design system: Animated UI overhaul — new brand cyan token, spring-based animations on dialogs/tooltips/switches/tabs, dark mode shadow strengthening, Geist font now actually loaded.
  • Notification delivery replaced polling with WebSocket push — no more setInterval in EditorLayout.
  • DatabaseService.addNotificationHistory returns the full inserted record for real-time broadcasting.
  • SettingsModal overhauled — per-operation loading states, skeleton loader, unsaved-changes indicator, all saves use PATCH /api/settings.
  • MonitorService evaluates limits and detects container crashes across all registered nodes concurrently.
  • MonitorService reads retention settings dynamically each cycle.
  • Developer settings scoped to the local node — reads/writes always target local via localOnly regardless of active node.
  • Dark mode scrollbar styling — no more white native scrollbars.
  • Rebranded "Templates" → "App Store", "Ghost Containers" → "Unmanaged Containers", "Observability" → "Logs".
  • Global logs display chronologically (newest at bottom) with smooth auto-scrolling; UTC → local browser timezone.
  • Historical CPU/RAM charts relocated to the Home Dashboard; data normalized (CPU relative to host cores, RAM to GB).
  • EditorLayout main workspace container keyed to activeView — every view switch triggers a fade-up entrance animation.

Removed

  • SSH/SFTP remote node adapters (IFileAdapter, LocalFileAdapter, SSHFileAdapter, SSHFileAdapter, ComposeService.executeRemote, ComposeService.streamLogs SSH path) — ~500 lines.