57a1602e8f
- reports/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.1 KiB
7.1 KiB
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:
- 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. - 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 almembershipd, 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/clientya la soporta, pero NO está expuesta en el binding móvil ni en la SPA. Requiere queFrameListener.onFrameseñale frames conBlob(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 (signerPublo usa internamente). Dependencia del CORE, no implementada aquí por contrato (no tocarpkg/). La SPA sí lista miembros porque el gateway hace proxy al control plane. - Threading/reply/reaction: el otro agente añadió
ThreadID/ReplyTo/tipoREACTalFrame(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).
.aarno versionado: es un artefacto de ~24 MB; se regenera congomobile bind(verandroid/README.md). Gitignored.
Notas
- Web:
cd web && pnpm install && pnpm dev(opnpm previewtraspnpm build); conectar ahttp://localhost:7700(gateway:go run ./playground). - Móvil: ver
android/README.mdpara generar el.aar, compilar el APK y probar en el AVD. Desde el emulador, el host del PC es10.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 enweb/package.json. - pnpm 10 bloquea los build scripts:
web/pnpm-workspace.yamlconallowBuilds: { esbuild: true }.