Files
message_bus/reports/0002-2026-06-06-frontend-web-movil.md
T
egutierrez 57a1602e8f chore: auto-commit (1 archivos)
- reports/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:32 +02:00

126 lines
7.1 KiB
Markdown

# Report 0002 — Cliente humano de unibus: SPA web + app Android nativa
- **Fecha:** 06/06/2026
- **Autor:** agente (Claude) — frente frontend
- **Ámbito:** `mobile/unibus.go`, `playground/server.go` (gateway), `web/` (SPA nueva), `android/` (app nueva)
- **Estado:** done (con gaps documentados)
## Resumen
Se construyó el cliente humano de unibus en dos frentes, ambos verificados end-to-end:
1. **Web**: SPA de chat (React + Vite + TypeScript + Mantine v9) contra el gateway Go
(`playground/server.go`), que monta un peer real por identidad y emite los mensajes
recibidos por SSE. Dos identidades crean una room cifrada E2E, una invita a la otra y
ambas chatean en vivo.
2. **Móvil**: app Android nativa (Kotlin + Jetpack Compose) sobre el binding gomobile
`mobile/unibus.go`. El cifrado y el transporte NATS corren **en el dispositivo** (cada
teléfono es un peer real del bus). La app genera identidad, conecta al `membershipd`,
crea/se une a una room y envía/recibe mensajes por el bus.
No se reimplementó protocolo ni criptografía en JS ni en Kotlin: todo delega en `pkg/client`
a través del binding y del gateway.
## Cambios
| Archivo / dir | Qué | Por qué |
|---|---|---|
| `mobile/unibus.go` | Añadidos `Card()` (exporta la identidad pública del peer como JSON portable), `Invite(roomID, peerCard)` y `Kick(roomID, endpointID)` al binding. | La UI móvil necesita invitar/expulsar; `Card()` permite el intercambio de identidad peer-a-peer (paste/QR) sin un gateway. |
| `playground/server.go` | Añadidos `GET /api/rooms?peer=` (rooms del peer), `GET /api/members?room_id=` (proxy al control plane) y middleware `withCORS` (preflight + headers) para el dev server de Vite. | La SPA necesita listar rooms/miembros y llamar al gateway desde otro origen. |
| `web/` | SPA nueva: conexión (gateway URL + identidad), navbar (crear/unir/listar rooms), panel central (mensajes en vivo por SSE + composer), panel lateral (miembros, invitar por peer, expulsar si owner). Mantine v9, `@tabler/icons-react`, sin Tailwind ni CSS manual. | Cliente web de chat. |
| `android/` | App Kotlin + Compose: pantalla de conexión (Host + NATS + identidad), `BusViewModel` que dirige el binding (llamadas de red en `Dispatchers.IO`, frames entrantes vía `StateFlow`), pantalla de chat (crear/unir room, enviar, recibir). `.aar` generado con `gomobile bind`. | Cliente móvil con E2E en el dispositivo. |
## Verificación (evidencia ejecutable)
### Builds
```
# Binding + gateway (Go, sin CGO)
$ CGO_ENABLED=0 go build ./mobile/ ./playground/ → OK
$ CGO_ENABLED=0 go vet ./mobile/ ./playground/ → OK
# .aar gomobile
$ gomobile bind -target=android -androidapi 21 -javapkg com.unibus.core \
-o android/app/libs/unibus.aar ./mobile → unibus.aar (24 MB)
# SPA
$ cd web && pnpm build
✓ tsc -b && vite build → 6949 modules transformed, dist/ generado (exit 0)
# APK
$ cd android && ./gradlew assembleDebug
BUILD SUCCESSFUL in 1m13s
→ app/build/outputs/apk/debug/app-debug.apk (41 MB, incluye libgojni.so)
```
### Web — flujo cifrado E2E por curl (determinista)
Gateway (`go run ./playground`, web :7700) + dos peers `ana`/`leo`:
```
1. POST /api/peer ana → endpoint cc9f8Gm_RQZ76lX2I6C0ZepmiC82AG19M_ajiaDW4P8
POST /api/peer leo → endpoint yT7GWz97tRcCx3i4st43d4LcGC85Tl4PDZenycUaQ_I
2. POST /api/room {peer:ana, encrypt:true} → room_id 01KTEVZEE7B7AQAD72WSDHH3Y3
3. POST /api/invite {peer:ana, target:leo} → {"status":"invited"}
4. POST /api/join {peer:leo} → {"encrypt":true,"subject":"room.general"}
5. SSE leo + POST /api/publish ana "hola leo, mensaje cifrado E2E":
leo recibe → data: {"sender":"cc9f8...","text":"hola leo, mensaje cifrado E2E","encrypted":true}
6. GET /api/rooms?peer=ana → [{"encrypt":true,"room_id":"01KTEVZEE...","subject":"room.general"}]
7. GET /api/members?room_id → [{ana,role:owner}, {leo,role:member}]
```
### Web — UI con dos pestañas (SPA real, `pnpm preview` :4173)
- Pestaña ana y pestaña leo conectadas como identidades distintas a la room cifrada.
- leo → ana: "hola ana, soy leo desde la SPA" (18:31:05) llegó **en vivo por SSE** a ana.
- ana → leo: "¡recibido leo! ana responde cifrado" (18:32:28), bucle bidireccional.
- Panel de miembros: ana **OWNER** + leo; botón expulsar visible solo para el owner;
selector de invitación con los peers conectados.
### Móvil — AVD Pixel_API34 (emulador headless)
```
$ adb install -r app-debug.apk → Success
$ adb shell am start -n com.unibus.app/.MainActivity
→ proceso vivo, sin FATAL/AndroidRuntime, libgojni.so cargada (lib/x86_64)
→ pantalla "Conectado como android" (newSession contra membershipd 10.0.2.2:8470/4250)
→ "Room creada · 01KTEWTZ8TB1NYEGNNR7F3MY5G"
→ enviado "peer movil real en el bus" → burbuja recibida con hora 18:39:35
```
La burbuja con timestamp prueba el camino completo **en el dispositivo**: el código no añade
el mensaje localmente al publicar; solo lo pinta `FrameListener.onFrame`, así que su aparición
demuestra que el frame viajó por NATS y volvió al peer. Captura en
`reports/assets/unibus_movil.png`.
## Gaps / pendientes
- **Media (PublishMedia/FetchMedia)**: el `pkg/client` ya la soporta, pero NO está expuesta en
el binding móvil ni en la SPA. Requiere que `FrameListener.onFrame` señale frames con `Blob`
(hoy entrega solo texto). Pendiente para v2.
- **ListMembers en el binding móvil**: necesita un método **público** en `pkg/client`
(`ListMembers(roomID)`); hoy es privado (`signerPub` lo usa internamente). **Dependencia del
CORE**, no implementada aquí por contrato (no tocar `pkg/`). La SPA sí lista miembros porque
el gateway hace proxy al control plane.
- **Threading/reply/reaction**: el otro agente añadió `ThreadID`/`ReplyTo`/tipo `REACT` al
`Frame` (ya en master). Ni la SPA ni la app móvil los usan todavía — gap de UI, no de backend.
- **Invite/Kick en la UI móvil**: el binding ya expone `Card()`/`Invite()`/`Kick()`, pero la
app Kotlin v1 no tiene aún la pantalla de intercambio de "card" (paste/QR) ni el botón de
expulsar. El chat cleartext y E2E self-echo funcionan; el flujo de dos teléfonos invitándose
queda para v2.
- **Auth del gateway web**: el gateway identifica peers por nombre (modelo del playground), sin
autenticación de sesión. Suficiente para uso local/LAN; endurecer es fase posterior (igual que
el control plane, que en v1 no tiene TLS ni auth en GETs).
- **`.aar` no versionado**: es un artefacto de ~24 MB; se regenera con `gomobile bind` (ver
`android/README.md`). Gitignored.
## Notas
- Web: `cd web && pnpm install && pnpm dev` (o `pnpm preview` tras `pnpm build`); conectar a
`http://localhost:7700` (gateway: `go run ./playground`).
- Móvil: ver `android/README.md` para generar el `.aar`, compilar el APK y probar en el AVD.
Desde el emulador, el host del PC es `10.0.2.2`; desde un teléfono físico, la IP LAN del PC.
- Mantine v9 exige **React 19** (peerDependency `^19.2.0`); con React 18 la SPA compila pero
no monta en runtime (`s is not a function`). Fijado React 19 en `web/package.json`.
- pnpm 10 bloquea los build scripts: `web/pnpm-workspace.yaml` con `allowBuilds: { esbuild: true }`.