* fix(fleet): resolve version detection pipeline for Docker builds
The Dockerfile backend-builder stage was missing a COPY of the root
package.json, causing generate-version.js to fall back to "0.0.0-dev"
at build time. At runtime, the filesystem walk also failed (root
package.json not in the final image), producing the string "unknown"
which the frontend rendered as "vunknown".
Changes:
- Dockerfile: copy root package.json into backend-builder stage
- CapabilityRegistry: return null (not "unknown") for unresolvable
versions; add isValidVersion() type guard; normalize remote meta
responses to strip "unknown"/"0.0.0-dev" sentinel values
- Fleet endpoints: hoist gateway version validation outside per-node
loops; treat unresolvable remote versions as "potentially outdated"
instead of silently marking them up to date
- FleetView: guard all version display points (card badge, update
button, gateway label, modal columns) via shared formatVersion()
- EditorLayout, CapabilityGate: use shared isValidVersion utility
- New frontend/src/lib/version.ts shared utility
- Docs: add troubleshooting section for version display edge cases
- Screenshots: updated Fleet Overview and Node Updates modal
* docs: update fleet node updates screenshot with live remote node
Compose v2.40.3 was compiled with Go 1.24.9, grpc 1.74.2, and
x/crypto 0.38.0 — all below the minimum safe versions identified
in the March 2026 security audit. Upgrading to v5.1.1 (Go 1.25.8,
x/crypto 0.46.0) resolves CVE-2025-68121, CVE-2025-61726,
CVE-2025-61729, CVE-2026-25679, and CVE-2025-47913.
CVE-2026-33186 (grpc ≥1.79.3) remains unresolved — no upstream
Docker CLI or Compose release includes grpc past 1.78.0 yet.
CVE-2026-33671 (picomatch) and CVE-2026-33747/33748 (buildkit) were
already patched in prior releases.
- Upgrade Docker base image from node:20-alpine to node:22-alpine
(Node 22.22.2, Alpine 3.23.3) to resolve 31 CVEs flagged by Docker Scout
- Add apk upgrade to runtime stage for latest Alpine security patches
- Force dompurify to 3.3.3 via npm overrides to fix two XSS advisories
(transitive dep of monaco-editor pinned at 3.2.7)
* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)
Introduces three Pro-tier features:
- RBAC: Multi-user system with admin/viewer roles, user management UI,
automatic migration from single-admin credentials, viewer restrictions
across the entire UI (read-only editor, hidden action buttons)
- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
rollback on health probe failure, manual rollback button, health probes
added to stack updates, webhook-triggered deploys use atomic rollback
- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
nodes (local + remote), stored centrally in SQLite, per-stack restore
with optional redeploy, graceful handling of offline nodes
* fix(settings): use correct ProGate prop name in UsersSection
* fix(settings): remove unused isPro prop from UsersSection
* fix(auth): fetch user info after login and setup so isAdmin is set correctly
* feat(pricing): revise pricing strategy and enforce variant-based seat limits
Raise Personal Pro from $49/yr to $69/yr with 3 viewer seats (up from 1).
Add $15/mo billing option for Team Pro. Mark lifetime pricing as a
90-day early-adopter offer. Store Lemon Squeezy variant_name on
activation/validation and enforce seat limits server-side per variant.
* feat(licensing): add Lemon Squeezy checkout, webhook, and billing portal integration
Server-side checkout URL generation (POST /api/checkout) with admin email
pre-fill and instance_id custom data. HMAC-SHA256 verified webhook endpoint
(POST /api/webhooks/lemonsqueezy) handling order, subscription, and payment
lifecycle events for automatic license activation. Customer billing portal
link stored from webhook events and exposed via GET /api/billing/portal.
In-app checkout buttons in Settings with manual license key fallback.
* fix(licensing): exempt Lemon Squeezy webhook from auth middleware
The catch-all auth middleware on /api/* was blocking the public webhook
endpoint. Added /webhooks/lemonsqueezy to the exemption list alongside
/auth/* and /webhooks/:id/trigger.
* feat(pricing): update pricing to final live rates
Personal Pro: $7.99/month, $69.99/year, $249 lifetime.
Team Pro: $49.99/month, $499.99/year, $1,499 lifetime.
Added personal_monthly checkout variant across backend, frontend, and website.
* refactor(licensing): remove server-side checkout/webhook for self-hosted model
Sencho is self-hosted — each user runs their own instance, so there is
no central server to receive webhooks or hold the store API key. Replaced
in-app checkout buttons with a "View Pricing" redirect to sencho.io and
kept manual license key activation as the primary flow.
- Delete LemonSqueezyService (checkout, webhook, HMAC verification)
- Remove POST /api/checkout, GET /api/billing/portal, POST /api/webhooks/lemonsqueezy
- Remove raw body parser and auth exemption for webhook route
- Remove all LEMONSQUEEZY_* env vars from .env.example
- Replace checkout buttons in SettingsModal with single "View Pricing" button
- Simplify LicenseContext checkout to open sencho.io pricing page
- Update licensing docs to reflect website-based purchase flow
* chore: normalize em-dashes to hyphens across codebase (linter)
* chore: remove accidentally tracked directories from index
* feat: add license gating system with Lemon Squeezy integration
Add Community/Pro tier infrastructure:
- LicenseService singleton with Lemon Squeezy license API integration
- /api/license endpoints (GET info, POST activate/deactivate/validate)
- 14-day Pro trial activated automatically on first boot
- 72-hour periodic validation with 30-day offline grace period
- LicenseContext provider for frontend tier awareness
- License settings tab with activation UI and status display
- ProBadge and ProGate reusable components for feature gating
- requirePro per-route guard for backend Pro-only endpoints
- Proxy bypass for /api/license routes (local-only, never proxied)
* feat: add user profile dropdown and reorganize top navigation
- Create UserProfileDropdown component with settings, billing, theme
toggle (System/Light/Dark), documentation links, and logout button
- Remove logout button from sidebar header
- Remove standalone settings button from top bar
- Move theme toggle from Settings modal to profile dropdown
- Inject app version via Vite define from root package.json
- Add globals.d.ts for __APP_VERSION__ type declaration
* refactor(settings): remove appearance tab from settings modal
Theme toggle was moved to the User Profile Dropdown in the previous
commit. Remove the now-redundant Appearance section, its nav button,
and the unused theme/setTheme props from SettingsModal.
* feat: add fleet view dashboard and about settings section
Fleet Overview: aggregates all nodes into a card grid showing status,
container counts, CPU/RAM/disk usage bars. Pro tier unlocks stack
drill-down with auto-refresh (30s). Backend endpoints /api/fleet/overview
and /api/fleet/node/:nodeId/stacks query nodes in parallel.
About section in Settings: displays version, license tier, status,
instance ID, and links to docs/changelog/issues.
Sidebar perf fix: stack status fetches now run in parallel via
Promise.allSettled instead of sequential for-loop, significantly
reducing load time for nodes with many stacks.
Also removes version number from User Profile Dropdown (now in About).
* fix(ci): resolve Docker build and E2E test failures
- Copy root package.json into frontend build stage so vite.config.ts
can read the app version during Docker multi-stage build.
- Update auth E2E test: logout button moved into User Profile Dropdown.
- Update nodes E2E test: Settings button moved into User Profile Dropdown.
* chore(deps): upgrade ESLint 9 → 10 with plugin compatibility fixes
- eslint: ^9 → ^10.0.0 (backend + frontend)
- @eslint/js: ^9 → ^10.0.0 (backend + frontend)
- eslint-plugin-react-refresh: ^0.4.24 → ^0.5.2 (ESM, config factory API)
- Update frontend eslint.config.js: destructured import for react-refresh,
call configs.vite() as factory function (0.5 API change)
- Downgrade new ESLint 10 rules (no-useless-assignment, preserve-caught-error)
to warnings for existing code patterns
- eslint-plugin-react-hooks stays at 7.0.1 (stable) with --legacy-peer-deps
until a stable release adds ESLint 10 peer support
* chore(deps): upgrade recharts 2.x to 3.8 with chart.tsx type fixes
recharts 3.x moved Tooltip/Legend props to context-based API.
Updated chart.tsx to use explicit prop interfaces with internal
recharts type imports (LegendPayload, TooltipPayload, TooltipFormatter).
* chore(deps): upgrade TypeScript 5.9 to 6.0
- Remove deprecated baseUrl from frontend tsconfig (paths works standalone in TS 6)
- Add react-is dependency required by recharts 3.x at build time
- Backend and frontend both compile and lint cleanly
* chore(deps): upgrade Vite 7.3 to 8.0 and @vitejs/plugin-react to 6.0
Vite 8 replaces Rollup+esbuild with Rolldown, significantly improving
build speed (~2s vs ~18s). No config changes required.
* fix(ci): add .npmrc with legacy-peer-deps for CI and Docker builds
typescript-eslint@8.x requires typescript <6.0.0 and
eslint-plugin-react-hooks@7.0.1 requires eslint <=9. Until upstream
packages release compatible versions, legacy-peer-deps is needed.
* docs: add logo assets and re-ignore CLAUDE.md
* fix(ci): copy .npmrc into prod-deps Docker stage
The prod-deps stage also runs npm ci with backend/package.json but was
missing the .npmrc needed to bypass peer dep conflicts.
* fix(docker): fix xx cross-compilation sysroot for node-pty and C++ modules
Three issues in the prod-deps cross-compilation stage:
1. xx-apk had `gcc` (C only) instead of `g++` — all three native modules
(bcrypt, better-sqlite3, node-pty) use C++, so libstdc++ headers must
be present in the target sysroot for xx-clang++ to link against them.
2. Missing `linux-headers` in the target sysroot — node-pty requires
<pty.h> and <termios.h> which live in the Linux kernel headers package.
3. Missing AR=xx-ar — node-gyp uses `ar` to create static archives during
the native build; without this override it falls back to the host ar
(amd64), producing wrong-arch .a files that fail at the link step.
* fix(docker): use native g++ for same-platform builds, xx-clang only for cross
xx-clang introduces sysroot flags that break node-gyp header resolution on
Alpine when TARGETARCH == BUILDARCH (e.g. the docker-validate CI job which
builds amd64-only). The native path now uses the standard g++ toolchain
(identical to what worked before the xx refactor). The cross-compilation
path (amd64 → arm64) continues to use xx-clang / xx-clang++ / xx-ar with
the target sysroot populated via xx-apk.
Node.js v20 uses ARMv8.1 LSE atomic instructions that the GitHub Actions
QEMU version does not support, causing SIGILL / "Illegal instruction" core
dumps when npm ci runs inside the arm64 emulated layer.
Solution: add a dedicated prod-deps build stage that runs on $BUILDPLATFORM
(amd64, native) and uses tonistiigi/xx + xx-clang to cross-compile native
modules (bcrypt, better-sqlite3, node-pty) for the TARGET architecture
without ever executing Node.js under QEMU. The final runtime stage simply
copies the pre-compiled node_modules from prod-deps.
Stage breakdown:
xx - cross-compilation helper (provides xx-clang, xx-apk)
frontend-builder - $BUILDPLATFORM, vite build (no native modules)
backend-builder - $BUILDPLATFORM, tsc compilation
prod-deps - $BUILDPLATFORM + xx-clang cross-compiles for $TARGET
final - TARGET platform, no compilation at all
- Add docker/setup-qemu-action@v3 to docker-publish.yml (was missing,
causing multi-platform builds to hang indefinitely)
- Add platforms: linux/amd64,linux/arm64 to build-push-action step
- Optimize Dockerfile with --platform=$BUILDPLATFORM on builder stages
so TypeScript compilation runs at native amd64 speed; only the lean
npm ci --omit=dev step runs under QEMU in the final stage, compiling
the three native modules (bcrypt, better-sqlite3, node-pty) for the
correct target architecture — reduces arm64 build time from 6+ hours
to ~15-30 minutes
The merge of PR58 into PR59's branch produced a corrupt docker-entrypoint.sh
(97 lines) where PR59's if block was cut off mid-way (missing exec su-exec
and closing fi) and then PR58's entire if block was appended. This caused:
/usr/local/bin/docker-entrypoint.sh: line 98: syntax error: unexpected
end of file (expecting "fi")
Fix: overwrite with the clean 71-line version and add a defensive
`sed -i 's/\r//'` step in the Dockerfile to strip Windows CRLF line
endings at build time, preventing this class of error even if CRLF
slips past .gitattributes in future.
Add docker-entrypoint.sh that runs as root at startup, fixes ownership
of the DATA_DIR volume (only files with wrong user or group), then drops
to the non-root sencho user via su-exec before starting Node.
This eliminates the SQLITE_READONLY crash that occurs when a host-mounted
data volume was created by root or chowned to the wrong UID. The pattern
mirrors the official PostgreSQL, Redis, and MariaDB Docker images.
- Install su-exec in Stage 3 (10KB Alpine tool, idiomatic alternative to gosu)
- Remove USER directive; entrypoint handles the privilege drop instead
- Use ENTRYPOINT + CMD so Node becomes PID 1 (correct SIGTERM handling)
- Add .gitattributes to enforce LF line endings for *.sh files
SECURITY (critical fixes):
- Add authMiddleware to /api/system/console-token (was publicly accessible)
- Validate api_url on node create/update to prevent SSRF (rejects localhost/loopback)
- Add rate limiting (5 req/15 min/IP) to /api/auth/login and /api/auth/setup
- Fix path traversal in env_file resolution — absolute/escaping paths rejected
- Add stack name validation to GET routes (was only on PUT/POST)
- Add helmet security headers middleware
- Restrict CORS to FRONTEND_URL in production
PRODUCTION READINESS:
- Add GET /api/health public endpoint + HEALTHCHECK in Dockerfile
- Add SIGTERM/SIGINT graceful shutdown handler (drains connections, closes DB)
- Run container as non-root sencho user in Dockerfile
QUALITY:
- Fix 4 silent empty catch{} blocks in EditorLayout (now show toast.error)
- Connect ErrorBoundary to root App in main.tsx
- Replace WebSocket.Server with named WebSocketServer import (ESM compat)
TESTING (new automated test suite):
- Install Vitest; 38 backend tests across 4 suites covering validation utilities,
health endpoint, auth middleware, login flows, SSRF protection, and path traversal
- Extract isValidStackName/isValidRemoteUrl/isPathWithinBase to utils/validation.ts
- Playwright E2E scaffolding: auth, stacks, nodes specs + shared login helper
- CI: run Vitest + ESLint on every PR