The E2E suite runs serially against one shared admin account, so a spec that
leaves server-side state dirty breaks every spec scheduled after it. The
two-factor spec was the worst offender: it enrols, consumes backup codes, then
disables, and its cleanup only ran when the block had captured both a secret
and two unused backup codes, with every error swallowed. A test failing
mid-chain destroys exactly that state, so MFA stayed enrolled and each later
login stopped at the challenge screen, paying the full readiness timeout three
times per test with retries until the job hit its ceiling.
Playwright also retries a serial block as a whole in a new worker process, so
the retry could not re-enrol either: the enrol endpoint answers 409 for an
already-enrolled account and the group failed again.
Reset the enrolment from beforeAll and afterAll through the existing emergency
CLI, which is the only path that does not require possession of a second
factor. The block is now self-healing on a retry and cannot leak state.
Two supporting changes:
- loginAs now recognises the challenge screen and a login that never left the
sign-in form, and names the cause instead of timing out. A cascade costs
seconds per test and says what to do about it.
- The suite bounds itself in CI, so a cascade fails the test step while the job
is still alive to upload the report. A job cancelled by its own timeout runs
no further steps, which is why the previous runs produced no evidence at all.
The job ceiling is raised above that bound as a backstop.
* ci: shard E2E, skip docs-only PRs, and drop the inert visual-regression gate
The visual-regression workflow never had committed baselines, so every run
skipped its comparison and passed. Remove it with its spec and Playwright
project.
E2E now runs as four parallel shards, each with its own app and database,
and no longer waits on the backend job. A single aggregator job keeps the
required check name. CI retries drop from 2 to 1 so a real failure no longer
runs three times.
A change-detection job skips the build, test, and image jobs when a PR only
touches docs/, .claude/, .opencode/, or markdown. It fails closed: an empty
file list, a detection failure, or a rename out of code runs everything.
* test(e2e): create the test admin in global setup so specs do not depend on file order
Sharded runs start each shard on a fresh instance. A spec that logs in
through the API in beforeAll failed with 401 when it ran first, because the
admin was previously created by whichever earlier spec clicked through the
setup screen.
The E2E suite includes real-Docker deploy and health-gate specs whose
timing varies on shared CI runners (cold image pulls, gate observation
windows). With no retries configured, a single transient timing blip
failed the whole Playwright job. Enable 2 retries in CI only; local runs
stay at 0 for fast, deterministic feedback. The existing
trace: 'on-first-retry' now captures the retried attempt for triage.
* test(mobile): single-source map for per-view mobile treatment
Declare how every top-level view behaves on a phone in one place:
MOBILE_TREATMENTS is a Record<ActiveView, ...>, so adding a new view without
classifying it (bespoke / responsive / desktop-only / detail) fails the type
check. BESPOKE_MOBILE_VIEWS is derived from it instead of hand-maintained, and
a unit test keeps the two in lockstep and pins the current bespoke set so a
change is deliberate. EditorLayout consumes the derived set; behavior is
unchanged.
* ci(visual): run the desktop-unchanged gate in its own job
Split the visual-regression spec into a dedicated Playwright "visual" project,
excluded from the default chromium project the functional E2E job runs, so a
missing or platform-mismatched baseline can no longer fail every PR.
Add a Visual Regression workflow: a compare job gates PRs into main against
committed baselines (and skips with a warning until they are seeded), and a
manual seed job regenerates the baselines on the Linux runner and commits them
to a feature branch (refusing main). Baselines are platform-specific, so they
must be produced on the runner rather than locally.
Drop the stack-detail view from the gate: a fresh CI app has no stack to open
and its live log stream is not deterministic; the shell plus the four content
views still catch a desktop base-class regression.
The release-only `update-screenshots` job opened a `chore/refresh-screenshots`
PR and immediately tried to squash-merge it. Branch protection (1 review,
6 status checks) rejected the merge on every release, leaving an open PR
behind. Screenshots will instead be refreshed manually after UI changes.
Removed:
- The `update-screenshots` job from ci.yml (~57 lines).
- The `paths-ignore: docs/images/**` push trigger filter; its sole purpose
was to break the auto-merge re-trigger cascade. Its absence also fixes a
latent bug where docs-only pushes to main would have skipped sync-docs.
- Four `head_ref != 'chore/refresh-screenshots'` guards in other jobs.
- The "doc screenshots" mention in the skip-bot-PRs comment.
Reworked the screenshot capture spec to be opt-in:
- playwright.config.ts now defines two projects. The default `chromium`
project ignores screenshots.spec.ts; a separate `screenshots` project
matches it and is invoked manually.
- The e2e CI job runs `--project=chromium` so the screenshots project
cannot accidentally run in CI.
- Updated the spec's module comment with the new manual invocation.
Net: 86 lines removed, 27 added.
* 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
- Fix rate limiter to allow 100 attempts in dev mode so E2E tests are
not blocked by failed-login attempts during test development
- Simplify logout button selector to use Lucide icon class (lucide-log-out)
instead of the fragile Tooltip-content locator chain that broke on navigation
- Rewrite stacks E2E spec: use waitForFunction to wait for sidebar to load,
delete leftover stacks via browser-context fetch before creating, and use
page.reload() to get a clean sidebar state
- Fix AlertDialogContent: remove asChild+motion.div pattern that triggered a
React.Children.only crash — Radix AlertDialog.Content injects a second
DescriptionWarning child internally, breaking Slot when asChild=true; replace
with CSS keyframe animations (data-[state=open]:animate-in)
- Fix final assertion in delete test to use exact text + listbox scope to avoid
false positives from similarly-named stacks like e2e-test-stack-*
- All 6 E2E tests pass (4 auth + 2 stacks); node tests skip gracefully
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