Commit Graph

8 Commits

Author SHA1 Message Date
Anso 02c3b006eb feat(fleet): refine the Fleet Overview toolbar (#1376)
* feat(fleet): tidy the Overview toolbar and shorten tab labels

- Move the Add node button into the Overview toolbar beside the
  Grid/Topology toggle so it sits with the view it acts on instead of
  showing on every Fleet tab. It stays admin-only.
- Collapse the node search to an icon button that expands to the full
  input on click and collapses again on blur once the query is empty,
  reclaiming toolbar width. An active query keeps it open.
- Match the sort-direction toggle and the sort dropdown to the outlined
  dark fill of the Filters button for a consistent toolbar row.
- Swap the Check Updates icon to the refresh-with-dot glyph.
- Rename two tabs: Fleet Actions to Actions, Dependencies to Map
  (display labels only; internal keys unchanged).

Update the feature docs to the new tab labels.

* feat(fleet): move Check Updates into the Overview tab toolbar

Check Updates is an Overview-specific action, but it lived in the shared
Fleet header toolbar that renders across every tab. Move it into the
Overview tab's own toolbar, to the right of the Add node button, so it
only appears where it applies.

The relocated button now spins and disables while a check is in flight,
matching the Refresh button's loading feedback. The shared header keeps
Refresh and Export Dossier.
2026-06-14 22:35:45 -04:00
Anso 77f1611971 feat: Compose Network Inspector and exposure intent guard (#1360)
* feat: add Compose Network Inspector facts engine

Render a stack's authored effective model and pair it with the live
Docker snapshot to derive per-stack networking facts: project networks
with external and internal flags, service-to-network membership and
aliases, published ports with host-binding scope, network_mode, and
extra_hosts, plus runtime drift (runtime-only attachments, foreign
networks, and declared-but-unused or missing networks).

Extend the effective-model parser with service network membership,
extra_hosts, and label keys (key names only, never values), and add a
key-space normalized network model with adapters from both the rendered
model and the raw declared compose so the Inspector and drift share one
comparison. Expose GET /api/stacks/:stackName/networking: advisory and
read-only, it renders the authored model only and never returns or logs
raw stderr, env values, or label values.

* feat: store and edit per-stack and per-service exposure intent

Add a stack_exposure_intent table (intent values constrained by a CHECK,
unique per node, stack, and service) with DAO methods to read, upsert,
clear one row, and clear all rows for a stack. The classification is
stored independently of the generated networking facts so a later
mismatch stays detectable; service rows are kept separately from the
stack-level row (service '').

Expose GET and PUT /api/stacks/:stackName/exposure: GET requires read
access, PUT requires edit access and validates the intent against the
allowed set. Sending intent null clears that row, returning the scope to
unset so a service inherits the stack intent again. Intent rows are
cleared when the stack is deleted and when the owning node is removed,
so a later same-named stack never picks up stale classification.

* feat: add exposure-aware Compose Doctor findings

Feed the Compose Doctor's effective-model context with the stored
exposure intent (resolved into a stack-level value plus per-service
overrides) and the dossier's documented access-URL ports, read fail-soft
so a metadata read error skips these checks rather than failing the
preflight. Add five deterministic findings on top of that context:

- a service classified internal or same-node that publishes a host port
  (same-node tolerates a loopback bind),
- a sensitive database or admin image published on all interfaces,
- a port-publishing stack with no exposure intent set,
- a published port not reflected in the documented access URLs,
- reverse-proxy labels with no documented URL or reverse-proxy intent.

The rules stay pure functions over the preflight context; the registry
completeness test pins the new rule set.

* feat: detect compose network drift in the drift ledger

Extend the spatial drift engine with two network-level findings: a
running container attached to a stack-owned or foreign network that
compose does not declare (one finding per service), and a declared
network that no running service uses or that is absent from the runtime
(one stack-level finding, every network named by its resolved runtime
name). The comparison reuses the same helper the Network Inspector uses,
so the two surfaces never disagree.

Network drift runs only when the stack has running containers and the
runtime is reachable, preserving the existing missing-runtime,
parse-error, and unreachable behavior. The findings persist through the
existing drift ledger and surface on the Drift tab, which now labels the
two new kinds.

* feat: link a Docker network back to its owning stack

Add a cross-component open-stack event and make the owning-stack badge on
a managed network in Resources a link: clicking it loads that stack on
its node and opens the editor, reusing the existing fleet navigation. A
latest-ref keeps the window listener current without re-subscribing each
render. Image and volume badges are unchanged; only a managed network
opts in via the new optional handler.

* feat: add the Networking tab to the stack detail panel

Add a capability-gated Networking tab that reads the per-stack networking
facts and exposure intent. It shows the project networks (with external,
internal, and created-by-stack flags), per-service network membership and
aliases, published ports with their host-binding scope, network_mode and
extra_hosts, and runtime drift, degrading to the declared model when the
runtime is unavailable. Users can classify the stack and each service
(internal, LAN, reverse proxy, public, and so on) or clear a row to
inherit; the controls are read-only when the user cannot edit, and a
broken exposure response never tears down the facts view.

A new compose-networking capability is added to both registries so older
nodes hide the tab, and the tab cross-links to the Doctor for the deploy
and security findings.

* docs: document the Compose Networking tab

Add a feature page covering the Networking tab: the network facts,
published ports and host bindings, the exposure-intent classification
and inheritance, the exposure-aware Doctor findings, runtime drift, and
a troubleshooting section. Register it in the docs navigation next to
Compose Doctor.

* feat: add a redacted network summary to the Stack Dossier export

Append a network exposure section to the dossier Markdown: the stack and
per-service exposure intents, the networks with their external and
internal flags, and each service's published ports with their binding
scope. It carries only names, intents, port numbers, and scope, never an
env value or a label value.

The summary is fetched only when the user exports (copy or download), so
opening the panel costs nothing, and it degrades to omitting the section
when the data is unavailable. The whole-fleet dossier export collects the
same summary per stack, rethrowing the unauthorized sentinel like the
sibling loaders.

* feat: add a Fleet networking filter for exposure and drift

Add a per-node networking summary that classifies a node's stacks as
exposed (a host port published beyond loopback), unknown-exposure
(publishes ports with no exposure intent set), or network-drift. It
reads each stack's compose with the light dependency parser and one
Docker snapshot, so it stays cheap across a node's full stack set, and
it skips drift when the runtime is unreachable rather than inventing it.

Serve it node-locally at GET /api/networking/summary, and aggregate it
fleet-wide at GET /api/fleet/networking-summary: the hub computes its own
summary in-process and reaches each remote through its node-local route,
degrading an unreachable or older node to a skip. Because the aggregate
lives under the proxy-exempt /api/fleet prefix it is never wrongly
proxied. The Fleet overview gains a networking filter chip backed by that
aggregate, fetched fail-soft and detached so it never gates the grid.

* fix: spin the Networking refresh button while it reloads

The refresh button silently refetched the same data, so a click gave no
feedback. Track a refreshing state and spin the icon while the load is in
flight, disabling the button, matching the Compose Doctor preflight
button.

* fix: apply effective per-service exposure intent to unclassified checks

The "unclassified exposure" decisions only consulted the stack-level intent
row, so a service classified directly (with no stack row) was still reported
as unclassified, and a service explicitly marked unknown over a classified
stack was missed.

Both the exposure-unclassified preflight rule and the networking summary's
unknown-exposure bucket now resolve the effective intent per publishing
service (service row overrides stack row), matching the precedence already
used by the exposure-internal-published rule.

* fix: resolve drift network names via the compose top-level name

When a compose file sets a top-level name:, Docker prefixes resource names
with that project name instead of the stack directory. The light dependency
parser dropped name:, so network-drift normalization compared runtime
networks against directory-prefixed names and reported false
network-undeclared / network-missing findings.

Carry the parsed project name through DeclaredCompose and use it when
normalizing declared networks for drift, while still filtering containers by
the stack directory.
2026-06-12 02:15:11 -04:00
Anso bef51a979f feat(fleet): tidy the Overview toolbar and shorten tab labels (#1361)
- Move the Add node button into the Overview toolbar beside the
  Grid/Topology toggle so it sits with the view it acts on instead of
  showing on every Fleet tab. It stays admin-only.
- Collapse the node search to an icon button that expands to the full
  input on click and collapses again on blur once the query is empty,
  reclaiming toolbar width. An active query keeps it open.
- Match the sort-direction toggle and the sort dropdown to the outlined
  dark fill of the Filters button for a consistent toolbar row.
- Swap the Check Updates icon to the refresh-with-dot glyph.
- Rename two tabs: Fleet Actions to Actions, Dependencies to Map
  (display labels only; internal keys unchanged).

Update the feature docs to the new tab labels.
2026-06-12 02:10:21 -04:00
Anso 54119be0c2 feat: move trivy auto-update, node labels, fleet topology, and single-scan SBOM to Community (#1336)
Rebalance several capabilities from the paid tier to the free Community tier:

- Managed Trivy auto-update toggle is admin-only, no longer tier-gated.
- Node labels (assign, view, manage) are available on every tier; cordon and
  FleetSync anchor reset stay paid.
- Fleet topology layout modes (Hub, Grouped, Free) are available on Community.
- Single-scan SBOM export (SPDX and CycloneDX) is admin-only on Community;
  SARIF export stays paid via a dedicated canExportSarif capability split out
  from the former shared SBOM flag.

Backend route guards and frontend affordances are updated together, with tier
and admin-role tests covering the Community-allowed and still-paid paths.
2026-06-08 08:58:14 -04:00
Anso 865d792874 feat(pricing): collapse to two tiers (#1309)
* feat(pricing): collapse to two tiers (Community + Admiral)

Collapse Sencho's pricing from three tiers (Community / Skipper / Admiral)
to two: a generous free Community tier and a single paid Admiral tier. The
Skipper tier is removed.

Now free in Community: auto-heal, auto-update, scheduled operations,
webhooks, notification routing, Fleet Actions and bulk operations, SSO
preset providers (Google / GitHub / Okta), unlimited users with admin and
viewer roles, and deploy safety (atomic deploys, auto-rollback, and
one-click rollback).

Admiral (paid) is focused on running and governing a fleet: blueprints,
Fleet Secrets, deploy enforcement, vulnerability report export, audit log,
host console, private registries, mesh networking, node cordon, managed
cloud backup, LDAP / Active Directory SSO, and the advanced RBAC roles
(deployer, node-admin, auditor) with per-resource scoped assignments.

Internally the license variant distinction is removed so tier is binary
(community / paid). License validation still verifies the Lemon Squeezy
store and product before granting paid status.

Docs and the contributor guide are updated to the two-tier model.

* docs(pricing): correct licensing page to two-tier pricing and tidy stale tier wording

The licensing docs page kept the old Admiral pricing plus a Founder
Lifetime column and an Enterprise paragraph after the two-tier collapse.
Update it to $12/month or $99/year, drop the lifetime and Enterprise
content, and link to the pricing page for current pricing.

Also fix stale "Skipper" wording in CLA.md, SUPPORT.md, one test title,
and three test comments. Historical CHANGELOG entries and the
retired-Skipper license-guard test are intentionally left as-is.

* docs: align licensing and SSO pages with the two-tier model

Correct the SSO overview so the Google, GitHub, and Okta presets read as
available on every tier, matching the provider table; only LDAP and Active
Directory require Sencho Admiral. Remove the lifetime-plan references from the
licensing, settings, and troubleshooting pages so they reflect subscription-only
Admiral pricing.

* fix(rbac): omit scoped permissions from /me on the Community tier

Scoped role assignments only take effect on the paid tier, but GET /api/permissions/me returned them unconditionally, so a downgraded instance with leftover assignments rendered per-resource affordances the API then rejected with 403. The endpoint now mirrors the permission middleware and includes scoped permissions only on the paid tier. Adds a regression test covering the downgrade case.

* docs: use custom-pricing wording on the contact page

The two-tier model has no Enterprise tier; reword the contact page's enterprise pricing/deals to custom pricing/deals so it does not imply a tier that no longer exists.
2026-06-04 17:45:53 -04:00
Anso dd2c2b22ec fix(federation): gate node cordon control on node:manage to match backend (#1277)
* fix(federation): gate node cordon control on node:manage to match backend

The cordon and uncordon control on the node card rendered whenever the
instance held an Admiral license, but the backend route also requires the
node:manage permission. Non-admin users without that permission (deployer,
viewer, auditor) saw a Cordon action the API rejected with 403. Gate the
control on the same permission the route enforces, so it renders only for
users who can use it.

Add developer-mode diagnostic logging to the cordon and pin handlers, and
route-level tests covering the cordon and uncordon permission, tier,
API-token, and input-validation boundaries.

* fix(federation): reject non-numeric node ids in cordon/uncordon

The cordon and uncordon routes validated the node id with parseInt, which
accepts numeric-prefix strings (parseInt('1abc', 10) === 1), so a request to
/api/nodes/1abc/cordon would operate on node 1. Require a strict positive
integer before parsing, and add tests for both routes.

* fix(federation): sanitize user-derived values in cordon and pin diagnostics

Route the node id, blueprint id, target node id, and reason length through
the shared log sanitizer before they reach the developer-mode diagnostic
log lines, and switch the format specifiers to %s to match. The values are
already validated integers, so this is defense in depth at the log sink and
keeps the diagnostics on the same sanitize-every-interpolated-value pattern
used elsewhere. No runtime behavior change: the lines stay gated behind the
developer_mode setting, off by default.
2026-06-02 09:48:19 -04:00
Anso 0953025036 fix(fleet): gate node update actions to admins and harden update tracking (#1272)
* fix(fleet): gate node update actions to admins and harden update tracking

Node update affordances now render only for admins, matching the admin-only
routes behind them. Previously a non-admin could open the Fleet view and see the
per-node Update button, Update all, retry, dismiss, and Recheck controls, then
get a 403 on click. Those controls are now hidden for non-admins, who still see
read-only update status.

Both update-status clear routes (per-node and bulk) now require admin, and the
bulk recheck throttles its forced "latest published version" lookup so a caller
cannot loop it to hammer the upstream registries; the response reports whether
the refresh actually ran so the UI can surface a "checked recently" note.

Completion detection no longer reports a node as Updated when it merely blips
offline and returns on the same version with an unchanged process start time.
That case stays in progress and is decided by the existing early-fail and
timeout heuristics, so a momentary network glitch is not mistaken for a
successful update. Failed and timed-out updates now emit an operator-visible
warning, and a periodic safety-net sweep bounds in-flight trackers when no
client is polling for status.

* fix(fleet): harden update completion and recheck failure handling

Refinements from review of the node self-update hardening:

- Completion signal 1 now requires a valid version, not merely a different one.
  A node whose /api/meta momentarily omits or mangles its version (online, same
  process) reported version=null, which compared unequal to the previous version
  and falsely marked the update completed. It now stays in progress and is
  decided by the early-fail/timeout heuristics.

- Terminal resolution is atomic: it re-reads the live tracker and transitions
  only if it is still in flight with the same start time, so two concurrent
  status polls cannot both warn or clobber each other's transition.

- The operator warning for a failed or timed-out update now redacts
  secret-shaped text (bearer/basic/token/password, credentialed URLs) from the
  underlying error before logging, in addition to stripping control characters.

- The Recheck button now surfaces an error toast when the request throws
  (network or auth failure), matching the existing non-ok-response path instead
  of only logging to the console.
2026-06-01 17:27:25 -04:00
Anso d8f73f8203 feat(fleet): show stack-label filtering in Fleet View on every tier (#1268)
* feat(fleet): show stack-label filtering in Fleet View on every tier

Stack labels and their assignments are a Community feature: the per-node
label reads are available to any authenticated user and are already used in
the per-node Stacks view. Fleet View, however, only fetched the fleet-wide
label palette and per-stack chips when the instance was on a paid tier, so a
Community user who had labelled their stacks saw no label dots on node cards
and no Tags filter in the Overview toolbar.

Drop the paid gate on the fleet label fetch so the palette, the per-stack
chips, and the Tags filter render for everyone who has labels. Node-level tag
aggregation (used for topology grouping) stays paid and is unchanged.

* fix(fleet): surface update-status failures and soften the reconnect timeout

Three reliability fixes in the Fleet View update path:

- The fleet update-status poll swallowed fetch errors in an empty catch, so a
  failing poll left a silently stale table with no breadcrumb. It now logs the
  failure (both thrown errors and non-ok HTTP responses) without toasting on
  every tick, and keeps the last-known statuses.
- The local-update reconnecting overlay declared "Update timed out" after five
  minutes, which falsely reported failure when a large image pull simply ran
  longer than the reconnect window. It now shows a non-failure "Taking longer
  than expected" state with a "Reload to check" action, and the timeout is a
  named constant that mirrors the backend update timeout.
- The fleet overview fan-out already logged a node that failed to report; the
  update-status and update-all fan-outs now log the rejected node and reason
  too instead of discarding it.

* test(fleet): backfill Fleet View hook, component, and update-tracker coverage

Adds unit and component coverage for the previously untested Fleet View
surface: all six Overview hooks (overview, update-status, polling cadence,
preferences, fleet labels, node labels), the NodeCard, OverviewTab,
OverviewToolbar, NodeUpdatesSheet, UpdateStatusBadge, and ReconnectingOverlay
components, and the FleetUpdateTrackerService state transitions.

* test(fleet): assert update-status poll preserves last-known statuses on failure

Adds an explicit case that seeds statuses from a successful poll, then fails
the next poll, and verifies the table keeps the seeded statuses (and logs
without toasting) rather than relying on the implementation implicitly.
2026-06-01 13:20:36 -04:00