mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
docs/tutorials-batch-1
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fcff8e9047 |
fix(monitor): collapse repeated host-metric alerts into per-window summary (F-11) (#1175)
* fix(monitor): collapse repeated host-metric alerts into per-window summary (F-11) A host metric over threshold previously dispatched one notification every 5 minutes for the duration of the breach, producing 7+ identical messages in 35 minutes and spamming Discord/Slack routes. Replace the hardcoded 5-minute cooldown for CPU/RAM/disk with a per-metric suppression window (default 60 min, configurable via host_alert_suppression_mins). The first breach fires immediately; subsequent cycles within the window are silently counted; the next dispatch after the window elapses carries a summary suffix listing how many cycles were suppressed and when the breach first crossed threshold. Recovery clears the counter so re-breach fires fresh. The pattern mirrors PolicyEnforcement.notifyTrivyMissingOnce: module-scope Map, in-memory only, in-cycle dedup, with a test-reset helper. The existing system_state row keeps post-restart re-fires bounded. Janitor and per-stack alert rules are unchanged; they already have adequate cadence and per-rule cooldown respectively. * fix(ci): restore backend and frontend checks * fix(e2e): remove create button timing race * fix(e2e): harden create double-click test * fix(monitor): clear persisted F-11 timestamp on recovery + clamp suppression window Independent audit on the previous commit surfaced two issues. 1. clearHostMetricSuppression early-returned on missing in-memory state, leaving a stale system_state.last_host_*_alert_ts row alive after a process restart. Scenario: breach fires + persists timestamp, process restarts, metric recovers before another evaluate cycle re-seeds the in-memory Map, recovery cleanup early-returns. Next re-breach inside the original window hits the restart-survivability branch and is silently suppressed instead of firing fresh. Fix: read persisted state in clearHostMetricSuppression and reset to '0' independently of in-memory presence. The read-before-write also skips redundant writes when the row is already cleared. 2. host_alert_suppression_mins is validated by zod on the bulk PATCH path but the single-key POST /api/settings path accepts allowlisted keys without re-validation. A 999999999-minute value would silence host alerts for centuries. Add MAX_HOST_ALERT_SUPPRESSION_MIN = 1440 mirroring the zod max, and clamp via Math.min in evaluateGlobalSettings. Two new vitest cases (restart-then-recovery-then-rebreach; the 1440 clamp) confirmed failing before the fix, passing after. The existing "metric drop" case updated to use a mock-backed persistence pattern consistent with the new restart-scenario tests. 73/73 monitor-service tests green; full backend suite 2507/2510 (same pre-existing Windows EBUSY flake on filesystem-backup.test.ts as baseline). |
||
|
|
9d8d8abcba |
fix(stacks): close dialog and toast on Empty create (F-2) (#1168)
* fix(stacks): close dialog and toast on Empty create (F-2)
The Empty branch of the Create Stack dialog left no visual confirmation
on a successful POST: no success toast, no busy state on the Create
button, and no double-click guard. The first thing a new operator does
is create a stack, so a click that looks like a no-op is the day-one
impression killer.
What changed
- Empty handler now mirrors the Git and docker-run patterns: a
creatingEmpty busy guard, Loader2 spinner on the Create button,
disabled Cancel mid-flight, and a "Stack <name> created." success
toast fired synchronously before the editor-load handoff.
- The Empty panel is wrapped in a form so the Enter key submits the
same path as clicking Create.
- Mapped 403 to a clearer permission message; fall back to the
backend's error string for other non-OK statuses.
- Capture the active node id at handler entry and pass it through
onStackCreated. The parent compares against the live node ref and
skips the editor load if the user switched nodes mid-create, with
an info toast pointing at the prior node.
- Tightened the create-a-new-stack E2E (no more silent .catch on the
dialog-close assertion, asserts the toast, no longer relies on a
page reload to see the sidebar entry) and added a regression spec
that the busy guard collapses double-clicks into a single POST.
* fix(stacks): close double-click race in create handler (F-2)
The setState-based busy guard could race a second click that landed
before React committed the disabled state, so two POSTs got dispatched
when a user double-clicked Create. The new useRef-based check is
read and written synchronously inside the handler so a re-entrant
invocation bails before issuing a second fetch.
The accompanying E2E (`create dialog: double-clicking Create fires
only one POST`) was also hanging to its 30s test timeout: the second
click's locator resolution could outlive the dialog when the first
POST resolved quickly, so Playwright waited for `[role="dialog"]` to
reappear. Both clicks now fire in the same microtask via Promise.all
with a 1s timeout on the second click so the locator-resolution path
fails fast instead of hanging the test.
* fix(stacks): keep busy guard active across modal close (F-2)
Two issues from independent review.
1. Important: resetting creatingEmpty / creatingEmptyRef inside the modal
close handler created an Escape/backdrop race. A user could close the
dialog mid-flight (clearing both flags), reopen, click Create again,
and slip past the guard before the first POST settled. The finally
block already owns that lifecycle, so the close handler no longer
touches either flag.
2. Nit: bring 403 messaging to parity with the Git branch. The Empty
handler now reads the backend's error field first and falls back to
the original hardcoded copy if it is missing, mirroring how
handleCreateStackFromGit surfaces backend permission errors.
* test(stacks): rebuild double-click spec around observable disabled-state (F-2)
The Promise.all racing approach to "double-click fires only one POST"
hung to the 30s test timeout on CI. The page snapshot at timeout confirms
the product fix works correctly (dialog closed, stack created, editor
navigated); the test itself was the flake. Race-based assertions on a
single React-state commit boundary are inherently timing-sensitive.
The rewrite turns the test inside-out:
- The route handler holds POST responses for 400ms so the in-flight
window is large enough to observe deterministically.
- await route.continue() (instead of void fire-and-forget) closes a
subtle stall pattern Playwright's docs flag under load.
- After the first click the test asserts the button reaches a disabled
state within the in-flight window, which proves the busy guard
activated at the React-state layer.
- A best-effort force-click against the now-disabled button exercises
the user-mash path; the synchronous useRef guard inside the handler
catches it whether or not the browser dispatches the click.
- expect(postCount).toBe(1) still owns the actual regression assertion.
No product code change. The synchronous creatingEmptyRef guard already
landed in
|
||
|
|
801a098a5b |
feat(files): per-stack file explorer (#780)
* feat(files): backend foundation for stack file explorer
Install multer for multipart file upload handling. Add
isValidRelativeStackPath to validation.ts to guard client-supplied
relative paths against traversal, absolute paths, NUL bytes, backslash
injection, and double-slash segments. Add isBinaryBuffer to a new
binaryDetect.ts utility for heuristic text/binary detection via
NUL-byte fast exit and non-printable byte ratio sampling.
* fix(files): reject bare dot segments in isValidRelativeStackPath
* feat(files): add safe stack-scoped file I/O methods to FileSystemService
Adds FileEntry interface and seven new public methods to FileSystemService
for stack-scoped file operations: listStackDirectory, readStackFile,
streamStackFile, writeStackFile, deleteStackPath, mkdirStackPath, and
statStackEntry.
Each method routes through a private resolveSafeStackPath helper that
enforces two-phase path containment: a pre-realpath lexical check plus a
post-realpath symlink-escape check. ENOENT targets are handled by walking
up to the deepest existing ancestor, realpaths that ancestor, and
reattaching the remaining suffix.
Binary detection delegates to isBinaryBuffer; path safety delegates to
isPathWithinBase. Protected file names and the MIME map are module-level
constants to avoid repeated allocation.
* feat(files): frontend API wrappers and Monaco language helper
* fix(files): tighten stackFilesApi error handling and localOnly support
* fix(files): FileSystemService safety and correctness fixes
* feat(files): add file explorer API endpoints to stacks router
* feat(files): FileTree and FileTreeNode components
* fix(files): route security hardening and stream cleanup
* fix(files): FileTree accessibility, icon stroke, stale fetch guard
Add strokeWidth={1.5} to all Lucide icons in FileTreeNode to match the
design system. Add aria-expanded to directory rows for accessibility.
Guard handleDirClick .then() callbacks against stale stack name
references when the component re-renders with a new stack. Add
toast.info fallbacks when compose.yaml or .env is clicked without a
navigation callback registered.
* feat(files): FileViewer, FileUploadDropzone, NewFolderDialog, DeleteFileConfirm
* fix(files): resolve code quality findings in file explorer components
- Move editorOptions useMemo above conditional returns in FileViewer (Rules of Hooks fix)
- Fix blob download: append anchor to DOM before click, defer URL revoke 100ms
- Keep protected-file confirm input visible during NOT_EMPTY recursive retry in DeleteFileConfirm
- Remove non-functional cursor-pointer/onClick from Community upgrade pill in FileUploadDropzone
- Add success toast on folder creation in NewFolderDialog
- Switch all (e as Error).message casts to instanceof Error narrowing
* test(files): unit tests for binary detection, stack path safety, and file explorer routes
- binary-detection.test.ts: covers isBinaryBuffer edge cases (empty, NUL,
PNG header, threshold boundary, sampleBytes parameter)
- filesystem-stack-paths.test.ts: covers isValidRelativeStackPath (accepts/
rejects matrix) and FileSystemService stack methods against a real temp dir
(listStackDirectory sort and protection flags, readStackFile text/binary/
oversized paths, writeStackFile/Buffer, deleteStackPath, mkdirStackPath,
traversal guard); platform-specific empty-dir/NOT_EMPTY cases skip on Windows
- stack-files-routes.test.ts: route-level integration tests for all seven
file explorer endpoints; covers auth gating, Community-tier 403 gates,
input validation, 413 TOO_LARGE upload limit, and 204/200 happy paths
* feat(files): StackFileExplorer container with lazy tree, viewer, and action bar
* fix(files): add Download button to explorer toolbar, fix Community upgrade pill, reset state on stack change
* test(files): add missing test coverage for file explorer routes and service
* feat(files): add Files tab to EditorLayout with StackFileExplorer integration
* fix(files): add defensive activeTab guard to saveFile and discardChanges
* test(files): unit tests for FileTree expand/collapse and FileViewer render modes
Covers the three FileViewer content modes (text/Monaco, binary panel,
oversized panel) and the FileTree expand/collapse/cache cycle: first
expand fetches the subdirectory, second click collapses without a fetch,
third click re-expands from the in-memory cache without a second fetch.
* test(e2e): file explorer community and skipper+ flows
Covers the full file-explorer feature surface in two describe blocks:
Community (read-only): intercepts /api/license to simulate community
tier, confirms the upgrade pill is visible in the left pane, and
asserts that the Save button is absent after opening a text file.
Skipper+ (full CRUD): uploads a text file and confirms it appears in
the tree; edits config/app.conf and saves via Monaco; deletes an
uploaded file and asserts the tree entry is gone; issues a raw HTTP
request to the download endpoint and checks for status 200 and the
content-disposition: attachment header.
Also adds data-testid="file-action-delete" to the action bar Delete
button in StackFileExplorer for stable targeting, and exports
waitForStacksLoaded from e2e/helpers.ts to eliminate the three
identical local copies in stacks, deploy-log-panel, and stack-files
spec files.
* fix(e2e): improve test isolation and selector stability in stack-files spec
Move beforeEach seed to beforeAll/afterAll so fixtures are created once per
suite, not before every test. Extract shared seedSuite/teardownSuite helpers
to eliminate the duplicate beforeAll/afterAll blocks. Wrap teardown in
try/catch so failures log a warning rather than masking test results.
Replace waitForTimeout(500) with a deterministic expect on the file tree
sentinel. Add data-testid="anatomy-files-btn" and data-testid="delete-confirm-btn"
to replace the fragile button text/positional selectors. Assert Save button
starts disabled before editing.
* docs(files): add stack file explorer documentation
Add user-facing guide for the stack file explorer feature covering
tier access (Community read-only, Skipper+ read-write), viewing
limits, upload/download caps, protected file routing, and
troubleshooting. Update the editor page to reference the new guide
and register the page in the navigation.
* fix(docs): use canonical Skipper tier name in file explorer overview card
* fix(files): resolve lint errors blocking CI
Remove unnecessary backslash escape before double-quote in the
Content-Disposition regex (no-useless-escape). Replace five synchronous
setState resets at the top of the FileTree mount effect with a React key
prop on the FileTree element in StackFileExplorer so remounting resets
state automatically, eliminating the react-hooks/set-state-in-effect
violation.
* test(files): fix e2e seeding to work on community-tier CI
Replace the browser-side paid upload/mkdir API calls in seedTestStack with
direct Node fs writes. The upload and folder endpoints require Skipper+ so
they returned 403 on CI, which runs with no license set. Stack creation
via POST /api/stacks stays as an API call since it is community-allowed and
keeps the backend registry in sync.
Add a per-test tier check in the Skipper+ beforeEach that skips gracefully
when the instance is community, matching the pattern in auto-heal-policies.
|
||
|
|
82aabfe64c |
feat(stack-view): identity header with health state and action hierarchy (#688)
* feat(stack-view): identity header with health state and action hierarchy Redesign the stack view header around three questions: what is this, is it healthy, what does it do. Surface Docker healthcheck state, primary image tag, and image digest; group actions by frequency. - Backend: extend /api/stacks/:name/containers with healthStatus (healthy / unhealthy / starting / none), Image, and ImageID by inspecting each container in parallel. - Frontend: replace flat CardHeader with breadcrumb, italic serif title, colored state pill with pulse, and a mono image/digest line with a one-click copy button for the full digest. - Frontend: action hierarchy - primary cyan Restart/Start, outline Stop and Update, and an overflow menu for Rollback, Scan config, and Delete. - Docs: new Stack header section and updated controlling-a-running-stack tables showing primary/secondary/overflow grouping. * test(e2e): open overflow menu to reach stack Delete action Destructive actions now live under the stack toolbar overflow menu rather than as a flat top-level button, so the delete flow must click More actions before selecting the Delete menu item. |
||
|
|
32a7d53b2b |
feat: RBAC, atomic deployments, fleet backups, and licensing (Pro) (#185)
* 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 |
||
|
|
9ba9a3a456 |
fix(e2e): wait for sidebar stacks to finish loading before assertions (#149)
* feat: add license gating system with Lemon Squeezy integration Add Community/Pro tier infrastructure: - LicenseService singleton with Lemon Squeezy license API integration - /api/license endpoints (GET info, POST activate/deactivate/validate) - 14-day Pro trial activated automatically on first boot - 72-hour periodic validation with 30-day offline grace period - LicenseContext provider for frontend tier awareness - License settings tab with activation UI and status display - ProBadge and ProGate reusable components for feature gating - requirePro per-route guard for backend Pro-only endpoints - Proxy bypass for /api/license routes (local-only, never proxied) * feat: add user profile dropdown and reorganize top navigation - Create UserProfileDropdown component with settings, billing, theme toggle (System/Light/Dark), documentation links, and logout button - Remove logout button from sidebar header - Remove standalone settings button from top bar - Move theme toggle from Settings modal to profile dropdown - Inject app version via Vite define from root package.json - Add globals.d.ts for __APP_VERSION__ type declaration * refactor(settings): remove appearance tab from settings modal Theme toggle was moved to the User Profile Dropdown in the previous commit. Remove the now-redundant Appearance section, its nav button, and the unused theme/setTheme props from SettingsModal. * feat: add fleet view dashboard and about settings section Fleet Overview: aggregates all nodes into a card grid showing status, container counts, CPU/RAM/disk usage bars. Pro tier unlocks stack drill-down with auto-refresh (30s). Backend endpoints /api/fleet/overview and /api/fleet/node/:nodeId/stacks query nodes in parallel. About section in Settings: displays version, license tier, status, instance ID, and links to docs/changelog/issues. Sidebar perf fix: stack status fetches now run in parallel via Promise.allSettled instead of sequential for-loop, significantly reducing load time for nodes with many stacks. Also removes version number from User Profile Dropdown (now in About). * fix(ci): resolve Docker build and E2E test failures - Copy root package.json into frontend build stage so vite.config.ts can read the app version during Docker multi-stage build. - Update auth E2E test: logout button moved into User Profile Dropdown. - Update nodes E2E test: Settings button moved into User Profile Dropdown. * fix(e2e): wait for sidebar stacks to finish loading before assertions The create-stack test raced against the async refreshStacks() fetch — the "Create Stack" button renders immediately but the stack list is still loading. Added data-stacks-loaded attribute to the CommandList so E2E tests can reliably wait for the fetch to complete. |
||
|
|
14c24c8245 |
fix(e2e): fix stacks timeout and nodes skip in CI
stacks.spec.ts: - waitForStacksLoaded: wait for 'Create Stack' button instead of [cmdk-item] > 0 CI starts with empty COMPOSE_DIR so there are no stacks; the button is always rendered in the sidebar regardless of whether any stacks exist nodes.spec.ts: - beforeEach: click Settings then 'Nodes' nav item to reach NodeManager The Add Node button lives inside Settings → Nodes; the previous beforeEach only clicked Settings but never navigated to the Nodes section, so the add-node button was never found and tests silently skipped |
||
|
|
f7471a1a18 |
fix(e2e): get all E2E tests passing and fix AlertDialog crash on delete
- 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 |
||
|
|
ce50db0fde |
security: pre-release hardening, automated testing, and production readiness
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
|