Update the architecture diagram, capability table, run examples and known
gaps to describe user management via the signed control-plane API instead
of the old store-only/--db path. Record the remaining gap honestly: the
membershipd binaries currently deployed on the cluster predate the /users
route and return 404, so the Users tab becomes functional in production
once the bus is updated to v0.10.0; the gateway already connects and signs
correctly against those nodes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With user management now wired through the control-plane API, the Users
tab is always functional against a live gateway. Remove the "Gestión de
users no disponible" alert and the writable gating (button disabled,
revoke hidden) that were driven by the old users_backend === "none"
case. The backend badge now reads the wiring in use ("control-plane" or
"sqlite"). Add user (handle + 64-hex sign-pub + role) and revoke (with
explicit confirmation) consume the gateway REST unchanged. Includes the
rebuilt SPA bundle embedded by the binary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gateway previously managed the bus allowlist only via a direct
membership store opened with --db, falling back to a "none" backend that
left the Users tab degraded in cluster (the control plane exposed no user
HTTP endpoint). The unibus control plane now exposes an admin-only user
API (GET/POST /users, POST /users/{signpub}/revoke), and pkg/client wraps
it with ListUsers/AddUser/RevokeUser that sign each request.
busRepo now drives those client methods whenever no direct store is
configured (the cluster default), so user management works in cluster
without KV/SQLite access — the bus verifies the operator's admin identity
with requireAdmin and writes to the same store the room handlers use. A
direct store (--db) is kept as an explicit single-node fallback. The
reported users_backend becomes "control-plane" (or "sqlite" with --db),
and ErrUsersUnavailable / the "none" path are removed since a connected
gateway can always reach the API.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- app.md: frontmatter (service, port 8480, systemd unibus-admin.service on magnus),
architecture, capabilities, security, known gaps. uses_functions:
sign_ed25519_go_cybersecurity. e2e_checks (build/vet/web_build/smoke_mock).
- deploy/unibus-admin.service: systemd unit (Restart=always per the SIGTERM gotcha).
- deploy/README.md: reproducible deploy steps (no secrets), Caddy additive-site recipe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPA (React 19 + Vite 6 + Mantine v9, dark/indigo, @fn_library-style):
- AdminShell: AppShell nav (Cluster/Rooms/Users), operator endpoint badge
- ClusterPage: per-node up/down + posture badges (enforce/acl/tls/cluster/store),
10s auto-refresh
- RoomsPage: room table (E2E/cleartext, persist, signed, epoch, role), create
modal, members drawer with kick(+rekey) and invite modal
- UsersPage: allowlist table (handle/role/status/sign_pub), add modal, revoke
with confirmation, degraded state when no store backend
- api.ts: single repository layer hitting /api; gateway decides mock vs live
Verified end-to-end against a local membershipd in BOTH postures:
- auth-off: create room, list rooms, signed members GET, add/revoke user
- enforce + TLS + nkey (production posture): TLS-pinned healthz, nkey NATS
connect, signed control-plane requests verified by the server, 403 surfaced
for a non-member room
pnpm build green (tsc + vite); go build/vet green; dist embedded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single Go binary: serves an embedded Mantine SPA and a small REST API over the
unibus control plane. Holds the operator ADMIN identity, signs every
control-plane request, never exposes a private key to the browser.
- internal/admin: Repo interface + mock + bus implementations, REST server
- repo_bus: rooms via pkg/client, members via signed GET (CanonicalRequest +
SignEd25519), cluster via /healthz (CA-pinned), users via membership.Store
- identity loaded from pass entry or 0600 file (operator-identity JSON)
- go build CGO_ENABLED=0 green; go vet clean
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>