Commit Graph

73 Commits

Author SHA1 Message Date
Anso c704cb54d2 fix(rbac): enforce operational permission parity (#1736) 2026-07-29 22:02:53 -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
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 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 cfb42af4e0 fix: assorted UI/UX polish fixes (#1670)
* fix(dashboard): replace Stack Health update badge with an icon

The pill badge duplicated space already used by the stack name column.
A CircleArrowUp icon after the name signals an update is available
without competing with the existing ArrowUp/ArrowDown sort indicators
in the same table.

* fix(dashboard): add accessible name to update-available icon

Icon-only indicators need an aria-label directly on the icon; title on
a non-interactive span is not reliably announced by screen readers.

* test(dashboard): cover the update-available icon's accessible name

The icon-only indicator and its aria-label fix had no regression
guard, unlike the equivalent update dot in StackRow.

* refactor(dashboard): compute the update-available label once per row

It was being derived twice (title and aria-label) from the same
row.outdatedServices input.

* fix: drop Community-tier pricing upsells from settings

Community operators no longer see the "See pricing" link in Licensing
or the "Need direct support?" callout in Support. The pricing link now
only shows for an expired paid license needing to renew.

* fix: make Resources images/volumes tables actually scrollable

The tables were wrapped in a Radix ScrollArea sized with max-h-[62vh].
Radix's viewport uses height:100%, which cannot resolve against an
ancestor whose computed height is auto (max-height alone isn't a
definite height), so the viewport silently grew past the visible box
and the extra rows were clipped with no way to reach them. Verified
live: several image rows were permanently unreachable, with no working
internal scrollbar and not enough outer page scroll to compensate.

Switched to an explicit h-[62vh], which the viewport can resolve
correctly, matching every other working ScrollArea in the codebase.
Falls back to h-auto below the md breakpoint so the bespoke mobile
layout keeps shrinking to content and scrolling via the outer page
instead of gaining a fixed-height inner scroll box.

* fix: apply ScrollArea definite-height fix across remaining lists

Radix ScrollArea needs an explicit height, not max-height, or the
viewport collapses and clipped rows become unreachable. Extend the
Resources fix to security, settings, git, and create/import surfaces,
and drop redundant outer wrappers where ModalBody already scrolls.

* fix: migrate Networking tables to Radix ScrollArea

Networks and Findings used native max-h + overflow-auto, which worked
but broke glass scrollbar consistency with Resources and the design
system. Switch them to ScrollArea with a definite height and the same
mobile fallback as the other inventory tables.

* fix: warn Classic bar users that the style is retiring soon

When Appearance Navigation is set to Classic bar, show the same warn
SettingsCallout pattern used for Constrained graphics. Preference is
kept until removal; no alternate style is named in the copy.

* fix: move Channels delivery retries below channel tabs

Put channel configuration first and keep Delivery retries as a shared
footer control under the Discord/Slack/Webhook/Apprise tabs.

* fix: drop redundant More masthead from Smart bar overflow menu

The trigger already reads More, so the dropdown masthead repeated the
same label. Leave titled mastheads on Compact Navigate and Add quick
link menus.

* test: align Smart More E2E with masthead removal

The overflow menu no longer shows a More heading. Assert the menu via
the Logs item and lock that the redundant masthead stays gone.

* fix: consolidate Fleet Map toolbar filters into a single row

Adopt the same retractable search control used on Fleet > Overview and
move the flag filters (missing deps, port conflicts, orphans, shared)
onto the toolbar row right after the Graph/List selector. The node
filter becomes a dropdown instead of individual toggle chips so it
does not clutter the row as fleet size grows.

* fix: move Networking Topology filters onto the search toolbar row

Merge the ownership selector and boolean filter chips (include system,
exposed, drift, missing external, shared) onto the same row as the
stack/network search inputs, matching the Fleet Map toolbar layout.

* fix: default the reclaimable-space banner off

Resources > Docker & Storage's "Show reclaimable-space banner" toggle
now defaults to off instead of on. Also flips the /settings fetch
failure path to fail closed (hide the banner) to match the new
default, instead of failing open.

* fix: raise Compact launcher quick links cap from 5 to 7

* fix: add Discord link to Settings Support Self-serve

Gives users a community chat channel alongside Documentation and
GitHub Issues, using the official Discord mark since lucide-react
has no brand icon for it.

* fix: stop container NET I/O metric row height jump

Give NET I/O more column share than CPU/MEM and keep metric
values on one line with truncate so three-digit rates cannot
grow the strip.

* fix: elevate Doctor tab between Activity and Drift

Make Compose Doctor easier to find in the anatomy strip by placing
it with the ops judgment cluster, ahead of Dossier and inventory tabs.
2026-07-26 03:25:07 -04:00
Anso 719180f156 fix(fleet): verify update status before removing readiness cards (#1697)
* fix(fleet): verify update status before removing readiness cards

Full-stack Apply now rechecks persisted status after the health gate starts, reloads the live preview before dropping a card, and invalidates the hub fleet aggregation so cleared updates cannot resurrect from a stale cache.

Closes #1686

* fix(fleet): align persisted update status with preview semver detection

Share digest-plus-tag detection so post-Apply sidebar status matches Fleet and Anatomy.

* fix(fleet): keep tag-only updates advisory for Compose automation

Expose digestUpdate vs tagUpdate from checkImage so scheduled and API auto-update only apply same-tag digest drift Compose can pull.

* docs: clarify scheduled auto-update applies digest drift only

Document that higher pinned tags stay advisory until Compose is changed, matching schedule and Run Now behavior.

* docs: require Compose pin edits for higher-tag advisories

Stop recommending Apply now or Update as remedies that cannot rewrite a pinned image tag.

* docs: clarify Apply now pulls pinned tags only

Align the detection-cadence bullet with digest-rebuild vs higher-tag guidance.

* fix(fleet): keep tag advisories after apply and scheduled updates

Tag-only previews were treated as cleared on Fleet reload, and scheduled/
Run Now paths wiped status without rechecking. Align post-update verification
with the manual Apply path (health gate first, recheck, no blind clear) and
block digest apply when sibling image checks failed.

* fix(fleet): clear eslint unused-arg and containers assignment
2026-07-26 03:09:21 -04:00
Anso 6688da97b1 fix(image-updates): match any local RepoDigest against the remote tag (#1695)
* fix(image-updates): match any local RepoDigest against the remote tag

Docker can list a stale multi-arch index digest ahead of the current one
on the same image. Selecting only the first RepoDigest caused false
same-tag rebuilds (for example redis:8.8.0) even when another digest
equaled the registry primary. Compare every matching candidate and keep
fail-closed behavior for empty, unknown-platform, and classification errors.

Fixes #1684

* fix(image-updates): surface digest verification failures to operators

Carry comparator errors into update-preview as check_error / verification_failed,
prefer failed checks over sticky has_update in Fleet and the sidebar, and keep
Update Guard from claiming no pending update when verification failed.

* fix(e2e): align sidebar truncation spec with check-failed precedence

StackRow now shows the check-failed icon over a stale update dot, but
this spec still asserted the old precedence and failed deterministically
in CI on every attempt.

* fix(fleet): treat verification-only previews as non-actionable

Fresh update-preview wins over sticky fleet booleans: disable Apply, exclude
from ready counts, and move verification-only stacks into the check-failures
advisory (including remote-labeled names).

* fix(fleet): move preview actionability helpers out of the view

Exporting non-components from AutoUpdateReadinessView tripped react-refresh
lint in CI. Keep the helpers in a shared lib module and drop an unused mock arg.

* fix(fleet): drop sticky cards when fresh preview clears the update

A successful no-update preview now removes the pending Fleet card instead of
leaving Apply enabled. Verification-only stacks still go to the advisory, and
empty-state copy no longer claims all-clear while checks remain unresolved.

* fix(image-updates): hold full-stack apply for review when another image fails verification

A confirmed update or rebuild on one image previously left the whole stack
fully actionable even when a different image in the same stack failed digest
verification: Anatomy claimed "safe to apply", Update Guard reported ready,
and Fleet's full-stack Apply stayed enabled, all while showing the
verification-failure text right next to those claims.

isActionableUpdatePreview now requires no verification failure anywhere in
the stack; a new isReviewRequiredUpdatePreview flags the mixed state so Fleet
still surfaces the card (not silently cleared) with Apply now disabled and a
"Review · unverified" badge. Anatomy's banner says "review required" instead
of a bump-based safety claim and withholds its Apply button. Update Guard's
pending-update signal downgrades from ok to attention. Per-service apply
(Fleet's per-image row) is deliberately left enabled since a service-scoped
update to the confirmed image does not touch the unverified one.

* fix(image-updates): treat rebuild_available symmetrically with has_update in Update Guard

updatePreviewSignal only downgraded to 'attention' inside the has_update
branch, so a rebuild-only stack (has_update false, rebuild_available true)
with a sibling verification failure fell through to the plain
verification-only 'unknown' branch and never mentioned the pending rebuild,
inconsistent with isReviewRequiredUpdatePreview on the frontend which treats
has_update and rebuild_available the same way.

Also adds desktop-card coverage for the mixed state (previously only the
mobile card was exercised) and locks in blocked/major-bump precedence over
the new review-required badge/banner in both Fleet and Anatomy.

* fix(image-updates): derive the mixed-verification review-hold from per-image detail, not the stack aggregate

has_update and check_error are independent per image: a tag-based update can
be confirmed via the registry's tag list even when that same image's own
digest comparison against the current tag errored (already covered by an
existing update-preview-service test). The stack-level verification_failed
and has_update flags can therefore both be true for the SAME single image,
which the previous review-hold treated identically to a genuinely different
image failing verification: Update Guard said "another image failed digest
verification" and Fleet told the user to "apply the confirmed service
individually" on a single-service stack where no such affordance exists.

isReviewRequiredUpdatePreview (and isActionableUpdatePreview) now walk the
preview's images to require a pure failure image (check_error, no has_update
of its own) alongside a genuinely different confirmed image or rebuild,
falling back to the old aggregate-only judgment when per-image detail is
unavailable. StackAnatomyPanel now imports the shared helper instead of
hand-rolling the same predicate, so Fleet and Anatomy cannot drift apart.
Backend updatePreviewSignal gets the same per-image treatment via a new
optional images parameter, threaded through from UpdateGuardService.

* fix(image-updates): fail closed on platform-unavailable indexes and legacy previews, allow anonymous tag listing

Four independent gaps from the same QA pass, all in the digest/tag
verification path this PR introduced or touches:

- compareLocalToRemoteTag now distinguishes a remote index with no descriptor
  at all for the local platform (including an empty or fully-filtered index)
  from a genuine mismatch: the former returns an error instead of reporting a
  speculative update. A node cannot pull a platform the index does not offer.
- selectLocalRepoDigests no longer falls back to a sole unrelated-repository
  RepoDigest when nothing matches the configured repo; comparing against a
  registry state that has nothing to do with the declared image risks a
  false update. Returns unresolved instead of guessing.
- isClearedUpdatePreview no longer treats a preview with verification_failed
  missing entirely (not merely false) as proof the stack is clean. The
  current backend always includes this field, so its absence identifies an
  older remote node's response, which cannot vouch for a clean result the
  way an explicit false can.
- listRegistryTags (and the underlying listRegistryTagsResult) no longer
  short-circuits to an empty list whenever no registry credentials are
  configured. getAuthToken already resolves anonymous tokens for public
  repositories; skipping it meant tag-based update detection silently never
  fired for any public image without a stored credential.

* fix(image-updates): correct platform-check overreach, add cache and advisory for prior fixes

Addresses code-review findings on the previous commit:

- The platform-unavailable check fired too eagerly: an index whose runnable
  descriptors legally omit platform (OCI-permitted, routed to exactDigests)
  has real pullable content, so it must not be confused with a genuinely
  empty or fully-filtered index. Now only errors when both platform-labeled
  descriptors and exactDigests are empty.
- listRegistryTags is now cached (15 min TTL): anonymous listing has no other
  rate limiting, and Fleet fans this out across every image on every reload.
- A legacy preview (kept rather than cleared) now also pushes a check-failure
  advisory entry explaining why, instead of rendering as an unexplained
  pending card.
- Corrected docstrings that described the old sole-unmatched-digest fallback
  and inverted how anonymous registry auth actually resolves.

Adds coverage for: nested-index and attestation-only-filtered platform
unavailability, a platform-less-but-populated index staying a match, the
unrelated-repo digest rejection wired through the real preview-computation
path (not just the registry-api unit), and legacy-preview interaction with
an actionable has_update:true.

* fix(image-updates): fail closed on mixed platform indexes, stop caching tag-list failures

Addresses a second review round on the previous commit, including an
empirically-verified regression:

- The exactDigests fallback was unconditional: an index mixing a
  platform-labeled descriptor for a DIFFERENT platform with an unlabeled leaf
  let that leaf stand in as this platform's content, reporting a speculative
  update for a genuinely incompatible platform. Now an unlabeled leaf is only
  trusted when it is the ONLY kind of descriptor in the index (nothing else
  claims a different platform); a mixed index errors instead.
- listRegistryTags was caching failed lookups for the full 15-minute TTL
  (a 429, an unreachable registry, or credentials not yet configured all
  looked identical to a real empty tag list). The fetcher now throws on
  failure so only a success is ever cached; CacheService's existing
  stale-on-error fallback still serves the last good list when one exists.
- The manual "Recheck" action now also drops the tag-list cache, so a newly
  published tag is visible immediately instead of waiting out the TTL.
- The legacy-preview advisory no longer fires when the same preview is
  already actionable on its own terms (a remote's own confirmed
  has_update/rebuild_available): pairing a "could not be checked" banner
  with an enabled Apply button next to it contradicted itself.
- Corrected docstrings and a self-contradictory inline comment left over
  from the prior fix.

New coverage: the exact mixed-index regression this round found and fixed,
cache hit/no-repeat-fetch and failure-not-cached behavior, and the
legacy-preview-plus-already-actionable non-contradiction.

* fix(image-updates): add digest_error unmasked field, reorder RiskBadge, fix actionability gates

Add digest_error to UpdatePreviewImage as an always-populated field that
is independent of check_status masking: a confirmed tag-based update on
the same image resolves check_status to 'ok' and nulls check_error, but
digest_error stays set since the image's current tag content was never
verified. Switch hasUnverifiedOtherImage to read digest_error.

Reorder RiskBadge precedence so reviewRequired is checked before
uncertain (both derive from check_error, so uncertain was unreachable).

Fix self-contradiction in test fixtures (digest_update:true +
check_error). Add masked-tag regression test with two-image fixture.
Simplify hasUnverifiedOtherImage per code review feedback.
2026-07-25 21:57:30 -04:00
Anso 0daddfde00 fix: reconcile sticky update indicators with Anatomy preview (#1698)
* fix: reconcile sticky update indicators with Anatomy preview

Sidebar, Updates filter, and Fleet treated retained partial/failed
scanner has_update as confirmed. Keep raw state for retention/notifications,
project confirmed-only to APIs, show distinct incomplete indicators, and
clear sticky rows only after an authoritative-negative preview.

Closes #1685

* test: align sidebar truncate E2E with failed-over-retained precedence

Purple update indicators are confirmed-only; hasUpdate with a failed
check correctly shows the failed trailing icon.

* fix: clear confirmed update rows on authoritative-negative preview

Address audit SF-1/SF-2/SF-3: observation-watermark clears for older
ok+has_update rows (DB + memory gens), Fleet checkability parity with
backend not_checkable, and Updates chip confirmed-only regressions.

* fix: tombstone equal-generation writers on preview clear

Advance the per-stack write generation when clearing at the observation
watermark so a scanner reserved before preview cannot recreate the row
after an authoritative-negative reconcile.

* fix: clear sticky updates with digest and tag preview parity

Share detection across scanner and preview, keep GET read-only with POST reconcile, gate Apply to digest and rebuild updates, and invalidate the hub fleet cache on clear.

* test: set digestUpdate on auto-update checkImage mocks

Scheduler and execute routes now gate Compose on digest drift; fixtures that expect an apply need digestUpdate so they exercise the update path.

* fix: clear unused lint errors on sticky update branch

Drop unused partial helper and fleet invalidate import; keep the CacheService inflight self-ref as let with an eslint exception so tsc stays green.

* fix: use inflight holder for CacheService prefer-const

Keep generation-aware ownership without a let self-reference that fights ESLint and tsc.
2026-07-25 15:42:19 -04:00
Anso a89498ae5b fix(ui): hide log service chips on single-service stacks (#1689)
Service chips only differentiate multi-service or multi-container log streams. Gate rendering with the same layout criterion already used in stack details, while keeping parsed prefixes and download attribution intact.
2026-07-23 20:22:57 -04:00
Anso dd54a2e483 feat: graduate Host Console to Community admins (#1669)
* feat: graduate Host Console to Community admins

Make Host Console available to Community and Admiral admins (system:console), add host-console-community for mixed fleets, and keep opaque API tokens off the host shell.

* docs: document Host Console deep links

Cover root and stack-scoped Console URLs, correct the phone treatment note, and pin parse/build round-trips in senchoRoute tests.

* fix: bind Host Console socket to the resolved node

Treat unresolved activeNode as loading, target the WebSocket with an explicit nodeId, and wait for stack deep-link hydration so the shell cannot open on the wrong node or compose root. Add regression coverage for node/stack retargeting and fail-closed directory resolution.

* fix: harden Host Console node binding, audit acting_as, and console_session tokens

Reject unknown or malformed nodeIds before spawning a PTY. Record hub operators in audit_log.acting_as for remote console_session bridges. Path-scope and one-time-consume console_session JWTs so Host Console mints cannot open container exec or be replayed.

* test: expect acting_as in audit CSV export header

Align the CSV export assertion with the P0-2B acting_as column added to audit log exports.
2026-07-23 12:59:53 -04:00
Anso 3b027957c4 fix(fleet): isolate corrupt snapshot file decrypt failures (#1650)
* fix(fleet): isolate corrupt snapshot file decrypt failures

A single damaged encrypted fleet-snapshot row no longer fails detail, restore, or off-site upload for the whole snapshot. Unavailable members are marked, restore is blocked before mutation, and cloud upload fails closed with no PutObject.

* fix(fleet): fail closed on damaged enc snapshot envelopes

Unrecognized enc: payloads no longer fall through as usable plaintext. Only clear legacy prose stays readable; delimiter-byte and similar envelope damage stays unavailable through restore and cloud upload.

* fix(fleet): subordinate legacy enc prose to envelope shape

Legacy exceptions no longer trigger from = or whitespace alone. Encryption-shaped payloads (length and hex density) stay unavailable through restore and cloud upload, while short genuine prose such as enc:hello remains usable.

* fix(fleet): preserve non-envelope enc legacy plaintext

Any non-empty enc: payload that is not encryption-shaped is kept verbatim, including punctuation forms such as enc:hello-world, while envelope-shaped damage remains unavailable.
2026-07-19 20:08:31 -04:00
Anso 63213c0960 feat: add service-scoped Compose update and restore (#1648)
* feat: add service-scoped Compose update and restore

Allow updating or rebuilding one declared Compose service on multi-service
stacks without recreating siblings, with recovery snapshots, health-gate
observation, and prune holds for rollback images. Full-stack update paths
and single-service UX stay unchanged.

* fix: sanitize service-scoped update log messages for CodeQL

* fix: address service-scoped update audit findings B-01 through B-07

* fix: complete service-scoped update audit metadata and surfaces

* test: wrap Updates readiness tests for deploy-feedback context

* fix: keep service recovery reachable without Deploy Progress

Make failed service-gate recovery discoverable when Deploy Progress is
disabled or dismissed, suppress stale image-scan notification side
effects, normalize ComposeService line endings, and add focused
regression coverage.

* fix: resurface ContainersHealth density and expand on multi-service stacks

Service grouping hid the summary strip and Compact/Detailed/Expand controls that still applied to multi-container stacks.
2026-07-19 02:42:29 -04:00
Anso 25586fc8ab feat(ui): add Classic, Smart, and Compact desktop navigation styles (#1642)
* feat(ui): add Classic, Smart, and Compact desktop navigation styles

Introduce a shared app-nav registry and reachable model so TopBar, the
command palette, and mobile menus share one destination source. Smart bar
is the default; Appearance gains a Navigation subsection with quick links.

* fix(e2e): stop clearing top-nav prefs on every reload

The desktop-navigation suite used addInitScript to wipe mode storage,
which re-ran on reload and undid the classic/compact values under test.

* fix(ui): harden nav PR docs and Compact quick-link coverage

Drop the partial docs-refresh import that left missing image assets and a stale Display screenshot, keep navigation-scoped operator docs against main, and add an E2E path that proves Compact quick-link add, persist, and render after reload.

* fix(ui): polish Compact quick links and menu mastheads

Align Smart/Compact menus with Theme chrome, keep pin labels always visible, and drive add capacity from persisted pins (max five) with a trailing + picker and per-pin remove.

* test(e2e): exact-match Compact Networking pin locator

Avoid Playwright strict-mode clash with Actions for Networking.

* fix(ui): simplify Compact quick link removal and fix + button trailing

Remove the (...) dropdown per quick link in Compact mode. Right-click
context menu remains as the sole on-bar removal affordance. Fix the +
add-button to trail quick links rather than pinning to the far right
by removing flex-1 from the quick-link rail.
2026-07-16 21:48:03 -04:00
Anso 8980910153 feat: node-scoped Networking operator page (#1603)
* feat: add node-scoped Networking operator page

Adds a Networking view with overview, topology, inventory, and findings.

Shared aggregate reads back the page; Resources keeps prune and redirects here.

Includes fail-closed network delete guards, operator docs, and /nodes/:slug/networking routing.

* fix: rename unused variable n to _n to satisfy no-unused-vars lint

* fix: keep top bar search clickable when nav grows

* feat: complete Compose-first Networking Phase 2 operator assistant

* fix: move networking action visibility helper out of component module

* feat(networking): complete Compose-first Networking operator page

Finish the node-scoped Networking page (Overview, Networks, Topology,
Findings) with design-system parity and correct finding semantics.

- Rebuild detail sheets on SystemSheet/SheetSection; align the tab band,
  masthead, and mobile tone with Fleet and Security.
- Encode the host-mode and exposure severity matrix; fix collision counts
  so intentional shared externals are not flagged; add one typed drift
  predicate shared by inventory, topology, badges, and overview counts.
- Preserve per-container attachments and IPs on topology node clicks;
  drawer-only click with an explicit logs action; ownership and boolean
  filters; bound large graphs before layout.
- Aggregate cached Compose Doctor findings into the Findings tab with
  honest source labels, structural merge and dedupe, staleness
  reconciliation, and a shared exposure-context helper both engines use.
- Networks tab: privacy-safe service search, precise ownership counts,
  schema v3 with version-2 adapters on every endpoint, pre-confirm delete
  reasons, and the shared sortable table with an internal scroll region.
- Interop: Fleet node-card networking signal with pending-intent
  navigation, stack-to-node backlink, and Dossier/Drift deep links.
- Enrich sanitized inspect with an allowlisted connected-container list;
  fetch topology once and filter client-side.
- Docs and tests across every new finding kind, adapter, and flow.

* fix(networking): correct drift count, exposure fail-soft, and inspect crash paths

Address code-review findings on the Networking page implementation:
- Fix the Overview drift count to use the shared drift-kind predicate instead
  of a hardcoded list that omitted external-network-missing.
- Gate Compose Doctor's unclassified-exposure and reverse-proxy-undocumented
  rules on exposure-context availability, so a DB read failure no longer
  fabricates findings (mirrors the live engine's existing fail-soft behavior).
- Guard the per-stack exposure-intent read in topology aggregation so a
  transient DB failure degrades to unknown intent instead of failing the
  whole response.
- Harden the network detail drawer against a partial inspect payload from an
  older remote node, and log the real error instead of a bare catch.
- Remove now-duplicated severity-rank and drift-kind helpers in favor of the
  shared modules; drop dead backend-only exports; widen the frontend schema
  version type to a plain number instead of casting past a literal type.
- Add coverage for the delete-guard precedence, the full host-mode severity
  matrix, the schema-2 compatibility adapter, and the sanitized connected-
  container allowlist; tighten two tests that were not exercising the
  behavior they claimed to.

* fix: add missing onOpenNodeNetworking prop to FleetView experimental test

The added required prop on FleetViewProps broke the merge-build when the
test file (on main but not on this branch) was compiled against the
updated FleetView interface.
2026-07-14 20:18:10 -04:00
Anso b70a529656 feat: add developer-mode startup and stack hydration timing (#1619)
* feat: add developer-mode startup and stack hydration timing

Instrument boot-to-list and detail hydration with commit-aligned milestones, truthful request stages, and destination/gateway debug duration logs so performance work is guided by measurements.

* fix: redact stack names and complete hydration request stages

Stop logging stack identifiers in containers debug timing, and record state_dispatch (plus detail fetch spans) so copied reports match the advertised stage breakdown.
2026-07-14 17:24:25 -04:00
Anso 0cd03c6f87 chore(ui): hide Mesh, Fleet Secrets, and Host Console behind experimental discovery (#1624)
Gate Routing, Secrets, Host Console, and Mesh dashboard/settings surfaces on the existing useExperimental readiness flag so immature operator surfaces stay out of the default UI while paid and admin backend gates remain unchanged.
2026-07-13 11:55:50 -04:00
Anso 78fbc151e7 fix(scheduler): show every Timeline lane dot and rename the Maintenance lane to Upkeep (#1618)
The Timeline lane marker lacked shrink-0, so inside the fixed 80px label
column the widest labels flex-shrank their color dot to zero width. The
Maintenance and Lifecycle dots collapsed and rendered as no dot at all,
while the shorter labels kept theirs. Add shrink-0 so every lane dot
renders at a consistent size.

Maintenance was the only label wide enough to fill the 80px column on its
own, which is what left its dot no room. Rename it to Upkeep so all five
labels fit and align. A registry test now pins each lane's key, order,
and label, and asserts every lane carries a color, so a future rewrite of
the action registry cannot silently rename a lane or drop a dot color.
2026-07-11 22:14:14 -04:00
Anso e7ac496009 feat(security): add triage status and OpenVEX justification parity (#1615)
* feat(security): add triage status and OpenVEX justification parity

Share triage options across SuppressionsPanel and the scan-sheet suppress dialog, require justification for not_affected and false_positive, and document the fields.

* fix(security): use design-system Select for triage dropdowns

Replace native selects so OpenVEX justification options use themed popover content instead of unreadable OS option lists in dark mode.

* fix(security): keep triage justification Select controlled

Pass an empty string instead of undefined so Radix Select does not flip between uncontrolled and controlled when the placeholder is shown.
2026-07-11 19:26:26 -04:00
Anso 362a18e91a feat(resources): show multi-stack usedByStacks on images (#1612)
Classify images with a deduped sorted stack reverse index, surface chips in the Images table and inspect sheet, and clear node-bound sheet selection on active-node change.
2026-07-11 02:23:28 -04:00
Anso 213d3d5d3a feat(resources): bind prune to fingerprinted itemized plans (#1611)
* feat(resources): bind prune to fingerprinted itemized plans

* fix(resources): repair prune plan volume usage and preview list

Source volume RefCount from docker df, keep preview rows from flex-shrinking, tighten managed image attribution and becomesFree, and stop audit summaries from claiming success on rejected prunes.
2026-07-11 00:11:37 -04:00
Anso 4834e2e51d feat(security): prefer digest identity in scan history (#1610)
Retain scans and History search by digest when available, keep imageRefLike for compatibility, and surface digests in compare.
2026-07-10 22:10:09 -04:00
Anso ba2e7bded9 feat: first-boot compose discovery and adopt-first sidebar (#1600)
* feat: add compose discovery for setup preflight and sidebar empty state

Expose read-only compose discovery via GET /api/stacks/discovery and setup

diagnostics. Replace the blank sidebar with path-aware discovery and move

adopt into a dedicated dialog with a three-tab Create Stack flow.

* test: assert post-setup handoff via sessionStorage read-back

The Setup preflight test spied on Storage.prototype.setItem to check the
post-setup adopt handoff. When the jsdom storage probe fails and the test
harness swaps in its in-memory storage stub (which does not extend Storage),
that stub's setItem never touches Storage.prototype, so the spy records zero
calls and the assertion fails even though the component wrote the value.

Read the value back with sessionStorage.getItem instead, matching how every
other storage test in the suite asserts. This is robust to both the native
jsdom storage and the in-memory fallback.

* fix(setup): surface compose discovery as a preflight check row

Drop the Setup discovery banner and non-working Review button. Show

counts as a pass row in EnvironmentChecks (Setup only) and keep

Enter Sencho as the handoff that opens adopt when candidates exist.

* test(setup): cover zero-count discovery row omission

* fix(stacks): widen adopt scan to any yaml and rename into place

Homelab layouts often use nginx.yml or plex.yml. Surface those for
adopt (except overrides), rename to compose.yaml on move so stacks
register, and reset the confirm UI when a move fails.
2026-07-10 08:42:52 -04:00
Anso bbcc4b59e4 fix: UI polish - tooltips, masthead, tabs, settings, and layout consistency (#1563)
* fix: increase masthead rail shimmer visibility

Bumped shimmer highlight opacity from 25% to 50% and reduced
animation duration from 11s to 5s, matching the login page
AuthCanvas shimmer precedent (via-white/60 at 4.5s).

* fix: reduce masthead rail opacity to match login page pattern

Changed all MastheadRail background classes from 100% to 70% opacity
(bg-brand → bg-brand/70, etc.) so the shimmer highlight reads as a surface
reflection rather than a detached floating bar. Matches the AuthCanvas
login card precedent.

* fix: improve masthead rail glow visibility

Replaced the invisible inset box-shadow glow (10px blur inside a 3px
element) with a white overlay that pulses opacity 0.15-0.55 over 4s.
Sped up glow animation from 5.5s to 4s for a more noticeable breath.

* fix: replace cursor-follow tooltip with standard Radix tooltip in sidebar rows

Replaced the Cursor/CursorFollow animate-ui primitives in StackRow
with the standard Radix Tooltip/TooltipTrigger/TooltipContent already
used throughout the app. The custom cursor dot that followed the mouse
is gone; tooltips now appear as static popovers on hover/touch.

* fix: wrap tooltip icon triggers in span for Radix compatibility

TooltipTrigger asChild requires a native element to forward props.
Lucide icon components did not reliably receive data-state and event
handlers, so tooltips on the check-failed and git-pending indicators
did not open. Wrapped each icon in a span.

* fix: rename Notification Routing to Routing in settings sidebar

* fix: replace Timeline/All tasks buttons with SegmentedControl

The two loose Button elements had indistinguishable selected/unselected
states (secondary vs ghost). Replaced with the SegmentedControl component
already used elsewhere on the same page, which has a clear active
highlight indicator.

* fix: standardize Blueprint empty state headings to font-heading

Replaced manual font-serif [font-style:var(--heading-style)] with the
font-heading class on the main heading and three step titles so the
Deployments empty state follows the same heading convention as the
rest of the app and respects the Calm/Signature theme toggle.

* fix: remove custom tooltip styling, use standard TooltipContent defaults

The RowTooltip wrapped its label in a font-mono text-stat-value span
instead of using the TooltipContent base styling (text-xs
text-popover-foreground) like every other tooltip in the app.

* Revert "fix: remove custom tooltip styling, use standard TooltipContent defaults"

This reverts commit 209fbd2742.

* fix: standardize all tooltips to match sidebar RowTooltip design

Wrapped every TooltipContent child in a font-mono text-xs
tabular-nums text-stat-value span so all tooltips across the app
share a consistent look. Previously some used plain text, others
used <p> elements with different fonts and sizes.

* Revert "fix: standardize all tooltips to match sidebar RowTooltip design"

This reverts commit 9e1d97d39e.

* fix: bake standard tooltip styling into TooltipContent component

Added font-mono tabular-nums text-stat-value directly to the
TooltipContent base styles so every tooltip in the app inherits
the same look without per-instance wrappers. Removed the now-
redundant span from StackRow's RowTooltip.

* fix: replace custom tooltip with shadcn/ui Radix tooltip

Replaced the glass/blur-styled tooltip with the standard shadcn/ui
pattern: bg-foreground/text-background, arrow pointer, proper
slide/fade animations, and w-fit max-w-xs sizing. This gives all
tooltips a consistent dark-solid look across the entire app.

* fix: apply glass/blur design to shadcn tooltip component

Replaced the solid bg-foreground/text-background style with the app's
glass design: bg-popover, border-glass-border, backdrop-blur, and
text-popover-foreground. Kept the shadcn structure (Portal, Arrow,
slide/fade animations).

* fix: replace native title tooltips with Radix tooltips on Fleet toolbar

Converted Refresh, Export Dossier, Search, Sort direction, Node Update,
and Manage Nodes buttons from native HTML title attributes to the shared
Radix Tooltip component so they match the app-wide glass tooltip design.

* fix: replace native title tooltip on Fleet Snapshots upload button

* fix: replace native title tooltips with Radix tooltips on Resources page

Converted all actionable button tooltips (Open stack, Inspect image,
Scan, Browse volume, and delete buttons) plus the SenchoBadge
informational tooltip. Wrapped disabled protected-resource delete
buttons in span triggers so tooltips fire on disabled elements.

* fix: replace native title tooltips with Radix tooltips on Security page

Converted scanner info icon, exploitability n/a badge, and scan
image button from native HTML title attributes to the shared Radix
Tooltip component.

* fix: replace native title tooltips with Radix tooltips on Schedules and Settings pages

Converted Run now, Execution history, Edit, Delete buttons on Schedules;
Download/Delete on Cloud Backup; Send test/Edit/Delete on Notification
Routing; Reset 2FA on Users; and Edit/Remove suppression on
Suppressions panel. Skipped SettingsSection/Modal title props which
are React component props, not native HTML attributes.

* feat: add copy button to execution history details column

Replaced the plain truncated Details cell with a flex layout containing
a copy button. Users can now copy the full error/output text instead
of relying on the native title tooltip to read long messages.

* fix: change tab highlight from neutral to brand color

Replaced bg-glass-highlight with bg-brand/20 on all TabsHighlight
instances and updated TabsTrigger active state to text-brand,
matching the SegmentedControl selected style. Works on both
dark and light themes.

* fix: restructure Resources page tabs to match Fleet/Security pattern

Moved tab band out of the outer card into a standalone full-width
element. Replaced custom FilterToggle with Fleet-style Button
toggles. Added search inputs per tab. Wrapped each resource table
in a card container matching the Security Images table design.

* fix: match Resources tab font size to Fleet and Security

Removed explicit text-xs from Resources TabsTrigger elements so they
use the default text-sm, matching Fleet and Security tab typography.

* fix: restore missing closing bracket on TabsTrigger tag

* fix: add aria-labels to schedule action buttons, update test selectors

Replaced native title attributes with aria-labels on Run now,
Execution history, Edit, and Delete buttons so the Radix tooltip
conversion does not break test selectors that relied on title.
Updated tests from findByTitle to findByRole.

* fix: add fingerprint-based dismiss to post-deploy scan banner

Created useScanBannerDismiss hook following the usePreflightDismiss
pattern. The banner now persists dismissal in localStorage keyed to a
fingerprint of scan status + attemptedAt. Dismissal survives page
reloads for the same scan outcome, and the banner automatically
reappears when a new scan runs or the status changes.

* fix: update Configuration Status card labels and add new rows

Notifications section: renamed Notification agents to Channels,
Notification routing to Routing, added Mute Rules row linking to
notification-suppression settings.

Security section: added Trivy installed Yes/No row, renamed
Vulnerability scanning to Scan policies.

Backend: added trivyInstalled and suppressionRules fields to
the /dashboard/configuration payload.

* fix: style Trivy row as badge and link to Security scanner setup

* fix: remove Mute button from stack anatomy panel header

* Revert "fix: remove Mute button from stack anatomy panel header"

This reverts commit 16843a81b5.

* fix: remove Mute button from stack anatomy panel header

* fix: remove unused stackMuteActions imports and props from StackAnatomyPanel

The removal of the ActivityMuteKebab rendering left orphaned imports
and props that cause TS6133 errors under strict mode. Clean up the
imports, prop type, destructuring, and caller prop passes.

* fix: refine anatomy tab sizing and add pulsing scroll chevron

Changed anatomy tabs from 12px fixed height to 11px with py-1
padding, matching the Fleet tab approach. Bumped Files/Edit buttons
from 10px to xs for better visibility. Added subtle animate-pulse
to the right scroll chevron to hint at overflow tabs, gated behind
the Reduced effects appearance setting.

* fix: link reclaim hero number to heading style

* fix: replace native title tooltips with Radix tooltips on stack detail page

Converted Expand/Collapse logs, Stats unavailable, Copy service URL,
Dismiss findings, Likely secret badge, Move up/down, and Remove
buttons from native HTML title attributes to the shared Radix
glass tooltip component.

* fix: replace native title tooltip on image source link button

* fix: add tooltips to container logs and bash session buttons

* fix: add tooltip to download logs button

* fix: add tooltip to copy digest button

* fix: replace cursor-follow tooltip with Radix tooltip on severity badge

* fix: add tooltips to density toggle buttons

* feat: add expand button to multi-container health panel

Added an expand/collapse toggle next to the density controls that
expands the containers panel to fill the column and hides the logs
section. The expand and logs-expand are mutually exclusive. Only
appears for stacks with more than one container.

* fix: reduce Structured/Raw terminal toggle font to 10px

* fix: set compact density as default for multi-container stacks

* fix: change Feedback to Open New Issue with GitHub issues link

* fix: replace ExternalLink with GitHub icon for Open New Issue menu item

* fix: restore ExternalLink import needed by other menu items

* fix: default host threshold alerts to off for new installations

* fix: resolve CI typecheck and test failures

Broaden MenuRowProps.icon type to accept custom SVG components alongside
lucide icons, fixing TS2741 on GitHubIcon import.

Update host_alerts_enabled seed test assertion to match the new opt-in
default of off (0).

* fix: update ContainersHealth tests for compact default density

Adjust three tests to reflect the new default of compact mode:
- Rename 'detailed mode is the default' to 'compact mode is the default'
- Update sparkline visibility checks to switch to detailed first
- Rename remount test and verify reset to compact (new default)
2026-07-06 04:25:01 -04:00
Anso 10fb93dcb1 feat(scheduler): schedule container restart, stop, and start (#1526)
* feat(scheduler): schedule container restart, stop, and start

Add container as a scheduled-task target type so operators can automate lifecycle actions against standalone containers by node and name, with matching UI pickers, validation, execution on local and remote nodes, and tests.

* fix(scheduler): stack service matching and container picker hygiene

Backfill Service on smartFallback containers so per-service stack restarts work when container_name is set. Match services by compose label and container name in stack routes and scheduled restarts. Exclude Sencho from GET /api/containers lists. Hide the Restart Stack service picker when a stack has only one service.

* test(scheduler): scope service checkbox assertion to Services block

The create dialog also has a Delete after run checkbox. Count checkboxes only inside the Services section so CI does not include unrelated form controls.

* fix(scheduler): narrow closest() result to HTMLElement in schedule test

The service-checkbox assertion passed an Element from closest() into
within(), which requires an HTMLElement, failing tsc -b in the frontend
build and Docker build stages. Use the closest<HTMLElement>() type
argument so the value type-checks without an unsafe cast.

* fix(scheduler): hide Sencho container on remote node picker lists

Remote container lists are proxied from peer Sencho instances, so id-only self filtering missed peers on older builds. Await SelfIdentity init, match ImageID, and drop official saelix/sencho images. Apply the same heuristic in the scheduled-operations UI and when the hub fetches remote containers for scheduled runs.

* test(monitor): add missing DatabaseService mocks for scan history cleanup

* test(scheduler): add missing markStaleScansAsFailed mock

SchedulerService.tick() calls db.markStaleScansAsFailed() to sweep stale
vulnerability scans. The scheduler-service test was missing this method in
its DatabaseService mock, causing TypeError failures during test initialization.

Added mockMarkStaleScansAsFailed to hoisted mocks and DatabaseService mock
object, returning safe default of 0 scans marked as failed.

* test(compose): add missing FileSystemService mocks for getStackContent/getEnvContent

* test(containers-route): mock SelfIdentityService to prevent initialize() crash

The excludeSelfContainers() helper calls SelfIdentityService.initialize(), which tries to access DockerController. Without a proper SelfIdentityService mock, the initialize() call fails silently, causing a 500 error on GET /api/containers.

Added SelfIdentityService mock with initialize(), isOwnContainer(), and isOwnImage() methods to prevent the crash.
2026-07-02 22:31:29 -04:00
Anso 60de0ddec2 fix(deploy-modal): hide frozen countdown label in inline feedback style (#1534)
In inline deploy feedback style, the auto-close countdown label
("closes in Xs") was visible but permanently frozen. canAutoClose
was already false in inline style (the banner owns the lifecycle),
but showCountdown was driven by !gateHoldsOpen, showing the label
even when no countdown timer could ever start.

Change showCountdown to canAutoClose so the label only appears
when the modal is actually eligible to auto-close (modal style,
succeeded, no observing gate). No gate contracts or modal-style
behavior change.

Adds regression tests: inline succeeded hides the label, modal
succeeded shows it.
2026-07-01 02:54:22 -04:00
Anso ba57c67048 fix: persist a one-time schedule's run time so edit and disable keep its year (#1499)
PR #1497 pinned an enabled one-shot's next_run_at on creation, but the chosen
instant did not survive two paths: editing reconstructed the date from the
yearless cron (current year), and a disabled one-shot nulled next_run_at with no
other store, so enabling it later recomputed from the cron. Both moved a
future-year one-shot to a different annual occurrence than the date displayed.

Persist the one-shot's absolute fire time in a dedicated run_at column
(additive, nullable; recurring schedules leave it null). Create and update store
run_at independently of the enabled state; next_run_at is derived from it when
enabled and null while disabled, so a disabled one-shot keeps its run_at and the
enable toggle restores the exact instant from the column rather than the cron.
The editor reconstructs a one-shot's date from the persisted run_at, so opening
and re-saving without changes preserves the originally chosen year.

No behavior change for recurring schedules or fresh installs; the column is added
by an additive migration safe for upgrades from v0.92.0.
2026-06-28 05:23:23 -04:00
Anso a6d431f0d7 fix: keep the schedule's stack selected on prefill and edit (#1496)
Opening the New Schedule modal from a stack's Schedule action, or editing
a stack-targeted task, set the node and the stack, but the node-change
effect then ran and cleared the stack, forcing the user to reselect it.

Move the stack-clear out of that effect into the Node picker's change
handler so it only fires on a user-driven node change. A programmatic node
set from prefill or edit now preserves the stack.
2026-06-28 03:18:27 -04:00
Anso 3bece54e72 feat: add a Simple mode to the New Schedule flow (#1495)
The schedule editor exposed only a raw 5-field cron input, which is
unfriendly for the common "daily at 3am" or "one-time next week" cases.

Add a Simple mode (now the default) that builds the cron from a
frequency and time: Once, Hourly, Daily, Weekly, and Monthly. Advanced
mode keeps the raw cron input as an escape hatch.

- Simple mode compiles to the existing cron_expression on save; no
  schema or scheduler changes.
- One-time schedules reuse the existing delete-after-run flag: selecting
  Once turns it on and locks it so the task runs a single time (cron has
  no year field, so without it the task would repeat yearly).
- Editing a task opens in Simple mode when its cron maps to one of the
  simple shapes, otherwise in Advanced; switching a custom cron to Simple
  warns that it will be replaced.
- Day-of-week uses frosted toggle chips and the time uses hour/minute
  selects so the controls match the rest of the editor.
2026-06-28 03:18:11 -04:00
Anso 5960c1e85e feat: show node and fleet targets on schedule timeline pills (#1480)
Timeline pills and the mobile schedule list now identify what each
scheduled run acts on instead of repeating the task name. Pills stay
compact (firing time plus a category-aware target) and carry the full
detail on hover:

- Stack actions show the stack name.
- Fleet snapshots show "Entire fleet".
- Fleet auto-updates and node-scoped prune/scan show the selected node.
- The hover tooltip adds the action label, task name, and node.

The mobile list resolves node names too, so prune and scan rows name the
node rather than the literal "system". A shared scheduleTargetDescriptor
helper removes the target-label logic that was duplicated across the
desktop and mobile views. The lifecycle lane is renamed "Stack lifecycle"
to match the action-picker category wording.
2026-06-26 22:15:06 -04:00
Anso 1bca75a999 fix: load the full vulnerability list in the scan detail sheet (#1483)
The scan detail sheet fetched only the first 500 vulnerabilities for its
interactive table, so severity filtering, row inspection, and suppression
management could not reach findings beyond the first page on a scan with more
than 500. The CSV export already paged the complete list, but that is not a
substitute for working with the findings in the table.

The sheet now loads every vulnerability via the same paged helper the CSV uses,
so the table, filter, pagination, inspection, and suppression all operate over
the complete set. The "showing first N of M, export CSV for the complete list"
notice is removed because the table is no longer capped. The CSV export reuses
the already-complete in-memory set rather than refetching.

Secrets and misconfigurations keep their existing per-request cap; they are not
the suppression-managed findings this blocker concerns and rarely exceed it.
2026-06-26 21:12:18 -04:00
Anso d9b7911f12 fix: distinguish failed image-update checks from "up to date" (#1470)
* fix: distinguish failed image-update checks from "up to date"

The image-update detector collapsed every failure (registry unreachable,
missing auth, rate limit, unresolved local digest) into hasUpdate:false and
dropped the captured reason, so a failed check was indistinguishable from a
current image and never raised a notification, even while a manual stack
update still pulled a newer image.

Detection now records a tri-state per stack (ok / partial / failed) with the
failure reason, exposed via a new GET /api/image-updates/detail (the boolean
GET / is unchanged so fleet aggregation is unaffected). A fully-failed check
preserves the last known has_update, so a transient outage neither erases a
real update nor flaps the notification state. The sidebar shows a muted
"couldn't check" indicator with the reason on hover, and the Update board
lists stacks whose check failed in a "could not be checked" advisory.

Detector hardening: the manifest digest lookup issues HEAD first (falling back
to GET) so it no longer draws down Docker Hub's anonymous pull-rate budget, and
local RepoDigest matching is normalized so official library/* images resolve
their digest instead of falling through to a silent "no update".

* fix: preserve confirmed updates through partial checks; tighten failure surfacing

Address review findings on the tri-state image-update detection:

- A partial check (some images errored) no longer erases a previously
  confirmed update; only a fully-ok check can lower has_update, so a single
  image's registry blip cannot drop the stack's update and re-fire the
  notification on recovery. Adds a regression test.
- The image-level catch stores getErrorMessage(e) rather than raw String(e),
  since that value surfaces verbatim in the sidebar tooltip and readiness
  advisory.
- useImageUpdates and the readiness detail fetch now log unexpected non-ok
  responses instead of silently leaving stale state.
- Remove an unused checkFailedCount derivation (the row indicator is driven by
  the checkStatus prop).
- Reword the recordStackCheckFailure docstring and the HEAD-first comment.
2026-06-26 16:16:34 -04:00
Anso e3b3c3b857 fix: export the full vulnerability list to CSV (#1472)
The scan detail sheet fetches a capped page of vulnerabilities for
display, then told operators to "Export CSV for the complete list".
The CSV writer only serialized the rows already in memory, so for a
scan with more findings than the page cap the CSV silently dropped
everything past the cap: the recovery path the notice promised did not
exist.

Export now pages past the API's per-request cap and serializes every
row when the loaded set is short of the total, falling back to the
in-memory rows when they are already complete. The CSV action shows a
spinner and disables while the export runs.
2026-06-26 15:34:57 -04:00
Anso 7320a86579 feat: add cron scheduling mode for image update checks (#1460)
* feat: add cron scheduling mode for image update checks

Adds a cron scheduling mode alongside the existing fixed-interval
dropdown in Settings > Automation > Image update checks. Users can
now set a 5-field cron expression (e.g. "0 3 * * 1") for precise
time-of-day scheduling of registry polls.

- Backend: ImageUpdateService gains mode/cronExpression fields and
  cron-based nextDelayMs() using the existing cron-parser dependency.
  PUT /api/image-updates/interval extended with transactional writes
  and server-authoritative cron validation matching the Scheduled
  Operations contract. Nicknames like @daily are supported.
- Frontend: UpdatesSection gains a SegmentedControl toggle and cron
  text input with cronstrue-powered live description. The frontend
  does advisory validation only; backend 400s are surfaced inline.
  SettingsPrimaryButton used for explicit "Save schedule" action.
- No cron jitter (the user chose a specific time). Interval mode
  keeps existing ±10% jitter.
- Tests: 15 new backend tests covering valid cron, invalid cron,
  6-field rejection, nickname support, backward compat, runtime
  fallback, and transactional writes.
- Docs: auto-update-policies.mdx, alerts-notifications.mdx, and
  openapi.yaml updated with new scheduling mode.

* fix: add mode and cronExpression to UpdatesSection test fixtures

The existing tests failed because the mock status object was missing
the new required fields (mode, cronExpression) added with cron
scheduling support. Without them, status.mode was undefined, causing
uiMode to never match 'interval' and the Select combobox to not render.

* fix: prevent SegmentedControl from stretching full-width in SettingsField

The flex-col container defaults items to align-self: stretch, making the
Interval/Cron toggle bar span the full card width. Add self-start so it
sizes to its content.
2026-06-25 19:47:57 -04:00
Anso f1f64ec7f6 feat: show container name in structured log output (#1452)
* feat: show container name in structured log output

Prepend a normalized container name prefix to each line in
ComposeService.streamLogs() so both the structured log viewer
and the raw terminal identify which container produced each entry.

- Backend: prepend displayName (normalized via normalizeContainerName)
  before LogFormatter.process() in sendOutput and flushBuffer.
- LogFormatter: refactor process() to handle both prefix-first and
  timestamp-first input orders via a while-loop; widen PREFIX_REGEX
  to accept dotted service names.
- Frontend: add containerName to LogRow, extract prefix in parseLine,
  render as an inline mono chip in the message column, and include
  the name in downloaded logs (omitting the bracket prefix when null).
- Tests: 14 new tests across log-formatter, compose-service streamLogs,
  and StructuredLogViewer chip rendering + download formatting.

* fix: guard LogFormatter loop to at most one prefix and one timestamp

The while-loop refactored for order-agnostic prefix/timestamp
parsing could continue matching beyond the intended single prefix
and timestamp. A log line like "redis | 2024-...Z api | started"
would falsely colorize "api |" as a second container prefix in
raw terminal output.

Add prefixFound/timestampFound boolean guards so the loop stops
after one prefix and one timestamp, regardless of input order.

* feat: per-service color alternation for log container chips

Add an Appearance setting that lets users switch between unified
cyan and per-service label-token colors for the container name chips
in the structured log viewer.

- Extract HUE_VARS and hashLabel() from NodeLabelPill into a shared
  utility at frontend/src/lib/label-colors.ts.
- Add useLogChipColorMode hook (browser-local localStorage,
  sencho.log-chip-color-mode key, unified by default).
- Add SegmentedControl in Settings > Appearance > Display.
- Apply inline label-token styles via style attribute in per-service
  mode; keep current text-brand/80 bg-brand/10 classes in unified mode.
- 14 new tests across label-colors, hook, and viewer chip rendering.
2026-06-25 14:13:38 -04:00
Anso 7982251dc6 feat(scheduler): add helper text and risk badges to scheduled action picker (#1449)
* feat(scheduler): add helper text and risk badges to scheduled action picker

Add a concise helper text and risk level badge to every scheduled action
in the create/edit modal. The six risk levels (Safe, Read-only, Interruptive,
Runtime change, Removes containers, Destructive) map to the four existing
design-system tones and render as a small dot+label chip next to the helper
text, following the same pattern as SeverityBadge.

Fix an ambiguous mobile label: update + target_type: fleet now resolves
through resolveTaskAction and renders 'update node stacks' instead of the
misleading 'update fleet'.

Add exact helper-text and risk-level assertions for all 10 actions, plus
component tests for default modal state, action-switch scenarios, and
mobile update+fleet rendering.

* docs: update stale scheduled-operations alt text for changed helper text
2026-06-25 02:37:12 -04:00
Anso 79f840ab6e fix: clear structured log viewer rows on stack switch (#1448)
The structured log viewer accumulated log rows across stack switches
because the useEffect cleanup closed the old WebSocket but never
cleared the committed rows state. Reset rows, row IDs, and auto-follow
at the top of the effect before connecting to the new stack. The level
filter is intentionally preserved across switches.

Closes #1444
2026-06-25 02:17:23 -04:00
Anso cc78873e65 feat(scheduler): group schedule action picker by operator intent (#1446)
* feat(scheduler): group schedule action picker by operator intent

Reorganize the New Schedule action picker from a flat dropdown to a
category-grouped list (Lifecycle, Updates, Security, Maintenance, Backups).

- Extend Combobox component with optional group field on ComboboxOption,
  rendering grouped sections with non-interactive headers when groups are
  present. Flat rendering is unchanged for all other callers.
- Reorder SCHEDULED_ACTIONS by category group and update seven action
  labels per the operator-intent spec.
- Add DEFAULT_SCHEDULED_ACTION_ID constant so picker order and form
  defaults are independently controllable.
- Wire grouped actionOptions into ScheduledOperationsView.
- Update all label references in docs and tests.
- Add Combobox grouping tests, registry order test, and default-constant
  test.

* fix(scheduler): correct Combobox grouping for interleaved groups, docs labels

- Replace last-group-append with Map-based group partitioning so
  interleaved or mixed-group options land in the correct group.
- Add interleaved-groups test and restore non-interactivity test.
- Update stale "Start Stack" references to "Start / Bring Up Stack"
  in doc action-label contexts.
- Update action-picker alt text to describe the new grouped order.
2026-06-25 01:40:31 -04:00
Anso db8bb70b7d fix(scheduler): reject 6-field cron in Scheduled Operations (#1435)
* fix(scheduler): reject 6-field cron in Scheduled Operations

Create and edit validation parsed cron with cron-parser, which accepts both
5- and 6-field expressions, while the form, presets, and docs all describe a
5-field cron. Because the scheduler ticks once per minute, a leading seconds
field can never improve precision, so a 6-field expression was silently
accepted but never honored on its stated schedule.

Add a field-count guard on both sides: the API rejects 6-field input at
create and edit with a clear message, and the form surfaces the same error
inline and disables save. Cron nicknames such as @daily still pass. Document
the five-field requirement in the cron reference.

* chore: merge main into scheduled cron validation

* fix: avoid logging policy bypass actor in debug output
2026-06-24 23:00:56 -04:00
Anso bc8c051962 feat(scheduler): consistent action targeting in Scheduled Operations (#1431)
Give every scheduled action an explicit, predictable target model
(Action then Node then Stack then Options then Schedule):

- System Prune now exposes a Node picker and requires a node, so it can
  no longer run silently on the default node.
- Vulnerability Scan and System Prune list local nodes only; both run on
  the hub-local Docker daemon and reject remote nodes on the backend.
- Restart Stack service discovery loads services from the selected node
  via fetchForNode instead of the active or local node.
- Fleet Snapshot shows a read-only "Scope: Entire fleet" summary.

Backend gains a shared local-node guard and prune node validation on
create and update, plus an executor-level remote-node guard, so the
frontend and backend validation now agree for every action.
2026-06-24 21:02:15 -04:00
Anso 0af7ad1df2 refactor(scheduler): drive scheduled-action metadata from a shared registry (#1428)
Scheduled-operation action metadata was duplicated across the backend route
validator, the DatabaseService action union, the desktop action picker, the
Timeline lanes, and the mobile labels/tones. Adding or renaming one action meant
editing all of them.

Introduce one registry per package as the single source within that package:

- backend/src/services/scheduledActionRegistry.ts owns the action list and
  target-type validation; routes/scheduledTasks.ts and DatabaseService import
  from it (BackendScheduledAction type, VALID_ACTIONS, validateActionTarget).
- frontend/src/lib/scheduledActions.ts owns the UI metadata (labels, short
  labels, categories, tones, target/node/stack/service flags, helper text) and
  drives the create-flow picker, the All Tasks label, the Timeline lanes, and
  the mobile schedule view.

Timeline lanes now group by semantic category (Lifecycle, Updates, Security,
Maintenance, Backups) sourced from the registry. The update-fleet UI alias is
made explicit via a backendAction field. Backend validation stays authoritative;
parity tests on each side keep the action sets in lockstep.
2026-06-24 20:09:13 -04:00
Anso bb4ddde35a feat(sidebar): surface partial status for multi-container stacks (#1426)
Bulk stack-status aggregation collapsed a stack to "running" as soon as any
container was up, so a multi-container stack with crashed containers showed a
green UP pill and the degradation was invisible from the sidebar.

Add a crash-aware "partial" state: a stack is partial when at least one
container is running and at least one has genuinely failed (exited with a
non-zero code, dead, or crash-looping). Cleanly finished one-shot containers
(exit 0) and clean restart-policy cycling do not count, so an app with a
completed init job stays UP. The exit code is read from the container Status
string, so no extra inspect calls are needed.

Render partial as an amber PT pill with a hover tooltip showing the
running/total count, fold it into the Down filter (needs-attention), and treat
it as running for context-menu lifecycle actions so operators keep
stop/restart/update. The dashboard stack-health table, cross-node search rows,
and the command palette all pick up the new state through the shared status
surfaces.
2026-06-24 19:57:49 -04:00
Anso 2c70e11485 feat(scheduler): flag one-shot tasks in the Scheduled Operations table (#1433)
A one-shot task (delete after successful run) self-deletes only on a
successful run; a failed one-shot is kept so it can be retried or
debugged. The table gave no signal for this, so a lingering failed
one-shot was indistinguishable from a stuck recurring task. Add a
"One-shot" chip to the status cell for every delete-after-run task,
with a tooltip that explains the keep-on-failure behavior when the
task has failed.
2026-06-24 19:04:42 -04:00
Anso f91227dada fix(search): don't open the command palette via Cmd/Ctrl+K while typing (#1414)
The global command palette's Cmd/Ctrl+K shortcut fired even when a text
field or the code editor held focus, stealing focus mid-edit and, on
macOS, clobbering the native Ctrl+K delete-to-end-of-line. Guard the
handler with isInputFocused() so the shortcut opens search only when the
user is not typing in a field; the toolbar search icon still opens it
from anywhere. Add unit tests for the keyboard path.
2026-06-22 19:47:53 -04:00
Anso de87c41b78 fix(updates): allow "Apply now" on the readiness board without a schedule (#1394) 2026-06-19 01:12:13 -04:00
Anso 058cf8f2c7 feat: make image-update check cadence configurable and visible (#1377)
* feat: make image-update check cadence configurable and visible

The background image-update scanner polled registries on a hardcoded
6-hour interval, with no way to see when it last ran or when the next
run was due. Operators testing updates read this as auto-update being
unreliable: a manual update checks the registry immediately and applies,
so the slow background scan rarely raised the "update available"
notification before the stack was already current.

Backend:
- ImageUpdateService reads image_update_check_interval_minutes (15-1440,
  default 120) and drives a single generation-guarded self-rescheduling
  timer with 10% per-run jitter so fleet nodes do not poll in lockstep.
  restartPolling() applies a new interval live, with no restart, and
  cannot leave a duplicate timer when a save lands mid-scan.
- GET /api/image-updates/status now returns checking, intervalMinutes,
  lastCheckedAt, nextCheckAt, and the manual-cooldown fields. New
  admin-only PUT /api/image-updates/interval persists the setting and
  reschedules.

Frontend:
- New Settings > Automation > Image update checks section to choose the
  interval (read-only for non-admins; admin enforced on the backend).
- The Auto-Update readiness view shows last-checked, next-check, and a
  ticking manual-recheck cooldown, and the copy distinguishes registry
  detection from scheduled auto-update execution.

Adds backend unit and route tests and frontend component tests, and
updates the auto-update documentation.

* fix: drop stale image-update status response in the readiness strip

loadCadence() ran on mount and again after a Recheck with no request
token, so a slow initial /image-updates/status response could resolve
after the recheck-triggered one and overwrite the fresh cooldown with
stale data, or set state after the view unmounted. Guard setCadence with
a monotonic token mirroring loadReadiness, and bump it on unmount. Adds a
regression test for the out-of-order resolution.
2026-06-15 20:06:13 -04:00
Anso 6cc66faa8c feat(mobile): standardize secondary pages and the stack list on the status masthead (#1374)
* feat(security): reflow the node Security page for mobile

Below the md breakpoint the Security page now reads as a phone surface
instead of a squeezed desktop, with no change to the desktop layout.

- Masthead stat cluster moves into a full-width 3-cell strip
  (critical / high / last scan) below the tab strip, since the masthead
  hides its inline cluster on a phone.
- The eight-section tab strip becomes a horizontally scrollable mono row
  with an edge mask-fade and a cyan underline on the active tab; every
  section stays reachable by scroll.
- The six totals render as a 3x2 hairline-divided grid instead of the
  640px-wide rail that forced a horizontal scroll.
- The Images tab becomes a filterable, scrollable list (severity dot,
  truncated ref, freshness, critical/high count tags) with a chip row,
  in place of the desktop table.
- A freshness footer band states scan recency and scanner version.

All mobile treatment is gated by useIsMobile() or max-md: utilities, so
the desktop view is byte-identical. Charts are reused full-width.

* feat(security): make the mobile Security page a bespoke masthead-led screen

On a phone the Security page now drops the global top bar and leads with
its masthead (the notifications + more-menu cluster moves into the
masthead's right slot), matching Home and Fleet so the mobile shell is
continuous across pages. The view is reclassified bespoke and rendered
through the masthead-led path; the desktop layout is unchanged.

The mobile "more" menu now lists every destination instead of omitting
the bottom-tab views, so the same menu opens the same set on every
screen rather than changing contents from page to page.

* refactor(mobile): extract shared PageHead, sub-tabs, and chip-row primitives

Add PageHead (the header for a pushed full-screen secondary view),
MobileSubTabs (the mono tab scroller with the cyan active underline), and
MobileChipRow (the cyan-filled filter chips) to the shared mobile-ui kit,
and rewire the Security page's tab strip and Images filter to consume
them. No visual change; this is the shared chrome the remaining mobile
pages reuse.

* feat(updates): make the mobile Updates page a bespoke masthead-led screen

Below the md breakpoint the Auto-Update Readiness page becomes a pushed
full-screen view: a PageHead (back chip, the rehomed notifications +
more-menu, a "fleet readiness" crumb, and a Recheck action) leads, then a
brand-tinted readiness hero, per-node sections, and one-up readiness
cards that reuse the same risk badge, version delta, and apply/disabled
logic as the desktop board. The desktop layout is unchanged.

Reclassifies auto-updates as bespoke and renders it through
renderMobileBespoke behind the same hub-only + capability gates as the
desktop content path. Also lifts the PageHead, sub-tabs, and chip-row
primitives' right-slot to host the rehomed global chrome.

* feat(app-store): make the mobile App Store a bespoke masthead-led screen

Below the md breakpoint the App Store becomes a pushed full-screen view:
a PageHead (back chip, the rehomed notifications + more-menu, and an
app-count crumb) leads, the category sidebar collapses into a horizontal
chip scroller, and the featured hero plus the tile grid (already
single-column on a phone) stack below. The featured hero, tile grid, and
deploy sheet are shared with the desktop layout, which is unchanged.

Reclassifies templates as bespoke and renders it through
renderMobileBespoke. Adds tests for the shared chip row and sub-tabs.

* feat(audit): make the mobile Audit Log a bespoke masthead-led screen

Below the md breakpoint the Audit Log becomes a pushed full-screen view:
a PageHead (back chip, the rehomed notifications + more-menu, an
entry-count crumb, and Refresh) leads, then the Stream/Table sub-tabs and
the stream view, where the signal-rail tiles fold to a 2x2 grid and the
day-banded activity stream reflows. The columnar table reads best on a
larger screen, so the Table tab points there on a phone. Desktop is
unchanged.

Reclassifies audit-log as bespoke behind the same hub-only + capability
gates as the desktop content path.

* feat(app-store): drop the featured hero and category chips on mobile

On a phone the App Store is now search plus a single-column list of
every matching app. The featured hero and the category chip row are
removed; the would-be-featured app is folded into the list so nothing is
dropped. Desktop keeps the featured hero, category sidebar, and grid.

* feat(resources): make the mobile Resources page a bespoke masthead-led screen

Below the md breakpoint Resources becomes a pushed full-screen view: a
PageHead (back chip, the rehomed notifications + more-menu, a docker
crumb) leads, then the reclaim hero, the disk-footprint segments, the
2x2 quick-clean grid, and the resource tabs. The raw resource tables
scroll horizontally to fit; the detail sheets stay full-screen. The main
content and the dialog/sheet overlays are shared with the desktop layout,
which is unchanged.

* chore(mobile): correct shared-primitive comments and dedupe the lazy fallback

Fix the "shared by" consumer lists on the mobile-ui primitives, point the
headerActions doc comments at the PageHead (not a masthead), drop the
stale "all eight sections" count on the Security tab strip, rename a
readiness-card test to match what it asserts, and extract a single
Suspense fallback for the four bespoke phone-screen lazy imports. No
behavior change.

* feat(mobile): codify the fade+arrow tab scroller and fix the Resources tab clip

Extract the horizontal tab scroller (edge fade + clickable chevron +
wheel-to-horizontal) out of the stack anatomy panel into a shared
ScrollableTabRow primitive, and adopt it in the stack anatomy tabs, the
mobile sub-tabs (Security / Audit), and the Resources resource tabs. On a
phone the Resources tabs now scroll horizontally, so the "Unmanaged" tab
and its count no longer clip out of the frame. Desktop is unchanged.

* feat(logs): make the mobile Logs page a bespoke masthead-led screen

Below the md breakpoint the global Logs view drops the TopBar for a
PageHead, hides the metrics rail, collapses the stream and level filters
into a single Filters dropdown, turns the search into an icon that
expands to an input, and folds the pause / clear / download controls into
an expanding floating action button that retracts after each action.
Desktop is unchanged.

Reclassifies global-observability as bespoke behind the same hub-only
gate as the desktop content path.

* feat(mobile): standardize secondary pages on the status masthead

Adopt the Home/Fleet/Security status masthead (cyan rail, kicker, serif-
italic state word + tone dot, meta line, notifications + more-menu in the
right slot) on every bespoke secondary page (Resources, App Store,
Updates, Audit, Logs), replacing the title-led PageHead, which is
removed. Each page derives a status word: Updates "Up to date" /
"N pending", Logs Streaming / Idle / Offline, Audit Healthy / Review /
Alerts, Resources Reclaimable / Tidy, App Store the app count.

The "< Stacks" back chip is dropped (the bottom tab bar and more-menu own
navigation), the page actions (Recheck, Refresh) move into the body, and
the Updates readiness hero folds into the masthead. Also make the
Resources tab tables scroll horizontally instead of clipping on the
right. Desktop is unchanged.

* feat(stacks): lead the mobile stack list with the status masthead

On phones the stack list now opens with the shared status masthead instead
of the global top bar plus an in-sidebar node row. The node switcher renders
as a compact kicker chip in the masthead, the serif word summarizes stack
health (down, updates, or all running), and notifications plus the more-menu
sit in the right slot. This matches the Home, Fleet, and Security pages.

The dropped top bar hosted global search, so the more-menu gains a Search
item that opens the command palette. The masthead kicker now accepts either
a styled kicker or a raw slot, enforced as a discriminated union so exactly
one source is supplied. Desktop is unchanged: the new chrome is gated to the
mobile shell and the sidebar rows hide via max-md only.

* fix(stacks): only call the list "All running" when every stack is up

The mobile stack masthead derived its health word from filterCounts, where
up counts running stacks and down counts exited ones. Any other status, and
the window before statuses load, counts as neither, so a list with no exited
stacks but some not yet running fell through to "All running" even though it
was not. Gate that label on up equal to all, and otherwise show the running
count out of the total so the headline stays honest while statuses settle.
2026-06-14 22:21:46 -04:00
Anso 3d39d856a3 feat: chart-led Security overview with sortable Images and History tables (#1364)
* feat: chart-led Security overview with sortable Images and History tables

Refine the Security page around the existing design system and add the
data the dashboard needs.

- Overview leads with four charts (30-day risk trend, severity donut, top
  exposed images, findings by type); the signal-rail counts become a
  secondary summary, and the scanner and deploy-enforcement posture follow.
- Images becomes a recessed table with search, a severity filter, sortable
  columns, a last-scan column, and inline scan actions; the findings cell is
  clickable into the scan sheet, and the per-row cursor tooltip is dropped
  where the columns already carry that information.
- Policies puts deploy-enforcement first, collapses the policy packs into an
  accordion, and uses the standard primary button for Add policy.
- Suppressions and acknowledgements move their titles and Add buttons outside
  the cards, matching the Fleet tab layout.
- History switches from the detail sheet to an inline table (search, sortable
  columns, two-scan compare, pagination); the now-unreachable scan-history
  overlay is removed.
- Add GET /api/security/overview/trend, a node-scoped daily critical/high
  rollup backing the risk-trend chart.
- Extract the shared image-scan hook and the severity classifier, and harden
  the overview data fetch so a malformed non-critical response can never read
  as a clean security state.

* fix: treat malformed Security responses as errors, not empty or clean states

Address an independent review of the data-fetch paths so a 200 with an
unexpected shape can never read as a benign "no findings" view.

- SecurityView: validate that the image-summaries body is a scan-summary map; an
  unexpected shape now sets the error state instead of an empty map. Isolate the
  trend fetch in its own self-catching promise so a transport failure on the
  non-critical chart can no longer poison the overview or summaries error state.
- useImageScan: only a "completed" poll counts as success (a malformed or unknown
  status now throws), and a failed post-scan summaries refresh is logged instead
  of silently dropped.
- HistoryTab: a 200 whose body lacks an items array is treated as an error, not
  an empty "no completed scans" list.
2026-06-12 14:35:03 -04:00
Anso 1b96f3b980 feat: add a compact icon-only top navigation toggle (#1363)
* feat: add a compact icon-only top navigation toggle

Add a browser-local "Top navigation labels" preference under Settings >
Appearance. With it off, the desktop top navigation renders icon-only;
each destination keeps an aria-label, gains a hover/focus tooltip, and
stays reachable from the command palette. The setting defaults on, so
current behavior is preserved, and the mobile navigation always keeps
its labels.

Also left-align the desktop nav (previously centered) and shorten the
longest nav label from "Auto-Update" to "Update" so the bar scans faster.

* feat: let the icon-only top nav be left or centered

Add a "Top navigation alignment" preference under Settings > Appearance
that appears only when top navigation labels are off. It places the
icon-only bar against the left edge (the default) or centered. With
labels on, the nav always stays left so the longer labels read from the
edge. The choice is browser-local and persists per device.
2026-06-12 10:49:36 -04:00
Anso 48cebf9501 fix: bind deploy progress, request, and health gate to the captured node (#1357)
* fix: bind deploy progress, request, and health gate to the captured node

A deploy/update/install/git-apply re-read the active node from localStorage
independently at three points: the progress WebSocket at mount, the POST at call
time, and the health-gate poll. If the active node changed between the click and
any of those, the operation, its live output, and its health verdict could
target different nodes, and the socket and POST splitting across nodes broke
output streaming.

Capture the operation's node once when it starts and thread it through every
leg. A new nodeId option on apiFetch overrides the active-node read, the
progress terminal takes a nodeId prop for its socket URL, the health gate polls
on the captured node, and a failed gate records its recovery entry only on the
node it ran on. The surface, the request, and the gate now always agree.

* fix: scope failed-gate recovery to the file list's node and harden node targeting

Addresses review findings on the captured-node binding:

- Track the node the stack file list was fetched for (filesNodeId) and record a
  failed gate's recovery entry only when it matches the gate's node. This closes
  a race where switching back to the gate's node could match a same-named stack
  from the previous node's still-loaded list before the new list lands, keying
  the record to the wrong file and blocking the correct one. refreshStacks now
  carries a sequence token so an out-of-order resolution cannot leave files and
  filesNodeId inconsistent.
- Make an explicit apiFetch nodeId authoritative over a caller-supplied
  x-node-id header.
- Add the missing stack-logs nodeId cases (null, and active-node fallback) to the
  terminal tests.
2026-06-11 14:36:20 -04:00
Anso e20f1fe415 feat: add an inline deploy-progress style for the stack detail (#1355)
* feat: add an inline deploy-progress style for the stack detail

Deploy progress gains a presentation choice under Settings > Appearance >
Display: Modal (the default centered overlay) or Inline. In Inline style a
compact status band on the stack detail shows the running operation, its
elapsed time, the live phase, the latest output line, and the post-update
health gate result. A "View output" button opens the full log modal on
demand, a dismiss control clears the band, and the band auto-clears a few
seconds after a clean completion.

The live progress socket is lifted to an always-mounted owner so the band
streams without the modal; the default Modal style is unchanged. Operations
carry their node so a band never bleeds onto a same-named stack on another
node.

The stack detail's redundant "CONTAINERS" section heading is removed; the
band reserves that vertical space.

* fix: keep inline deploy progress reachable off the stack detail

Review of the inline presentation found a gap: a failed operation, an App
Store install, or navigating away leaves the inline session with no visible
surface, since the band only renders on the operation's own stack detail.
Restore the minimized pill as the inline fallback, shown only when the band
is not covering the session, so there is always a click-through to the log
without ever overlapping the band. Closing the modal for a failed op now
ends the session (the band has stepped aside) instead of only hiding it.

Also document the unsupported mid-operation style switch, and refresh the
deploy-progress, settings, appearance, and app-store docs for the renamed
"Deploy progress" setting and the Modal/Inline choice.
2026-06-11 10:33:57 -04:00