* feat(git-sources): link stacks to Git repositories with diff-and-apply workflow
Add Git Sources so any stack can point at an HTTPS Git repository, branch, and
compose file path. Pulls fetch + validate the incoming commit, store a
diffable pending snapshot, and apply writes only after explicit confirmation
(or automatically, per the configured apply mode). Sibling .env sync is
optional. Works on the Community tier.
Apply modes:
- Review only: mark pending, wait for manual apply in the diff dialog
- Auto-write: write compose + env to disk, do not redeploy
- Auto-deploy: write files and run docker compose up -d
Webhook integration: webhooks can target the new "git-pull" action to trigger
a sync from CI. Per-source debounce prevents runaway pipelines from hammering
the repository host. Tokens are encrypted at rest and never returned to the
frontend.
Docs and tests included. Screenshots and Playwright E2E flows to follow.
* fix(git-sources): drop unnecessary useMemo on commit sha slice
React Compiler's lint rule rejected the manual dependency list because the
inferred dep ('pull') was less specific than the written one ('pull?.commitSha').
The computation is a cheap 7-char slice, so drop the useMemo entirely rather
than fight the rule.
* test(git-sources): add Playwright E2E flows and drop orphan source rows on stack delete
- E2E coverage: non-HTTPS URL rejected client-side, unreachable repo surfaces
a toast error on save, and configure+remove walks the AlertDialog confirm path.
- Deleting a stack now also drops its linked Git source row so a future stack
with the same name starts clean rather than inheriting a stale config.
* feat(registries): add stateless test endpoint, ECR caching, URL and host hardening
Adds a POST /api/registries/test endpoint so credentials can be verified
before being persisted. Caches ECR authorization tokens in memory until
their AWS-reported expiry (minus a safety margin) instead of fetching on
every compose invocation. Normalizes registry URLs on save so the
stored values match the keys Docker expects in ~/.docker/config.json,
fixes a bidirectional host-match bug in getAuthForRegistry that could
cross-match overlapping hostnames, and surfaces per-registry decryption
failures as warnings in the deploy log stream instead of swallowing
them. Also strips the Authorization header on cross-host redirects in
the test probe, rejects non-http(s) schemes on save, and validates the
shape of returned ECR authorization tokens before use.
* refactor(registries): align UI with design system and add in-form test button
Swaps the registry type dropdown from shadcn Select to the project's
Combobox, applies the canonical card bevel and top-border hover styling
to the form container and each registry row, restyles the delete
button to the ghost + muted destructive pattern, uses strokeWidth 1.5
on every Lucide icon, and routes all toast errors through the
standard defensive chain. Adds a Test connection button inside the
form so credentials can be verified before saving.
* test(registries): cover RegistryService and deploy warnings surface
Adds unit coverage for URL normalization, the encrypt/decrypt round
trip through create and resolveDockerConfig, exact-host matching in
getAuthForRegistry, resolveDockerConfig warnings on decryption
failure, ECR token cache hit/miss and invalidation on update, the
stateless testWithCredentials path for 200, 401 with and without a
challenge, network errors, and ECR success and failure including
malformed tokens. Extends the ComposeService tests to verify that
warnings from resolveDockerConfig reach the deploy log stream.
* docs(registries): document test-before-save flow and troubleshooting
Describes the in-form Test connection button, the two-point testing
flow from the registries list, the cached ECR token behavior during
deploys, the per-registry warning Sencho emits when a stored secret
cannot be decrypted, and adds a Troubleshooting section covering
common 401 causes, ECR token handling, warning interpretation, and
per-node credential scoping.
* fix(notifications): stop Sencho version notifications from silently skipping
Three independent defects combined to make version-update notifications
silently fail even while Fleet overview correctly surfaced an update button:
- The in-memory 6-hour cooldown was advanced before the network fetch,
so a single transient failure at boot could lock the check for the
rest of the container lifetime. Moved the cooldown update inside the
success branch so failures retry on the next eval cycle.
- MonitorService called the raw version fetch directly, bypassing the
CacheService wrapper (TTL, inflight dedup, stale-on-error) that Fleet
uses, so the two paths could diverge. Unified both on a shared
getLatestVersion() helper in utils/version-check.ts.
- The dedup key could carry stale state from a previous build and never
self-clear. It now self-heals when the running version reaches the
previously-notified version, so future releases re-fire as expected.
Added diagnostic logs gated on debug mode for each skip branch, plus
three regression tests covering cooldown-on-failure, cooldown-on-success,
and dedup self-heal.
* docs(notifications): drop legacy-upgrade framing from alerts troubleshooting
Sencho has not shipped publicly, so troubleshooting entries written in
'this used to happen but now does Y' mode reference a past that does not
exist for any reader. Rewrote the version-notification, image-update,
and crash-alert troubleshooting entries to describe current behavior
positively without referring to prior builds, upgrade paths, or legacy
fixes.
* chore(security): accept CVE-2026-33810 in bundled Docker CLI 29.4.0
Trivy now flags CVE-2026-33810 (Go stdlib crypto/x509 DNS constraint
bypass, fixed in Go 1.26.2) in the Docker CLI static binary we ship.
Docker CLI 29.4.0 is the latest upstream release and still links Go
1.26.1; no newer static binary exists yet.
Same exposure profile as the already-accepted CVE-2026-32280: the
Docker CLI and compose plugin only validate certificates from
well-known registry CAs and the local Docker socket, not from
attacker-controlled CAs with crafted DNS name constraints. Revisit
on the next Docker CLI release that rebuilds against Go 1.26.2 or
later.
* fix(notifications): replace polling with Docker event stream for container lifecycle detection
Replaces the 30-second MonitorService crash-detection poll with a causal,
per-node Docker events stream. Eliminates false crash alerts on intentional
stops (docker stop, compose down, stack restart/update), detects OOM kills
as a distinct alert category, and surfaces real crashes in real time.
A new DockerEventManager spawns one DockerEventService per local node. Each
service consumes the filtered container event stream, classifies die events
against recent kill/oom state, and reconciles container state via snapshot
diffing on connect and reconnect. Rate limiting, exponential backoff with
jitter, and parse-error tolerance keep the stream resilient under load and
during daemon interruptions.
MonitorService retains host limits, janitor, version check, and stack metric
alerts; crash and healthcheck detection move out entirely.
* fix(tests): silence require-imports lint in hoisted mock factory
* fix(network-topology): harden with edge-case fixes, logging, and test coverage
Fixes stale data on node switch (topology now refreshes when active node changes),
adds manual refresh button to toolbar, and guards against containers with empty
Names arrays by falling back to a 12-char short ID. Adds operational and diagnostic
logging around topology fetches, and introduces 25 unit tests covering happy paths,
system network filtering, stack resolution, container deduplication, edge cases,
and error handling.
* docs: refresh screenshots
* fix(container-exec): harden with security fixes, validation, and test coverage
- Enforce admin role at WebSocket upgrade for container exec sessions
- Validate container is running before creating exec
- Fix bash-to-sh fallback (move .start() inside try/catch)
- Register container-exec as a capability for fleet visibility
- Add standard and diagnostic logging for exec lifecycle
- Fix false Admiral license claim in API docs
- Fix design system violations in BashExecModal (hardcoded colors)
- Remove duplicate legacy xterm dependencies
- Add 18-test suite covering auth, validation, fallback, and cleanup
* fix(container-exec): use correct Duplex type for exec stream
The stream variable was typed as NodeJS.ReadWriteStream, which lacks
.destroy(). Dockerode's Exec.start() returns stream.Duplex per its
type definitions. This caused tsc to fail while Vitest (which skips
full type checking) passed.
- Add authMiddleware to GET/POST /api/agents (previously unauthenticated)
- Add input validation to POST /api/agents (type, URL, enabled)
- Add URL host validation via new URL() to notification-routes POST/PUT
- Add priority type validation and enabled boolean check to routes
- Add name length limit (100 chars) to routes POST/PUT
- Add stack pattern dedup and whitespace filtering
- Add NaN guard to DELETE /api/notifications/:id
- Add dispatch_error to NotificationHistory TypeScript interface
- Extract cleanStackPatterns() and validateHttpsUrl() helpers
- Add standard and diagnostic logging to agents and routes endpoints
- Add comprehensive integration tests for notification-routes CRUD
- Add agents auth and validation tests
- Add dispatch error recording unit tests
* fix(alerts): harden with security fixes, design compliance, and test coverage
Add authMiddleware to all alert endpoints, validate notification test
dispatch inputs, fix restart_count metric via Docker inspect, correct
network metric units, replace any types with DockerContainerStats
interface, add webhook timeouts and dispatch error tracking.
Frontend: migrate Select to Combobox, add ScrollArea and delete
confirmation AlertDialog, fix icon strokeWidth to 1.5.
Add update availability notifications for both Sencho version updates
(6-hour check in MonitorService) and stack image updates (state
transition detection in ImageUpdateService). Extract shared version
fetch logic into utils/version-check.ts.
Add diagnostic logging gated behind developer_mode for MonitorService
breach state machine and NotificationService dispatch routing.
Tests: 24 new alert API integration tests, restart_count and version
check unit tests (688 total passing). Docs updated with HTTPS
requirement, update notifications section, and troubleshooting guide.
* fix(alerts): remove unused TEST_USERNAME import in alerts-api tests
Rate-limit OIDC callback route and clear state cookie on all paths.
Validate GitHub API responses and add server-side config validation.
Sync SSO user roles on every login respecting seat limits.
Add LDAP connection timeout, bind DN warning, and multiple entry logging.
Replace Select with Combobox and apply card design tokens in SSOSection.
Expose OIDC scopes field in settings UI.
Fix hardcoded colors to use design system tokens.
Add standard and diagnostic logging throughout SSO flow.
Add tests for role sync, seat limits, LDAP escaping, and config validation.
Remove unused login-form.tsx template.
Update docs and screenshots for SSO feature.
* fix(audit): harden audit log with summary fixes, design compliance, and test coverage
Replace stale /compose/* route summaries with current /stacks/* patterns,
add ~30 missing route summaries for container ops, labels, fleet updates,
SSO, templates, and auto-update. Fix X-Forwarded-For extracting full
proxy chain instead of first client IP. Pre-sort pattern table at module
load for O(1) lookup instead of sorting per request.
Add Calendar and DatePicker UI components (react-day-picker v9 + date-fns)
replacing browser-native date inputs. Align AuditLogView with design system:
Combobox instead of Select, design token colors, strokeWidth 1.5, card bevel,
tabular-nums on all numeric cells.
Add diagnostic logging gated behind developer mode for audit middleware,
query, and export endpoints. Extract getAuditSummary to testable utility.
Add 32 tests covering summary resolution, DB round-trips, API permissions,
export formats, and middleware integration.
* fix(audit): use correct Chevron prop types for react-day-picker compatibility
Security fixes:
- Map deploy-only API tokens to deployer role (not admin)
- Add token versioning to invalidate sessions on password/role changes
- Reject deleted users immediately in auth middleware (no 24h JWT grace)
- Use DB role instead of JWT role so changes take effect instantly
- Block password setting on SSO-provisioned users
- Check proxy variant in scoped permission resolver
Cleanup and logging:
- Remove orphaned role assignments when stacks/nodes are deleted
- Add standard logging for login, user CRUD, role assignments, password changes
- Add diagnostic logging gated behind developer_mode setting
- Extract issueSessionCookie helper (DRY across 5 JWT signing sites)
Frontend:
- Replace Select with Combobox in UsersSection (design system compliance)
- Add strokeWidth={1.5} to Lucide icons
- Hide password fields for SSO-provisioned users
Testing:
- Add 42-test RBAC suite covering user CRUD, token versioning, scoped
assignments, permissions endpoint, password management, seat limits,
last-admin protection, and orphan cleanup
Extract duplicated snapshot capture functions from index.ts and
SchedulerService.ts into a shared module (snapshot-capture.ts). Fix
audit log route patterns that used singular 'snapshot' instead of
plural 'snapshots'. Apply design system to FleetSnapshots component
(card styling, strokeWidth, font-mono, tabular-nums, ScrollArea).
Add safePage pagination, loading toast for creation, and fix the
restore dialog race condition with a controlled AlertDialog. Add
diagnostic logging gated behind Developer Mode. Add tests for
restore endpoint, admin role enforcement, and edge cases. Update
docs with troubleshooting section and refresh screenshots.
* fix(scheduler): clean up stale runs on startup and auto-disable invalid cron tasks
- Add markStaleRunsAsFailed() bulk DB method with status index
- Clean up orphaned 'running' records on scheduler startup
- Auto-disable tasks when cron expression becomes invalid at execution time
- Promote CRUD debug logs to standard logs for scheduled task admin actions
- Add diagnostic logging for task pre-checks, action timing, and prune fallback
* refactor(scheduling): extract shared types and fix design system violations
- Extract ScheduledTask, TaskRun, NodeOption to shared types file
- Extract getCronDescription and formatTimestamp to shared utilities
- Fix formatTimestamp falsy-zero null check
- Tighten last_status type to 'success' | 'failure' | null
- Add strokeWidth={1.5} to all action icons per design system
- Add sr-only DialogDescription for accessibility
- Wrap Sheet run history in ScrollArea
- Fix delete button styling to match design system pattern
- Change manual trigger toast from "executed" to "triggered"
* test(scheduler): add tests for snapshot, remote update, stale cleanup, and cron invalidation
- Add stale run cleanup tests (bulk markStaleRunsAsFailed, logging)
- Add cron invalidation test (auto-disable, error message)
- Add executeSnapshot tests (fleet capture, empty stacks)
- Add executeUpdateRemote tests (proxy success, remote error)
- Document stale run cleanup and cron auto-disable in troubleshooting docs
* fix(scheduler): harden auto-update policies with cascade deletes, error reporting, and UI fixes
- Fix orphaned task runs on policy/node deletion with transaction-wrapped cascade deletes
- Make manual trigger non-blocking (202 Accepted) to prevent proxy timeouts
- Distinguish registry check failures from clean "no update" results via structured ImageCheckResult
- Trim whitespace-only policy names in both frontend and backend validation
- Add strokeWidth={1.5} to action icons per design system
- Add sr-only DialogDescription for Radix accessibility
- Replace Select with Combobox for frequency picker
- Wrap run history sheet content in ScrollArea
- Support concurrent Run Now indicators via Set-based state
- Abort stale stack fetches on node switch with AbortController
- Add standard and diagnostic logging to SchedulerService and ImageUpdateService
- Add tests for cascade deletes, image checking, and scheduler edge cases
- Add troubleshooting section to auto-update docs
* fix(tests): resolve lint errors in image-update-service tests
Remove unused mock variables (mockGetImage, mockGetDocker) and unused
ImageCheckResult type import. Replace CommonJS require('yaml') with
ESM import to satisfy no-require-imports rule.
* chore(deps): bump Docker CLI to 29.4.0 and Compose to v5.1.2
Resolves Trivy CVE-2026-32282 (Go stdlib symlink follow in Root.Chmod)
by upgrading to releases that ship Go 1.25.9. Compose v5.1.2 also bumps
grpc to 1.80.0, resolving CVE-2026-33186.
* chore(security): accept CVE-2026-32282 in .trivyignore, update stale refs
Go stdlib symlink-following in Root.Chmod (CVE-2026-32282) affects both
Docker CLI 29.4.0 (Go 1.26.1) and Compose v5.1.2 (Go 1.25.8). Fix
requires Go 1.25.9 or 1.26.2; no upstream static binary ships a patched
runtime yet. The vulnerable code path requires a chroot context with
attacker-controlled filesystem, which does not apply to our usage.
Also updates version references from v5.1.1/v29.3.1 to v5.1.2/v29.4.0
for existing CVE entries, and notes that Compose v5.1.2 resolved
CVE-2026-33186 (grpc bumped to 1.80.0) for the compose binary.
- Extract log parsing utilities (normalizeContainerName, parseLogTimestamp,
detectLogLevel, stripControlChars, demuxDockerLog) to shared module,
eliminating duplication between polling and SSE endpoints
- Fix timestamp regex to accept timezone offsets (+HH:MM/-HH:MM), not just Z
- Replace any[] with typed GlobalLogEntry interface
- Add SSE heartbeat (30s) to prevent reverse proxy timeouts
- Add X-Accel-Buffering: no header for nginx SSE compatibility
- Replace swallowed catch blocks with console.warn diagnostics
- Fix SSE not reconnecting on node switch (missing dep in effect array)
- Add settings change event so devMode/pollRate updates apply without remount
- Add log level filter (ALL/ERROR/WARN/INFO) to toolbar
- Replace overflow-auto div with ScrollArea for design system compliance
- Add strokeWidth={1.5} to all toolbar icons
- Include stack name in download format
- Surface fetch errors with inline banner
- Add 39 unit tests for all log parsing utilities
- Update docs with level filter documentation and refreshed screenshot
Add authMiddleware to all 13 fleet endpoints that were previously
accessible without authentication. Add NaN validation for parseInt
params, stackName validation on snapshot restore, and description
length cap on snapshot creation. Clean up updateTracker entries on
node deletion to prevent memory leaks.
Replace hardcoded colors with design system tokens, swap Select for
Combobox, replace overflow-y-auto with ScrollArea, fix card styling
(shadow-card-bevel, border tokens). Fix stale container data by
always refetching on stack expand with a loading guard against
concurrent requests.
Add operational logging for state-changing fleet operations and
diagnostic logging gated behind Developer Mode. Add 20 fleet tests
covering auth enforcement, input validation, tier gating, and
snapshot CRUD lifecycle.
Show a pulsating warning dot next to host ports that are already in
use by a running container. Hovering over the dot reveals which
Sencho-managed stack or external app occupies the port.
Adds GET /api/ports/in-use endpoint that returns a map of bound host
ports with ownership info, and a getPortsInUse method on
DockerController that reuses the existing container-to-stack
resolution logic.
* fix(app-store): handle orphaned stack directories on template deploy
When a stack deployed via the App Store is later removed through Docker
Desktop or the CLI (instead of through Sencho), its directory remains on
disk without a compose file. The deploy endpoint previously rejected any
re-deploy with a 409 if the directory existed, even if empty.
Now the endpoint checks for a compose file before rejecting. If the
directory exists but contains no compose file, it is treated as an
orphaned remnant: cleaned up automatically and the deploy proceeds.
Also makes FileSystemService.hasComposeFile public so the deploy
endpoint can reuse it instead of duplicating the compose file check.
* docs(app-store): document orphaned stack directory cleanup behavior
- Sanitize error messages in all delete/prune/create/inspect endpoints
to prevent Docker internals from leaking to the frontend
- Add CIDR, IPv4, and Docker resource ID input validation
- Add requirePaid gate to network topology endpoint
- Add invalidateNodeCaches after image/volume/network mutations
- Fix design system violations: card borders, destructive button variant,
visible DialogDescription, overflow-auto replaced with ScrollArea,
hardcoded Tailwind colors replaced with tokens
- Gate purge button behind isAdmin to prevent silent 403s
- Fix shared inspect loading state to be per-network-row
- Parse error response bodies for meaningful toast messages
- Add clipboard API fallback for non-HTTPS contexts
- Render Options section in network inspect sheet
- Add operational and diagnostic logging for resource operations
- Extend validation and DockerController test suites
- Update docs with Options field in network inspect
* fix(app-store): harden App Store with auth, validation, bug fixes, and design compliance
Add authMiddleware to GET /api/templates and POST /api/templates/deploy
endpoints. Add isValidStackName and isPathWithinBase checks to the deploy
endpoint. Replace fs.existsSync with async fsPromises.access. Extract
FileSystemService to local variable to avoid repeated getInstance calls.
Fix template mutation bug where PUID/PGID/TZ duplicated on re-open by
working on a copy instead of mutating state. Make env_file conditional
in generated compose YAML (only when env vars exist). Add port validation
(range 1-65535) with visual feedback and deploy blocking. Add env key
collision warning toast for custom variables.
Replace any types with proper LSIO API interfaces. Change catch types
from any to unknown with getErrorMessage. Add structured logging with
[Templates] prefix and diagnostic logging gated behind Developer Mode.
Align with design system: remove hardcoded bg-white and text-red-500,
use ScrollArea, fix destructive button variant, add tabular-nums and
strokeWidth 1.5, use cn() for conditional classes. Add empty-registry
state distinct from no-search-results.
Add 16 unit tests for TemplateService covering compose generation,
conditional env_file, env string generation, and cache clearing.
Update App Store docs with port validation and env collision details.
* refactor(app-store): remove unused interface exports
Remove export keyword from interfaces that are only used within their
own file: TemplateEnv, TemplateVolume, and TemplatesResponse in
TemplateService.ts; TemplateEnv and Template in AppStoreView.tsx.
No external consumers import these types.
* fix(app-store): remove unused fs default import
The fs.existsSync call was replaced with fsPromises.access in the
deploy endpoint, leaving the fs default import unused. Remove it
to fix the ESLint no-unused-vars error in CI.
* fix(stacks): harden stack management with security fixes, validation alignment, and logging
Validate WebSocket stack names with isValidStackName() to close a
path-traversal gap on the /api/stacks/:stackName/logs WS endpoint.
Align POST /api/stacks to use the canonical validator (allows underscores).
Replace error: any catch blocks with error: unknown + type narrowing.
Add cache invalidation to PUT /api/stacks/:stackName/env.
Rename DELETE param from :name to :stackName for consistency.
Add standard [Stacks] lifecycle logs and diagnostic [Stacks:debug] logs
gated behind the Developer Mode toggle (with 5s TTL cache).
Extract shared isDebugEnabled() and getErrorMessage() utilities.
Frontend: roll back optimistic status on API failure, guard unsaved
changes when switching stacks, pre-check duplicate names in App Store.
* docs(settings): update Developer Mode description to mention debug diagnostics
ImageUpdateService previously discovered stacks by iterating Docker
containers, which meant stacks without containers (e.g. after
docker compose down) were silently excluded from update checks.
Switch to a hybrid discovery approach: enumerate stacks from the
filesystem via FileSystemService.getStacks(), parse compose files
for image refs with .env variable resolution, then augment with
container-based image discovery for running stacks.
Also cleans up stale stack_update_status entries when stacks are
deleted or no longer exist on disk, and replaces the plain
update-available tooltip with an animated cursor follow pattern.
* feat(dashboard): drop CPU column and relative timestamp from Stack Health and status bar
The Stack Health table's CPU column duplicated data already surfaced in
the top ResourceGauges and the CPU Usage historical chart. The health
status bar's 'just now' timestamp was cosmetic: no consumer relied on
lastUpdated state for polling, staleness detection, or conditional
rendering. Removing both tightens the dashboard and eliminates a dead
prop chain through useDashboardData.
* refactor: remove dead admin_email field from setup flow
The Setup form captured an admin email under 'Used for license recovery.
Never shared with third parties.' but the value was written to
global_settings and read nowhere: no license recovery, SMTP, or support
contact flow consumed it. Rather than building UI on top of the dead
field, delete the input, the payload key, and the backend persistence.
Any orphaned row from prior setups is harmless and the frontend ignores
unknown settings keys.
* feat(settings): use Radix ScrollArea with per-section scroll memory
Settings Hub used a native-scroll div that snapped to the top every time
the user switched subsections and exposed the default browser scrollbar.
Wrap the nav and content panes with the shadcn ScrollArea (Radix under
the hood, type='hover') and expose a viewportRef so the modal can stash
each section's scrollTop in a ref and restore it via useLayoutEffect on
switch. Style the thumb with translucent foreground tokens so it reads
as glass against popovers and dialogs. Replaces a hand-rolled scroll
hook and ad-hoc CSS utility.
The "Updating Sencho..." overlay used to dismiss prematurely while the
image pull was still running, after which the local node card would get
stuck in "updating" and eventually surface a generic "Timed Out" error
while the container remained on the old version.
Three root causes are addressed:
1. The image pull was synchronous (`execFileSync`), which blocked the
Node event loop. The overlay's health probe saw the server come back
the moment the pull finished and reloaded the page, even though the
container had not restarted yet. The pull is now async via
`promisify(execFile)`, so /api/health and /api/fleet/update-status
keep serving throughout.
2. The overlay reloaded on the first 200 from /api/health regardless of
whether the underlying process had actually restarted. /api/health
now exposes the gateway boot timestamp, and the overlay captures it
pre-update and only reloads when it observes a different value. A
wasOffline-then-online fallback handles the case where the pre-update
fetch failed.
3. Helper container spawn errors from `docker run` were silently
discarded, so a failed compose recreate never surfaced anywhere.
Errors are now captured into `lastUpdateError` via the execFile
callback and surfaced through the existing /api/fleet/update-status
error path.
A 3-minute early-fail heuristic on the local node block surfaces a clear
failure message when the helper fails silently, instead of waiting the
full 5-minute timeout for an unknown failure.
Replaces five ad-hoc in-process caches (project name map, templates, latest
version, fleet update status, remote node meta) with a single internal
CacheService that provides TTL, inflight-promise deduplication to protect
against thundering herd, stale-on-error fallback, and per-namespace
hit/miss/stale/size counters for observability.
Wraps the hot-path dashboard endpoints in the cache with write-path
invalidation: /api/stats (2s), /api/system/stats (3s), and
/api/stacks/statuses (3s). Keys are namespaced by nodeId so switching nodes
never serves another node's data. Every route that mutates container or
stack state calls invalidateNodeCaches(nodeId), which also drops the global
project-name-map, so user actions stay instantly reflected in the UI.
For /api/system/stats the cheap per-request network rx/tx block is kept
outside the cache so live-updating charts stay smooth while the expensive
systeminformation.currentLoad() CPU sample (~200ms) is reused across the
TTL.
Adds admin-only GET /api/system/cache-stats returning per-namespace
counters for operators who want to observe cache effectiveness.
Enables the compression middleware site-wide for JSON responses. Large
payloads like /api/templates shrink roughly 5x on the wire. SSE endpoints
are explicitly excluded via a Content-Type filter so live log tails and
metric streams are not buffered.
Bumps vitest hookTimeout to match testTimeout (15s) so parallel fork
workers do not hit the default 10s hook limit under CPU contention.
Adds 35 new tests (26 unit for CacheService, 9 integration for cached
endpoints) covering TTL expiry, inflight dedup, stale-on-error,
namespace invalidation, entry-cap safety guard, and write-path
invalidation end-to-end through Express routes.
* fix(fleet): add Docker Hub fallback for version detection on private repos
The GitHub Releases API returns 404 for private repos, causing the
latest version fetch to silently fail and fall back to the gateway's
own version (defeating the update detection fix from PR #454).
Now tries GitHub first, then falls back to Docker Hub tags API which
is always public. Adds console.warn logging on fetch failures per
Directive 7.
* ci: trigger CI re-run
* fix(api): add tiered rate limiting to prevent polling lockouts
Replace the single global rate limiter (100 req/min/IP) with a tiered
system that separates high-frequency polling endpoints from standard
API traffic:
- Polling tier (300/min): /stats, /system/stats, /stacks/statuses,
/metrics/historical, /health, /meta, /auth/status, /auth/sso/providers,
/license. Exempt from the global limiter but governed by their own
safety net to prevent resource exhaustion.
- Standard tier (200/min): All other endpoints, raised from 100.
- Webhook tier (500/min): POST /webhooks/:id/trigger, dedicated limiter
for CI/CD platforms sharing datacenter IPs.
- Auth tier: Unchanged (5-10 attempts / 15 min).
Enterprise adaptations:
- Authenticated requests keyed by user session (JWT sub/username) instead
of IP, preventing shared NAT/VPN environments from pooling budgets.
- Internal node-to-node traffic (node_proxy tokens) bypasses all rate
limiters entirely.
Includes comprehensive stress tests (21 cases) validating tier
separation, node proxy bypass, and per-user keying.
* chore(deps): bump axios to 1.15.0 to fix SSRF vulnerability
Addresses GHSA-3p68-rc4w-qgx5 (NO_PROXY hostname normalization bypass).
* 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.
Replace N+1 Docker API calls (inspectNetwork per network) with a
container-centric approach that fetches all networks and containers
in 2 parallel calls, then maps relationships in memory.
Add dagre auto-layout algorithm for hierarchical DAG visualization,
replacing the static two-row layout that caused edge spaghetti at scale.
Add "Show system networks" toggle, enrich container nodes with running
state indicators, stack badges, and base image names. Clicking a
running container opens its log viewer directly from the topology graph.
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
The gateway's console-token fetch to remote nodes was missing the
x-sencho-tier and x-sencho-variant headers. Without them, the remote's
requireAdmiral guard fell back to its own local license (Community),
rejecting the request with 403 and surfacing as a 502 "Connection error"
in the browser.
Additionally, the remote's WS upgrade handler for host-console checked
only the local LicenseService instead of respecting proxy tier headers
on console_session tokens. Both paths now propagate and honour proxy
tier headers consistently.
* fix(schedules): filter auto-update policies from Scheduled Operations view
Auto-update policies (action=update) were appearing in both the
Auto-Update tab and the Scheduled Operations tab. Added server-side
action/exclude_action query params to GET /api/scheduled-tasks.
ScheduledOperationsView now requests ?exclude_action=update and
AutoUpdatePoliciesView requests ?action=update, so each view only
shows its relevant tasks.
* fix(schedules): use typeof guard for query param type safety
Express can parse ?action[]=x as an array. Use typeof === 'string'
guard instead of unsafe type assertion.
* 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): backward-compatible tier/variant enforcement and self-healing variant detection
Accept legacy tier ('pro') and variant ('personal', 'team') names from older
remote nodes, normalizing them to current values ('paid', 'skipper', 'admiral')
in authMiddleware. This fixes distributed license enforcement failing between
v0.38.3 and v0.38.0 nodes due to the tier rename in v0.38.1.
Also fixes:
- Self-healing getVariant() that cross-checks stored variant_type against
product/variant name metadata on every call, correcting stale cached values
from previous buggy resolution logic
- Unguarded API responses in ResourcesView causing potential t.map crashes
- Fleet update status now polls on a 120s interval (was only fetched on mount)
- fetchRemoteMeta failures now logged for diagnosability
* fix(fleet): resolve remote node capability detection failures
Exempt /api/meta and /api/health from the global rate limiter so
capability fetches are never blocked by proxied traffic. Add
backend-side caching (3-min TTL) with stale-while-revalidate to
absorb transient failures. Shorten frontend failure cache to 30s
for faster recovery. Evict meta cache on node deletion.
Accept legacy tier ('pro') and variant ('personal', 'team') names from older
remote nodes, normalizing them to current values ('paid', 'skipper', 'admiral')
in authMiddleware. This fixes distributed license enforcement failing between
v0.38.3 and v0.38.0 nodes due to the tier rename in v0.38.1.
Also fixes:
- Self-healing getVariant() that cross-checks stored variant_type against
product/variant name metadata on every call, correcting stale cached values
from previous buggy resolution logic
- Unguarded API responses in ResourcesView causing potential t.map crashes
- Fleet update status now polls on a 120s interval (was only fetched on mount)
- fetchRemoteMeta failures now logged for diagnosability
Rename internal variant values from 'personal'/'team' to 'skipper'/'admiral',
aligning code with user-facing tier names. Variant type is now resolved once
at activation/validation and stored in DB via license_variant_type, instead
of string-matching the Lemon Squeezy variant_name on every read. Also captures
variant_id for future lookups. Pre-existing installs auto-migrate on first
getVariant() call.
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(stacks): per-stack action tracking, optimistic status, and bulk status endpoint
Replace global loadingAction mutex with per-stack tracking so users can
fire actions on multiple stacks concurrently. Add optimistic status
updates to fix sidebar showing "--" after stop/start. Add bulk
GET /api/stacks/statuses endpoint using a single docker.listContainers
call instead of N docker compose ps invocations (~21s → ~110ms for 3
stacks). Falls back to per-stack queries for remote nodes on older
versions.
* fix(stacks): remove stale 'start' action check from deploy button label
* 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.