mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 23:32:19 +00:00
35bb74425be6d906d13dc9a2fd67f7c8acd5b534
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
35bb74425b |
feat: guide missing external network creation during deploy (#1645)
* feat: guide missing external network creation during deploy Detect missing external networks before Compose runs, prompt or auto-create safe bridge networks, and keep unsupported declarations blocked with trusted deploy provenance. * test: align deploy context and settings fixtures with missing-network gate Update caller spies, EffResource expectations, StacksSection save keys, and git-source spy cleanup so CI matches the new deployStack context and auto-create setting. * fix: drop unused renderError binding in missing-network resolver Satisfies no-unused-vars so backend ESLint CI passes; callers already key only on model presence. * fix: use HTTP-safe clipboard helper in missing-network dialog navigator.clipboard fails on plain HTTP LAN hosts; route copy actions through copyToClipboard so Docker and Compose copy buttons work on self-hosted instances. * fix: simplify missing-network dialog actions and copy label Drop the Compose snippet escape hatch, move secondary actions under More, and rename the terminal copy action to Copy create command so the footer is a clear Cancel / Create decision. |
||
|
|
bb35c1bc92 |
feat: add sidebar update indicator toggle and Stack Health badge (#1570)
* feat: add sidebar update indicator toggle and Stack Health badge - Add image_update_sidebar_indicators setting (default off, node-scoped) - Gate the Updates filter chip and sidebar status indicators on the setting - Add "Update available" badge to Stack Health table (always visible) - Extend ImageUpdateStatus with sidebarIndicators boolean - Poll /api/image-updates/status alongside /detail in useImageUpdates - React to SENCHO_SETTINGS_CHANGED for instant toggle propagation - Reset sidebar state on node switch; generation-guard stale responses - Disable toggle when status is null (loading) or field is absent (old node) - Wire stackUpdates through ViewRouter → HomeDashboard → StackHealthTable - Update settings registry, operator docs, and sidebar/dashboard docs * fix: guard against stale node renders, memo drift, and cross-node error toasts - Track owning node ID in useImageUpdates state so React never renders node B with node A's data before the passive effect resets (P2) - Replace incorrect stackUpdates dependency with sidebarStackUpdates in chipFilteredFiles useMemo (P3) - Guard the error toast in handleSidebarIndicatorsChange so a stale PATCH failure from node A does not surface while viewing node B (P3) * fix: default sidebar update indicators to on (opt-out) The sidebar indicators are a safe convenience that most users want. Switching the default from off to on matches the opt-out convention used by prune_on_update, reclaim_hero, and health_gate_enabled. |
||
|
|
26d557a701 |
feat: purge scan data for deleted images and stacks (#1467)
Vulnerability scan rows were never cleaned up when their image was removed from Docker or their stack was deleted, so the Security Overview (including the Top exploit-risk findings card) kept surfacing findings for artifacts that no longer exist. Scan results now reflect what is still on the host: - Deleting a stack immediately purges its stack:<name> compose-config scan. - A background reconciliation in the monitor janitor removes scans whose image is gone from the node, or whose stack folder no longer exists. It is fail-safe: a scan is only removed when its artifact is positively known to be gone, the Docker image list is read with a timeout (skipped on failure), and stack scans are reconciled only when the stack list is non-empty. - An opt-out "Remove scans for deleted images and stacks" setting (on by default, per-node) lets operators retain scan history for removed artifacts. Scan deletes remove child findings explicitly, since SQLite foreign-key cascade is not enabled on the connection. |
||
|
|
b7dd9dc1b0 |
feat: add ON/OFF toggle for host threshold alerts (#1456)
* feat: add ON/OFF toggle for host threshold alerts Add host_alerts_enabled setting (default ON) as a master switch for CPU, RAM, and disk host threshold evaluation. When OFF, the four threshold controls in Settings > Host Alerts are disabled and MonitorService skips the systeminformation calls and alert dispatch entirely, while clearing stale suppression state so re-enabling starts fresh. The dashboard Configuration Status card shows "Off" when host threshold alerts are disabled. Crash capture, health gate, deploy guardrails, stack alert rules, and the Docker janitor are all unaffected. * fix: exit NumberChip edit mode when externally disabled When the host threshold alerts master toggle is turned OFF while a NumberChip is in edit mode, force-exit edit mode so the chip renders the greyed-out button state consistently with the other chips. |
||
|
|
57a0856ffc |
feat(stacks): per-stack environment inventory and secret-safe guardrails (#1397)
* feat(stacks): per-stack environment inventory and secret-safe guardrails
Add an Environment tab to Stack Anatomy that derives a per-stack inventory
of environment variables from the compose files and env files. Each variable
shows its source, whether Compose interpolates it or injects it into a
container, and a status (present, missing, unused, duplicate, or shell-only),
plus likely-secret classification. The inventory works from variable names
only: a value is never read, returned, or logged, and a likely secret shows
presence only. A copy env checklist action exports names and status without
values.
Surface a missing required env_file as a Compose Doctor preflight finding,
and add an opt-in node setting that refuses a deploy or update when a
required ${VAR:?...} variable is unset or empty, before any backup, pull, or
up runs. Default off.
The Environment tab is capability-gated so it hides on older remote nodes.
* fix(stacks): harden env-file reader against a stat-then-open race
Open the env-file handle first and fstat the open handle instead of
stat-ing the path before opening, removing the check-then-use window in
readEnvFileKeys. Use a secure mkdtemp directory for the out-of-base test
path instead of a predictable name in the temp root.
* fix(stacks): resolve nested env_file paths per compose file, reconcile inline keys per service
Resolve each env_file relative to the directory of the compose file that
declared it, so a nested multi-file Git override (infra/prod.yml referencing
./prod.env) lands next to that file instead of the stack root. The root
compose file is unaffected, since its directory is the stack directory.
Reconcile inline environment provenance per service, so a key an override
removed from one service's effective env is not labeled compose-inline just
because another service injects the same name from a different source.
|
||
|
|
38aabe7064 |
feat: health-gated updates and rollback readiness (#1354)
* feat: classify stack deploy and update failures with suggested next actions Failed deploy and update responses now carry a failure classification (cause category, headline, and suggested next step) derived from the compose error output. The recovery panel and chip render the classification and include it in copied diagnostics, and gateway-style failures surface as a node-unreachable cause. * feat: add update and rollback readiness reports for stacks Before a manual update, Sencho now shows an advisory readiness verdict computed from the stored preflight result, open drift findings, live container health, the pending image change, the rollback backup slot, and node disk headroom. The Stack Dossier gains a rollback readiness section that states what a rollback can restore and explicitly discloses that volume and bind-mounted data are not covered. Toolbar and sidebar updates now share one update path, and admins can create a fleet snapshot from the readiness dialog before updating. Nodes that do not advertise the capability keep the direct update flow. * feat: observe stack health after updates with a post-deploy health gate After a deploy or update succeeds, Sencho now watches the stack for a configurable observation window and records a passed, failed, or unknown verdict: containers must stay running, healthchecks must report healthy, and restart loops or disappearing containers fail the gate. The deploy panel shows the observation live and holds off auto-closing until the verdict lands, a failed gate surfaces the existing recovery actions including rollback, and the stack timeline records update started and gate verdict events. Scheduled, webhook, bulk, and git-source updates are gated the same way; rollbacks and installs are deliberately not. The gate is observational only and can be tuned or disabled per node under host alert settings. * docs: document health-gated updates and rollback readiness New operator page covering the update readiness dialog, the post-update health gate and its settings, the rollback readiness disclosure, and classified failures, with cross-links from the atomic deployments and deploy progress pages. The API reference gains the readiness and health-gate endpoints, the healthGateId success field, and the failure classification schema on deploy and update error responses. * feat: withhold the success verdict while the health gate observes An update used to show a green Succeeded that a failed health gate then contradicted moments later. The deploy modal now reports Verifying health while the gate observes, shows success only when the gate passes, and makes a failed or unknown gate the headline result; success toasts soften to a verifying message while a gate runs. The mobile recovery card groups its actions behind one bottom-right Take action menu so it stays compact on a phone, with the classified cause still visible on the card. A successful image update now also counts as the last known-good marker in rollback readiness, and the docs gain screenshots of the readiness dialog, gate states, dossier section, and settings. * fix: harden log format strings and the env existence path check Log calls that interpolated the stack name into the console format string now use constant format strings with placeholder arguments, and envExists validates path containment inline at its filesystem access, matching the established patterns used elsewhere in the same files. * test: adapt deploy modal success specs to the post-deploy health gate The deploy feedback modal now withholds its success verdict while the health gate observes the new containers, showing "Verifying health" until the gate passes. The two success-path E2E tests waited for "Succeeded" within the gate's 90s default window and timed out. Shorten the observation window to the 15s minimum for these tests via the settings API, assert the verify-then-succeed sequence the modal actually renders, and restore the default window afterward so the test value does not leak into later runs. * fix: serialize health gate polling and harden gate observation Address race conditions in the post-update health gate found in review. Backend: the gate poller used setInterval, so a Docker observe slower than the 5s tick could overlap the next poll and corrupt the restart and missing-container accounting, and a wedged socket could leave a poll pending forever. Polling is now single-flight: each cycle self-schedules the next only after it settles, and the observe is bounded by an 8s timeout so a hung probe counts as a poll error and resolves the gate unknown after three in a row. Frontend: the gate poller could overlap requests, letting a slow earlier "observing" response overwrite an already-applied terminal verdict. It is now single-flight with a terminal latch, so a late response can never roll the UI back from passed or failed. Also reject a non-digit nodeId on the snapshot coverage route instead of letting parseInt coerce it, document that turning off the deploy progress panel opts out of the live gate UI while the gate still runs server-side, and add gate-coverage tests for the webhook, git source, and auto-update apply paths plus the new single-flight, observe-timeout, and recovery cases. |
||
|
|
710647a44f |
feat(snapshots): preserve stack dossiers with fleet snapshots (#1339)
* feat(snapshots): preserve stack dossiers with fleet snapshots Fleet snapshots can now optionally capture each stack's Dossier notes alongside its compose and .env files, so a recovery restores the operational knowledge around a stack, not just its configuration. - Opt-in global setting "snapshot_documentation" (default off), toggled from the renamed Fleet settings section. - Capture reads local dossiers from the database and remote dossiers over the Distributed API proxy; only stacks with notes are recorded, and secret values are never included. - Captured notes are stored encrypted at rest in a new fleet_snapshots column and surfaced in the snapshot detail view behind a badge. - Cloud and downloaded archives gain a documentation.json (archive_version 2). - Restore stays conservative: dossier notes are written back only when the operator explicitly opts in, on both single-stack and restore-all paths. - Existing snapshots and archives remain valid; behavior is unchanged when the setting is off. * fix(snapshots): harden dossier-notes restore against bad input and partial failures Address review findings on the documentation-snapshots restore path: - Parse `restoreNotes` strictly (=== true) on single-stack restore, matching restore-all, so a stray non-boolean can never opt in to overwriting notes. - Guard findSnapshotDossier: require an array of stacks and real dossier content, so a malformed or all-blank entry can't clobber current notes. - Make the dossier-notes write non-fatal relative to the file restore: a notes failure (e.g. a remote dossier PUT) is caught, reported via `notesError`, and no longer 500s the single restore or fails the stack in restore-all once the files are already written. - Surface the partial outcome in the UI: a warning toast on single restore, a summary note on restore-all, and gate the "Documentation captured" badge and restore-all notes control on captured stacks while rendering capture warnings. Adds tests for strict parsing, malformed/blank blobs, remote notes restore (success + non-fatal failure, single and bulk), and scheduled capture-on. * fix(snapshots): drop unused binding in restore-all remote notes test The restore-all remote notes test destructured a node id it never uses (restore-all is driven by snapshot id alone), tripping no-unused-vars and failing the lint step. Bind only the snapshot id. |
||
|
|
308949282c |
feat(resources): reclaim banner controls and accurate reclaim math (#1318)
* feat(resources): reclaim banner controls and accurate reclaim math Make the Resources Hub reclaim banner match what it advertises and give operators control over when it appears. - "Review & prune" now reclaims every category the banner lists (unused images, stopped containers, and dangling volumes) instead of images only, so the banner clears in one action. Pruning runs volumes first, while stopped containers still reference their named volumes, so a stopped stack's data is never cascaded into deletion. - Add a "Show reclaimable-space banner" toggle under Settings, System, Docker hygiene (on by default, per node) and a dismiss control on the banner that snoozes it until the reclaimable total grows again. - Fix the reclaimable-space math: count only containers a prune can actually remove (created, exited, dead) and size them by their writable layer, so a small, un-prunable remainder no longer keeps the banner up. * fix(resources): show the reclaim banner when the settings fetch fails A failed or empty /settings load left the banner's enabled flag at the previously active node's value, so switching from a node with the banner turned off to a node whose /settings errored kept the new node's banner hidden. Set the flag unconditionally after the staleness guard so a failed fetch falls back to the default-on state for the current node. |
||
|
|
716daf77d0 |
feat(updates): auto-prune dangling images after updates (#1316)
* feat(updates): auto-prune dangling images after updates Each update pulls a fresh image and recreates containers, leaving the replaced image behind as a dangling layer that previously had to be pruned by hand. A new "Prune dangling images after updates" toggle under Settings > System > Docker hygiene reclaims these automatically. The setting is on by default and opt-out. When enabled, a successful stack update (manual or scheduled) and a Sencho self-update each remove the dangling image layers they orphaned. Only untagged layers are touched; tagged images, volumes, and data are never removed. The toggle requires an admin account and is per node: each instance honors its own value, so a remote node self-update applies that node's own preference. A prune failure never affects the update result: on the stack path it is caught and logged after the update has already succeeded, and on the self-update path the helper-shell prune runs only after a clean recreate and cannot change the exit code or the recorded update error. * security(self-update): shell-quote label-derived values in helper command Address review feedback on the prune-on-update change: - The self-update helper command interpolated the compose service name and config-file paths (both read from Docker Compose labels) straight into a shell string. Shell-quote them via shQuote so a label carrying shell metacharacters stays inert data and cannot break the exit-code capture, error-file write, or prune guard. - Correct the settings copy and docs: the prune is a standard dangling-image prune, so it reclaims every untagged layer on the node, not only the one the current update orphaned. Tagged images, volumes, and data remain untouched. - Add tests: shell-metacharacter neutralization and prune-output suppression in the self-update command, and an atomic-update case asserting a prune failure does not trigger a rollback. * fix(updates): omit the reclaim figure when the daemon reports zero bytes End-to-end testing on a Docker daemon backed by the containerd image store showed the post-update prune removing a dangling image while the prune API returned SpaceReclaimed=0, so the stream printed "reclaimed 0.0 MB" even though an image was removed. Show the reclaimed figure only when the daemon reports a non-zero value; otherwise the line reads "=== Pruned dangling images ===". The overlay2 store still reports real figures and shows them. Add a test covering both branches. |
||
|
|
865d792874 |
feat(pricing): collapse to two tiers (#1309)
* feat(pricing): collapse to two tiers (Community + Admiral) Collapse Sencho's pricing from three tiers (Community / Skipper / Admiral) to two: a generous free Community tier and a single paid Admiral tier. The Skipper tier is removed. Now free in Community: auto-heal, auto-update, scheduled operations, webhooks, notification routing, Fleet Actions and bulk operations, SSO preset providers (Google / GitHub / Okta), unlimited users with admin and viewer roles, and deploy safety (atomic deploys, auto-rollback, and one-click rollback). Admiral (paid) is focused on running and governing a fleet: blueprints, Fleet Secrets, deploy enforcement, vulnerability report export, audit log, host console, private registries, mesh networking, node cordon, managed cloud backup, LDAP / Active Directory SSO, and the advanced RBAC roles (deployer, node-admin, auditor) with per-resource scoped assignments. Internally the license variant distinction is removed so tier is binary (community / paid). License validation still verifies the Lemon Squeezy store and product before granting paid status. Docs and the contributor guide are updated to the two-tier model. * docs(pricing): correct licensing page to two-tier pricing and tidy stale tier wording The licensing docs page kept the old Admiral pricing plus a Founder Lifetime column and an Enterprise paragraph after the two-tier collapse. Update it to $12/month or $99/year, drop the lifetime and Enterprise content, and link to the pricing page for current pricing. Also fix stale "Skipper" wording in CLA.md, SUPPORT.md, one test title, and three test comments. Historical CHANGELOG entries and the retired-Skipper license-guard test are intentionally left as-is. * docs: align licensing and SSO pages with the two-tier model Correct the SSO overview so the Google, GitHub, and Okta presets read as available on every tier, matching the provider table; only LDAP and Active Directory require Sencho Admiral. Remove the lifetime-plan references from the licensing, settings, and troubleshooting pages so they reflect subscription-only Admiral pricing. * fix(rbac): omit scoped permissions from /me on the Community tier Scoped role assignments only take effect on the paid tier, but GET /api/permissions/me returned them unconditionally, so a downgraded instance with leftover assignments rendered per-resource affordances the API then rejected with 403. The endpoint now mirrors the permission middleware and includes scoped permissions only on the paid tier. Adds a regression test covering the downgrade case. * docs: use custom-pricing wording on the contact page The two-tier model has no Enterprise tier; reword the contact page's enterprise pricing/deals to custom pricing/deals so it does not imply a tier that no longer exists. |
||
|
|
0683aa9395 |
fix(settings): reflect role, tier, and node scope in settings panels (#1300)
* fix(settings): reflect role, tier, and node scope in settings panels Three gate fixes so the Settings panels match what the backend enforces. Non-admin roles saw editable fields and a Save button on System Limits, Developer, and App Store, but writes require admin, so Save always failed. These panels now render read-only for non-admins (controls disabled, Save hidden) while still showing the values. The Developer panel is node-scoped but read and wrote the controlling instance regardless of the selected node, so a remote node's debug mode and retention windows could not be changed from the UI. It now targets the active node like System Limits. The settings shell derived the Admiral entitlement locally; it now consumes the backend-provided value the API authorizes against, and that value is corrected to require an active paid tier so an expired Admiral license no longer reports as Admiral. * fix(settings): gate audit_retention_days writes behind Admiral audit_retention_days configures the Admiral-only audit log (the audit-log routes require Admiral, and the Developer settings UI only shows the field to Admiral operators), but the settings POST/PATCH handlers only required an admin role. A non-Admiral admin (for example Skipper, or an expired-Admiral admin whose tier dropped to community) could still set it through the API. Gate writes to that key with requireAdmiral on both the single-key POST and the bulk PATCH paths, matching the audit-log routes and the UI. Other keys remain writable by any admin. * fix(settings): reject unknown keys on PATCH instead of silently stripping The bulk settings PATCH validated the body with a Zod object schema that strips unknown keys by default, so a request carrying a disallowed key (for example an auth_* secret) returned 200 as a no-op instead of being rejected. No secret was written, but it diverged from the single-key POST path, which rejects disallowed keys, and could hide client drift. PATCH now rejects any key outside the allowlist with a 400 before validation or write, keeping the bulk path fail-closed and consistent with POST. |
||
|
|
4596a90474 |
fix(settings): serialize GET /api/settings from an allowlist (#1299)
The settings read returned the entire global_settings map minus a small denylist of auth keys. Other subsystems persist their config in the same table, so cloud backup values (endpoint, bucket, access key) were returned to any authenticated user, including read-only roles, bypassing the redaction the dedicated cloud-backup endpoint applies. Project the response from the operational allowlist instead, so only the keys the settings UI reads are returned and any key written to global_settings by another subsystem is excluded by default. Remove the now-unused denylist constant. |
||
|
|
42e8d3a78c |
feat(security): per-image scroll + retention cap in scan history (#1231)
* feat(security): per-image scroll + retention cap in scan history Long scan histories for hot images used to monopolise the Scan history sheet: a single image with dozens of scans pushed every other image off screen, and the underlying vulnerability_scans table grew without bound. Each image group's table now renders inside its own ScrollArea capped at max-h-64 (~6 rows visible) so a busy image scrolls independently while the list of images stays navigable. A new global setting scan_history_per_image_limit (default 50, min 5, max 1000) backs both a window-function query that caps the response per image_ref and a prune step that runs on the existing MonitorService cleanup tick. The response now carries cappedImageRefs + perImageLimit so the UI can render a "Capped at N · older scans pruned" hint on groups sitting at the ceiling without a second settings round-trip. Single-image deep-dive (imageRef query param) bypasses the cap so a user clicking into one image can still see its full history. The prune uses self-contained subqueries to avoid SQLITE_MAX_VARIABLE_NUMBER issues on first-run installs with large backlogs, and explicitly deletes child rows from vulnerability_details, secret_findings, and misconfig_findings inside a transaction since FK cascade is not enabled at the connection level. Settings → Developer → Data retention gains a "Scan history per image" field. * fix(security): skip searchDraft debounce on mount to stop page-reset race The searchDraft debounce useEffect fires once on initial mount with the unchanged value and, 300ms later, unconditionally calls setPage(0). When a user (or a test) paginates inside that 300ms window, the pending debounce silently undoes the page advance. CI surfaced this as a flaky 3rd fetch in the "advances offset when the user pages forward" test once the per-image cap work added enough state-update overhead to push the click past the 300ms threshold on the slower Linux jsdom run. Track searchDraft with a ref and exit the effect when the value has not actually changed, so the debounce only runs in response to real user typing. |
||
|
|
aa3d99a594 |
fix(mesh): re-evaluate data plane every 10s and add opt-in auto-recreate (#1184)
* fix(mesh): re-evaluate data plane every 10s and add opt-in auto-recreate MeshService.dataPlaneStatus was written exactly once at boot in setupMeshNetwork() and never re-evaluated. After the operator removed sencho_mesh at runtime (or it was recreated externally, or Sencho was disconnected from it), /api/health and the dashboard banner kept returning the stale boot-time discriminator until the next process restart. Adds a 10s revalidator that inspects the current Docker truth in one network-inspect call and transitions dataPlaneStatus to reflect it. Short-circuits in not_started / not_in_docker / subnet_invalid (states that cannot change within this process) and in concurrent ticks. Transitions are idempotent on stable state, so the timer can tick indefinitely on a healthy mesh without log noise. New 'not_found' reason value for the network-was-removed-at-runtime case. Existing reasons (subnet_mismatch, subnet_overlap, attach_failed) also surface from the revalidator when their underlying conditions arise post-boot. transitionDataPlane keeps message and subnet fields fresh across consecutive observations even when reason is unchanged, so /api/health never reports stale numbers (e.g. two consecutive subnet_mismatch observations against different external subnets). Adds an opt-in mesh_auto_recreate global setting (default off). When on, the revalidator additionally calls attemptInPlaceRecreate() after surfacing not_found. The helper hard-prefers the boot-chosen subnet (this.meshSubnet) and never iterates candidates, because changing the subnet here would invalidate every existing extra_hosts override on disk. A real conflict on the original subnet is reported as subnet_overlap and preserved during the 60s recreate throttle window so the operator-actionable reason is not flapped back to not_found between attempts. Self-attachment is checked via Name match (operator --hostname X matches container Name /X) or full container-ID prefix for hex HOSTNAMEs >= 12 chars (Docker default short ID). Non-hex HOSTNAMEs cannot collide with container IDs at all so a Name miss is conclusive; short hex HOSTNAMEs preserve the prior status as 'unknown' rather than risking a false-positive prefix match. Frontend surfaces: - types/mesh.ts: 'not_found' added to MeshDataPlaneReason. - MeshDataPlaneBanner: 'not_found' headline copy. - Settings > System > Mesh data plane: TogglePill bound to mesh_auto_recreate, default off, helper text explains the tradeoff. Backend coverage in backend/src/__tests__/mesh-data-plane-revalidate.test.ts (25 cases): short-circuits, idempotent stable-state, recovery from subnet_mismatch / subnet_overlap, transition to not_found / subnet_mismatch / attach_failed, transient-Docker anti-flap, re-entrancy guard, name match path, ID-prefix path, short hex hostname ambiguity, non-hex hostname certainty, transition message refresh on observation drift, auto-recreate off (default), auto-recreate success with senchoIp preservation, auto-recreate overlap classification with no subnet drift, throttle window preserves classified reason, throttle release. Lifecycle test covers timer wiring in start()/stop(). Existing mesh-setup-error-classification suite (27 cases) still green. Resolves: F-4 in the v1.0 audit tracker. * fix(mesh): address Codex review of PR #1184 Three findings from the independent review: BLOCKER: attemptInPlaceRecreate() called recordSetupFailure() on create / attach failures, which clears this.senchoIp. The next revalidator tick's attachment check is guarded on senchoIp, so with it null the check is skipped and the snapshot path can silently flip the status back to ok against a network where Sencho is in fact not attached. Also: a later successful recreate would call ensureSelfAttached() with senchoIp null, which short-circuits, so the network gets recreated without binding Sencho. Replaced the recordSetupFailure() calls in attemptInPlaceRecreate with a new recordRecreateFailure() that uses transitionDataPlane and preserves senchoIp. Added two tests: create-fails-then-succeeds (verifies senchoIp survives the failure and the later retry binds Sencho correctly) and create-succeeds-attach-fails-then-next-tick (verifies the snapshot path surfaces attach_failed on the next tick instead of falsely reporting ok). SHOULD-FIX 1: single-key POST /api/settings wrote String(value) without re-validating against the per-key schema, so an allowlisted enum-shaped key like mesh_auto_recreate could persist arbitrary strings ('banana', 'true') that the bulk PATCH would later refuse. Routed the single-key path through SettingsPatchSchema.safeParse so both write paths validate identically. Added regression tests for an invalid mesh_auto_recreate value, a valid mesh_auto_recreate write, and an out-of-range numeric value. SHOULD-FIX 2: the new Mesh data plane subsection lived inside a section the registry exposes to non-admins, who would see the toggle and only learn it was admin-only after the save 403'd. Gated the subsection on `isAdmin` from useAuth so non-admins do not see the control. The other system controls keep their existing visibility pattern (read-only for non-admins). 71/71 backend tests green (revalidate + mesh-setup + settings-routes). 276/276 frontend tests green. tsc clean on backend + frontend. |
||
|
|
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). |
||
|
|
836e384d17 |
perf(backend): cache global_settings reads in DatabaseService (#814)
getGlobalSettings() runs a SELECT * on every call and is hit from 22 files, including the auth middleware (every authenticated request), the WebSocket upgrade handler (every connection), and the debug-mode gate (every diagnostic log line). Cache the result inside the service on first read and invalidate on updateGlobalSetting(). The cached snapshot is Object.freeze'd and the public return type is now Readonly<Record<string, string>> so accidental mutations are caught at compile time. The settings GET handler that delete'd private keys now takes a defensive shallow copy first. The 5-second TTL cache in utils/debug.ts is now redundant and removed; the service-level cache is strictly fresher (invalidates on write rather than going stale for up to 5s). |
||
|
|
f6a7898798 |
refactor(backend): add route tests then extract settings, scheduled-tasks, agents (phase 4b) (#737)
Round B of Phase 4. Writes integration tests for three under-covered route groups BEFORE extracting them, then does the extraction once the new tests pass against the monolith. index.ts drops from ~4,206 to ~3,678 lines. New test coverage (42 new assertions): - settings-routes.test.ts (14) — auth, admin gating, private-key stripping, allowlist, single-key write, bulk PATCH validation + partial update - scheduled-tasks-routes.test.ts (18) — list/create/get/toggle/delete/runs, action+target_type matrix, cron validation, tier gating on non-admin - agents-routes.test.ts (10) — GET/POST, admin gating, channel type + HTTPS URL validation, boolean enabled check, upsert semantics Each suite was verified against the inline monolith first, then the route extraction was performed byte-for-byte and all suites re-run to ensure no regression. New route files: - routes/settings.ts — GET/POST/PATCH with PRIVATE_SETTINGS_KEYS strip, ALLOWED_SETTING_KEYS allowlist, and SettingsPatchSchema zod bulk schema - routes/scheduledTasks.ts — 9 endpoints (list, create, get, update, delete, toggle, run-now, runs history, runs CSV export). File-local helpers parseTaskId, validateActionTarget, validateOptionalFields collapse duplication across create+update handlers. Uses shared escapeCsvField from utils/csv.ts. - routes/agents.ts — notification-channel GET/POST. Owns NOTIFICATION_CHANNEL_TYPES and validateHttpsUrl locally because the notification-routes block still inlines identical copies; the helpers will converge once those routes extract in a later slice. |