# Report 0017 — Rollout consolidado unibus 0.11.0 al cluster de 3 nodos (ROLLING, gate R3)

- **Fecha:** 07/06/2026
- **Autor:** agente (Claude Code)
- **Ámbito:** `apps/unibus` (membershipd) + `apps/unibus_admin` (panel) — cluster HA producción magnus + homer + datardos
- **Estado:** done — los 3 nodos en 0.11.0, R3 pleno, panel redeployado, sin rollbacks

## Resumen

Rollout rolling del binario `membershipd` 0.11.0 a los tres nodos del cluster, uno a uno
con gate de reconvergencia R3 entre cada nodo, activando en el mismo ciclo de restart el
monitoring NATS loopback limpio (`UNIBUS_NATS_MONITOR=1`, desacoplado del debug-log). Un
solo ciclo de restart por nodo desbloqueó las tres cosas previstas: la API `/users` (panel
admin, antes 404 con request firmado), las métricas NATS en `127.0.0.1:8222`, y el cierre
del gap del binario viejo (report 0012). Después, re-deploy del panel admin en magnus. El
cluster mantuvo quórum (2/3) en todo momento; cada nodo reconvergió a R3 (6/6 streams KV a
`followers_current=2/2`) antes de tocar el siguiente. No fue necesario ningún rollback.

## Cambios

| Fase | Acción | Resultado |
|---|---|---|
| 0 — Integrar | `git merge --no-ff quick/nats-monitor-flag` → master (commit `f31580d`), app.md → 0.11.0 | Sin conflictos; `web/` no tocado |
| 0 — Verificar | `go build ./...` + tests `membership`/`client`/`embeddednats`/`cmd/membershipd` | Todo verde; push a `origin/master` |
| 1 — Build | `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/membershipd_0.11.0 ./cmd/membershipd` | ELF static x86-64, sha `f6096a8e…`, `--help` OK |
| 2 — Rollout | magnus → homer → datardos: backup + drop-in `UNIBUS_NATS_MONITOR=1` + binario + restart + gate R3 | 3/3 reconvergidos, sin perder quórum |
| 3 — Verificar | healthz 3/3, `/users` firmado 200 3/3, 8222 3/3, sin debug, R3 6/6 a 2/2 | Todo verde |
| 4 — Panel | rebuild master (Users cableado) + backup + deploy magnus + restart `unibus-admin.service` | `/api/users` 200, end-to-end vía Caddy 200 |

Detalle del flag (0.11.0): función pura `natsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor)`
con `monitor = monitorEnv=="1" || debug` y `noLog = !debug`. Con `MONITOR=1` el endpoint de
monitoring abre en `127.0.0.1:8222` (bind loopback hardcoded) dejando el log del nats-server
en silencio (`NoLog` true). El acoplamiento inverso se mantiene (`DEBUG` implica `MONITOR`).
Drop-in systemd aditivo: `/etc/systemd/system/membershipd-cluster.service.d/nats-monitor.conf`
con `[Service]` + `Environment=UNIBUS_NATS_MONITOR=1` — no toca el unit base ni `cluster.env`.

El binario nuevo es drop-in del unit existente: se verificó que acepta los 17 flags del
`ExecStart` (incluido `--internal-id-file`, presente solo en el unit de datardos). No se
modificó ningún unit ni `cluster.env`.

## Verificación

### Unit y baseline (confirmados, no asumidos)

```
ExecStart=/opt/unibus/membershipd … (EnvironmentFile=/opt/unibus/cluster.env)
HTTP_PORT=8470  NATS_CLIENT_PORT=4250
healthz baseline (3 nodos): {"posture":{"enforce":true,"acl":true,"tls":true,"cluster":true,"store":"kv"},"status":"ok"}
8222 baseline: CERRADO en los 3 (esperado, sin drop-in)
binario viejo: magnus/homer 29337645 b; datardos 29374815 b (0012, con --internal-id-file)
```

### Hallazgo clave del gate R3 — métrica autoritativa

La métrica `followers_current` de `/jsz` **solo es autoritativa consultada en el LEADER del
stream**. Un nodo follower (recién reiniciado) reporta su vista desactualizada de los otros
peers (`current=false`, `lag` constante) durante minutos sin que sea un problema real. La
señal autoritativa por nodo es el **`/healthz` del nats-server** (`127.0.0.1:8222/healthz`):
devuelve 503 si cualquier stream del que el nodo es miembro está lagging, 200 sólo cuando
todos están current. Se usó ese endpoint como gate primario por nodo, y la vista del
stream-leader para el conteo explícito `2/2`.

### Gate R3 por nodo (orden magnus → homer → datardos)

