* fix(fleet): detect updates via GitHub Releases instead of gateway self-comparison
The fleet update check compared each node's version against the gateway's
own version, so the local node could never appear outdated. Now fetches
the actual latest release from GitHub Releases API with a 30-minute
in-memory cache and thundering-herd protection. The Recheck button
invalidates this cache via ?recheck=true to force a fresh lookup.
* docs(fleet): update docs to reflect GitHub Releases version detection
Replace "Gateway version" references with "Latest version" to match the
new label. Document that version comparison uses the latest GitHub release
rather than the gateway's own version, and that Recheck refreshes the
cached latest version.
The self-update feature failed on remote nodes because SelfUpdateService
ran `docker compose -f <host_path>` inside the container, where the host
compose file path does not exist. The fix splits the update into two
steps: (1) pull the latest image directly via `docker pull`, and (2)
spawn a short-lived helper container that mounts the compose directory
from the host and runs `docker compose up --force-recreate`.
Additional changes:
- Use execFileSync/execFile with argument arrays instead of shell strings
to eliminate shell injection surface from Docker label values
- Add Signal 4 completion detection: mark update as completed when the
remote version matches the gateway version (with 15s elapsed guard)
- Extend early failure heuristic from 90s to 3 minutes for slow pulls
- Distinguish "node unreachable" from "node lacks self-update capability"
in error messages; use silent skip in update-all to avoid res crashes
- Add requireAdmin guard to POST /api/system/update
- Handle comma-separated compose config file paths (multiple -f flags)
- Update fleet docs with self-update mechanism, troubleshooting entries
Lifetime licenses have no recurring subscription, so the Lemon Squeezy
customer portal cannot generate a URL. The Manage Subscription button in
Settings already had the isLifetime guard, but the Billing button in the
profile dropdown did not, causing a confusing "No billing portal
available" error.
- Add !license.isLifetime guard to UserProfileDropdown (matches
LicenseSection pattern)
- Move lifetime detection into getBillingPortalUrl() so the service owns
all billing eligibility logic
- Change return type to { url } | { error } discriminated union for
clear error propagation
Show a loading notification with spinner and indeterminate progress bar
while Resource Hub operations are in progress, replacing the dead moment
between confirmation and result.
* fix(auto-update): proxy update execution to remote nodes via Distributed API
Remote auto-update policies previously failed because the scheduler tried
to access the Docker daemon directly on remote nodes. Now the scheduler
detects remote nodes and proxies the update execution via HTTP to the
remote Sencho instance's new /api/auto-update/execute endpoint, which
runs image checks and compose updates locally on the remote machine.
* test(auto-update): add getNode mock to NodeRegistry in scheduler tests
The executeUpdate method now calls NodeRegistry.getNode() to detect
remote nodes. The test mock for NodeRegistry was missing this method,
causing the two executeUpdate tests to fail.
* fix(fleet): resolve stuck update states and improve update UX
The fleet node update flow had several bugs: the in-memory update tracker
never cleared terminal states (timeout, failed, completed), leaving nodes
permanently stuck with no way to retry or dismiss. The Recheck button
only re-fetched stale state without clearing it, and the POST trigger
rejected retries with 409 even after timeout.
Backend fixes:
- Add DELETE endpoints (single node + batch) to clear tracker entries
- Fix 409 race: detect expired timeouts and clear terminal states before
re-triggering
- Populate error messages in the tracker for timeouts and failures
- Include error field in the update-status API response
- Auto-expire completed entries after 60 seconds
Frontend fixes:
- Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges
- Show error details via animated cursor hover (CursorFollow pattern)
- Recheck button now batch-clears all terminal states before fetching
- Recheck shows loading spinner and disables while checking
- Extract NodeCardProps interface for readability
* fix(fleet): detect update completion via process start time
Remote nodes that cannot report their version (e.g. older builds)
caused updates to always time out because completion detection
relied solely on version comparison. The gateway now tracks the
remote node's process start time from /api/meta and detects
container restarts by comparing it across polls.
Also extracts a createTracker() factory to eliminate repeated
object construction across 5 call sites.
* docs: add troubleshooting for first-update timeout on old nodes
Adds a new troubleshooting entry explaining why the first remote
update on nodes running pre-v0.40.0 always times out (neither
version nor process start time can be detected). Documents the
fix: dismiss, recheck, and confirm the node updated.
Also adds a screenshot of the timed-out state with retry/dismiss
buttons to the remote updates feature page.
* fix(fleet): detect update completion via offline detection and error reporting
The update completion detection relied on version change and process
start time, both of which fail on nodes running older Sencho versions
that report "unknown" and lack the startedAt field. This caused every
update to time out after 5 minutes.
Add three-signal detection: version change, process restart (startedAt),
and offline/online detection (node went unreachable during update and
came back). Also add a 90-second early failure heuristic for when the
remote image pull fails silently, and surface pull errors from
SelfUpdateService via /api/meta so the gateway can report them
immediately.
* fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities
Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583.
* fix(deps): bump vite in backend lockfile to resolve audit failures
Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5.
* 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
* fix(licensing): resolve Admiral variant detection and lifetime license handling
The Lemon Squeezy variant name for Admiral licenses contains "Admiral"
(not "Team"), but getVariant() only checked for "team" and "personal".
This caused Admiral licenses to be misidentified as Skipper, locking all
Admiral-exclusive features.
- Map "admiral" variant names to internal "team" value, "skipper" to "personal"
- Add isLifetime field to LicenseInfo API response
- Hide "Manage Subscription" button for lifetime licenses (no billing portal)
- Show "Duration: Lifetime" instead of empty renewal date
- Hide upgrade cards for active Admiral users
- Add 23 unit tests covering variant resolution, tier computation, and lifetime detection
- Add troubleshooting entries for wrong tier label, locked features, and billing portal errors
* fix(licensing): address code review findings
- Fix nested ternary in LicenseSection JSX; restore conditional rendering
to avoid showing an empty "N/A" row for non-subscription states
- Clean up test file: use shared svc variable, remove redundant comments,
add trialDaysRemaining assertions, rename describe block
Eliminate all references to "Pro" across backend, frontend, and docs.
Internal tier value renamed from 'pro' to 'paid'; user-facing text now
uses the thematic tier names (Community, Skipper, Admiral).
- Rename LicenseTier 'pro' to 'paid' in backend and frontend types
- Rename requirePro guard to requirePaid, error code PRO_REQUIRED to PAID_REQUIRED
- Rename ProGate.tsx to PaidGate.tsx with updated copy
- Fix: trial users can now see upgrade/purchase cards in Settings
- Update all docs and openapi.yaml to use correct tier names
* feat(stacks): state-aware sidebar context menu and Open App action
- Context menu now adapts to stack state: running stacks show
Stop/Restart/Update, stopped stacks show Deploy only
- Added "Open App" shortcut to open a stack's web UI directly
from the sidebar (visible when running with a published port)
- Backend bulk status endpoint enriched with mainPort detection
- Reduced manual image update check cooldown from 10 to 2 minutes
- Rate limit error message now derives from the configured constant
* fix(stacks): use const for bulkPorts (prefer-const lint)
* feat(license): distributed license enforcement across multi-node setups
The primary instance's license tier is now asserted to remote nodes on
every proxied HTTP and WebSocket request via trusted headers. Remote
nodes honor the assertion only when the request carries a valid
node_proxy JWT, preventing unauthorized elevation from browsers or API
tokens. Falls back to local license tier for direct access.
* fix(test): remove unused vi import in distributed-license tests
Add the ability to check for outdated nodes and trigger over-the-air
updates from Fleet View. Nodes self-update by pulling the latest Docker
image and recreating their container via the "last breath" pattern.
Backend:
- SelfUpdateService: self-container identification via HOSTNAME + Docker
Compose labels, triggers pull + force-recreate
- CapabilityRegistry: runtime capability disabling via disableCapability()
- POST /api/system/update (202 + deferred self-update)
- GET /api/fleet/update-status (version comparison across fleet)
- POST /api/fleet/nodes/:nodeId/update (single node)
- POST /api/fleet/update-all (bulk remote update)
- In-memory update tracker with 5-min timeout
Frontend:
- Node Updates modal with summary stats, search filter, table layout,
per-node Update buttons, and bulk Update All
- Version badges and update-available indicators on node cards
- ReconnectingOverlay for local node updates (polls /api/health)
- 5s fast-poll when any node is actively updating
- UpdateStatusBadge shared component for consistent badge rendering
Requires Skipper (Pro) tier. Nodes must be deployed via Docker Compose
with Docker socket access.
* feat(nodes): add capability-based node compatibility negotiation
Each Sencho instance now exposes /api/meta with its version and supported
capabilities. When the user switches nodes, the frontend fetches this
metadata and disables features the remote node doesn't support via a
CapabilityGate overlay. Version is shown in the node switcher dropdown
and connection test results.
- Backend: CapabilityRegistry with static capability list and fetchRemoteMeta helper
- Backend: /api/meta (public) and /api/nodes/:id/meta (auth) endpoints
- Frontend: NodeContext enhanced with per-node meta caching (5min TTL)
- Frontend: CapabilityGate component with typed Capability union
- Frontend: 13 features wrapped with capability gates
- Docs: node-compatibility.mdx + OpenAPI spec updates
* fix(nodes): revert to require() for package.json version reading
The static import fails in the Docker multi-stage build because the
root package.json is not copied into the backend-builder stage. The
require() call resolves at runtime when the file is available.
Route stack alerts to specific Discord, Slack, or webhook channels instead
of the single global endpoint. Includes per-rule enable/disable, priority
ordering, and automatic fallback to global agents when no rule matches.
- Add notification_routes table, interface, and CRUD in DatabaseService
- Add routing logic in NotificationService.dispatchAlert with optional stackName
- Pass stack context from MonitorService (crash/health) and SchedulerService
- Add 5 API endpoints gated with requireAdmin + requireAdmiral
- Add NotificationRoutingSection UI with Combobox stack picker, channel tabs
- Parallel webhook dispatch via Promise.allSettled
- 10 unit tests covering routing, fallback, and edge cases
- Documentation with screenshots at docs/features/notification-routing.mdx
* feat(labels): add stack_labels schema and DatabaseService CRUD methods
* feat(labels): add label CRUD, assignment, and bulk action API routes
* feat(labels): add oklch label color palette for light and dark themes
* feat(labels): add LabelPill and LabelDot reusable components
* feat(labels): add LabelAssignPopover component for inline label management
* feat(labels): add label pill bar, label dots, and label assignment to sidebar
* feat(labels): add label filtering and label dots to fleet view
* feat(labels): add label-scoped bulk actions (deploy/stop/restart all)
* docs: add Stack Labels feature documentation
* fix(labels): use context menu sub-menu for label assignment and add settings integration
Replace broken Popover-inside-ContextMenu pattern with native Radix
ContextMenuSub for reliable label toggling on right-click. Wrap
ContextMenuSubContent in a Portal to prevent overflow clipping. Add
"Manage labels..." item that opens Settings directly to Labels section.
Fix close button overlap in LabelsSection header. Add LabelsSection
settings component with full CRUD, assignment counts, and ProGate.
Add initialSection prop to SettingsModal for deep-linking. Include
screenshots for documentation.
* docs: update stack labels documentation with screenshots and corrected instructions
* fix(labels): address security and quality issues from code review
- Add NaN validation on parseInt(req.params.id) in label routes
- Scope updateLabel/deleteLabel by nodeId to prevent cross-node IDOR
- Validate labelIds belong to correct node in setStackLabels
- Add requireAdmin check on bulk action endpoint
- Replace error: any with error: unknown and proper narrowing
- Remove unused Label import from index.ts
- Remove unused isPro prop from LabelsSection
- Add strokeWidth={1.5} to Check icons per design system
* chore: update CHANGELOG with stack labels feature
* feat(nodes): add per-node scheduling and update visibility
Add Schedules and Updates columns to the Nodes table showing active
task counts, next run times, and auto-update status per node. A calendar
action button navigates to filtered schedule/auto-update views.
Backend changes:
- Add node_id to stack_update_status table (migration + unique index)
- Cascade cleanup on node deletion (scheduled_tasks + update status)
- Pre-check target node existence/status before executing scheduled tasks
- New GET /api/nodes/scheduling-summary endpoint
- New GET /api/image-updates/fleet endpoint with 2-minute cache
- Parallelize remote node fetches with Promise.allSettled
- Wrap deleteNode cascade in a transaction
Frontend changes:
- NodeManager: Schedules/Updates columns with summary data fetch
- EditorLayout: sencho-navigate event listener for cross-component nav
- ScheduledOperationsView/AutoUpdatePoliciesView: filterNodeId prop,
filter bar UI, pre-selected node in create dialog
* feat(labels): add stack_labels schema and DatabaseService CRUD methods
* feat(labels): add label CRUD, assignment, and bulk action API routes
* feat(labels): add oklch label color palette for light and dark themes
* feat(labels): add LabelPill and LabelDot reusable components
* feat(labels): add LabelAssignPopover component for inline label management
* feat(labels): add label pill bar, label dots, and label assignment to sidebar
* feat(labels): add label filtering and label dots to fleet view
* feat(labels): add label-scoped bulk actions (deploy/stop/restart all)
* docs: add Stack Labels feature documentation
* fix(labels): use context menu sub-menu for label assignment and add settings integration
Replace broken Popover-inside-ContextMenu pattern with native Radix
ContextMenuSub for reliable label toggling on right-click. Wrap
ContextMenuSubContent in a Portal to prevent overflow clipping. Add
"Manage labels..." item that opens Settings directly to Labels section.
Fix close button overlap in LabelsSection header. Add LabelsSection
settings component with full CRUD, assignment counts, and ProGate.
Add initialSection prop to SettingsModal for deep-linking. Include
screenshots for documentation.
* docs: update stack labels documentation with screenshots and corrected instructions
* fix(labels): address security and quality issues from code review
- Add NaN validation on parseInt(req.params.id) in label routes
- Scope updateLabel/deleteLabel by nodeId to prevent cross-node IDOR
- Validate labelIds belong to correct node in setStackLabels
- Add requireAdmin check on bulk action endpoint
- Replace error: any with error: unknown and proper narrowing
- Remove unused Label import from index.ts
- Remove unused isPro prop from LabelsSection
- Add strokeWidth={1.5} to Check icons per design system
* chore: update CHANGELOG with stack labels feature
* feat(resources): add network management with create, inspect, and topology visualization
Add full Docker network CRUD: create networks with custom drivers, subnets,
and IPAM config; inspect network details including connected containers and
IP addresses; interactive topology graph visualization (Pro-gated to
Skipper/Admiral tiers). Includes backend routes, DockerController methods,
unit tests, documentation with screenshots, and updated changelog.
* refactor(resources): address code review findings for network management
- Add batch GET /api/system/networks/topology endpoint to eliminate N+1
HTTP calls from the topology view
- Export DockerNetwork type from ResourcesView, remove duplicate in
NetworkTopologyView
- Wire up isInspectLoading state to Eye button (spinner + disabled)
- Remove unnecessary wrapper div around FilterToggle
- NetworkTopologyView is now self-contained (fetches from batch endpoint,
no longer needs networks prop)
* fix(resources): align pre-existing UI with design system standards
- Replace hardcoded red-500 on image/volume delete buttons with
text-destructive/60 hover:bg-destructive tokens
- Replace shadow-sm with shadow-card-bevel on all cards (Disk Footprint,
Quick Clean, Resource Tabs, Unmanaged Containers)
- Add strokeWidth={1.5} to all action button Trash2 icons
- Type network drivers as union type instead of raw strings (backend
NetworkDriver type + frontend NETWORK_DRIVERS constant)
* fix(resources): add generic type args to useNodesState/useEdgesState
Fixes TS2345 build error in CI where tsc -b (strict mode via
tsconfig.app.json) infers never[] from untyped empty array literals
passed to React Flow hooks.
* fix(resources): replace explicit any in catch blocks with unknown narrowing
ESLint no-explicit-any errors in CI for three catch blocks added by the
network management feature.
* feat(stack-management): add scan stacks folder button to detect manually-placed compose files
Users who place Docker Compose files directly into the stacks directory
(via SCP, file manager, etc.) can now click the folder-search icon next
to "Create Stack" to immediately discover and surface those stacks in
the sidebar without a full page reload.
* docs(troubleshooting): add scan stacks folder troubleshooting section
Covers common issues: compose file not in subdirectory, unrecognized
filenames, empty directories, and already-tracked stacks.
* docs: remove security-sensitive implementation details from public documentation
Generalize or remove internal architecture details that could aid targeted
attacks — CVE tables, database schema, rate limit thresholds, proxy internals,
encryption algorithm names, and WebSocket middleware bypass info.
* test(metrics): fix flaky minute-bucket aggregation test
Floor baseTime to the start of the current minute so baseTime + 5000
never crosses a minute boundary and produces 2 buckets instead of 1.
- Webhook HMAC: capture raw request bytes via express.json verify callback
instead of re-serializing with JSON.stringify
- AES-256-GCM: use NIST-recommended 12-byte IV (backward compatible with
existing 16-byte IVs)
- Node proxy tokens: add 1-year default expiry (previously no expiry)
- Host console env filtering: pattern-based approach blocking SECRET,
PASSWORD, TOKEN, KEY, CREDENTIAL keywords (previously only 4 explicit keys)
- CORS: deny cross-origin requests when FRONTEND_URL is unset in production
(previously fell back to allowing all origins)
* feat(auto-update): add auto-update policies and fix image update detection
Auto-Update Policies (Skipper+ tier):
- New scheduled task action type 'update' for check-then-update flow
- Dedicated AutoUpdatePoliciesView with CRUD, cron presets, and run history
- Conditional tier gating: Skipper gets auto-update, Admiral gets full scheduled ops
- Backend executeUpdate: checks digests, pulls only if newer, atomic redeploy
Image Update Detection fixes (all tiers):
- Fix stack name key mismatch: use working_dir label instead of project label
- Add 5-minute periodic frontend polling for background check results
- Replace fixed 3s timeout with polling-based manual refresh via /api/image-updates/status
- Clear update status after successful stack update
* fix(ui): remove Skipper tier badge from Auto-Update Policies header
* fix(ui): remove auto-update action from Scheduled Operations view
Admiral users have a dedicated Auto-Update view — showing update tasks
in Scheduled Operations too was confusing duplication. Each view now
owns a distinct, non-overlapping set of action types.
* fix(auto-update): fix node-stack linking and add All Stacks option
- Stack dropdown now re-fetches when node selection changes using
fetchForNode, and resets the selected stack
- Node selector moved above stack selector with stack disabled until
a node is picked
- Added "All Stacks" wildcard option that checks and updates every
stack on the selected node
- Backend executeUpdate refactored to iterate over all stacks when
target_id is "*", with per-stack error isolation
* refactor(ui): replace Select dropdowns with searchable Combobox component
Add a reusable Combobox component with inline search and use it for
Node/Stack selectors in both Auto-Update Policies and Scheduled
Operations dialogs. Also fixes node-stack linking bug where changing
node didn't update the stack list.
* fix(ui): resolve CI TypeScript errors in Combobox and ScheduledOperationsView
Add missing searchPlaceholder prop to ComboboxProps interface and remove
dead 'update' action filter that conflicted with the narrowed type union.
* fix(ui): use Geist Sans font in toast component
The toast renders via React portal on document.body, bypassing the app's
font inheritance. Add explicit font-family declaration using var(--font-sans)
to match Sencho's design system.
Show an inline warning banner in the Add Node form when the user enters
an http:// URL, recommending HTTPS or VPN for public internet connections.
HTTP remains fully supported for private networks (LAN, VPN, VPC).
Also returns an optional `warning` field in POST/PUT /api/nodes responses
for API-only consumers, and expands the multi-node security documentation
with concrete deployment guidance (reverse proxy, VPN, private network).
- Dispatch error alerts via NotificationService when scheduled tasks fail,
with info-level recovery notifications when a previously-failing task succeeds
- Per-service restart targeting: scheduled stack restarts can target specific
services instead of restarting the entire stack
- Prune label filter: scheduled prune operations can be scoped to resources
matching a specific Docker label
- CSV export button in the execution history panel for one-click download
- Fix: prune_targets was silently dropped on task creation (missing in INSERT)
* feat(host-console): gate Host Console behind Admiral tier
Move the Host Console from the Community (free) tier to Admiral,
enforcing the gate at every layer: UI nav visibility, AdmiralGate
wrapper, POST /api/system/console-token endpoint, and the WebSocket
upgrade handler for /api/system/host-console.
* test(auth): mock Admiral license for console-token test
The console-token endpoint now requires Admiral tier. Mock
LicenseService in the test to return pro/team so the happy-path
test passes in CI where no license is activated.
* fix(stacks): resolve permission denied error when deleting stacks with root-owned files
When Docker Compose creates files as root inside a stack directory, the
non-root Sencho process cannot remove them. This adds a Docker-based
fallback: if fsPromises.rm fails with EACCES/EPERM, Sencho spawns a
short-lived Alpine container to clean up the root-owned files.
Also enhances docker compose down with --volumes --remove-orphans to let
Docker clean up its own resources before filesystem deletion.
* docs: clarify that pre-existing root-owned stacks can be deleted
* fix(stacks): include Docker stderr in fallback deletion error message
Fixes CI lint failure: 'stderr' was assigned but never read in
forceDeleteViaDocker(). Now surfaces Docker stderr output in the error
message when the fallback cleanup fails.
- Configurable retention: audit_retention_days setting (1-365 days, default 90)
replaces hardcoded 90-day retention, exposed in Settings > Data Retention
- Export: one-click CSV/JSON export of filtered audit data via new
GET /api/audit-log/export endpoint (capped at 10,000 entries)
- Auditor role: read-only role with system:audit permission for viewing
and exporting audit logs without admin privileges (Admiral tier)
- Enhanced filtering: full-text search across summaries/paths/usernames,
date range picker, and expandable row details showing request path,
IP address, node ID, and entry ID
* feat(rbac): add Deployer & Node Admin roles with scoped permissions (Team Pro)
Add intermediate RBAC roles gated to Team Pro tier:
- Deployer: can deploy/restart/stop/start stacks but cannot edit compose files, delete stacks, or access system settings
- Node Admin: full stack and node management within scope, no system settings access
- Scoped permissions: assign roles per-stack or per-node for fine-grained access control
- Permission engine with checkPermission/requirePermission guards replacing requireAdmin on stack/node routes
- Frontend can() function with /api/permissions/me endpoint for client-side permission checks
- User management UI updated with 4-role selector and scoped permission editor
- Documentation updated with permission matrix, scoped permission docs, and screenshots
* fix(rbac): remove unused RoleAssignment import to fix lint error
Add centralized credential storage for private Docker registries with
support for Docker Hub, GHCR, AWS ECR, and self-hosted registries.
- New `registries` table with AES-256-GCM encrypted secrets
- RegistryService with CRUD, test connectivity, Docker config generation
- 5 API endpoints gated by requireTeamPro + requireAdmin
- ComposeService injects credentials via temp DOCKER_CONFIG on deploy/pull
- ImageUpdateService passes stored credentials for private registry checks
- AWS ECR just-in-time token refresh via @aws-sdk/client-ecr
- RegistriesSection UI in Settings Hub with type-aware form
- Documentation with screenshots
- Fix "Run Now" audit log showing "Created scheduled task" instead of "Triggered scheduled task" by adding wildcard-based route matching with specificity sorting
- Add triggered_by column to track whether runs were started by the scheduler or manually via Run Now
- Add configurable prune targets (containers, images, networks, volumes) with checkbox UI
- Add pagination to execution history with offset-based navigation
- Document Run Now behavior on disabled tasks and add screenshots
Adds the ability to schedule recurring Docker operations (stack restarts,
fleet snapshots, system prunes) via cron expressions with full execution
history logging. Includes Run Now for on-demand execution.
* fix(api-tokens): harden scope enforcement and add expiration support
- Fix deploy-only allowlist to match actual routes (deploy, down, restart,
stop, start, update) instead of non-existent /up, /pull, /compose/* paths
- Block API tokens from auth-sensitive routes (password change, node token
generation) that bypass scope enforcement middleware
- Add WebSocket scope enforcement: read-only/deploy-only tokens can only
access logs and notifications, not host console or container exec
- Prevent API token self-replication: tokens cannot create, list, or revoke
other tokens regardless of scope
- Map deploy-only tokens to admin role so they pass requireAdmin on deploy
routes (scope middleware still restricts which endpoints they can reach)
- Add optional token expiration (30, 60, 90, 365 days or no expiry)
- Add token name length validation (max 100 characters)
- Surface fetchTokens errors in frontend instead of swallowing silently
- Fix docs: correct deploy-only scope description and GitHub Actions example
* fix(api-tokens): block all sensitive management endpoints from API tokens
User management, SSO configuration, node management, license management,
and console access are now human-session-only. Add comprehensive unit
tests for scope enforcement, blocked endpoints, expiration, and revocation.
* fix(api-tokens): fix TS18048 possibly-undefined in test
- Fix deploy-only allowlist to match actual routes (deploy, down, restart,
stop, start, update) instead of non-existent /up, /pull, /compose/* paths
- Block API tokens from auth-sensitive routes (password change, node token
generation) that bypass scope enforcement middleware
- Add WebSocket scope enforcement: read-only/deploy-only tokens can only
access logs and notifications, not host console or container exec
- Prevent API token self-replication: tokens cannot create, list, or revoke
other tokens regardless of scope
- Map deploy-only tokens to admin role so they pass requireAdmin on deploy
routes (scope middleware still restricts which endpoints they can reach)
- Add optional token expiration (30, 60, 90, 365 days or no expiry)
- Add token name length validation (max 100 characters)
- Surface fetchTokens errors in frontend instead of swallowing silently
- Fix docs: correct deploy-only scope description and GitHub Actions example
Add long-lived API tokens with three permission scopes (read-only,
deploy-only, full-admin) for CI/CD pipelines, scripts, and automation.
- Database: api_tokens table with SHA-256 hashed storage
- Auth: extend middleware to authenticate Bearer API tokens
- Scope enforcement: middleware restricts actions per token scope
- API: CRUD endpoints gated behind Team Pro + admin
- UI: ApiTokensSection in Settings Hub with create/revoke/copy flows
- Docs: new api-tokens.mdx with usage examples and screenshots
* feat: SSO & LDAP authentication for Team Pro
Add SSO integration allowing Team Pro users to authenticate via LDAP/Active Directory, Google, GitHub, and Okta identity providers. SSO works alongside password authentication with auto-provisioning and role mapping.
- LDAP bind+search authentication with group-based role mapping
- OIDC/OAuth2 flows with PKCE and CSRF protection for Google, GitHub, Okta
- Auto-provisioning: first SSO login creates a Sencho account automatically
- Role mapping via LDAP group membership or OIDC JWT claims
- SSO settings UI in Settings → SSO with per-provider config and test connection
- SSO login buttons on login page with LDAP toggle
- Environment variable seeding for infrastructure-as-code workflows
- Secrets encrypted at rest via CryptoService (AES-256-GCM)
- Seat limit enforcement during auto-provisioning
- Full documentation: feature docs, quickstart guides, env var reference
* fix: resolve ESLint errors in SSO feature
- Remove unnecessary escape characters in regex character classes
- Remove unused `issuer` variable from OIDC callback handler
- Fix setState-in-effect lint error in Login.tsx by using useState initializer
- Suppress set-state-in-effect for SSOSection fetch pattern (matches existing codebase convention)
* feat: audit logging, secrets at rest, and legacy cleanup
- Add Team Pro audit log: records all mutating API actions with user
attribution, searchable timeline UI with filtering and pagination
- Add AES-256-GCM encryption at rest for node API tokens via CryptoService
- Drop 9 legacy SSH/TLS columns from nodes table (dead since v0.7)
- Remove orphaned MaintenanceModal.tsx (dead code, never imported)
- Add requireTeamPro backend guard for team-tier features
- Add audit log cleanup (90-day retention) to MonitorService
- Add docs page and navigation entry for audit log feature
* fix: remove unused AUTH_TAG_LENGTH constant from CryptoService
Community users now see Personal Pro and Team Pro cards with feature
highlights and direct Lemon Squeezy checkout links. Personal Pro users
see only the Team Pro upgrade option. Trial and Team Pro users see no
upgrade cards since they already have full access.
* chore: remove CLAUDE.md and MANUAL_STEPS.md from git tracking
These files contain internal project instructions that should not be
public. Both are now in .gitignore to prevent re-tracking.
* chore: remove build output and generated files from git tracking
Remove backend/public/ (frontend build artifacts) and backend/cookies.txt
from tracking. Add demo-video/ and website/ to .gitignore.
* docs: align mintlify theme with website and app branding
Update colors to brand cyan, add Geist font, dark mode default,
gradient background, navbar with GitHub link and CTA, and footer
matching the website structure.
* chore: add shadcn MCP server configuration
* feat: add stack context menu, tier icons, centered logo, and support section
- Add right-click context menu on sidebar stacks with full actions
(alerts, check updates, deploy, stop, restart, update, delete)
- Mirror same actions in the three-dot dropdown menu
- Create TierBadge component with Globe/Crown/Users icons for
Community/Pro/Team tiers, replacing ProBadge across the app
- Center logo and app name in sidebar header
- Add tiered Support section in Settings with docs, GitHub issues,
and tier-appropriate email support channels
- Remove duplicate links from About section
* docs: update changelog and stack management docs for context menu
* 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 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
Add custom webhooks allowing external CI/CD systems (GitHub Actions, GitLab CI, etc.)
to trigger stack actions via HTTP POST with HMAC-SHA256 signature authentication.
Includes webhook CRUD management UI in Settings, execution history tracking,
one-time secret reveal, enable/disable toggle, and comprehensive documentation.
Add fleet health summary cards, container-level drill-down, node sorting
and filtering, fleet-wide search, and critical node detection to the
Fleet View. Fix ProGate to wrap placeholder content and add error toasts.
- Add 5 new Tier 1 doc pages: configuration, stack-management, editor,
multi-node, and alerts-notifications
- Update introduction, quickstart, and features/overview to reflect
current feature set and link to new pages
- Restructure mint.json with Getting Started / Features / Reference /
Operations navigation groups
- Add Playwright-captured screenshots for all major UI screens
- Add sync-docs CI job: runs on push to main, copies /docs into sencho-docs repo via DOCS_REPO_TOKEN
- Scaffold /docs with mint.json, getting-started/introduction.mdx, getting-started/quickstart.mdx, features/overview.mdx
- Update CHANGELOG