d43ffae3ae
- 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>
125 lines
8.0 KiB
Markdown
125 lines
8.0 KiB
Markdown
# 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-repo `dataforge/unibus_admin`, rama `master`
|
|
- **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_ui` MEMBER/REVOKED (sin botón revoke por estar revocado) y
|
|
`operator` ADMIN/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 desde `pass`: **conecta y firma**
|
|
(`/api/me` devuelve el endpoint real del operador `vI8HXcintzK-…`). `GET /api/users`
|
|
→ `404 page not found` desde 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/me` OK (pasa el
|
|
`enforce` de 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):
|
|
|
|
```bash
|
|
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
|
|
|
|
1. **Cluster sin la ruta `/users` (despliegue).** Los `membershipd` desplegados
|
|
(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 un `membershipd` master
|
|
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 repo `unibus`/su
|
|
deploy): actualizar los binarios del cluster.
|
|
2. **Mapeo de código de estado.** Un `409` (re-alta idempotente) del control plane llega a
|
|
la SPA como `502` con 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
|
|
si `pkg/client` expone el status estructuradamente.
|
|
3. **Replicación `followers 2/2`.** No se inspeccionó la replicación KV cross-node porque la
|
|
ruta `/users` no está viva en el cluster; el panel no expone métricas de JetStream
|
|
(varz/jsz) — gap ya conocido de la pestaña Cluster.
|