| Nodo | Backup | membershipd healthz | nats /healthz | meta (leader/size/pending) | 8222 /varz | [DBG]/[TRC] | quórum otros 2 |
|---|---|---|---|---|---|---|---|
| **magnus** | `membershipd.bak.20260607-192416Z` | 200 enforce+acl+tls+cluster+kv | 200 ok | homer / 3 / 0 | 200 | 0 | homer 200, dd 200 |
| **homer** | `membershipd.bak.20260607-193313Z` | 200 idem | 200 ok (t=0) | datardos / 3 / 0 | 200 | 0 | magnus 200, dd 200 |
| **datardos** | `membershipd.bak.20260607-193452Z` | 200 idem | 200 ok (t=0) | homer / 3 / 0 | 200 | 0 | magnus 200, homer 200 |

Re-elecciones de meta-leader observadas (esperadas, quórum 2/3 intacto): inicial `homer` →
restart homer → `datardos` → restart datardos → `homer` (final). Tras completar los tres, la
vista del stream-leader (magnus) confirmó los 6 buckets KV
(`users/rooms/members/room_keys/rooms_by_member/nonces`) a `followers_current=2/2`:

```
streams KV a 2/2: 6/6 (stream-leader=magnus); meta-leader=homer cluster_size=3 pending=0
```

### FASE 3 — verificación post-rollout (3 nodos)

```
                magnus    homer    datardos
membershipd     200       200      200
posture         enforce+acl+tls+cluster+store=kv (homogénea)
/users firmado  200       200      200     (3 users idénticos — KV replicado R3)
8222 varz/jsz/connz  200/200/200   200/200/200   200/200/200
nats /healthz   ok        ok       ok
journal DBG/TRC 0         0        0
```

`/users` firmado se verificó con un verificador throwaway (`client.Connect` + `ListUsers`
del propio `pkg/client`, identidad operator desde `pass unibus/operator-identity` en archivo
local 0600, TLS+nkey contra cada nodo por su IP pública; SAN del cert cubre las IPs). Antes
del rollout, un GET `/users` firmado daba 404 (ruta ausente en el binario viejo); ahora da
200 con la allowlist:

```
operator        role=admin  status=active
gapcheck_user   role=member status=revoked   (residuo E2E del report 0012)
gapcheck_user2  role=member status=revoked   (residuo E2E del report 0012)
```

El verificador y la copia local de la identidad se eliminaron (`cmd/usercheck` borrado del
working tree; `/tmp/operator.id` con `shred`). Working tree de unibus limpio
(`master...origin/master`).

### FASE 4 — deploy del panel admin

```
panel binario nuevo: /opt/unibus_admin/unibus_admin (sha 812831…), backup unibus_admin.bak.20260607-194114Z
service unibus-admin.service: active; journal limpio ("users backend: control-plane", "bus TLS+nkey: ON", "cluster nodes probed: 3")
GET /api/users (loopback 8480):            HTTP 200 — lista real (3 users), ya no 502
GET /api/cluster:                          3 nodos up:true, posture homogénea, latencias 0/10/5 ms
end-to-end vía Caddy (basic auth admin):   GET https://admin-…/api/users -> HTTP 200 con lista
end-to-end sin auth:                       HTTP 401 (Caddy basic_auth protege)
```

El binario del panel se reconstruyó desde master (frontend `web/dist` ya embebido, no se tocó
ningún `web/`). El Caddyfile no se modificó: el site (`reverse_proxy 127.0.0.1:8480` +
`basic_auth`) quedó intacto; no hizo falta `caddy reload` (mismo puerto/config). La cadena
completa navegador → Caddy(TLS+auth) → panel → bus firmado `/users` → KV R3 está probada.

## Seguridad

- Secretos siempre desde `pass`/path, nunca en git ni argv: identidad operator a archivo
  local 0600 (borrada con `shred`); password del panel pasado a curl por stdin (`-K -`),
  nunca en la línea de comandos.
- Monitoring vía `UNIBUS_NATS_MONITOR` (drop-in), **nunca** `UNIBUS_NATS_DEBUG`: confirmado
  0 líneas `[DBG]`/`[TRC]` en journald de los 3 nodos. 8222 bind loopback hardcoded.
- Backups reversibles por nodo conservados en `/opt/unibus/` (membershipd) y
  `/opt/unibus_admin/` (panel) — listados arriba.
- Gate R3 estricto, un nodo abajo a la vez; quórum 2/3 nunca perdido. magnus (host crítico):
  sólo se reinició `membershipd-cluster.service`. Posture homogénea mantenida.

## Gaps / pendientes

- **Verificación visual de la pestaña Users con navegador: no realizada.** Se verificó la
  cadena funcionalmente end-to-end (Caddy basic_auth → panel → bus → `/api/users` 200 con la
  lista real). No se abrió el navegador para no exponer el `admin-panel-password` en la URL
  (el basic_auth de Caddy obliga a meter las credenciales en la URL o en un dialog nativo no
  automatable de forma segura). El render de la SPA es determinista sobre `/api/users`, que
  responde 200.
- Los usuarios `gapcheck_user`/`gapcheck_user2` (revoked) son residuos de los checks E2E del
  report 0012, no usuarios reales; pueden limpiarse del KV si se desea (no bloquea nada).
- Binarios temporales en `/tmp` (locales y de los nodos) eliminados. Los backups `.bak.*`
  se conservan a propósito para reversibilidad.
