- reports/0001-2026-06-07-unibus-grafana-monitoring.md - reports/0008-2026-06-07-unibus-admin-users-wired.md - reports/0008-2026-06-07-unibus-decentralization-audit.md - reports/0009-2026-06-07-unibus-cluster-hardening.md - reports/0010-2026-06-07-unibus-android-native.md - reports/0011-2026-06-07-unibus-cluster-deploy.md - reports/0012-2026-06-07-unibus-deploy-gaps-closed.md - reports/0013-2026-06-07-unibus-admin-panel.md - reports/0014-2026-06-07-unibus-users-http-admin-api.md - reports/0015-2026-06-07-unibus-web-wired.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.0 KiB
Report 0008 — unibus_admin: pestaña Users cableada a la API del plano de control
- Fecha: 07/06/2026
- Autor: agente (Claude Opus 4.8)
- Ámbito:
projects/message_bus/apps/unibus_admin(gateway Go + SPA Mantine), sub-repodataforge/unibus_admin, ramamaster - Estado: done (código + verificación E2E del código) — con un gap de despliegue del bus documentado abajo
Resumen
La pestaña Users del panel de administración de unibus estaba degradada en cluster
porque el gateway sólo sabía gestionar la allowlist abriendo el membership.Store
directo con --db, y caía a users_backend=none (estado informativo) cuando no había
acceso directo al store. El control plane de unibus ahora expone una API HTTP admin-only
de users, y pkg/client la envuelve con ListUsers/AddUser/RevokeUser firmados. Este
trabajo cablea el gateway a esos métodos (camino principal: API HTTP firmada, funciona
en cluster), deja la pestaña Users operativa (sin el estado "degradado"), y verifica la
cadena completa list → add → revoke (con idempotencia 409) end-to-end contra un
membershipd master real. --db queda como fallback single-node explícito.
Cambios
| Archivo | Cambio |
|---|---|
internal/admin/repo.go |
Eliminado ErrUsersUnavailable y el camino "none"; comentarios del interface Repo y de MeInfo.UsersBackend actualizados ("control-plane" | "sqlite"). |
internal/admin/repo_bus.go |
ListUsers/AddUser/RevokeUser usan r.cli.* (API firmada) cuando no hay store directo; con --db siguen usando el store. UsersWritable() → true (el gateway conectado siempre puede; el control real es requireAdmin en runtime). Backend por defecto "control-plane", "sqlite" con --db. |
internal/admin/server.go |
Handlers /api/users simplificados (cualquier error del control plane → 502 con su mensaje, que ya trae el código embebido, p.ej. (HTTP 409)); quitado el manejo de ErrUsersUnavailable y el import errors. |
main.go |
--db documentado como opcional (fallback single-node); por defecto el backend es control-plane. Logs de arranque acordes. |
web/src/pages/UsersPage.tsx |
Quitado el Alert "Gestión de users no disponible" y el gating writable (botón Add deshabilitado / revoke oculto). El badge muestra backend: <control-plane|sqlite>. La tabla (handle/rol/estado/sign_pub/creado), el modal Add (handle + sign-pub hex64 + rol) y revoke con confirmación se mantienen. |
web/dist/* |
Bundle SPA recompilado (embebido por el binario). |
app.md |
Diagrama, tabla de capacidades, ejemplos de arranque y gaps actualizados al cableado control-plane + el gap de despliegue del bus. |
El gateway reutiliza los métodos del cliente del bus (pkg/client), no reimplementa
firma ni HTTP. No se tocó el repo unibus (sólo se importa por replace => ../unibus).
Verificación
Mecánica (verde)
go vet ./... → VET_EXIT:0
go build -o unibus_admin . → BUILD_EXIT:0 (19 MB, embebe la SPA)
cd web && pnpm build → tsc -b + vite build OK (sin errores de tipo)
Cobertura E2E del código (verde) — contra membershipd master real
El cluster de producción aún no expone la ruta /users (ver Gaps), así que la cadena
del panel se verificó contra un membershipd compilado de master (con la ruta) arrancado
localmente (sqlite, --bus-auth soft), con el operador sembrado como admin
(membershipd user add --role admin). El gateway firma como el operador; el bus verifica
con requireAdmin. Secuencia ejecutada por la API del panel (/api/users):
| Paso | Comando (gateway en :18481) | Resultado |
|---|---|---|
| list (antes) | GET /api/users |
[operator(admin,active)] |
| add | POST /api/users {gapcheck_admin_ui, member} |
201 {"status":"added"} |
| list | GET /api/users |
gapcheck_admin_ui active/member + operator |
| re-alta (idempotencia) | POST /api/users (mismo sign_pub) |
409 user already registered (unchanged); revoke it first |
| revoke | POST /api/users/revoke {sign_pub} |
200 {"status":"revoked"} |
| list (final) | GET /api/users |
gapcheck_admin_ui revoked + revoked_at; operator intacto |
sign_pub de prueba: 1cb658d9d3f23e6bf5791a3865a1eec731f3df30ad4b85f6fca0074b8bdc98a4
(aleatorio, role member). Limpieza: revocado al final (revoke es la limpieza; no hay hard
delete). El operador admin y la DB de prueba vivían en /tmp efímero, ya borrados. No se
tocó la allowlist del cluster de producción.
SPA (verde) — verificada visualmente (browser MCP)
Panel servido en :18481, pestaña Users:
- Badge
backend: control-plane(sin Alert degradado), botón "Añadir user" activo. - Tabla con
gapcheck_admin_uiMEMBER/REVOKED (sin botón revoke por estar revocado) yoperatorADMIN/ACTIVE (con botón revoke). - Modal "Añadir user al bus" renderiza Handle +
sign_pub (hex, 64)+ Rol(member) + Cancelar/Añadir.
Conectividad contra el cluster vivo (verde, salvo la ruta ausente)
- Gateway local apuntado a homer (
141.94.69.66:8470) + datardos (51.91.100.142:8470), TLS+nkey con la CA del bus, identidad operator desdepass: conecta y firma (/api/medevuelve el endpoint real del operadorvI8HXcintzK-…).GET /api/users→404 page not founddesde el control plane (ruta ausente en esos binarios). - Panel efímero arrancado en magnus (puerto 18482, sin tocar el
unibus-admin.service) contra el control plane loopback de magnus (KV, enforce, TLS):/api/meOK (pasa elenforcede magnus firmando como operador),GET /api/users→404. El binario efímero y su log se borraron; el service real quedóactive.
Estado del deploy en magnus
No se re-desplegó el service. El panel unibus-admin.service sigue active en magnus
(healthz 200). Build linux/amd64 del binario nuevo: listo (probado en magnus en puerto
efímero). El re-deploy queda bloqueado por una dependencia: re-desplegar ahora cambiaría
la pestaña Users de un Alert informativo a un error 502 (GET /users → 404), porque el
membershipd de magnus aún no expone la ruta. Procedimiento para cuando el bus esté en
v0.10.0 (aditivo, sin tocar otros sites del Caddyfile):
cd web && pnpm install && pnpm build && cd ..
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o unibus_admin .
scp unibus_admin magnus:/opt/unibus_admin/unibus_admin
ssh magnus 'systemctl restart unibus-admin.service' # el unit ya arranca sin --db (backend control-plane)
ssh magnus 'curl -fsS http://127.0.0.1:8480/healthz' # + comprobar la pestaña Users vía el subdominio Caddy
El deploy/unibus-admin.service no necesita cambios para el camino control-plane (no usa
--db). Verificar tras el restart que ExecStart no incluye --db (si lo incluyera,
quitarlo para usar la API del plano de control).
Gaps / pendientes
- Cluster sin la ruta
/users(despliegue). Losmembershipddesplegados (magnus/homer/datardos) son anteriores al merge de la API de users y devuelven 404. La verificación E2E del panel contra el cluster vivo de producción no es posible hasta actualizar el bus a v0.10.0. La cadena está verificada contra unmembershipdmaster real (mismo código que correrá en el cluster), y el gateway ya conecta y firma bien contra los nodos reales. Acción fuera de este aislamiento (toca el repounibus/su deploy): actualizar los binarios del cluster. - Mapeo de código de estado. Un
409(re-alta idempotente) del control plane llega a la SPA como502con el mensaje del bus (que incluye(HTTP 409)). La SPA muestra el mensaje íntegro, así que la UX es correcta y accionable; no se parseó el string de error para propagar el código exacto (se evitó un parser frágil — KISS). Mejora menor opcional sipkg/clientexpone el status estructuradamente. - Replicación
followers 2/2. No se inspeccionó la replicación KV cross-node porque la ruta/usersno está viva en el cluster; el panel no expone métricas de JetStream (varz/jsz) — gap ya conocido de la pestaña Cluster.