mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
19 KiB
19 KiB
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.ymlwas triggered byrelease: types: [published](GitHub Release event) instead ofpush: tags: v*— pushing a git tag never fired the workflow. Changed trigger topush: tags: v*and updatedenableconditions fromgithub.event_name == 'release'tostartsWith(github.ref, 'refs/tags/v')so anyv*tag push automatically builds and publisheslatest+ semver tags to Docker Hub without requiring a manual GitHub Release.
Added
- feat(ci): Automated versioning via
release-please— on every push tomain, therelease-pleaseworkflow opens or updates a Release PR with a generated CHANGELOG entry andpackage.jsonversion bump. Merging the Release PR creates thevX.Y.Ztag which triggersdocker-publish.yml. Version bumps follow Conventional Commits:fix:→ patch,feat:→ minor,feat!:/BREAKING CHANGE:→ major.
0.1.0 - 2026-03-24
Security
- Fixed: Missing
authMiddlewareonGET /api/notifications,POST /api/notifications/read,DELETE /api/notifications/:id,DELETE /api/notifications,POST /api/notifications/test, andPOST /api/system/console-token— any unauthenticated client could reach these endpoints. - Fixed: Remote node
api_urlaccepted without validation — an attacker could set it tohttp://localhost:6379to SSRF into internal services. Now validates: must be a well-formedhttp://orhttps://URL and the hostname may not belocalhost,127.x.x.x,[::1], or0.0.0.0. - Fixed:
env_filepaths incompose.yamlwere accepted without boundary checking — absolute paths like/etc/passwdcould 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:
stackParamquery parameter on/api/system/host-consolenow validated againstpath.resolve+startsWith(baseDir)to prevent directory traversal when setting the PTY working directory. - Fixed:
HostTerminalServiceno longer forwards fullprocess.envto spawned PTY shells —JWT_SECRET,AUTH_PASSWORD,AUTH_PASSWORD_HASH, andDATABASE_URLare stripped before the shell is spawned. - Fixed: Host Console and container exec WebSocket endpoints now reject
node_proxyscoped JWT tokens with HTTP 403. - Fixed:
GET /api/settingsno longer leaksauth_username,auth_password_hash, orauth_jwt_secretto the frontend. - Fixed:
POST /api/settingsenforces a strict allowlist of writable keys — auth credential keys and unknown keys are rejected with a 400 error. - Added: Rate limiting on
/api/auth/loginand/api/auth/setup— 5 attempts per 15-minute window per IP, usingexpress-rate-limit. - Added:
helmetmiddleware for security response headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, etc.). - Changed: CORS is now restricted to
FRONTEND_URLenv var in production; development continues to allow any origin.
Added
Infrastructure & CI
linux/arm64platform support in the Docker Hub publish workflow (Raspberry Pi 4/5, Oracle ARM VMs) — native modules (bcrypt,better-sqlite3,node-pty) cross-compiled viatonistiigi/xxto eliminate theSIGILLcrash caused by Node.js v20 using ARMv8.1 LSE atomic instructions unsupported by GitHub Actions QEMU.docker/setup-qemu-action@v3step todocker-publish.yml— without it multi-platform builds hung indefinitely.- Automated Docker Hub CI/CD pipeline publishing
devandlatesttags. - Automated documentation pipeline with Mintlify sync and screenshot refresh CI job.
HEALTHCHECKdirective inDockerfile— Docker polls/api/healthevery 30 s and restarts an unhealthy container.GET /api/healthpublic endpoint returning{ status: "ok", uptime }.docker-entrypoint.sh— runs as root, fixes$DATA_DIRvolume ownership, then drops to the non-rootsenchouser viasu-execbefore starting Node. EliminatesSQLITE_READONLYcrashes on host-mounted volumes.- Non-root
senchosystem user inDockerfile; process no longer runs as root. - Graceful shutdown — backend listens for
SIGTERM/SIGINT, drains HTTP connections, stopsMonitorServiceandImageUpdateService, 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 withnpm run test:e2e. - CI workflow runs Vitest unit tests and ESLint on every PR.
isValidStackName,isValidRemoteUrl,isPathWithinBaseextracted tobackend/src/utils/validation.tsfor reuse and testability.
Multi-Node & Distributed API
- Distributed API proxying using
http-proxy-middlewarefor 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). nodeContextMiddlewarein Express to dynamically extractx-node-idheaders and?nodeId=query parameters for WebSocket upgrades.NodeRegistryservice 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, andAppStoreView. SettingsModalscopes 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_proxytoken for a short-livedconsole_sessionJWT (60 s TTL) before forwarding. localOnlyoption onapiFetch— omitsx-node-idso 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_statustable; 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 system —
motionpackage andanimate-uilibrary; new brand cyan token; spring-based dialog/tooltip/tab animations;prefers-reduced-motionrespected 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.matchMedialistener. PATCH /api/settingsbulk-update endpoint — validates all values via Zod schema, persists atomically in a single SQLite transaction.system_stateSQLite table — separates runtime operational state from user-defined config inglobal_settings.- Configurable
metrics_retention_hours(default: 24 h) andlog_retention_days(default: 30 d) —MonitorServicereads 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 downsweeps ghost networks before deployment files are deleted. - Custom Environment Variable injection tool in deployment UI.
ErrorBoundarycomponent now wraps root<App />inmain.tsx.- Git Flow branching strategy and branch protection.
Fixed
Authentication & Proxy
- Login loop caused by remote node auth failure —
apiFetchnow only firessencho-unauthorizedwhen thex-sencho-proxy: 1header is absent (i.e., a genuine local session failure, not a remote node auth error). authMiddlewareand WS upgrade handler now evaluatebearerToken || cookieToken(Bearer first) — cookie no longer shadows a valid Bearer token on node-to-node proxy calls.- Remote node proxy stripping the
/apipath prefix — addedpathRewrite: (path) => '/api' + pathto restore the full path when forwarding to remote instances. - Remote node HTTP proxy body forwarding — replaced
proxyReq.write(JSON.stringify(req.body))(raced againsthttp-proxy'sprocess.nextTick(proxyReq.end)) with a conditional JSON body parser that skipsexpress.json()for remote-targeted requests; the rawIncomingMessagestream is left unconsumed sohttp-proxy'sreq.pipe(proxyReq)forwards it intact. - Remote node proxy forwarding the browser's
sencho_tokencookie to the remote instance — stripped inproxyReqso only the Bearer token is used. - Remote WebSocket upgrades forwarding the browser
cookieheader — stripped beforewsProxyServer.ws()so the remote'sauthMiddlewareuses the Bearer token exclusively. nodeContextMiddlewareblocking/api/nodeswhenx-node-idreferences a deleted node — exempted alongside/api/auth/so the frontend can re-sync a stale node ID.- Backend memory leak from
createProxyMiddlewarecalled inside the request handler on every API call — refactored to a single globally-instantiated proxy using therouteroption. remoteNodeProxyerror handler unsafely castproxyRestoResponseon WebSocket/TCP-level errors — type-narrowed before sending 502.
WebSocket & Streaming
- Container stats WebSocket flooding React with up to 20+
setStatecalls per second — replaced with a ref-buffer + 1.5 s flush interval pattern. streamStatsDocker stats stream leaking after WebSocket client disconnect —ws.on('close')handler callsstats.destroy(); allws.send()calls guarded withreadyState === OPEN.streamStatsandexecContainercalled unawaited — unhandled promise rejections now chain.catch(), log the error, and close the WebSocket cleanly.- Per-connection
WebSocket.Serverinstances for stack logs and host console never closed after upgrade —wss.close()called immediately afterhandleUpgrade. - WebSocket notification reconnect upgraded to exponential backoff (1 s → 30 s max) instead of flat 5-second retry;
ws.onerrorlogs 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
cookieheader stripped before forwarding to remote;nodeIdquery param stripped from forwarded URL. - LogViewer returning 404 on remote nodes —
nodeIdquery param stripped fromproxyReq.pathinonProxyReq.
UI & Frontend
- Blank page on HTTP deployments (root cause — Helmet 8 default CSP
upgrade-insecure-requestsand HSTS) —upgradeInsecureRequests: nullandstrictTransportSecurity: falseset 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
workerSrcmissing (Monaco editor workers) — addedworker-src 'self' blob:. - CSP
connectSrcimplicit — added explicitconnect-src 'self' ws: wss:. - Docker socket
EACCESroot: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, replacedkey={idx}with monotonic_idcounter. HomeDashboardcreate-stack error handling — reads JSON error body before throwing; uses defensive toast pattern.AlertDialogContentusingasChildwithmotion.divwrapper crashing on delete-stack confirmation — replaced with CSS keyframe animations.- animate-ui
auto-height.tsximportingWithAsChildwithouttypekeyword — crashed browser module loader. - animate-ui
switch.tsxdouble-spreading Radix props ontomotion.buttonDOM 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.
AppStoreViewandGlobalObservabilityViewusing rawfetch()instead ofapiFetch()— all calls now injectx-node-id.HostConsoleWebSocket URL missing?nodeId=query parameter.- "Open App" button opening
http://localhost:{port}for remote node containers — resolves hostname from remote node'sapi_url. - Dashboard cards showing stale local-node data after switching to a remote node — polling effects now depend on
activeNode?.idand clear state immediately on node change. refreshStackscrashing withSyntaxErrororTypeErrorwhen the remote proxy returns a non-JSON response — checksres.okbefore callingres.json().- Four empty
catch {}blocks inEditorLayout— now surface errors viatoast.error(). StackAlertSheetnot fetching notification agent status from the active node on open.SettingsModalNotifications tab hidden when a remote node is active — now visible and configurable on remote nodes.POST /api/alertsnow validates the request body with a Zod schema — rejects unknown metric/operator values, negative thresholds, and missing fields with a structured 400.WebSocket.Serverreplaced with named importWebSocketServerfromwsto fix ESM/CJS interop.NodeProvidermounted outside the auth gate — moved inside the authenticated branch sorefreshNodesno longer fires before authentication.- Infinite re-fetch loop in
NodeContext—refreshNodesuseCallback no longer depends onactiveNodestate; replaced withuseRef. - Infinite page reload loop —
apiFetchreplacedwindow.location.href = '/'with asencho-unauthorizedcustom 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_tokento be non-empty. - ESLint CI step — replaced all
anyannotations with proper types, fixed unused catch variables. [DEP0060] DeprecationWarning: util._extendfromhttp-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
GlobalObservabilityViewSSE 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.
MonitorServicecrash (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.jsontodocs.jsonv2 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
setIntervalinEditorLayout. DatabaseService.addNotificationHistoryreturns the full inserted record for real-time broadcasting.SettingsModaloverhauled — per-operation loading states, skeleton loader, unsaved-changes indicator, all saves usePATCH /api/settings.MonitorServiceevaluates limits and detects container crashes across all registered nodes concurrently.MonitorServicereads retention settings dynamically each cycle.- Developer settings scoped to the local node — reads/writes always target local via
localOnlyregardless 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).
EditorLayoutmain workspace container keyed toactiveView— every view switch triggers a fade-up entrance animation.
Removed
- SSH/SFTP remote node adapters (
IFileAdapter,LocalFileAdapter,SSHFileAdapter,SSHFileAdapter,ComposeService.executeRemote,ComposeService.streamLogsSSH path) — ~500 lines.