hyperhive/docs/swarm/ui.md
iris fab2a0dedc docs: fix genuine passive-voice hits in docs/swarm
Read all 94 write-good.Passive hits across docs/swarm/ (ca.md,
README.md, secrets.md, services.md, sso.md, ui.md) in context. 44 are
genuine catches with a nameable, usually already-established actor
(swarm-controller, authelia, swarmctl, the controller, the gateway,
this module, hyperhive itself, or 'the operator' for manual actions) —
rewritten to active. 50 are legitimate passives or false catches, left
alone: predicate-adjective state descriptions (is expected/misconfigured/
broken), negative-capability idioms (no X is needed/placed, can't be
Yed/listed/fetched), config-state conditionals (whenever/when X is
enabled/configured/set), requirement-list labels (is required),
'is tracked as' idiom, backward-looking changelog facts with no actor
(was removed/verified/introduced), ambiguous-actor statements left
conservatively alone (agents are created and destroyed — could be
hive-c0re or swarm-controller, doc doesn't say), and a couple of
deliberately-parallel idiom pairs.

Several sibling-inconsistency fixes: a passive clause sitting next to
an already-active sibling describing the same fact/mechanism (ca.md's
two-bullet consumer list, README's 4-item WireGuard-mesh bullet list,
README's controller-registers-hooks paragraph, sso.md's followed-a-302
sentence).

Verified via vale on the whole directory, diffed against main's exact
baseline (not just the Passive count): write-good.Passive 94 -> 50
exactly, every other category unchanged (1 pre-existing
Microsoft.Contractions error at services... at secrets.md:182,
8 TooWordy, 1 Microsoft.We, 1 Microsoft.FirstPerson — same counts,
same locations).
2026-09-08 15:54:16 +02:00

5.3 KiB

Swarm UI

The swarm's own web surface, served by the gateway on the swarm apex (services.hyperhive.swarm.domain) and readable only by operators.

Distinct from the per-hive dashboard, which lives on the hive domain and answers for one host. This one is the view across hives.

Enabling

services.hyperhive.deploy.swarm-ui.enable = true;   # defaults to deploy.swarm-controller.enable

Derived from the controller rather than from allSwarmServices: the UI is a view onto the controller's state and reaches it over that daemon's socket, so the host that runs the controller is the host that can serve the UI. A hive that merely uses a swarm has nothing to serve.

The UI answers on services.hyperhive.swarm.domain and nothing else. It shares that name with the swarm-controller it fronts — one service to a reader and to a certificate — so there is no separate option to pin.

The apex must differ from services.hyperhive.domain. The gateway's default server already answers for the hive domain, and two vhosts claiming one server_name don't error — nginx picks one — so this is an assertion rather than a runtime surprise.

🔑 You must be in the admins group

This is the step that separates "protected" from "locked out." The vhost's auth_request asks authelia "is there a session"; the rule that makes it mean "is this an operator" is an access_control entry requiring group:admins. An account without that group authenticates fine and still gets bounced.

swarmctl user add <you> --group admins

admins deliberately, not a new word: ../getting-started/setup.md has told every operator to create exactly that group since the bootstrap step existed, so an account made by following the guide already passes. This is the first rule that consumes a group name — inventing a second one would have meant those accounts silently failing a check they were supposed to pass.

An account created without any group needs re-adding with the flag — swarmctl reads the existing entry out of users.yml, so the group is what changes.

Why a group and not a list of usernames: agents are getting authelia accounts of their own (matrix SSO), and authenticated would then include every agent in the hive. The group is the only thing standing between "an operator's page" and "anyone with a session."

What it costs to be reachable

hyperhive publishes the apex to the hive's resolver like every other swarm service, so agent containers can resolve it. That's deliberate and it's not a hole: reachability isn't the access control here. An agent that resolves the name and connects still has no operator session, and the subrequest denies it.

Two wiring sites

Adding a swarm service name means touching two things. Missing the second ships as a different flavour of "works from the host, broken from a container":

site file
vhost + gateway.localNames the service's own module (for example nix/host-modules/swarm-ui.nix)
certificate name nix/host-modules/swarm.nix (serviceDomains)

The DNS record and the local-dev /etc/hosts entry need no separate edit: both derive from services.hyperhive.gateway.localNames, which a service's own module already has to push its domain into to be resolvable — see nix/host-modules/hive-gateway/dnsmasq.nix and .../default.nix's networking.hosts. vhosts.nix itself is scoped to the surface the hive's own domain serves (dashboard, per-agent routing, matrix discovery); a swarm service declares its own vhost next to its own options, the way swarm-ui.nix and swarm-authelia.nix do.

⚠️ The certificate one is the hardest to predict and the most visible when missed. serviceDomains is both the services sub-CA's nameConstraints set and the leaf's SAN list, and the apex is a sibling of forge.<swarm> / chat.<swarm> / auth.<swarm>, not a parent — no CA in the hierarchy issues for it implicitly. Left out, the vhost falls back to the hive leaf and the swarm's front page opens with a name mismatch.

The swarm UI's header carries a single 🔗 button, visible on every route, opening a popover of links to other swarm-wide services — authelia, matrix, forge, this UI's own swagger docs. Backed by GET /api/links (swarm-controller), which serves services.hyperhive.swarm.controller.links (a listOf { label, icon, url }, same shape as the per-agent hyperhive.dashboardLinks).

Rather than one central hardcoded list, each service's own module contributes its own entry when it's actually enabled on the controller's host — swarm-authelia.nix, hive-matrix.nix and hive-forge/default.nix all do, the same list-merge idiom gateway.localNames uses above. Adding a link for a new service is a nix-only change to that service's own module (or an operator adding an entry directly); no swarm-controller or swarm-ui change needed. Empty list hides the button rather than showing an empty popover.

Cross-references