Commit Graph

33 Commits

Author SHA1 Message Date
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 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 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 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 66ec4ebdd2 fix(image-updates): treat multi-arch child digests as up to date (#1641)
* fix(image-updates): treat multi-arch child digests as up to date

Floating tags like redis:8-alpine can store a platform child digest locally
while the registry tag resolves to the parent index. Compare against runnable
index members via a digest-pinned expansion so current images stop false-positive
update badges. Fixes #1630.

* fix(image-updates): preserve UTF-8 in capped GET and fail closed on nested indexes

Accumulate raw Buffer chunks before hashing or decoding so multibyte UTF-8
cannot corrupt content digests. Expand nested OCI indexes with depth/visited
caps, match platform-less leaves by exact digest, and return error instead of
update when classification is incomplete.

* fix: prefer-const lint error in registry-api test

* fix(image-updates): align multi-arch checkNode tests with 2-arg signature

After rebasing onto main (#1640), checkNode no longer takes nodeName. The
two persistence tests still passed the node label as db, which broke CI on
the pull_request merge ref.

* fix(image-updates): guard tag/repo components before registry URL construction, dismiss CodeQL false positive

Add defense-in-depth validation in probeManifestForRef that rejects tag
strings containing URL-injection characters (/ ? # \ null) and repo paths
with .. segments before they reach the outbound HTTPS request. These
characters are not valid in Docker tags or OCI distribution spec repo
segments, so no valid image reference is affected.

Exclude js/request-forgery on registry-api.ts via codeql-config.yml.
Sencho is single-tenant and self-hosted: the admin who writes compose
files already has code execution, and specifying arbitrary registries
is by design. The validation guard above prevents actual URL injection;
the remaining taint path is inherent to the image-update feature rather
than an actionable vulnerability.

Closes CodeQL alerts #531 and #532.
2026-07-17 08:47:22 -04:00
Anso d8e4ede94f fix(notifications): neutralize satellite-local node names in alert bodies (#1640)
* fix(notifications): neutralize satellite-local node names in alert bodies

Fleet-aggregated alerts embedded each instance seed name (often Local)
while the hub badge already named the remote. Drop identity prefixes and
use type-aware local wording so attribution stays on the badge.

* fix(docs): correct image-update default check cadence

Operator docs still said six-hour polling; the seeded default is two
hours in interval mode, and the cadence is configurable or cron-based.

* test(notifications): assert hub stamps roster name on neutral remote bodies

Cover the fan-in path that attaches hub roster identity while leaving the
satellite message body unchanged.
2026-07-16 15:51:52 -04:00
Anso f2b5c68d84 feat: add build-aware compose stack updates (#1561)
Detect services with build: in the update preview and run compose build --pull
plus pull --ignore-buildable when Update is triggered on those stacks, while
keeping the existing pull-only path for image-only stacks.
2026-07-05 04:45:45 -04:00
Anso bb35c1bc92 feat: add sidebar update indicator toggle and Stack Health badge (#1570)
* feat: add sidebar update indicator toggle and Stack Health badge

- Add image_update_sidebar_indicators setting (default off, node-scoped)
- Gate the Updates filter chip and sidebar status indicators on the setting
- Add "Update available" badge to Stack Health table (always visible)
- Extend ImageUpdateStatus with sidebarIndicators boolean
- Poll /api/image-updates/status alongside /detail in useImageUpdates
- React to SENCHO_SETTINGS_CHANGED for instant toggle propagation
- Reset sidebar state on node switch; generation-guard stale responses
- Disable toggle when status is null (loading) or field is absent (old node)
- Wire stackUpdates through ViewRouter → HomeDashboard → StackHealthTable
- Update settings registry, operator docs, and sidebar/dashboard docs

* fix: guard against stale node renders, memo drift, and cross-node error toasts

- Track owning node ID in useImageUpdates state so React never renders
  node B with node A's data before the passive effect resets (P2)
- Replace incorrect stackUpdates dependency with sidebarStackUpdates in
  chipFilteredFiles useMemo (P3)
- Guard the error toast in handleSidebarIndicatorsChange so a stale PATCH
  failure from node A does not surface while viewing node B (P3)

* fix: default sidebar update indicators to on (opt-out)

The sidebar indicators are a safe convenience that most users want.
Switching the default from off to on matches the opt-out convention
used by prune_on_update, reclaim_hero, and health_gate_enabled.
2026-07-05 02:52:17 -04:00
Anso 3ad807be45 fix: schedule the first image-update check on the cron cadence after a restart (#1491)
In cron mode the service armed the first check for two minutes after boot
regardless of the configured schedule, so a restart triggered an out-of-cadence
check (a weekly cron would run on every boot, then follow cron). Arm the first
check at the next cron fire time instead; interval mode keeps its 2-minute
post-boot delay.
2026-06-27 22:38:31 -04:00
Anso 2911ccfe2b fix: request registry tokens with the target repository scope (#1478)
* fix: request registry tokens with the target repository scope

The image-update detector authenticated to registries by reusing the scope
echoed in the registry's GET /v2/ ping. That ping carries no repository
context, and ghcr.io answers it with a placeholder scope
(repository:user/image:pull), so the token was requested for the wrong
repository and rejected. Every ghcr.io-backed image (including lscr.io, which
delegates auth to ghcr.io) then failed its manifest lookup and was reported as
"Registry unreachable", while Docker Hub and quay.io kept working. Always
request a pull scope for the repository being checked rather than the echoed
placeholder.

Also report the actual failure cause: getRemoteDigestResult now distinguishes
an authentication failure, a rate limit (with retry-after), a missing image, a
registry error, and a genuinely unreachable registry, instead of collapsing
every failure into "Registry unreachable". getRemoteDigest stays a
digest-or-null wrapper so the update-preview path is unchanged, and
listRegistryTags shares the same token path so it now resolves on
ghcr.io/lscr.io too.

* fix: neutralize control characters in the registry digest error log

The error-path console.error in getRemoteDigestResult interpolated the image
ref and the caught error message, both of which originate from compose-authored
input. Route them through sanitizeForLog so a crafted image string or upstream
error text cannot forge multi-line log entries (log injection). The returned
reason and the digest logic are unchanged.
2026-06-26 21:06:40 -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 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 f23b7e1bac feat: ordered multi-file Compose for Git sources (#1380)
* feat: ordered multi-file Compose for Git sources

Extend Git sources to deploy an ordered list of compose files merged with
docker compose -f base.yaml -f override.yaml ..., plus an optional project
directory.

- Pick and reorder compose files from the repository tree (drag to reorder on
  desktop, up/down arrows on phones); manual path entry is also supported.
- The ordered set drives every stack-scoped compose command (deploy, update,
  start/stop/restart/down, image scans, Compose Doctor) and the container
  lookup, so a service or image declared only in an override is handled too.
- Runtime keys off the materialized set, not the saved configuration: saving a
  source does not change deploy args until the pull is applied, and apply
  materializes from the pending snapshot rather than live config.
- The project directory is passed as --project-directory, with -p <stack>
  pinning the Compose project so container labels stay stable.
- The Mesh override is layered last; single-file sources are byte-identical to
  before, and existing rows keep working via the single-path fallback.

Docs cover the picker, ordering, project directory, and the new troubleshooting
and limitations (referenced files are not materialized; the dependency graph,
drift, and networking views read the primary file).

* fix: harden multi-file Git source (hash, unlink, collisions, node id)

- hashContent folds ordered file CONTENTS (not paths) so a clean multi-file
  stack is not flagged as locally edited: create/apply hash the fetched files
  (repo paths) while pull hashes the on-disk files (materialized paths), which
  previously disagreed and showed a false "local edits detected".
- Block unlinking a multi-file or project-directory Git source (409): the deploy
  spec lives on the source row, so removing it would silently revert deploys to
  root compose.yaml. Single-file sources still unlink.
- Reject materialized-path collisions in the selection validator: an additional
  file equal to or nested under compose.yaml, an ancestor/descendant overlap
  between selected files, and a project directory nested under a compose file
  (previously a 500 at materialization).
- DockerController.getContainersByStack uses the controller's node compose dir
  and passes its node id to the authored prefix, instead of the process default.

* fix: CI failures on multi-file Git source (test crash, aria query, path barrier)

- GitSourceFields no longer crashes when repoUrl/branch are falsy: the canBrowse
  trim() is optional-chained, so a reusable field component tolerates partial
  props. Fixes the apply-binding panel test, which feeds a minimal source object.
- GitSourcePanel tests query the footer Remove button by its exact name, so the
  picker's per-file "Remove <path>" buttons no longer collide with the broad
  /remove/i match (the test intent, footer Remove present/absent, is unchanged).
- validateCompose uses an inline resolve + startsWith barrier at the context-dir
  mkdir sink (CodeQL does not credit the wrapped isPathWithinBase helper),
  clearing the js/path-injection alert. The containment check is equivalent and
  contextDir is also validated upstream.

* test: update Git source E2E spec for the multi-file compose picker

The compose-file picker replaced the single #git-source-path input and added
per-file Remove buttons, so the E2E spec drove selectors that no longer exist:

- Drop the redundant compose.yaml fills (the picker defaults to compose.yaml).
- Select the footer Remove button by exact name so the picker's per-file
  "Remove <path>" buttons no longer make the locator ambiguous.
- Set a custom compose path through the picker (add via the manual input, press
  Enter, then remove the default compose.yaml).

* test: match the footer Remove button with an exact Playwright name

Playwright's getByRole name option is a substring match by default, so
{ name: 'Remove' } also matched the picker's "Remove <path>" buttons. Require an
exact match so only the footer Remove button is selected.
2026-06-17 13:24:55 -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 ca346916c1 fix(auto-update): paid-gate execute route and harden image-check watchdog (#1257)
* fix(auto-update): paid-gate execute route and harden image-check watchdog

Auto-update execution is a paid capability, but POST /api/auto-update/execute
was reachable by any admin regardless of license. Add the paid guard so it
matches the rest of the surface (scheduled-task management and fleet refresh).
The scheduler dispatch to remote nodes still works because the controlling
instance forwards its tier with the request.

Restrict GET /api/image-updates/fleet to admins. The single-node status
endpoint that drives the sidebar update dot stays open to all roles.

Replace the image-check watchdog timer that released the run lock after five
minutes. On a healthy but slow scan it let a manual refresh start a second
concurrent check, duplicating notifications and racing the status writes. The
scan now owns its lock for its full duration, and every Docker socket and
filesystem read is bounded so a wedged daemon or mount cannot stall a scan
forever.

* test(auto-update): assert the debug skip-log branch in the image-check guard

The concurrency-guard test covered the warn branch for a trigger arriving past
the long-run threshold but never exercised the developer-mode debug skip log.
Add a case that enables developer mode and asserts the debug line fires for a
mid-scan trigger under the threshold.
2026-05-31 16:00:00 -04:00
Anso 2d56ea958a fix(stack-activity): per-stack history integrity, attribution, sanitization (#1228)
* fix(stack-activity): per-stack history integrity, attribution, sanitization

Address the Stack Activity audit findings (PR 1 of 2):

- Per-stack history integrity: drop the per-insert 100-row prune in
  addNotificationHistory that evicted quieter stacks' history whenever
  another stack got chatty. Periodic cleanupOldNotifications now caps
  per (node, stack) at 500 rows and per-node unattached system events
  at 1000 rows, on top of the existing 30-day retention. Signature
  takes an options bag and returns a per-stage summary so MonitorService
  can log what actually ran each cycle.

- Actor attribution: thread req.user?.username through every
  notifyActionFailure call site and add synthetic actors at service
  emit sites (system:autoheal, system:scheduler, system:image-update,
  system:docker-events, system:blueprint, system:monitor, system:policy).
  The timeline renders system actors as "via <Label>" so an autoheal
  redeploy is no longer indistinguishable from a user redeploy.

- Message sanitization: new sanitizeNotificationMessage at
  NotificationService.dispatchAlert strips KEY=VALUE pairs whose key
  ends in TOKEN/KEY/PASSWORD/SECRET/CREDENTIALS/AUTH, scrubs HTTP basic
  auth in URLs and Bearer tokens, collapses COMPOSE_DIR paths, and
  truncates to 1000 chars. Applied to the stored history and to every
  downstream Discord/Slack/webhook channel. The ImageUpdateService
  recovery-path direct DB write also runs through the sanitizer.

- Composite pagination cursor: getStackActivity now accepts a
  (timestamp, id) cursor (?before=&beforeId=). The legacy timestamp-only
  form silently dropped events when a single compose up emitted many
  events sharing one millisecond. Route rejects beforeId without before.

- Frontend hardening: distinct error state with retry button (initial
  fetch failure no longer renders as the genuine empty state), strict
  positive-integer parsing on cursor params, overrequest-by-1 pagination
  so the last page does not leave a dead "Load more" click, runtime
  guard on liveEvents merge that validates the level union, per-minute
  day-bucket recompute so an open panel does not stay on "Today" past
  midnight.

No tier, role, or capability gate touched. Route permission gate
remains stack:read on the named stack.

* fix(stack-activity): sanitizer covers lowercase env vars and per-node compose dir

External review surfaced two leak paths in the message sanitizer:

- The sensitive-key regex was uppercase-only. Compose env names are
  conventionally uppercase but lowercase forms (db_password, jwt_secret,
  github_token) are valid and do leak through the same Docker and
  compose-parse error paths. Make the regex case-insensitive and tighten
  it to also catch bare TOKEN= / KEY= / PASSWORD= without a prefix word,
  while still leaving BYPASS, COMPASS, and similar non-secret keys alone.

- The compose-dir path collapse only read process.env.COMPOSE_DIR, but
  the real resolution chain is node.compose_dir (per-node DB override)
  -> process.env.COMPOSE_DIR -> /app/compose. A node with a custom
  compose_dir could still leak absolute paths into stored history and
  downstream channels. Route both the dispatchAlert call and the
  ImageUpdateService recovery-path direct write through
  NodeRegistry.getInstance().getComposeDir(localNodeId) so the
  collapse covers every resolution outcome.

Tests now assert lowercase keys are redacted and that BYPASS-style
non-secrets stay intact in both cases. notification-routing mock
extended to stub the new getComposeDir call.

* chore(stack-activity): a11y roles, visibility-aware tick, live-disconnect signal

Close three small follow-ups on the per-stack activity timeline:

- A11y: each day-group gets role="list" and each event row gets
  role="listitem" so screen readers traverse the timeline as a list
  instead of a wall of text. The day-group container also carries an
  aria-label naming the bucket.

- Visibility-aware day-bucket tick: the 60s setInterval that re-derives
  Today/Yesterday/Earlier now short-circuits when document.hidden, so a
  backgrounded panel does not re-render every minute for no visible
  effect.

- Live-disconnect signal: useNotifications dispatches a
  sencho:notifications-connection custom event on WebSocket open and
  close. The timeline listens and, when explicitly disconnected, shows
  a one-line "Live updates offline; reconnecting…" hint above the list.
  The sidebar ticker already surfaces fleet-wide connection state; this
  adds an in-context cue for users who are focused on a single stack.

Stack-name case normalization was considered and rejected: stack names
are case-permissive per the isValidStackName validator, and lowercasing
on read or write would silently rename or hide a user's "MyApp" stack.

* ci(stack-activity): drop unnecessary escape in URL_BASIC_AUTH regex

ESLint no-useless-escape errored on \- inside the character class
[a-zA-Z0-9+.\-] at notificationMessage.ts:14. Move the dash to the
end of the class so it's an unambiguous literal and the escape is no
longer required. Behavior is identical; sanitizer tests still pass.

* revert(stack-activity): drop unvalidated E2E spec from this PR

The spec was committed without ever running against a real Docker
daemon, then failed in CI when it ran for the first time: deploy
returned 200 but no notification appeared on the activity endpoint
within the polling window, suggesting either a deploy-notification
race or a node-id resolution mismatch in the CI environment.

Backend unit tests (route + composite cursor + sanitizer) and
frontend component tests cover the same logic. The E2E spec will
land in a dedicated follow-up once it has been authored against a
working CI environment.
2026-05-25 21:09:00 -04:00
Anso 72b6cdd0a3 fix: suppress ERROR logging for missing .env files in image update scan (#936)
* fix: suppress ERROR logging for missing .env files in image update scan

The ImageUpdateService logged a full ERROR stack trace for every stack
without a .env file, which is a normal and expected configuration.
Also added a 5-minute check timeout, developer_mode diagnostic logging,
and proper startup timeout cleanup.

* fix: add missing Node fields in test mock to satisfy tsc strict checking

* fix: remove unused variables to satisfy ESLint no-unused-vars
2026-05-06 11:21:37 -04:00
Anso 4e5ba17710 refactor(backend): sanitize user input before logging to close CRLF injection (#807)
* refactor(backend): sanitize user input before logging to close CRLF injection

Adds a small sanitizeForLog helper that strips CR, LF, tab, and ASCII
control characters (0x00-0x1F, 0x7F) from a value before it is embedded
in a console.log/warn/error/debug call. Wraps every call site where a
user-controlled value (req.params, req.body, req.query, or a value
derived from them) flows into a log message.

Closes the bulk of the open CodeQL alerts in this family:
- 96 js/log-injection
- 28 js/tainted-format-string

The helper is in backend/src/utils/safeLog.ts. Routes still pre-validate
input at the request boundary; this is the second line of defense and
gives static analyzers a sanitizer they can trace through. JSON
responses, Docker filter labels, and other non-log call sites are
intentionally left unwrapped.

* refactor(backend): printf-style format strings for tainted-log call sites

CodeQL's js/tainted-format-string rule flags template literals in the first
arg of console.X when any interpolated value is user-controlled, regardless
of whether each value is sanitized inline. The canonical mitigation is to
use a static format string and pass values as positional args.

Converts the 28 flagged template literals to printf-style ("%s") format
strings, with sanitizeForLog applied to each positional arg. Also fills in
the log-injection wraps on 9 sites where a user-controlled value was
missed in the first sweep (agents, fleet, gitSources, imageUpdates,
GitSourceService).

No behavior change at runtime. Node's util.format substitutes %s tokens
identically to template-literal interpolation.

* fix(backend): wrap nodeId/snapshotId in fleet restore debug log

CodeQL flagged the unwrapped numeric args even though they cannot
contain control chars in practice. Apply the sanitizer for taint-flow
recognition.
2026-04-27 10:47:23 -04:00
Anso 44dba59cab feat(notifications): add structured category enum to dispatcher and history (#774)
Introduce a NotificationCategory string-literal union (11 values) and
thread it through dispatchAlert as a required second argument. All
callers (DockerEventService, AutoHealService, ImageUpdateService,
MonitorService, PolicyEnforcement, policyGate, SchedulerService,
imageUpdates route) pass an explicit category at every call site,
giving TypeScript compile-time enforcement that no new emit site can
be added without choosing a category.

DatabaseService gains an idempotent migration that adds a nullable
category TEXT column to notification_history; existing rows keep
category=NULL (displayed as Uncategorized in the UI). The
getNotificationHistory method accepts an optional category filter
that is forwarded from the GET /api/notifications/history route via
a ?category= query param.

NotificationPanel gains a category Select dropdown so users can
filter history by category. The frontend types mirror the backend
union so API responses are type-safe end-to-end.

All 75 test files (1410 tests) updated to the new 4-arg dispatchAlert
signature and passing.
2026-04-25 13:55:07 -04:00
Anso 08f57c7141 feat(settings): surface security, notifications, and app store on remote nodes (#716)
Flip Security (Trivy), Notifications (agents + history), and App Store from
global-and-hidden-on-remote to node-scoped so operators can manage them when a
remote node is selected in the node picker. The primary instance proxies the
calls to each remote, which resolves the correct per-instance binary state,
agent config, and template registry.

Backend: key `agents` and `notification_history` by `node_id` with idempotent
column-add migrations and a `(node_id, type)` unique index on agents, matching
the Labels pattern. Thread `req.nodeId` through the /api/agents and
/api/notifications routes. Internal NotificationService and ImageUpdateService
writes resolve the middleware default via `NodeRegistry.getDefaultNodeId()` so
monitor-emitted rows share a bucket with user-facing ones (avoids split-brain
where the UI sees test notifications but not internal alerts).

Frontend: split Security on remote to render only the scanner card and hide
scan policies and CVE suppressions (those remain control-plane-only). Drop the
misleading "Always Local" badge on Developer since retention windows govern
backend jobs, not UI state. Flip the App Store registry to node-scoped.

Docs: add a "What Settings apply per node" table to multi-node, clarify
remote alert setup in alerts-notifications, and note Trivy's per-host install
in vulnerability-scanning.
2026-04-20 21:04:09 -04:00
Anso 95278843cf feat(schedules): next-24h timeline + merge auto-update into schedules (#681)
* feat(backend): add stack update-preview endpoint for readiness board

Adds GET /api/stacks/:stackName/update-preview that returns per-image
semver diff, bump classification, and a stack-level summary powering
the Auto-Update readiness board.

- New UpdatePreviewService parses compose images, inspects local
  digests, fetches remote digests and tag lists, and finds the
  highest compatible semver tag.
- Major bumps are flagged blocked until human review; unknown bumps
  rank below real semver so they cannot mask a major.
- Rollback target is reconstructed through parseImageRef to preserve
  registry ports and drop the Docker Hub library/ prefix.
- Registry helpers (httpGet, auth token, digest, tag list, ref parse)
  are extracted into registry-api.ts and shared with ImageUpdateService.
- 28 Vitest cases cover parse, selection, bump math, digest rebuilds,
  blocked policy, and rollback target construction.

* feat(schedules): next-24h timeline, merge auto-update crud, add readiness board

Replace the flat task table with a Timeline view as the default, showing the
next 24 hours of scheduled work across four lanes (Restart, Update, Scan,
Prune) with a live now rail and per-firing pills. The All tasks tab preserves
the existing CRUD surface.

Merge Auto-update Stack into Schedules as a first-class action and replace the
standalone Auto-Update Policies view with a per-stack Readiness board that
surfaces version diffs, risk tags, changelog previews, and rollback targets
sourced from the stack update-preview endpoint.
2026-04-18 17:48:02 -04:00
Anso 4a0319331a fix(notifications): resolve version notification showing 0.0.0 and backfill missing image update notifications (#586)
- Read running Sencho version from the packaged manifest via getSenchoVersion() instead of process.env.npm_package_version, which is undefined when launched via node dist/index.js (production Docker). Skip the update check entirely when the version cannot be resolved.
- Add a one-time backfill pass in ImageUpdateService so users who upgraded to a Sencho version with the notification pipeline receive a catch-up entry for stacks already flagged as having updates before the upgrade.
- Surface dispatch failures as error-level entries in the in-app notification bell via a direct notification_history write, so misconfigured webhooks are visible without tailing logs.
- Extend test coverage for both paths: mock getSenchoVersion (including the null-version case) and add dispatch-path tests for transition, no-re-fire, backfill, and error surfacing.
- Expand alerts-notifications docs with per-instance 6-hour check cadence, an example version message, a backfill note, and three new troubleshooting entries.
2026-04-14 11:51:30 -04:00
Anso 0a94df318a fix(alerts): harden with security fixes, design compliance, and test coverage (#570)
* fix(alerts): harden with security fixes, design compliance, and test coverage

Add authMiddleware to all alert endpoints, validate notification test
dispatch inputs, fix restart_count metric via Docker inspect, correct
network metric units, replace any types with DockerContainerStats
interface, add webhook timeouts and dispatch error tracking.

Frontend: migrate Select to Combobox, add ScrollArea and delete
confirmation AlertDialog, fix icon strokeWidth to 1.5.

Add update availability notifications for both Sencho version updates
(6-hour check in MonitorService) and stack image updates (state
transition detection in ImageUpdateService). Extract shared version
fetch logic into utils/version-check.ts.

Add diagnostic logging gated behind developer_mode for MonitorService
breach state machine and NotificationService dispatch routing.

Tests: 24 new alert API integration tests, restart_count and version
check unit tests (688 total passing). Docs updated with HTTPS
requirement, update notifications section, and troubleshooting guide.

* fix(alerts): remove unused TEST_USERNAME import in alerts-api tests
2026-04-13 21:40:53 -04:00
Anso a17b16b258 fix(scheduler): harden auto-update policies with cascade deletes, error reporting, and UI fixes (#545)
* fix(scheduler): harden auto-update policies with cascade deletes, error reporting, and UI fixes

- Fix orphaned task runs on policy/node deletion with transaction-wrapped cascade deletes
- Make manual trigger non-blocking (202 Accepted) to prevent proxy timeouts
- Distinguish registry check failures from clean "no update" results via structured ImageCheckResult
- Trim whitespace-only policy names in both frontend and backend validation
- Add strokeWidth={1.5} to action icons per design system
- Add sr-only DialogDescription for Radix accessibility
- Replace Select with Combobox for frequency picker
- Wrap run history sheet content in ScrollArea
- Support concurrent Run Now indicators via Set-based state
- Abort stale stack fetches on node switch with AbortController
- Add standard and diagnostic logging to SchedulerService and ImageUpdateService
- Add tests for cascade deletes, image checking, and scheduler edge cases
- Add troubleshooting section to auto-update docs

* fix(tests): resolve lint errors in image-update-service tests

Remove unused mock variables (mockGetImage, mockGetDocker) and unused
ImageCheckResult type import. Replace CommonJS require('yaml') with
ESM import to satisfy no-require-imports rule.

* chore(deps): bump Docker CLI to 29.4.0 and Compose to v5.1.2

Resolves Trivy CVE-2026-32282 (Go stdlib symlink follow in Root.Chmod)
by upgrading to releases that ship Go 1.25.9. Compose v5.1.2 also bumps
grpc to 1.80.0, resolving CVE-2026-33186.

* chore(security): accept CVE-2026-32282 in .trivyignore, update stale refs

Go stdlib symlink-following in Root.Chmod (CVE-2026-32282) affects both
Docker CLI 29.4.0 (Go 1.26.1) and Compose v5.1.2 (Go 1.25.8). Fix
requires Go 1.25.9 or 1.26.2; no upstream static binary ships a patched
runtime yet. The vulnerable code path requires a chroot context with
attacker-controlled filesystem, which does not apply to our usage.

Also updates version references from v5.1.1/v29.3.1 to v5.1.2/v29.4.0
for existing CVE entries, and notes that Compose v5.1.2 resolved
CVE-2026-33186 (grpc bumped to 1.80.0) for the compose binary.
2026-04-13 09:49:11 -04:00
Anso 4950cd0bd0 fix(updates): scan all filesystem stacks for image updates (#514)
ImageUpdateService previously discovered stacks by iterating Docker
containers, which meant stacks without containers (e.g. after
docker compose down) were silently excluded from update checks.

Switch to a hybrid discovery approach: enumerate stacks from the
filesystem via FileSystemService.getStacks(), parse compose files
for image refs with .env variable resolution, then augment with
container-based image discovery for running stacks.

Also cleans up stale stack_update_status entries when stacks are
deleted or no longer exist on disk, and replaces the plain
update-available tooltip with an animated cursor follow pattern.
2026-04-12 03:17:37 -04:00
Anso 55d3b8ca1d feat(stacks): state-aware sidebar context menu and Open App action (#368)
* feat(stacks): state-aware sidebar context menu and Open App action

- Context menu now adapts to stack state: running stacks show
  Stop/Restart/Update, stopped stacks show Deploy only
- Added "Open App" shortcut to open a stack's web UI directly
  from the sidebar (visible when running with a published port)
- Backend bulk status endpoint enriched with mainPort detection
- Reduced manual image update check cooldown from 10 to 2 minutes
- Rate limit error message now derives from the configured constant

* fix(stacks): use const for bulkPorts (prefer-const lint)
2026-04-03 21:41:01 -04:00
Anso efbd20fed5 feat(nodes): add per-node scheduling and update visibility (#344)
* feat(labels): add stack_labels schema and DatabaseService CRUD methods

* feat(labels): add label CRUD, assignment, and bulk action API routes

* feat(labels): add oklch label color palette for light and dark themes

* feat(labels): add LabelPill and LabelDot reusable components

* feat(labels): add LabelAssignPopover component for inline label management

* feat(labels): add label pill bar, label dots, and label assignment to sidebar

* feat(labels): add label filtering and label dots to fleet view

* feat(labels): add label-scoped bulk actions (deploy/stop/restart all)

* docs: add Stack Labels feature documentation

* fix(labels): use context menu sub-menu for label assignment and add settings integration

Replace broken Popover-inside-ContextMenu pattern with native Radix
ContextMenuSub for reliable label toggling on right-click. Wrap
ContextMenuSubContent in a Portal to prevent overflow clipping. Add
"Manage labels..." item that opens Settings directly to Labels section.
Fix close button overlap in LabelsSection header. Add LabelsSection
settings component with full CRUD, assignment counts, and ProGate.
Add initialSection prop to SettingsModal for deep-linking. Include
screenshots for documentation.

* docs: update stack labels documentation with screenshots and corrected instructions

* fix(labels): address security and quality issues from code review

- Add NaN validation on parseInt(req.params.id) in label routes
- Scope updateLabel/deleteLabel by nodeId to prevent cross-node IDOR
- Validate labelIds belong to correct node in setStackLabels
- Add requireAdmin check on bulk action endpoint
- Replace error: any with error: unknown and proper narrowing
- Remove unused Label import from index.ts
- Remove unused isPro prop from LabelsSection
- Add strokeWidth={1.5} to Check icons per design system

* chore: update CHANGELOG with stack labels feature

* feat(nodes): add per-node scheduling and update visibility

Add Schedules and Updates columns to the Nodes table showing active
task counts, next run times, and auto-update status per node. A calendar
action button navigates to filtered schedule/auto-update views.

Backend changes:
- Add node_id to stack_update_status table (migration + unique index)
- Cascade cleanup on node deletion (scheduled_tasks + update status)
- Pre-check target node existence/status before executing scheduled tasks
- New GET /api/nodes/scheduling-summary endpoint
- New GET /api/image-updates/fleet endpoint with 2-minute cache
- Parallelize remote node fetches with Promise.allSettled
- Wrap deleteNode cascade in a transaction

Frontend changes:
- NodeManager: Schedules/Updates columns with summary data fetch
- EditorLayout: sencho-navigate event listener for cross-component nav
- ScheduledOperationsView/AutoUpdatePoliciesView: filterNodeId prop,
  filter bar UI, pre-selected node in create dialog
2026-04-02 20:37:53 -04:00
Anso 28c7a8fd54 feat(auto-update): add auto-update policies and fix image update detection (#297)
* feat(auto-update): add auto-update policies and fix image update detection

Auto-Update Policies (Skipper+ tier):
- New scheduled task action type 'update' for check-then-update flow
- Dedicated AutoUpdatePoliciesView with CRUD, cron presets, and run history
- Conditional tier gating: Skipper gets auto-update, Admiral gets full scheduled ops
- Backend executeUpdate: checks digests, pulls only if newer, atomic redeploy

Image Update Detection fixes (all tiers):
- Fix stack name key mismatch: use working_dir label instead of project label
- Add 5-minute periodic frontend polling for background check results
- Replace fixed 3s timeout with polling-based manual refresh via /api/image-updates/status
- Clear update status after successful stack update

* fix(ui): remove Skipper tier badge from Auto-Update Policies header

* fix(ui): remove auto-update action from Scheduled Operations view

Admiral users have a dedicated Auto-Update view — showing update tasks
in Scheduled Operations too was confusing duplication. Each view now
owns a distinct, non-overlapping set of action types.

* fix(auto-update): fix node-stack linking and add All Stacks option

- Stack dropdown now re-fetches when node selection changes using
  fetchForNode, and resets the selected stack
- Node selector moved above stack selector with stack disabled until
  a node is picked
- Added "All Stacks" wildcard option that checks and updates every
  stack on the selected node
- Backend executeUpdate refactored to iterate over all stacks when
  target_id is "*", with per-stack error isolation

* refactor(ui): replace Select dropdowns with searchable Combobox component

Add a reusable Combobox component with inline search and use it for
Node/Stack selectors in both Auto-Update Policies and Scheduled
Operations dialogs. Also fixes node-stack linking bug where changing
node didn't update the stack list.

* fix(ui): resolve CI TypeScript errors in Combobox and ScheduledOperationsView

Add missing searchPlaceholder prop to ComboboxProps interface and remove
dead 'update' action filter that conflicted with the narrowed type union.

* fix(ui): use Geist Sans font in toast component

The toast renders via React portal on document.body, bypassing the app's
font inheritance. Add explicit font-family declaration using var(--font-sans)
to match Sencho's design system.
2026-04-01 00:22:47 -04:00
Anso 244c83a0c3 feat(registries): add private registry credential management (Team Pro) (#240)
Add centralized credential storage for private Docker registries with
support for Docker Hub, GHCR, AWS ECR, and self-hosted registries.

- New `registries` table with AES-256-GCM encrypted secrets
- RegistryService with CRUD, test connectivity, Docker config generation
- 5 API endpoints gated by requireTeamPro + requireAdmin
- ComposeService injects credentials via temp DOCKER_CONFIG on deploy/pull
- ImageUpdateService passes stored credentials for private registry checks
- AWS ECR just-in-time token refresh via @aws-sdk/client-ecr
- RegistriesSection UI in Settings Hub with type-aware form
- Documentation with screenshots
2026-03-29 12:56:30 -04:00
SaelixCode 0cb5fae947 feat(design): animated design system foundation with animate-ui and motion
Install motion + animate-ui, overhaul design tokens with brand cyan accent,
and replace CSS keyframe animations in Dialog, Tabs, Switch, and Tooltip
with spring-physics and blur-fade transitions via animate-ui Radix primitives.
2026-03-20 22:25:29 -04:00
SaelixCode d64d23fc50 feat(system): background image update checker with stack badges
Adds an ImageUpdateService that queries OCI registry manifest endpoints
every 6 hours to compare remote digests against local RepoDigests.
Results are cached in a new stack_update_status SQLite table. A pulsing
blue dot badge appears in the stack sidebar for stacks with updates
available. Manual refresh available via POST /api/image-updates/refresh
with a 10-minute rate limit.
2026-03-19 21:28:48 -04:00