Commit Graph

1843 Commits

Author SHA1 Message Date
Anso a29d451875 fix(resources): attribute free images to managed prune by repository family (#1769)
* fix(resources): attribute free images to managed prune by repository family

After a stack update, unused previous tags lost compose labels and dropped out of managed prune. Match free image repositories still used by managed containers (excluding foreign Compose projects), and clarify the fleet dry-run unlock on the prune card footer.

* fix(resources): omit stackName on repo-match managed prune items

Repository sharing is not ownership; the confirm list must not show stack attribution for repo-matched free images. Pin repository-key normalization with exact Set assertions so the duplicated parser cannot drift silently.

* fix(fleet): bind prune fingerprints to image tag sets

RepoTag churn on an already-planned image Id no longer leaves the dry-run fingerprint unchanged, so fleet preflight rejects the whole execute when any node retags. Itemized delete also refuses on reference drift and reports multi-repository refuse without implying a clean no-op.
2026-08-04 16:38:37 -04:00
Anso 011f084e24 chore: hide the What's New setting until an entry exists (#1770)
The nav trigger already stays out of the top bar while entries.json is
empty, but the Settings toggle rendered unconditionally, so About showed
a control whose helper text described a sparkle icon that could not
appear. The Preferences section now follows the same rule as the trigger.

Also gives the toggle an accessible name. Its visible text is only
ON/OFF, so a screen reader previously announced the state without the
setting it belongs to.
2026-08-04 13:04:51 -04:00
Anso 92d974b13e fix: condition --volumes in downStack() on the removeVolumes option (#1764)
* fix: condition --volumes in downStack() on the removeVolumes option

ComposeService.downStack() hardcoded --volumes on every stack delete,
ignoring the "Also remove associated volumes" checkbox and destroying
volumes the operator asked to keep. The sibling Take-down path (runDown)
already conditions --volumes correctly.

- Add options?: { removeVolumes?: boolean } to downStack()
- Default to data-preserving (no --volumes when option absent)
- DeletedStackDeletionService reads the persisted intent flag
- Templates rollback passes removeVolumes: true (clean up failed deploy)
- Blueprint withdraw passes removeVolumes: false (volumes preserved)

* docs: update Delete row to reflect conditional volume removal

The Delete row now describes that volumes are removed only when the
operator opts in, matching the behavior introduced by the downStack fix.

* fix: add capability gate for delete pruneVolumes and fix QA findings

Four P0 issues found in live QA:

P0-1/P0-4 - No capability gate on delete's pruneVolumes:
  Add stack-delete-prune-volumes capability so the frontend hides the
  "Also remove associated volumes" checkbox on nodes that don't support
  conditional volume removal on delete. Without this, an operator on an
  old node sees a VOLUMES KEPT promise the old node silently breaks.
  Frontend-only gate: no API or proxy gate because the old node's
  fallback (always destroy) is correct for the checked case.

P0-2 - Checkbox state leaked across dialogs:
  Reset pruneVolumes in onConfirm before calling the parent, so a
  previously checked box doesn't appear pre-checked when the dialog
  opens for a different stack.

P0-3 - Delete not bound to the active node:
  Capture activeNode.id at delete time and pass it as an explicit
  nodeId to apiFetch, matching the Take Down pattern. Without this,
  switching the active node while the dialog is open silently deletes
  the wrong stack on the wrong node.

* fix: update test assertions for nodeId binding and showVolumeOption gate

P0-3 added nodeId to apiFetch DELETE calls — two useStackActions tests
now expect the parameter. P0-1 gated the volume checkbox behind
showVolumeOption — the confirming test now passes the prop.

* fix: gate volume hint on showVolumeOption to prevent false promise

On nodes without stack-delete-prune-volumes, volumes are always
destroyed. Showing VOLUMES KEPT was a lie. Now the hint is hidden
entirely when the capability is absent.

* fix: gate delete against nodes that cannot guarantee volume preservation

Hiding the checkbox and the misleading hint stopped the false promise but
not the data loss: an unchecked delete against a node lacking
stack-delete-prune-volumes still reached that node and its downStack()
still destroyed volumes unconditionally, now with no warning at all.

- remoteNodeProxy.ts: block an unacknowledged DELETE /stacks/:name
  (no pruneVolumes=true) to a remote lacking the capability, mirroring
  the existing removeVolumes gate on the down route. An explicit
  pruneVolumes=true always proxies through since that matches what an
  unsupported remote does anyway.
- DeleteStackDialog: rework around a three-state model (supported /
  unsupported / unknown) instead of a boolean. A node whose capabilities
  have not been confirmed (meta not yet fetched, or a failed probe) is
  now treated like a supported node, not forced onto the destructive
  path just because its state is unresolved.
- Fix deleteStack's error toast, which surfaced the raw JSON response
  body instead of the parsed error message.
- Fix CreateStackDialog's orphan-stack rollback (docker-run import),
  which silently no-op'd against a node requiring acknowledgement.
- Update node-compatibility.mdx and stack-management.mdx to describe
  the new gate.

* test: advertise stack-delete-prune-volumes on the scoped-evidence fixtures

These mock remotes simulate nodes capable enough to run scoped-stack-auth-evidence
RBAC and were pinned before stack-delete-prune-volumes existed, so the new delete
gate now blocked their unacknowledged DELETE calls before reaching the mock server,
failing the grant-tuple-cleanup assertions the tests actually check.
2026-08-04 13:04:21 -04:00
Anso 0b046bfa52 chore: add in-app What's New scaffolding (#1767)
* feat: add whats-new entries data model

* feat: add useWhatsNewPreference hook

* feat: add whats-new breathing animation

* feat: add WhatsNewTrigger sparkle icon

* feat: add whatsNew slot to TopBar

* feat: add WhatsNewModal

* feat: wire whats-new sparkle icon and modal into EditorLayout

* feat: add What's New opt-out toggle to Settings

* fix: seed What's New watermark for zero-entry releases

A release that ships with entries.json still empty must stamp a
watermark on first run, or an existing install can never distinguish
itself from a genuinely fresh install once a later release adds its
first real entry, silently swallowing that entry's unseen signal.

* fix: constrain WhatsNewModal height and clarify settings copy

Bound the dialog to 85vh as a flex column so ModalBody's fill can
constrain the entry list to scroll while the header and footer stay
pinned, matching ConfirmModal's pattern. Also clarifies the "Show
What's New" helper text in Settings.

* fix: drop What's New screenshots that fail to load

Screenshots are authored by hand alongside the entry, so a typo'd or
not-yet-added filename is a realistic mistake. Previously that left the
browser's broken-image placeholder and alt text inside the card; now the
image is dropped and the title, blurb, and doc link still render.

* style: replace em dash in watermark comment with a comma

* fix: make "Never show again" actually hide What's New

Turning the feature off previously only stopped the breathing animation
and left the sparkle icon in the top bar, which is not what that label
means anywhere else. Opting out now removes the trigger entirely and
closes the modal, and Settings > About is the single way back.

Also brings the trigger in line with its top bar siblings: it now uses
the search trigger's hover treatment and gains a visible keyboard focus
ring, and the modal bounds itself with dvh rather than vh so the footer
cannot sit under a mobile URL bar. The modal is rendered in the desktop
branch only, since the bespoke mobile screens drop the top bar that
carries its trigger.

* test: cover the empty-entries and loader paths of What's New

The shipped state has an empty entries.json, so the branch where there is
no newest entry is the one actually running, yet nothing exercised it.
Adds a sibling hook test mocking that state to pin the empty-string
watermark write, and a loader test for the newest-is-last contract the
watermark depends on plus the malformed-entry filter.

Drops the unreachable array check in the loader and the assertion that
mirrored it: TypeScript types the JSON import, so a non-array file fails
the build rather than reaching that branch.

* refactor: fold the What's New storage writers into one helper

writeEnabled and writeLastSeenId were identical apart from the key and
the value encoding, duplicating the comment explaining why a failed
write is non-fatal. The boolean encoding now sits at its single call
site. Also hoists the reversed entry list to module scope, since the
source array is a module constant, and factors the repeated preference
mock in the trigger test behind a helper.

* chore: keep the What's New icon hidden until an entry exists

With entries.json empty there is nothing to announce, so a permanent
sparkle in the top bar would only ever open a modal reading "Nothing new
to show yet". The trigger now renders nothing in that state, leaving the
modal's empty state as a runtime fallback for entries that fail
validation rather than the shipping experience.

This keeps the scaffolding invisible until the first entry is authored,
which is the change that actually surfaces the feature to users.
2026-08-04 02:20:11 -04:00
Anso 4be3319a07 fix: surface NOT_EMPTY error code so DeleteFileConfirm can offer recursive delete (#1765)
* fix: surface NOT_EMPTY error code so DeleteFileConfirm can offer recursive retry

The parseApiError helper discards the backend's machine-readable code field,
returning only the human-readable message. DeleteFileConfirm tried to detect
non-empty directory refusals by matching the substring NOT_EMPTY against the
message text, but the actual server message is "Directory is not empty" (with
spaces, not underscores), so the two-step "Delete all" confirmation flow was
dead code.

Add a NotEmptyError class (mirroring the existing UploadConflictError pattern)
and intercept HTTP 409 responses in deleteStackPath so the code is preserved.
Replace the fragile string match in DeleteFileConfirm with instanceof.

* fix: extend NOT_EMPTY fix to volume roots and fix stale viewer after recursive delete

P0-1: sendFsError's helper/ExecError branch (volume-browser deletes) never
attached a code field to 409 responses, so the frontend NotEmptyError was
never thrown for named-volume non-empty directories. Map ExecError 409s
whose message matches 'not empty' to code: NOT_EMPTY.

P0-2: The context-menu delete onDeleted callback used an exact-match check
(ctxDeletePath === selectedPath) to decide whether to clear the viewer.
When deleting a folder containing the open file, the viewer stayed open
showing now-deleted content. Use the existing openFileAffectedBy helper
(which checks ancestor paths) instead, matching bulk-delete behavior.
2026-08-04 01:37:55 -04:00
Anso 5d89a10754 fix(ui): show delayed busy feedback on confirm actions (#1763)
Wire ConfirmModal and BusyButton so async confirms lock immediately, show
spinner and progressive labels after duration-base, and block dismiss mid-flight.
Connect stack delete and take-down to the existing stackAction map so the dialog
is not idle until the toast.
2026-08-03 22:14:24 -04:00
Anso 0ba09ebdee feat: add ntfy notification channel (#1761)
* chore: bump brace-expansion and fast-uri via npm audit fix

Resolves GHSA-rgw5-rvv9-x895 (brace-expansion DoS via unbounded
intermediate arrays). Both transitive dev dependencies updated:
- brace-expansion 5.0.8 -> 5.0.9
- fast-uri 3.1.4 -> 3.1.5

* chore: also bump frontend deps via npm audit fix

Fixes brace-expansion and postcss in the frontend lockfile so
npm audit --audit-level=high passes on both packages.

* chore: bump ip-address transitive dep via npm audit fix

Resolves three new ip-address advisories (GHSA-mwp4-54f8-5fhr,
GHSA-4xrf-jv44-h6hh, GHSA-22jq-vg5j-6vgg) published between prior
push and CI run.

* feat: add ntfy notification channel

Add ntfy (https://ntfy.sh) as the fifth notification channel alongside
Discord, Slack, Webhook, and Apprise. ntfy speaks its native protocol:
plain-text POST body with Content-Type, Title, Priority, and Tags
headers. Priority maps info/warning/error to ntfy's default/high/urgent.

URL validation allows both HTTP and HTTPS (common for LAN self-hosting)
but rejects embedded credentials, consistent with Apprise. Token auth
via ntfy's documented ?auth= query parameter is supported.

* fix: correct ntfy channel test cases for Linux URL parsing and required type field

- notification-channels.test.ts: replace http:///topic host check with a
  cross-platform invalid-URL case (WHATWG parser treats triple-slash
  authority differently on Linux vs Windows)
- ConfigurationStatus.test.tsx: add ntfy agent slot to makePayload and
  inline agents fixtures (required by the expanded ConfigurationAgents
  type)

* fix: remove unused import and update 0/4 masthead assertions to 0/5

* ci: exclude NotificationService.ts from js/request-forgery CodeQL rule

Notification channel dispatch methods (Discord, Slack, Webhook, Apprise,
ntfy) all call fetch() with admin-configured URLs and notification bodies
that may embed stack or path data. This matches the trust model already
documented for registry-api.ts: single-tenant self-hosted, admin owns
the server, outbound posting is the intended behavior. The write path is
gated by requireAdmin or requirePermission(node:manage), and every
dispatch runs with a 10s AbortSignal.timeout.

* ci: also exclude NotificationService.ts from js/file-access-to-http

Notification messages may embed stack names, paths, or compose-derived
content. Same trust model as js/request-forgery: admin owns the server
and the configured endpoints, write path is gated.

* fix: correct ntfy channel tab copy and validation error message

The ntfy settings tab was reusing the generic webhook label, helper, and
placeholder (Webhook URL / JSON payloads / https://...). Give ntfy its own
copy: label names the server-and-topic URL, helper states plain-text delivery
and the mandatory topic path, placeholder matches the routing section.

Also fix the routing-rule validation toast: the guard correctly exempts ntfy
from the HTTPS check but the error message was not updated alongside it, so
ntfy URLs received a misleading HTTPS-required message.

* fix: strip trailing slash from ntfy topic URL before dispatch

A topic URL like https://ntfy.sh/mytopic/ validates fine (the check strips
the trailing slash internally) but was stored and dispatched with the slash
intact, causing the real ntfy server to 404. Normalize before fetch so the
request reaches the correct topic path.

Also add ntfy to the Channels card description in the settings registry.
2026-08-03 19:29:41 -04:00
Anso d0f1b9211a chore: bump brace-expansion and fast-uri via npm audit fix (#1762)
* chore: bump brace-expansion and fast-uri via npm audit fix

Resolves GHSA-rgw5-rvv9-x895 (brace-expansion DoS via unbounded
intermediate arrays). Both transitive dev dependencies updated:
- brace-expansion 5.0.8 -> 5.0.9
- fast-uri 3.1.4 -> 3.1.5

* chore: also bump frontend deps via npm audit fix

Fixes brace-expansion and postcss in the frontend lockfile so
npm audit --audit-level=high passes on both packages.

* chore: bump ip-address transitive dep via npm audit fix

Resolves three new ip-address advisories (GHSA-mwp4-54f8-5fhr,
GHSA-4xrf-jv44-h6hh, GHSA-22jq-vg5j-6vgg) published between prior
push and CI run.
2026-08-03 16:47:48 -04:00
Anso 71be1634a5 feat: surface ZFS ARC reclaimable as dashboard context line (#1760)
* feat: surface ZFS ARC reclaimable as dashboard context line

Add arcReclaimable to the HostMemory interface and MemoryWire shape so
the reclaimable ARC amount computed by readReclaimableArc() is exposed
through /api/system/stats and /api/fleet/overview. Show it as a context
line on the dashboard memory tile, matching the balloon pattern.

ARC continues to feed the gauge percentage as before; this is a
display-only addition for operator visibility.

* docs: clarify ARC line requires nonzero reclaimable, not just readable stats
2026-08-03 13:44:20 -04:00
Anso 1c9060a855 fix(ui): flatten single-container Update onto the service row (#1759)q
* fix(ui): flatten single-container Update onto the service row

On multi-service stacks, put Update/Rebuild on the container card (left of
image source) when a service has one matching container, and keep the shared
header only for multi-replica services.

* fix(ui): show per-service Update only when an image update is confirmed

Registry services were always showing Update because eligibility checked
declaredImage/hasBuild only. Gate Update on a confirmed pending check so
the button clears after a successful recheck; keep Rebuild for build-backed
services.
2026-08-03 10:10:09 -04:00
Anso 5f5cd5ea51 feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community (#1758)
* feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community

* fix(fleet-secrets): update reachability test for Secrets community graduation

* fix(fleet-secrets): address review findings

* fix(fleet-secrets): add HTTP-level Community admin push/import tests and non-admin tab-hidden test
2026-08-03 08:42:46 -04:00
Anso 5240957ff3 feat: validate scheduled task target existence at creation time (#1757)
* feat: validate scheduled task target existence at creation time

Reject POST/PUT /api/scheduled-tasks with 400 when the target stack,
container, or node does not exist. Previously only structural format was
validated; a task targeting a deleted stack would return 201 and fail
forever at execution time with noisy error logs.

Node existence is now validated for every action that requires a node
(previously only scan/prune got this check). Stack and container
existence is validated on local nodes; remote nodes are skipped since
the check would require a proxy call (execution-time validation still
serves as the safety net there).

Permission checks run before existence checks, so unauthorized callers
receive 403 regardless of whether the target exists.

* fix: close node-existence oracle in scheduled task creation

Move validateActionNode (which contains a getNode DB lookup) after the
permission gate in both POST and PUT handlers, so unauthorized callers
always receive 403 regardless of whether a fleet or system target node
exists. Previously a viewer probing a nonexistent fleet node would get
400 ("node not found"), leaking node ID enumeration through the error
code difference.

The stack/container existence check (validateTargetExists) was already
correctly positioned after permission; this fix extends the same
discipline to the node-existence path.
2026-08-03 00:10:07 -04:00
Anso bfabdc2444 fix(fleet-secrets): invalidate stale preview when target inputs change (#1756)
* fix(fleet-secrets): invalidate stale preview when target inputs change

Clear the preview plan and push results whenever a target input
(selectedLabels, labelMode, stackName, or envFile) changes, so the
operator cannot review a diff computed from different inputs.

A version counter in a ref drops in-flight preview/push responses
when inputs change while a request is still pending.

* fix(fleet-secrets): always surface push results, add invalidation test

Remove the version guard from handlePush so a completed secret write
always reports its outcome to the operator, even if target inputs changed
while the push was in flight. The guard remains on handlePreview (read-only).

Add a component test covering: plan cleared on input change, stale
in-flight preview discarded, and push results always surfaced.
2026-08-02 23:45:53 -04:00
Anso 2ad1212bbb test(mfa): cover session invalidation on admin MFA reset (#1755)
Add seedMfaUserWithToken helper that wraps seedMfaUser and returns a
signed JWT with the current token_version claim, removing the manual
sign-and-read-boilerplate from callers.

Add an integration test in the MFA reset describe block that exercises
the full invalidation cycle: a target user's pre-reset JWT is accepted
before reset and rejected with 401 'Session invalidated' after an admin
resets their MFA. The test also confirms a pre-minted admin JWT survives
the reset unchanged.
2026-08-02 22:46:16 -04:00
Anso 41bf075eb0 feat(recovery): make rollback-recovery image lifecycle visible and controllable (#1753)
* feat(recovery): make rollback-recovery image lifecycle visible and controllable

GitHub discussion #1751 asked why Sencho creates sencho-rb/<id>/<service>:hold
images during automatic updates and how to clean them up. That surfaced a real
safety bug alongside the missing visibility: the manual single-image delete
route did not consult the held-image predicate every other deletion path
already honors, so a user could delete a rollback-protected image straight
through the Images tab and silently break automatic recovery for that update.
A short/truncated id also bypassed the predicate's full-id lookup.

Fixes:
- POST /images/delete now resolves the submitted id to its canonical form and
  checks the unified held-image predicate before deleting, returning 409
  IMAGE_HELD_FOR_ROLLBACK for a protected image.
- The Images tab no longer mislabels a protected image as plain "Unused"; a
  fully-synthetic hold image is kept out of the generic inventory entirely and
  surfaced instead in a new Resources -> Rollback tab, with an additive
  "Rollback protected" badge for images that still carry a normal tag too.

New capability:
- Two settings (Deploy Guardrails): superseded-generation retention (days,
  replaces a hardcoded 7) and a cap on retained generations per stack.
- A new Resources -> Rollback tab lists every generation (stack, short id,
  state, retention) with an admin-gated manual release action, including
  releasing the current generation with an explicit warning that automatic
  rollback becomes unavailable until the next successful update. Release is
  a single atomic, server-revalidated transition so a stale UI read can never
  release a row that has since become ineligible.

Also consolidated three near-duplicate implementations of the held-image
predicate (two of which relied on a require() of a sibling .ts file that
silently failed to resolve under the test runner and was never actually
exercised by a real test before this change) into one shared module.

Known follow-up, not fixed here: an orphaned sencho-rb tag whose recovery row
no longer exists (DB restore, node re-add) is invisible in both the Images
and Rollback tabs with no UI path to reclaim it.

* fix(audit): add summary mapping for rollback generation release

* fix(security): sanitize prune target in log sinks and cover release RBAC

Closes two open js/log-injection findings on the system prune route by
applying the same inline sanitizeForLog barrier the rest of the file
already uses. The prune target is validated against an enum by
parsePruneTargets before reaching these sinks, so the findings were false
positives, but the barrier is cheap and removes the standing alerts on a
file this change already touches. Also wraps the generation id in the
release log line for consistency with the stack name beside it.

Adds coverage for gaps a QA pass identified:
- Release endpoint refuses a viewer and a deployer (Admin-only), leaving
  the generation and its artifacts untouched.
- Viewer can still read the generations list, matching the sibling
  Resources routes.
- The predicate the prune routes build reports full-stack rollback holds,
  not just service-scoped ones, and re-reads per call so a hold taken
  between plan and delete still gates the delete.
- After releasing the current generation, no rollback point is claimed
  for the stack through any consumer of the current-generation lookup.
2026-08-02 21:55:22 -04:00
Anso 97be019696 feat(fleet): add Node details sheet to the node card kebab (#1752)
* feat(fleet): add Node details sheet to the node card kebab

Every Fleet node card now carries a "Node details" kebab item, open to
any role that can see the card (previously the kebab only rendered
for users with node-manage permissions, so plain viewers had none).
The sheet shows connectivity, live capacity, Compose workload,
version/capability compatibility, and governance info (labels, cordon
reason and date, default-node flag, Compose directory, registration
date) using data the Fleet page already fetches, plus one lazy call
to the existing node meta endpoint for capabilities. Wired into both
the desktop card and the mobile bespoke Fleet screen.

* fix(fleet): correct Node details sheet timestamp units and update-status fallback

QA against a live 3-node fleet found that last_successful_contact and
pilot_last_seen come back from the fleet-overview endpoint in Unix
seconds, but the sheet passed them straight into a milliseconds-only
formatter, rendering values like "20647d ago" instead of "just now".
Both are now converted before formatting.

The Compatibility section's update-status badge also fell through to
a confident "Up to date" whenever updateStatus was absent (e.g. on
mobile, which doesn't poll update status) instead of reflecting that
there was no data to back the claim; it now renders "Unknown" in that
case. The local node no longer shows a misleading "Last successful
contact: Never". Reworded the "read-only sheet" language in the docs
page to describe the sheet accurately, since the Governance section's
label picker stays editable for node managers by design.
2026-08-02 21:35:50 -04:00
Anso 48c89d217d fix(mesh): close error-listener race in pilot tunnel reverse-dial connect (#1754)
acceptReverseLocal swapped a pre-connect error handler for permanent
close/error handlers inside the socket's 'connect' callback, briefly
leaving 'error' with the old handler removed and the new ones not yet
attached. A Node EventEmitter 'error' event with zero listeners
throws instead of being swallowed, and CI's runner timing hit this
window intermittently (backend-tests-pilot-tunnel-bridge-reverse-
route-events.test.ts's post-handshake-close test), surfacing as an
unhandled ECONNRESET exception even though every assertion passed.

Replace the swap with a single 'error' listener attached at socket
creation that branches on a connected flag, so the socket is never
without an error listener at any point in its lifecycle.
2026-08-02 21:08:07 -04:00
Anso c613010199 feat: account for VM memory ballooning in host memory reporting (#1750)
* feat: account for VM memory ballooning in host memory reporting

Extend hostMemory.ts with a readBalloonedMemory() function that parses the
Balloon: field from /proc/meminfo, following the same fail-open pattern as
the ZFS ARC integration. When a nonzero balloon is detected, effective
memory fields (effectiveUsed, effectiveFree, effectiveUsagePercent) are
computed and exposed through /api/system/stats and /api/fleet/overview.

All consumers that derive meaning from host memory now prefer effective
values when present: the dashboard gauge, Fleet card RAM bar, mobile
views, health verdict, health status bar stat tile, and host RAM alerts.

Backward compatible: missing /proc/meminfo or absent Balloon: line
preserves exact current behavior. Old remote nodes without the new fields
continue rendering normally.

* refactor: extract shared helpers for balloon memory wiring

Extract readCandidateFile() and logSelectedPath() in hostMemory.ts to
deduplicate ARC and balloon file-read logic. Add memoryToWire() to
centralize the optional-field spread used by /api/system/stats and
/api/fleet/overview. Add getNodeMemUsed()/getNodeMemTotal() helpers
in nodeUtils.ts for frontend byte-text consumers.

* fix: make desktop fleet masthead aggregate balloon-aware

The desktop fleet overview's memory aggregate in useFleetOverview.ts still
summed raw memory.used, while the mobile fleet aggregate and per-node cards
already used effective values. Update to use getNodeMemUsed/getNodeMemTotal
helpers.

* fix: revert balloon adjustment from alerting and health decisions

Ballooned memory is host-reclaimed (unlike ZFS ARC, which the guest can
reclaim on demand). The guest cannot get ballooned pages back until the
hypervisor deflates them, so treating ballooned memory as available for
alerting or health can mask real memory pressure.

Keep balloon parsing, wire fields, and the dashboard context line as
informational-only. The memory gauge, health verdict, and host RAM alerts
now use the standard ARC-adjusted working-set percentage regardless of
balloon. Updated configuration.mdx and dashboard.mdx to document that
balloon data is informational and does not influence alerting.
2026-08-02 20:48:38 -04:00
Anso a74905ff1e fix(auth): align WebSocket token_version checks with HTTP (#1744)
* test(auth): cover legacy no-tv JWT rejection on WebSocket upgrades

* fix(auth): treat missing tv claim as token_version 1 on WebSocket upgrades
2026-08-02 17:13:44 -04:00
Anso ba017ee665 fix: forward scoped node-admin permission for Settings writes through remote proxy (#1748)
* fix: forward scoped node-admin permission for Settings writes through remote proxy

The proxy forwards only the user's global role via PROXY_ROLE_HEADER
to remote nodes. Scoped role assignments live only on the hub's
role_assignments table and are never transmitted, so a scoped Node
Admin could not save settings on their granted remote node through
the hub proxy.

Add a settings-write pre-authorization gate in runGatedProxy that
buffers the body, extracts required permission buckets from
SETTING_WRITE_PERMISSIONS, checks them hub-side, and elevates
PROXY_ROLE_HEADER to 'node-admin' when the scoped check passes.
The gate is fail-closed: empty or unparseable bodies require
checkNodeManage on the hub, matching the existing
requireSettingsWritePermission empty-keys branch.

Fixes the gate-parity gap where scoped node:manage worked locally
but not through the proxy for Settings writes.

* fix: remove unused UserRole import from remoteNodeProxy.ts

* test(self-update): poll instead of a fixed delay in triggerUpdate assertion

The 600ms sleep raced the route's 500ms post-response timer plus the
persist/watch work executeClaimedCommunityUpdate does before calling
triggerUpdate, leaving too little margin under CI's forked test pool.
Poll with vi.waitFor instead, matching the pattern already used
elsewhere in this suite.
2026-08-02 03:00:09 -04:00
Anso 7c02f6eeb5 feat: add stack:read permission gate to Settings Labels section (#1747)
The Labels section registry entry had no requiredPermission, leaving it
visible to every authenticated operator. The backend already enforces
stack:read on GET and stack:edit on write endpoints, and the frontend
component already hides edit controls behind can('stack:edit'). Adding
stack:read to the registry declares the contract explicitly.

All five built-in roles hold stack:read, so this has no observable
effect on current users. It becomes a functioning gate automatically
if a future role or scoped user type is introduced without the permission.
2026-08-02 02:03:57 -04:00
Anso 209c9c5d53 feat: add target-aware RBAC authorization to Scheduled Operations (#1745)
* feat: add target-aware RBAC authorization to Scheduled Operations

Replace the blanket requireAdmin gate on all 9 scheduled-tasks endpoints
with per-action permission checks derived from the centralized action
registry. Each scheduled action now declares the existing permission it
requires: stack lifecycle actions need stack:deploy on the target stack,
node-wide operations need node:manage on the target node, prune stays
admin-only via system:settings, and snapshot requires unscoped
node:manage.

Key changes:
- Backend registry: add permission field and resolveTaskPermissionScope
- Routes: replace requireAdmin with requireTaskPermission, filter GET
  listing by permission, add two-phase PUT check
- Scheduler: revalidate creator permission at execution time, auto-disable
  on revocation (TaskAuthorizationError)
- Database: new creator_user_id column with migration and backfill
- Frontend: canScheduleAction/canScheduleAny helpers, reachability gate
  via scheduledOpsAccessible, stack context menu uses canDeploy
  not isAdmin, permissions field on all ScheduledActionDefinitions
- Expose checkPermissionForSubject for in-process callers (scheduler)

No new PermissionAction values are added. Uses the existing stack:deploy,
node:manage, and system:settings matrix. Scoped Admiral grants work on the
exact (nodeId, stackName) target per SEN-438.

* test: add RBAC coverage for scheduled operations authorization

* test: update frontend tests for scheduled-ops RBAC gate changes

- useStackMenuItems: gate Schedule task on canDeploy not isAdmin;
  add test for canDeploy=true, non-admin case
- buildNavigationModel: default scheduledOpsAccessible to true
  (default ctx is admin, who can always schedule)
- useViewNavigationState: add stack:deploy to admin can() mocks
  so canScheduleAny resolves correctly

* fix: keep checkPermission unchanged, add checkPermissionForSubject standalone

The earlier refactoring that made checkPermission delegate to
checkPermissionForSubject changed the call order of effectiveTier(req)
relative to the admin bypass, which subtly broke the Community tier
clamping in the audit-log route. Keep checkPermission byte-identical
to the original and expose checkPermissionForSubject as a standalone
function used only by the scheduler revalidation path.

* fix: prefix unused selectorType parameter in resolveTaskPermissionScope

* fix: address audit findings — existence oracle, revalidation test, action filtering

Three corrections from the independent PR audit:

RC-1 (action filtering): Wire canScheduleActionAnywhere into the action
picker in ScheduledOperationsView so actions the user can never schedule
(prune without system:settings, node:manage actions without a scoped
grant) are filtered from the picker entirely. Add canScheduleAction
check on the Create button against the currently selected target, so
the submit button is disabled when the caller cannot schedule the
chosen action on the chosen target.

RC-2 (existence oracle): Six by-ID endpoints (GET /:id, DELETE /:id,
PATCH /:id/toggle, POST /:id/run, GET /:id/runs/export, GET /:id/runs)
now return a uniform 404 when permission is denied on an existing task,
so an unauthorized caller cannot distinguish "task does not exist" from
"task exists but you are not authorized." Added
requireTaskExistsPermission helper for the 404 variant; POST create and
PUT merged-scope checks keep requireTaskPermission (403).

RC-3 (revalidation test): Fixed the orphan-creator test to assert the
post-execution task state (auto-disabled, explicit error message) rather
than wrapping executeTask in a try/catch with a no-op else branch, since
executeTask catches TaskAuthorizationError internally and returns
normally.

Added canScheduleActionAnywhere helper and AuthContext mock to
ScheduledOperationsView tests (38/38 pass).

* fix: remove unused TaskAuthorizationError import from test

* fix: use 404 on PUT phase-1 unauthorized-access check

The PUT two-phase check's first phase (ownership verification)
now uses requireTaskExistsPermission (404) instead of a manual
403, consistent with the six other by-ID endpoints. This prevents
a caller from probing task-ID existence via the PUT route.

* fix: address QA findings — reorder prechecks, surface permission reason

Two live-confirmed fixes from the 3-node fleet QA pass:

Finding #4 (offline-node error ordering): Swap the order of the node-
reachability precheck and the creator-permission revalidation in
SchedulerService.executeTask. Authorization now runs first, so a
revoked grant is always surfaced as an explicit auto-disable with a
clear error message, even when the target node is offline. Previously
the reachability check ran first, hiding the revocation behind a
misleading "target node is offline" error and leaving the task enabled
indefinitely while the node was down.

Finding #7 (unexplained Save dead-end): When the Create/Update button
is disabled because canScheduleAction denies the selected action-target
combination, a muted text line now appears below the button:
"You do not have permission to schedule this action on the selected
target." This gives scoped users meaningful feedback instead of a
silently disabled button with no explanation.

* chore: remove redundant !!formName guards

The earlier short-circuit conditions in isSaveDisabled and
saveDisabledReason already guarantee formName is truthy by the
time the canSaveWithCurrentTarget check runs. The !!formName
guard is a no-op — flagged by GitHub code-quality as 'Useless
conditional: This negation always evaluates to true.'
2026-08-02 01:15:37 -04:00
Anso 2e2b095b00 fix(proxy): forward scoped stack evidence for alerts, auto-heal, and node-wide image refresh (#1749)
* fix(proxy): forward scoped stack evidence for alerts, auto-heal, and node-wide image refresh

Extend the remote proxy scoped-evidence mechanism beyond /stacks/*
routes. Three new gates in runGatedProxy:

- Alerts POST: reuse the already-buffered body from the existing
  isAlertCreateRoute block, extract stack_name, check hub-side
  scoped permission, and forward SCOPED_STACK_AUTH_EVIDENCE headers.
- Auto-heal POST: same pattern with new body buffering and encoding
  rejection (no pre-existing buffering exists for this route).
- Node-wide image refresh: elevate PROXY_ROLE_HEADER to node-admin
  when the user has a scoped node:manage grant on the target node,
  matching the Settings pre-auth gate pattern.

Also extend classifyStackApiPath to recognize
/image-updates/refresh/:stackName as a named-stack route (stack:deploy),
ready for when PR #1743 adds the per-stack refresh endpoint.

Explicitly excluded: ID-based routes (DELETE /alerts/:id,
PATCH/DELETE /auto-heal/policies/:id) where the hub cannot resolve
remote-owned IDs to stack names; GET routes where stack:read is
globally granted to every role; and POST /auto-update/execute where
multi-stack/wildcard targets need a different evidence format.

* chore(proxy): add RBAC diagnostic logging to scoped permission path

Add developer_mode-gated diagnostic logs to checkPermission to expose
which check is failing when a scoped user is denied: effective tier,
DB query parameters, and node-scoped assignment lookups.

* chore(rbac): log effective tier and license status when scoped checks are blocked

Add an always-visible console.warn in checkPermission when the
effective tier prevents scoped role-assignment lookups, logging
both the resolved tier and the raw license_status DB value. This
surfaces the failure reason in container logs without requiring
developer_mode, so QA can diagnose why scoped users are denied.

* chore(rbac): sanitize scoped-tier log values to satisfy CodeQL log-injection check
2026-08-01 23:37:35 -04:00
Anso 15801318d6 fix(rbac): permission-gate alerts, auto-heal, and image updates (#1743)
* fix: gate alerts and auto-heal routes on stack:edit/stack:read permissions

Replace requireAdmin with requirePermission across backend/src/routes/alerts.ts
and backend/src/routes/autoHeal.ts, mirroring the stack:read/stack:edit model
already used by stacks, blueprints, git sources, and settings. Adds the
previously-missing permission gate on the auto-heal history route, and adds
ownership-aware deletion for alerts via a new DatabaseService.getStackAlert(id)
lookup.

* fix: gate image-update fleet, per-stack refresh, and auto-update execute on RBAC permissions

Replace requireAdmin with requirePermission/checkPermission across
backend/src/routes/imageUpdates.ts (imageUpdatesRouter and autoUpdateRouter),
mirroring the permission-aware model already used by alerts and auto-heal.

GET /fleet drops its admin gate to match the auth-only read model shared with
GET / and /detail. POST /fleet/refresh now requires node:manage. A new route,
POST /refresh/:stackName, lets a caller with stack:deploy on that stack trigger
a per-stack recheck, distinct from the node-wide POST /refresh. The auto-update
executor now pre-checks stack:deploy across every resolved target before any
work starts, so a denied stack in a bulk request fails the whole call instead
of partially executing; the "*" wildcard additionally requires global
stack:deploy up front since it expands to every stack on the node, including
the empty case where a per-stack check would otherwise have nothing to gate.

* fix: evaluate permission before checks-enabled state in auto-update execute

The checks-enabled short-circuit in autoUpdateRouter POST /execute ran before
target parsing and before any permission check, so a node with image-update
checks disabled returned 200 to any authenticated caller regardless of
stack:deploy grants. Move the checks-enabled check to run after the resolved
stackNames have cleared requireExactStacks, so permission is always evaluated
first.

Add coverage: a denied role still gets 403 PERMISSION_DENIED (not the
disabled-checks 200) while checks are disabled node-wide, and a scoped-only
user whose stack:deploy grant covers every stack on the node is still denied
target="*" (the wildcard requires global stack:deploy, per the earlier fix),
proving that tradeoff against a real on-disk stack rather than the always-
empty fresh test instance.

* fix: gate alerts, auto-heal, and image-update controls on frontend permission checks

Match the backend RBAC gates for alerts, auto-heal, and per-stack image
updates with matching frontend checks, replacing raw isAdmin/node:manage
gates with scoped can() calls:

- Alerts/Auto-Heal menu items and their keyboard shortcuts now gate on
  stack:read (canViewMonitor), including the window-level keyboard
  shortcut handler that previously bypassed the menu item gate entirely.
- Check updates now gates on stack:deploy (previously node:manage) and
  calls the new per-stack POST /image-updates/refresh/:stackName
  endpoint instead of the node-wide refresh. Since the endpoint runs the
  recheck synchronously and returns the result directly, the old
  node-wide /status polling loop is removed in favor of handling the
  response inline.
- StackAlertSheet's alert and auto-heal policy mutation controls gate on
  stack:edit instead of isAdmin.
- The Fleet Image Updates refresh button (mobile and desktop) gates on
  node:manage, hidden rather than disabled to match the existing
  convention for node:manage-gated affordances.

* fix: cover the stack:edit deny path for StackAlertSheet gates

The useAuth mock in StackAlertSheet.test.tsx returned can: () => true
unconditionally, so canEditAlerts, canEditAutoHeal, and PolicyRow's
canEdit prop were never exercised with a denial. Make the mock
per-test-controllable (matching the vi.fn() pattern already used in
NodeCard.test.tsx) and add one deny-path test per tab asserting the
mutation controls are absent while reads stay visible.

Also adds an aria-label to the alert row's delete button so the deny
test can assert on its absence, matching the aria-label convention
PolicyRow's own toggle/delete controls already use.

* fix: surface accurate warnings and loading feedback on stack update checks

checkUpdatesForStack ignored the backend's StackRecheckResult outcome
and always showed a success toast, even when verification failed or
an update is still present. It also gave no feedback while the
multi-second per-image registry probe was in flight.

Add a loading toast on request start, and branch the result toast on
outcome/warning instead of unconditional success. The backend reuses
its post-update reconciliation copy for this pre-update discovery
check, so the two generic "update command completed" strings are
replaced with accurate pre-update wording; a genuine stack-specific
warning (e.g. a compose render failure) is still shown as-is.

Also update docs/features/rbac.mdx: stack:edit now covers alert and
auto-heal management, stack:deploy covers per-stack image-update
checks, and the Deployer role description reflects both.

* fix: add per-stack cooldown rate limit for image-update recheck route

The per-stack POST /refresh/:stackName route bypassed the existing
node-wide manual-refresh cooldown. A caller with stack:deploy could
hammer the registry with unbounded concurrent recheck calls.

Add tryMarkStackRecheck in ImageUpdateService, sharing the same
2-minute cooldown window, keyed per (nodeId, stackName). The route
handler returns 429 when denied. The mark is written synchronously
before the first await so concurrent calls on the same tick are blocked.
2026-08-01 21:07:21 -04:00
Anso 5cc4566eb1 test(rbac): add five-role parity coverage across proxy, WebSocket, and mixed-version surfaces
Adds a reusable five-role persona fixture and extends RBAC test matrix to cover proxy, WebSocket, Pilot-agent, and mixed-version transport surfaces.
2026-07-31 23:06:57 -04:00
Anso 8ac4876ad5 Add Scorecard workflow for supply-chain security 2026-07-31 22:54:36 -04:00
Anso ce4b91e90f fix(auth): gate scoped JWTs on container-exec WebSocket (#1741)
Partial-auth (mfa_pending) and enroll-only (pilot_enroll) tokens skipped
the admin check on /ws because any set scope was treated as already gated.
Reject those scopes in the shared upgrade pipeline, and deny unknown scopes
on the generic path while allowing api_token, console_session, and pilot_tunnel.
2026-07-30 18:21:59 -04:00
Anso a1e2846d7d feat: expose Community audit log via system:audit navigation (#1740)
* feat(rbac): make Settings authorization permission-aware

Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected.

* fix(rbac): tighten settings permission buckets and tests

Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes.

* fix(rbac): tighten Settings scoped grants and CI assertions

Empty settings PATCH fails closed, node:manage is scoped to the active
node, system-only Settings stay hidden without system:settings, and
Check updates / webhooks mutate gates follow the permission matrix.

* fix(rbac): defer Settings section fallback until authz is ready

Keep deep links to permission-gated sections (e.g. license) intact while
can() is still fail-closed during permission metadata load.

* feat: expose Community audit log via system:audit navigation

Gate the Audit view on the system:audit permission instead of paid tier,
so Community admins can open the existing 14-day recent-activity window.
Export, anomaly flags, and stats remain Admiral-only.

* test: clarify synthetic Community admin mock lacks system:audit

Document that mockCommunityAdmin is a gate-isolation helper, not the
real Admin permission matrix where system:audit is always present.
2026-07-30 12:50:55 -04:00
Anso a3026f47a8 feat(rbac): make Settings authorization permission-aware (#1738)
* feat(rbac): make Settings authorization permission-aware

Align Settings visibility and mutations with the existing permission matrix so Node Admin can edit node-scoped operational settings while system and credential surfaces stay Admin-protected.

* fix(rbac): tighten settings permission buckets and tests

Collapse settings key permission maps into one source of truth, and cover mixed PATCH atomicity plus image-update enabled writes.

* fix(rbac): tighten Settings scoped grants and CI assertions

Empty settings PATCH fails closed, node:manage is scoped to the active
node, system-only Settings stay hidden without system:settings, and
Check updates / webhooks mutate gates follow the permission matrix.

* fix(rbac): defer Settings section fallback until authz is ready

Keep deep links to permission-gated sections (e.g. license) intact while
can() is still fail-closed during permission metadata load.

* docs(settings): clarify Notifications channels vs routing authz

Channels use node:manage via /api/agents; routing and mute stay Admin-only.
2026-07-30 10:25:13 -04:00
Anso c704cb54d2 fix(rbac): enforce operational permission parity (#1736) 2026-07-29 22:02:53 -04:00
Anso a65bf4d46a fix(rbac): handle permission metadata failures (#1735) 2026-07-29 15:51:27 -04:00
Anso 44d6078241 feat(fleet): show itemized prune plans (#1734)
* feat(fleet): itemize prune review plans

Build and display fingerprint-bound prune candidates for every reviewed
fleet node. Preflight all node plans before mutation and preserve detailed
removed, skipped, failed, and partial outcomes.

Add safe resource metadata projection, managed ownership attribution,
runtime contract validation, transport parity coverage, and operator docs.

Closes #1724

* fix(security): harden stack path lookup

Use a Map for Compose working-directory ownership resolution so untrusted
path strings cannot become object property writes.

* fix(fleet): harden prune execution safeguards
2026-07-29 14:30:18 -04:00
dependabot[bot] 538a41b771 chore(deps): bump the all-npm-frontend group in /frontend with 25 updates (#1733)
* chore(deps): bump the all-npm-frontend group

Bumps the all-npm-frontend group in /frontend with 25 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.20` | `1.1.23` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.8` | `1.3.11` |
| [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/context-menu) | `2.3.4` | `2.3.7` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.20` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.21` | `2.1.24` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.20` | `1.1.23` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.12` | `2.1.15` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.20` | `1.1.23` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.15` | `1.2.18` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.4` | `2.3.7` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.12` | `1.1.15` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.4` | `1.4.7` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.0` | `1.3.3` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.18` | `1.1.21` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.13` | `1.2.16` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.25.0` | `1.27.0` |
| [monaco-editor](https://github.com/microsoft/monaco-editor) | `0.55.1` | `0.56.0` |
| [motion](https://github.com/motiondivision/motion) | `12.42.2` | `12.43.0` |
| [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.6.4` | `1.6.7` |
| [recharts](https://github.com/recharts/recharts) | `3.10.0` | `3.10.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.1.2` |
| [eslint](https://github.com/eslint/eslint) | `10.7.0` | `10.8.0` |
| [globals](https://github.com/sindresorhus/globals) | `17.7.0` | `17.8.0` |
| [jsdom](https://github.com/jsdom/jsdom) | `29.1.1` | `30.0.1` |
| [typescript](https://github.com/microsoft/TypeScript) | `6.0.2` | `7.0.2` |


Updates `@radix-ui/react-alert-dialog` from 1.1.20 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-checkbox` from 1.3.8 to 1.3.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-context-menu` from 2.3.4 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/context-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/context-menu)

Updates `@radix-ui/react-dialog` from 1.1.20 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.21 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-hover-card` from 1.1.20 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card)

Updates `@radix-ui/react-label` from 2.1.12 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-popover` from 1.1.20 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.15 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.4 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.12 to 1.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slider` from 1.4.4 to 1.4.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-slot` from 1.3.0 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-tabs` from 1.1.18 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-tooltip` from 1.2.13 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `lucide-react` from 1.25.0 to 1.27.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.27.0/packages/lucide-react)

Updates `monaco-editor` from 0.55.1 to 0.56.0
- [Release notes](https://github.com/microsoft/monaco-editor/releases)
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.55.1...v0.56.0)

Updates `motion` from 12.42.2 to 12.43.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.42.2...v12.43.0)

Updates `radix-ui` from 1.6.4 to 1.6.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/1.6.7/packages/react/radix-ui)

Updates `recharts` from 3.10.0 to 3.10.1
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.10.0...v3.10.1)

Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.7.0 to 10.8.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0)

Updates `globals` from 17.7.0 to 17.8.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.7.0...v17.8.0)

Updates `jsdom` from 29.1.1 to 30.0.1
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.0.1)

Updates `typescript` from 6.0.2 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-context-menu"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-hover-card"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: lucide-react
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: monaco-editor
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: motion
  dependency-version: 12.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: radix-ui
  dependency-version: 1.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: recharts
  dependency-version: 3.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: globals
  dependency-version: 17.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: jsdom
  dependency-version: 30.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-frontend
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-frontend
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): use Monaco's public worker entrypoint

* fix(deps): retain TypeScript 6 compatibility

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: SaelixCode <dev@saelix.com>
2026-07-29 12:58:43 -04:00
Anso 3c934066f3 fix(networking): ignore verified Mesh attachments in drift (#1729) 2026-07-29 12:55:30 -04:00
dependabot[bot] 55644cf87d chore(deps): bump the all-npm-backend group in /backend with 9 updates (#1731)
* chore(deps): bump the all-npm-backend group in /backend with 9 updates

Bumps the all-npm-backend group in /backend with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `13.0.1` | `13.0.2` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.6.0` | `8.6.1` |
| [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) | `1.38.10` | `1.40.0` |
| [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.33.0` | `5.33.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.1.2` |
| [eslint](https://github.com/eslint/eslint) | `10.7.0` | `10.8.0` |
| [typescript](https://github.com/microsoft/TypeScript) | `6.0.2` | `7.0.2` |
| [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1092.0` | `3.1097.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1092.0` | `3.1097.0` |


Updates `better-sqlite3` from 13.0.1 to 13.0.2
- [Release notes](https://github.com/WiseLibs/better-sqlite3/releases)
- [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v13.0.1...v13.0.2)

Updates `express-rate-limit` from 8.6.0 to 8.6.1
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.6.0...v8.6.1)

Updates `isomorphic-git` from 1.38.10 to 1.40.0
- [Release notes](https://github.com/isomorphic-git/isomorphic-git/releases)
- [Commits](https://github.com/isomorphic-git/isomorphic-git/compare/v1.38.10...v1.40.0)

Updates `systeminformation` from 5.33.0 to 5.33.1
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.33.0...v5.33.1)

Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.7.0 to 10.8.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0)

Updates `typescript` from 6.0.2 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

Updates `@aws-sdk/client-ecr` from 3.1092.0 to 3.1097.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1097.0/clients/client-ecr)

Updates `@aws-sdk/client-s3` from 3.1092.0 to 3.1097.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1097.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: better-sqlite3
  dependency-version: 13.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: express-rate-limit
  dependency-version: 8.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: isomorphic-git
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: systeminformation
  dependency-version: 5.33.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1097.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1097.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): retain TypeScript 6 compatibility

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: SaelixCode <dev@saelix.com>
2026-07-29 12:53:32 -04:00
dependabot[bot] 754d8af8f8 chore(deps): bump the all-actions group across 1 directory with 2 updates (#1732)
Bumps the all-actions group with 2 updates in the / directory: [docker/login-action](https://github.com/docker/login-action) and [actions/stale](https://github.com/actions/stale).


Updates `docker/login-action` from 4.4.0 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...dbcb813823bdd20940b903addbd779551569679f)

Updates `actions/stale` from 10.4.0 to 11.0.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/1e223db275d687790206a7acac4d1a11bd6fe629...4391f3da665fdf50b6810c1a66712fb9ba21aa93)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: actions/stale
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 12:29:46 -04:00
dependabot[bot] 8deea8af7e chore(deps-dev): bump @playwright/test in the all-npm-root group (#1730)
Bumps the all-npm-root group with 1 update: [@playwright/test](https://github.com/microsoft/playwright).


Updates `@playwright/test` from 1.61.1 to 1.62.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-root
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 12:29:43 -04:00
Anso 9922d8e765 feat(rbac): make stack-scoped grants node-specific (#1727)
* feat(rbac): make stack-scoped grants node-specific

Qualify stack role assignments as (nodeId, stackName), migrate legacy rows to the default node, and forward bound multi-action evidence on Proxy/Pilot hops so scoped users keep least-privilege remote access without shipping the full grant table.

* fix: mirror scoped-stack-auth-evidence capability to frontend, sanitize node id in role assignment log

Backend added the scoped-stack-auth-evidence capability without the
matching frontend entry, failing the capability parity test. The role
assignment log also interpolated the node id without sanitizeForLog,
unlike the rest of the line.

* fix(rbac): honor node-wide scopes and fix proxied DELETE cleanup

Node-scoped grants now authorize that role's stack actions on the same node in the backend resolver, frontend can(), and remote evidence. Proxied DELETE cleanup uses the gate-stashed route because pathRewrite mutates req.path before proxyRes. Add proxy integration coverage and drop the stale scoped-permissions screenshot.

* fix(rbac): preserve node-qualified grants during repair
2026-07-29 09:42:14 -04:00
Anso d698eb46f9 fix: bump golang.org/x/text to v0.39.0 in Docker CLI and Compose builds (#1728)
CVE-2026-56852 (norm.Iter infinite loop on crafted input) affects the
transitive x/text dependency in both the docker CLI and docker-compose
binaries bundled into the image.
2026-07-28 18:14:31 -04:00
Anso b0b423b234 feat(fleet): reapply Compose configuration without a version update (#1716)
* feat(fleet): reapply Compose configuration without a version update

Add a distinct Fleet Reapply configuration path so Compose-managed nodes can recreate Sencho from the current on-disk project when already up to date, without pulling or rewriting the image reference.

* fix(fleet): confirm remote reapply and close concurrent tracker race

Require confirmation for remote compose reapply, and lock dispatch before the remote POST so a second request cannot overwrite a successful in-flight tracker.

* fix(ui): icon-only Reapply control so Up to date badge can breathe

Collapse the Node updates Reapply label into a tooltip so the status pill no longer wraps in the Status column.

* feat(editor): Save & Reapply self-stack via fleet compose reapply (#1726)

* feat(editor): Save & Reapply self-stack via fleet compose reapply

Eligible admins can apply on-disk Compose edits to Sencho's own stack from the editor using the same confirm, dispatch, and reconnect path as Fleet Node Updates.

* fix(editor): gate Save & Reapply label to self-stack only

Ordinary stacks were labeled Save & Reapply whenever the node was
reapply-eligible. Require the selected file to be the self-stack for the
toolbar label and diff confirm CTA.

* fix(ui): move compose diff action label helper out of dialog module

Keep ComposeDiffPreviewDialog component-only so react-refresh Fast Refresh
lint passes after the Save and reapply stacked merge.
2026-07-28 14:26:46 -04:00
Anso 543e4ef256 feat(compose-doctor): show All Clear when findings are acknowledged (#1721)
When every finding is acknowledged, the Doctor summary uses the success All Clear banner with distinct copy instead of the muted acknowledged chip, so operators can see at a glance that no active findings remain.
2026-07-28 14:26:16 -04:00
Anso 78475d96ef fix(compose-doctor): resolve effective healthcheck coverage (#1713)
* fix(compose-doctor): resolve effective healthcheck coverage

Compose Doctor now classifies healthcheck coverage from the Compose model, running containers, and local images so image-provided HEALTHCHECKs are not false positives. Update Guard shares the same presence helper so test NONE is not treated as active.

* fix(compose-doctor): fix healthcheck project label and empty compose HC

Use the Compose project name for runtime container listing so stacks whose name: differs from the directory still get runtime evidence. Treat empty or timing-only healthcheck objects as absent rather than active.

* fix(compose-doctor): treat inherited healthcheck as All Clear note

Inherited image healthchecks no longer block All Clear; they surface under a notes section and cannot be acknowledged.
2026-07-28 14:26:00 -04:00
Anso c90e9606f1 fix(dashboard): scope alert-rule count to stacks on the active node (#1722)
Intersect stack_alerts with FileSystemService.getStacks() so Configuration
Status (dashboard and fleet local row) counts only rules for stacks that
exist on that node, not orphaned names in the global table.
2026-07-28 14:17:53 -04:00
Anso 72cdbb0eaa feat(schedules): auto-update stacks by Stack Label (#1717)
* feat(schedules): auto-update stacks by Stack Label

Add a reusable selector_type/selector_value on scheduled tasks so admins
can schedule image updates against live Stack Label membership across the
fleet or one node, reusing fleet label resolution and the existing
auto-update orchestrator.

* fix(image-updates): sanitize auto-update execute failure logs

Use a static format string and sanitizeForLog so CodeQL no longer
flags user-controlled stack names and error text in the execute catch.

* fix(ui): space Scope label from fleet/node segmented control

Match the Schedule row layout so the inline SegmentedControl no longer
sits flush against the Scope label.

* fix(ui): remove redundant wrapper around Scope segmented control
2026-07-28 13:00:47 -04:00
Anso fa503ddf27 feat: add node-scoped opt-out for image update detection (#1715)
* feat: add node-scoped opt-out for image update detection

Operators who use an external update authority can disable Sencho registry
polling per node without losing explicit stack Update, pull, or redeploy.

* test: fix mocks and lint for image-update checks opt-out

Scheduler tests need isChecksEnabled on the ImageUpdateService mock, and the UpdatesSection older-node fixture must not leave an unused binding.

* fix: gate update-preview and recheck when detection is off

Anatomy was still calling stack update-preview (and contacting registries)
while checks were disabled. Short-circuit those routes and skip recheckStack
writes so disabled nodes stay quiet until detection is re-enabled.
2026-07-28 10:10:04 -04:00
Anso e175db8e62 feat(auth): add SSO-only authentication mode (#1714)
* feat(auth): add SSO-only authentication mode

Let administrators disable interactive local password login when SSO is configured, with backend enforcement, activation safeguards, and host CLI recovery.

Closes #1709

* fix: resolve CI failures in auth mode PR

- Add useLicense mock to SSOSection test to prevent crash from
  AuthenticationModePanel rendering without LicenseProvider
- Remove username from authMode console.log calls that CodeQL flags
  as clear-text logging of sensitive information

* fix(auth): keep SSO-only on named disableSso and fail-closed login

Named provider disable no longer reverts authentication_mode. Login initializes localLoginEnabled false so a status fetch failure cannot reveal the password form. Center a single OIDC provider button on the login card.

* fix(auth): move SSO-only authentication mode from Admiral to Community tier

Security-hardening features belong on the Community tier per the existing
Community rebalance. The reporter of #1709 noted that disabling local
password login after configuring SSO is a basic security measure, not an
enterprise governance feature. LDAP provider configuration remains
Admiral-gated via requireTierForSsoProvider.

* fix(ui): keep SSO Active badge and ON toggle in sync

Provider cards mounted before config fetch finished with enabled:false, so a saved Active provider showed OFF until the local draft was resynced. Drive both the badge and TogglePill from the synced local config.

* feat(auth): auto-redirect to sole OIDC provider under SSO-only

When authentication mode is SSO only and exactly one OIDC provider is enabled (no LDAP), skip the login chooser and send the browser to that provider's authorize URL. Returning sso_error stays on the login page so the failure message remains visible.

* fix(ui): move oidcAutoRedirectUrl out of Login for fast refresh

Exporting the helper alongside the Login component tripped react-refresh/only-export-components and failed Frontend lint CI. Keep Login as a component-only module and colocate the helper with its unit tests under lib/.
2026-07-28 10:01:05 -04:00
Anso 60092dd462 feat: live-refresh stack detail container and health state (#1712)
* feat: live-refresh stack detail container and health state

Keep the open stack's container cards in sync with Docker via state-invalidate events and a visibility-aware poll, without reloading compose, env, or logs.

* fix: remove unused _ms parameter from visibilityInterval mock

Fixes the @typescript-eslint/no-unused-vars ESLint error in CI lint job.

* fix: stop stack detail live-refresh when leaving the editor

Gate poll and invalidate handling on editor visibility, refresh the
current selection after a mid-flight stack switch, and skip starting
visibilityInterval when the tab is already hidden.

* fix: avoid return in finally for stack detail live-refresh

Satisfy no-unsafe-finally by gating the trailing refresh with a positive
condition instead of early returns inside the finally block.
2026-07-28 09:24:18 -04:00
Anso 681ecc7047 fix(auth): keep active sessions alive and add stay-signed-in (#1711) 2026-07-28 07:36:50 -04:00
Anso 2d88d9f8a8 ci: recategorize credit-changelog-contributors fix as non-releasable (#1720)
* Revert "fix: credit issue openers linked to a PR without closing text (#1718)"

This reverts commit ea38113194.

* ci: credit issue openers linked to a PR without closing text

The contributor-credit script only ever scanned CHANGELOG.md text for
issue/PR numbers. An issue linked to a PR only through GitHub's
Development sidebar (no typed "closes #N" anywhere) never produces
any text to scan, so its external opener was silently never credited.

Query the GitHub GraphQL API for each changelog PR's closingIssuesReferences
and credit those openers under the PR's own visible bullet.
2026-07-27 10:38:03 -04:00
Anso ea38113194 fix: credit issue openers linked to a PR without closing text (#1718)
The contributor-credit script only ever scanned CHANGELOG.md text for
issue/PR numbers. An issue linked to a PR only through GitHub's
Development sidebar (no typed "closes #N" anywhere) never produces
any text to scan, so its external opener was silently never credited.

Query the GitHub GraphQL API for each changelog PR's closingIssuesReferences
and credit those openers under the PR's own visible bullet.
2026-07-27 10:17:48 -04:00