Files
wdmUI/backlog/blocao-sprint-backlog.md

780 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Blocao · Sprint backlog
**Producto**: Blocao
**Empresa**: Blocao Labs
**Stack**: OpenWrt (router) + Balena/RK3588 (cell) + EU sovereign hub (futuro)
**Estado del documento**: backlog inicial post-mockups · pre-sprint 0
---
## Cómo leer este documento
Cada user story tiene la forma:
> `[BL-NN]` **Como** `<rol>`, **cuando** `<situación>`, **veo / puedo / espero** `<resultado>`.
> *Backend*: contra qué APIs habla la UI.
> *Tests de aceptación*: qué tiene que pasar para considerarla cerrada.
> *Estimación*: S (≤1 día), M (2-3 días), L (4-7 días), XL (>1 semana).
> *Depende de*: otras stories que tienen que estar antes.
Roles:
- **Operator** — usuario final del sistema (vigilancia, forensics, casos).
- **Installer** — quien despliega un sitio físicamente.
- **Ops/SRE** — quien mantiene la flota desde el hub.
- **Investigator** — operador con caso abierto, foco forense.
- **End-customer** — propietario del sitio, no toca la consola pero pide informes.
---
## Épicas / sprints sugeridos
| Sprint | Épica | Foco | Duración |
|---|---|---|---|
| 0 | Foundation | repositorio, CI, image builder OpenWrt, balena-compose Cell | 2 sem |
| 1 | First boot | wizard funcional + provisioning end-to-end | 2 sem |
| 2 | Engineering plane | router APIs, paneles WAN/VLAN/MQTT/GitOps reales | 3 sem |
| 3 | Camera onboarding | scan + auth + Frigate config GitOps | 2 sem |
| 4 | Operator plane | SYNOPSIS + Forensics MVP con un solo edge AI | 3 sem |
| 5 | Health & ops | selftest + observability + Cell↔Router orchestration | 2 sem |
| 6 | Hub & fleet | sites overview + bridge MQTT + remote auth | 3 sem |
| 7 | Hardening | evidence chain capa 1 + auditoría + rebranding final | 2 sem |
Total: **~19 semanas** hasta producto demoable a clientes con flota.
---
# ÉPICA 0 — FOUNDATION
Antes de UI: la infraestructura sobre la que todo se monta.
### `[BL-00.1]` Repo monorepo con estructura clara — *L*
**Como** team, **necesito** un monorepo con paquetes separados pero con tooling compartido (lint, format, CI).
Estructura propuesta:
```
blocao/
├── apps/
│ ├── console/ # SPA (este index.html eventualmente migrado a React/Svelte)
│ ├── router-luci-app/ # paquete OpenWrt luci-app-blocao-console
│ ├── cell-services/ # docker-compose Balena del Cell
│ └── hub/ # backend del hub (Go o Rust)
├── packages/
│ ├── design-tokens/ # paleta complementary, tipos, espacios
│ ├── ui-components/ # web components reutilizables
│ └── proto-mqtt/ # esquemas de topics/payloads en JSON Schema
├── infra/
│ ├── image-builder/ # OpenWrt Image Builder config
│ ├── balena/ # balenaCloud manifests
│ └── hub-deploy/ # terraform/ansible para Hetzner
├── docs/
│ └── ...
└── tools/
```
**Tests de aceptación**: clone, `make bootstrap`, `make build` produce artefactos de los 4 apps en CI.
**Depende de**: nada.
### `[BL-00.2]` CI/CD pipeline base — *M*
**Como** team, **quiero** que cada PR pase lint + tests + build de los apps que toca, y que tags publiquen artefactos.
**Tests de aceptación**: PR con cambio en `apps/console/` solo dispara su pipeline; tag `v0.1.0` publica imagen Balena, paquete OpenWrt, y artefacto del hub.
**Depende de**: BL-00.1.
### `[BL-00.3]` Image Builder OpenWrt con paquete blocao base — *L*
**Como** installer, **quiero** poder flashear un router con firmware Blocao listo (sin necesidad de instalar paquetes manualmente al primer arranque).
Incluye:
- `luci-app-blocao-console` (vacío al inicio, irá creciendo)
- `mosquitto`, `mosquitto-bridges`
- `chrony` con NTS
- `tailscale`
- `mwan3` para WAN failover
- `git`, `ucode`, `rpcd`, `uhttpd-mod-ubus`
- defaults sensatos en `uci-defaults`
**Tests de aceptación**: imagen flashea en GL.iNet GL-MT6000 o equivalente; arranque inicial llega a `http://blocao-router.local/` y se ve el wizard.
**Depende de**: BL-00.1.
### `[BL-00.4]` Balena fleet provisioning para Cell — *M*
**Como** ops, **quiero** que un RK3588 nuevo enrolado a la fleet de Balena descargue y arranque el stack Blocao Cell automáticamente.
Stack `docker-compose.yml`:
- `frigate` (custom build con RKNN)
- `enricher` (genera embeddings CLIP, reID, hashing)
- `forensic-engine` (API de query natural language → fusion)
- `healthd` (selftest + report al MQTT)
- `console-static` (sirve SPA)
- `caddy` (reverse proxy + TLS)
**Tests de aceptación**: device nuevo en fleet recibe los containers en 10min, `mosquitto_sub -t 'hai/_registry/announce' -h <router>` muestra anuncio del nuevo Cell.
**Depende de**: BL-00.1, BL-00.3.
### `[BL-00.5]` Esquemas MQTT de topics y payloads — *M*
**Como** team, **necesito** un contrato versionado entre Cell, Router, Hub para que no haya ambigüedad sobre qué se publica y bajo qué topic.
Documentado como JSON Schema en `packages/proto-mqtt/`:
- `hai/<device_id>/announce`
- `hai/<device_id>/health`
- `hai/<cam_id>/events/<event_type>`
- `hai/<cam_id>/state/<key>`
- `hai/_registry/heartbeat/<id>`
- `_cmd/<command>`
- ...todos los demás del panel MQTT
**Tests de aceptación**: contract tests en CI que validan que los publishers cumplen el schema.
**Depende de**: BL-00.1.
---
# ÉPICA 1 — FIRST BOOT
El wizard que ya tienes prototipado, pero funcional.
### `[BL-01.1]` Wizard step 1 · Region — *S*
**Como** installer, **al primer arranque** del router, **veo** el wizard y selecciono país.
Backend: `POST /api/router/setup/region {country, retention_days, timezone, nts_servers}` → escribe a UCI y al repo GitOps local.
**Tests de aceptación**: seleccionar Argentina configura `chrony` con `time.cloudflare.com` + `ntp.argentina.gob.ar`, timezone UTC-3, retention default 30d en Frigate config plantilla.
**Estimación**: S (1d).
**Depende de**: BL-00.3.
### `[BL-01.2]` Wizard step 2 · Site identity — *S*
**Como** installer, **introduzco** site ID, label humano, dirección, coordenadas.
Backend: `POST /api/router/setup/site` → escribe `/etc/blocao/site.yml`, comitea al GitOps repo, propaga a hostname y MQTT topic prefix.
**Tests de aceptación**: site ID `BL-LAB` aparece como prefix en `mosquitto_sub`, hostname del router es `bl-lab-router.local`.
**Estimación**: S (1d).
**Depende de**: BL-01.1.
### `[BL-01.3]` Wizard step 3 · WAN con tests inline — *M*
**Como** installer, **selecciono** modo WAN y veo tests en directo (DHCP, DNS, ping, NTS, hub reachability).
Backend: `POST /api/router/setup/wan` aplica config; `GET /api/router/setup/wan/test` corre los chequeos en streaming SSE.
**Tests de aceptación**: cambiar de DHCP a IP estática y volver no rompe la sesión actual del wizard (confirma rollback de seguridad).
**Estimación**: M (2-3d).
**Depende de**: BL-01.2.
### `[BL-01.4]` Wizard step 4 · Camera VLAN quarantine — *M*
**Como** installer, **confirmo** la VLAN-10 de cámaras, **veo** las 5 reglas de cuarentena ya aplicadas como verde-en-vivo.
Backend: aplica firewall rules a VLAN-10; ejecuta `egress test` desde un namespace network simulando una cámara.
**Tests de aceptación**: el egress test desde dentro del namespace VLAN-10 falla cuando intenta resolver `google.com`; pasa cuando intenta llegar a la IP del Cell por puerto 554.
**Estimación**: M (2d).
**Depende de**: BL-01.3.
### `[BL-01.5]` Wizard step 5 · Hub & operator auth — *M*
**Como** installer, **pego** el fleet token del hub y **creo** el usuario operador inicial.
Backend: `POST /api/router/setup/hub` valida token contra hub, intercambia por mTLS cert; `POST /api/router/setup/operator` crea bcrypt + escribe a `/etc/blocao/auth.db`.
**Tests de aceptación**: tras el step, `mosquitto_sub` desde el hub recibe el `_registry/announce` del nuevo router con su site_id.
**Estimación**: M (3d).
**Depende de**: BL-01.4 + un hub mínimo (ver BL-06.1).
### `[BL-01.6]` Wizard step 6 · Apply provisioning — *M*
**Como** installer, **veo** el progreso de los 8 pasos de provisioning, **confirmo** que termina, y soy redirigido al Synopsis.
Backend: orquestador `apply.sh` que ejecuta los 8 pasos (UCI generate, git commit, firewall apply, dnsmasq, hub register, tailscale up, mosquitto, uhttpd reload), reporta progreso por SSE.
**Tests de aceptación**: el provisioning es idempotente (correrlo dos veces no rompe nada); si falla en el paso 5 (hub register), los 1-4 quedan aplicados pero el 6-8 no.
**Estimación**: M (3d).
**Depende de**: BL-01.5.
### `[BL-01.7]` Standalone mode (sin hub) — *S*
**Como** installer en sitio air-gapped, **selecciono** "standalone" y el wizard salta el step 5 de hub.
Backend: skip hub registration, configurar mosquitto sin bridge, permitir auth solo local.
**Tests de aceptación**: provisioning completa sin internet activo; sistema funcional para single-site forensics.
**Estimación**: S (1d).
**Depende de**: BL-01.5.
---
# ÉPICA 2 — ENGINEERING PLANE
Las vistas que mapean al router. Foco: APIs reales detrás de cada panel.
### `[BL-02.1]` `hai.uc` — RPCs custom del router en ucode — *L*
**Como** developer, **necesito** un fichero `/usr/libexec/rpcd/hai.uc` que exponga los métodos que la consola consume sin pasar por LuCI.
Métodos:
- `hai.overview` (agrega WAN, VLAN, services, drift en una sola respuesta)
- `hai.git_status` (sha applied/HEAD/remote, drift report)
- `hai.git_log` (últimos N commits)
- `hai.git_apply`, `hai.git_rollback`, `hai.git_fetch`
- `hai.cells_list` (registry de Cells descubiertas)
- `hai.cell_proxy` (reverse proxy a Cell por su ID)
- `hai.bridge_status` (mosquitto bridge state)
- `hai.healthcheck` (selftest agregado)
- `hai.cam_scan_vlan10` (devuelve descubrimiento)
**Tests de aceptación**: `ubus call hai overview` devuelve JSON válido bajo schema.
**Estimación**: L (5-7d).
**Depende de**: BL-00.3.
### `[BL-02.2]` Reverse proxy `/api/cell/*` y `/api/router/*` — *M*
**Como** developer, **quiero** que el navegador hable solo con el router (mismo origen) y este reenvíe a la Cell según haga falta.
Implementación: nginx o Caddy embebido en `luci-app-blocao-console`. `/api/router/*``/ubus`. `/api/cell/*` → IP/puerto de la Cell descubierta. `/api/cell/<cell-id>/*` para multi-Cell.
**Tests de aceptación**: navegador no hace ninguna petición cross-origin; CORS no aparece nunca.
**Estimación**: M (2-3d).
**Depende de**: BL-02.1.
### `[BL-02.3]` Vista WAN funcional — *M*
**Como** ops, **veo** estado WAN en directo y puedo cambiar tipo de conexión.
UI: la vista WAN actual del index.html (todavía stub).
Backend: `network.interface dump` + `uci.get/set/commit/apply` con timeout rollback de 90s.
**Tests de aceptación**: cambiar DHCP→IP estática y rolllback automático si pierdes la conectividad de la propia consola.
**Estimación**: M (3d).
**Depende de**: BL-02.1, BL-02.2.
### `[BL-02.4]` Vista VLAN-FW como matriz interactiva — *L*
**Como** ops, **veo** la matriz de zones×forwardings y puedo tocar reglas con visual feedback.
UI: implementar la matriz que dibujamos en mockup engineering. Cada celda click → editor de regla.
Backend: parsear `/etc/config/firewall` (UCI), exponer como matriz, escribir cambios via `uci.set + commit + apply`.
**Tests de aceptación**: cambiar `cam→hai` de DENY a ALLOW genera un commit en GitOps; revertir el commit revierte la regla en próximo reconcile.
**Estimación**: L (5d).
**Depende de**: BL-02.3.
### `[BL-02.5]` Vista MQTT — broker stats + bridge — *L*
**Como** ops, **veo** estado del broker, clientes conectados, estado del bridge, throughput.
UI: la vista MQTT prototipada.
Backend: `$SYS/#` topics del propio mosquitto + `mosquitto_ctrl` para clientes; `bridge` info de `mosquitto.conf` parseada.
**Tests de aceptación**: matar un cliente con `kill -9` aparece como disconnected en <2s.
**Estimación**: L (4d).
**Depende de**: BL-02.2.
### `[BL-02.6]` Vista MQTT — live tail real — *M*
**Como** ops, **veo** los mensajes pasando por el broker en tiempo real con filtro glob.
UI: la zona del live tail con SSE/WebSocket en lugar de simulación.
Backend: bridge ws→MQTT, `mosquitto_sub -t '<filter>' -v` streaming via WebSocket. Auth: token de la sesión operator.
**Tests de aceptación**: filtro `hai/+/events/#` muestra solo eventos de cámaras; pausa real congela el feed.
**Estimación**: M (3d).
**Depende de**: BL-02.5.
### `[BL-02.7]` Vista MQTT — topic tree + ACL viewer — *M*
**Como** ops, **veo** el árbol de topics activos con sus ACLs y políticas de bridge.
UI: la zona derecha del MQTT panel.
Backend: parsear `mosquitto.conf` ACLs + bridge sections; combinar con stats de mensajes en últimos N minutos.
**Tests de aceptación**: cada topic listado tiene una entrada en el ACL configurado; cualquier topic no listado se considera implícitamente denied.
**Estimación**: M (3d).
**Depende de**: BL-02.5.
### `[BL-02.8]` Vista GITOPS — triplet + drift detection — *L*
**Como** ops, **veo** Applied/HEAD/Remote y drift en tiempo real, puedo lanzar fetch/reconcile/rollback.
UI: el panel GitOps prototipado.
Backend: cron cada 5min ejecuta `git fetch` + comparación de SHAs + comparación SHA256 de ficheros vivos vs aplicados; resultado en `/var/lib/blocao/state-applied.json`.
**Tests de aceptación**: hacer `vi /etc/config/firewall` y guardar (sin git commit) genera drift visible en <30s.
**Estimación**: L (5d).
**Depende de**: BL-02.1.
### `[BL-02.9]` Vista TAILSCALE — peers, ACLs, status — *S*
**Como** ops, **veo** peers de Tailscale activos con su última fecha de handshake.
Backend: `tailscale status --json`.
**Estimación**: S (1d).
**Depende de**: BL-02.2.
### `[BL-02.10]` Vista SERVICES — lista unificada router+cell — *M*
**Como** ops, **veo** todos los daemons (router init.d + cell containers) en una sola tabla.
Backend: `service list` (router) + Cell expone `/api/cell/services` que envuelve `balena-engine ps`.
**Estimación**: M (2d).
**Depende de**: BL-02.2, BL-00.4.
### `[BL-02.11]` Vista LOGS unificada — *L*
**Como** ops, **veo** logs de router (logread) + Cell (journalctl/balena logs) en streaming, filtrable por servicio.
Backend: agregador en Cell que se suscribe a logs del router via SSH/MQTT y los une con sus locales; expone via SSE.
**Tests de aceptación**: filtrar por `frigate` muestra solo líneas del container; filtrar por `firewall` muestra solo del router.
**Estimación**: L (5d).
**Depende de**: BL-02.10.
---
# ÉPICA 3 — CAMERA ONBOARDING
Los pasos del flow de añadir cámara.
### `[BL-03.1]` Discovery — escaneo VLAN-10 — *L*
**Como** installer, **lanzo** el escaneo y **veo** los dispositivos detectados con vendor/modelo/capacidades.
Backend en router:
- DHCP lease enumeration (`uci show dhcp`).
- ARP table walk (`ip neigh`).
- ONVIF probe via multicast WS-Discovery.
- nmap puertos 554, 80, 443, 8080.
- HTTP fingerprint para identificar vendor (Hikvision, Dahua, Reolink, Axis).
- Match contra BD local de modelos conocidos + CVEs.
**Tests de aceptación**: 4 cámaras en VLAN-10 son detectadas en <15s con vendor identificado en al menos 3.
**Estimación**: L (5-7d).
**Depende de**: BL-02.1.
### `[BL-03.2]` Default credentials probe — *M*
**Como** installer, **veo** automáticamente si la cámara responde a credenciales de fábrica.
Backend: lista de combinaciones `(user, pass)` típicas por vendor; intenta cada una con timeout corto; reporta resultado.
**Tests de aceptación**: una cámara Hikvision con admin/12345 es marcada como "DEFAULT CREDS" en discovery list.
**Estimación**: M (2d).
**Depende de**: BL-03.1.
### `[BL-03.3]` Authenticate + force change password — *M*
**Como** installer, **fuerzo** rotación de password de la cámara antes de añadirla.
Backend: para cada vendor soportado, implementar API call de change password (Hikvision `/ISAPI/Security/users`, Dahua RPC, Reolink `cmd=Login` + `cmd=SetUser`, ONVIF `SetUser`).
**Tests de aceptación**: tras "force change" la cámara no responde más a la password vieja.
**Estimación**: M (3d).
**Depende de**: BL-03.2.
### `[BL-03.4]` Test stream + preview — *M*
**Como** installer, **veo** un frame en directo con detecciones de Frigate antes de guardar.
Backend: el Cell recibe orden de probar el stream temporalmente, devuelve un frame al router para visualización; ejecuta inferencia ad-hoc para preview.
**Tests de aceptación**: el preview muestra el frame con boxes en menos de 5s desde click.
**Estimación**: M (3d).
**Depende de**: BL-03.3, BL-00.4.
### `[BL-03.5]` Configure form — name, location, retention, objects — *S*
**Como** installer, **introduzco** datos humanos de la cámara y la política.
UI: el form que prototipamos.
Backend: validación de inputs.
**Estimación**: S (1d).
**Depende de**: BL-03.4.
### `[BL-03.6]` Save — generate Frigate config patch + GitOps commit — *L*
**Como** installer, **al guardar** la cámara, genera el patch de `frigate.yml`, comitea, y verifica end-to-end.
Backend:
1. Generar la sección `cameras:<id>` del frigate.yml.
2. Commit local al repo blocao-config.
3. Push a hub si configurado.
4. Trigger reconcile del Cell.
5. Esperar primer evento end-to-end (cámara → Frigate → MQTT → router) para confirmar.
**Tests de aceptación**: tras "FINISH" del wizard, el primer evento de detección llega al MQTT en <60s.
**Estimación**: L (5d).
**Depende de**: BL-03.5, BL-02.8.
### `[BL-03.7]` Security checklist — verificación contínua — *M*
**Como** installer, **veo** el checklist lateral con cada item validado en tiempo real durante el flow.
Backend: cada check es una función pequeña que el router ejecuta en paralelo:
- VLAN check (siempre OK por design).
- DNS sinkhole (verifica iptables count).
- Default creds (resultado de BL-03.2).
- Telnet open (port scan).
- HTTP/HTTPS (port scan).
- Firmware version vs CVE database (lookup local).
- UPnP (intento + check).
- Phone-home (count durante el periodo de scan).
- MAC binding (check de DHCP static leases).
**Tests de aceptación**: cada check se actualiza independientemente; un check rojo no bloquea el flow pero lo marca.
**Estimación**: M (3d).
**Depende de**: BL-03.1, BL-03.2.
### `[BL-03.8]` Add camera manually — *S*
**Como** installer con cámara rara (sin ONVIF, RTSP path no estándar), **introduzco** datos a mano.
UI: form alternativo desde el "+ ADD MANUALLY" del discovery.
**Estimación**: S (1d).
**Depende de**: BL-03.5.
### `[BL-03.9]` CVE database actualizable — *M*
**Como** ops, **quiero** que la BD de CVEs de cámaras se actualice periódicamente sin firmware update.
Backend: BD versionada en GitOps repo; `cron` semanal `git pull` la actualiza. Si standalone, advierte de "stale" tras 30 días.
**Tests de aceptación**: añadir un CVE nuevo al repo y `git pull` lo hace disponible para discovery sin restart.
**Estimación**: M (2d).
**Depende de**: BL-03.7.
---
# ÉPICA 4 — OPERATOR PLANE
SYNOPSIS y FORENSICS, las vistas del operador.
### `[BL-04.1]` SYNOPSIS — diagrama SCADA estático funcional — *L*
**Como** operator, **al abrir la consola**, **veo** el estado completo del sitio en una sola pantalla.
UI: la vista que acabamos de hacer, ya con datos reales.
Backend: `hai.synopsis` agrega: cámaras (estado, fps), router (interfaces, services), cell (containers, NPU, disk), VLANs activas, gauges de capacidad.
**Tests de aceptación**: cargar la vista completa en <500ms; auto-refresh cada 5s.
**Estimación**: L (5d).
**Depende de**: BL-02.1.
### `[BL-04.2]` SYNOPSIS — animación de flujos en tiempo real — *M*
**Como** operator, **veo** los flujos animados (RTSP, MQTT, bridge) reflejando actividad real.
Backend: WebSocket que publica throughput por flow cada 1s; la velocidad de la animación CSS se ajusta en función del throughput.
**Tests de aceptación**: cuando MQTT publica más, las partículas van más rápido visiblemente.
**Estimación**: M (3d).
**Depende de**: BL-04.1, BL-02.6.
### `[BL-04.3]` SYNOPSIS — alertas activas en panel lateral — *M*
**Como** operator, **veo** las alertas más relevantes ordenadas por severidad/recencia.
Backend: agregador de alertas que combina:
- Selftest fails (BL-05.x).
- Drift detected.
- Camera offline.
- Storage warnings.
- Bridge disconnect.
Cada alerta es clickable → lleva a la vista relevante con el contexto preseleccionado.
**Tests de aceptación**: 5 alertas simultáneas se ordenan correctamente; click en cam-04 offline lleva a vista CAMS con cam-04 destacada.
**Estimación**: M (3d).
**Depende de**: BL-04.1.
### `[BL-04.4]` Forensic engine MVP con un edge AI — *XL*
**Como** investigator, **escribo** una query natural y **recibo** resultados de Frigate.
Backend en Cell:
- Endpoint `POST /api/forensic/query {nl_text, time_range}`.
- LLM pequeño local (o llamada a hub) para parsear NL → query estructurada.
- Por ahora solo Frigate como backend: convertir a `/api/events?after=...&label=person&zone=...`.
- Devolver hits con score, snapshots, eventos.
**Tests de aceptación**: query "personas en la entrada anoche entre 2 y 4 AM" devuelve hits relevantes con timestamps en el rango.
**Estimación**: XL (2 sem).
**Depende de**: BL-03.6.
### `[BL-04.5]` Forensic chat UI con autocomplete — *L*
**Como** investigator, **escribo** y veo sugerencias inline (ghost text) basadas en vocabulario conocido.
Backend: API de autocomplete que sabe de cámaras existentes, zonas, objetos detectables, rangos típicos.
**Tests de aceptación**: empezar a escribir "muestra personas en la entr..." sugiere "...en la entrada hall durante los últimos N minutos".
**Estimación**: L (4d).
**Depende de**: BL-04.4.
### `[BL-04.6]` Forensic timeline con swimlanes — *L*
**Como** investigator, **veo** los hits como markers en un timeline con swimlane por edge AI.
UI: el componente que prototipamos.
Backend: el resultado de la query incluye un array de eventos con `(ai_source, timestamp, type, payload)`.
**Tests de aceptación**: 50 eventos se renderizan en <300ms; click en marker carga clip relacionado.
**Estimación**: L (5d).
**Depende de**: BL-04.4.
### `[BL-04.7]` Evidence player con clip + metadatos — *M*
**Como** investigator, **abro** un clip y veo el video con detecciones, audio waveform si aplica, evidencia relacionada.
Backend: streaming HLS desde Frigate envuelto por el reverse proxy del router; metadatos del evento; lista de "evidence within ±60s".
**Tests de aceptación**: clip carga en <2s; scrubbing funciona suave.
**Estimación**: M (3d).
**Depende de**: BL-04.4.
### `[BL-04.8]` Pin to case — *M*
**Como** investigator, **pino** un clip a un caso abierto.
Backend: SQLite local en Cell con tabla `cases`, `case_evidence`, `case_annotations`.
**Tests de aceptación**: pinear 3 clips a un caso, verlos en lista, descripción persistente.
**Estimación**: M (3d).
**Depende de**: BL-04.7.
### `[BL-04.9]` Vista CASES — listado y detalle — *M*
**Como** investigator, **veo** mis casos abiertos, click → detalle con todos los pinned y notas.
UI: vista nueva (actualmente stub).
Backend: CRUD sobre la BD de cases.
**Estimación**: M (3d).
**Depende de**: BL-04.8.
### `[BL-04.10]` Vista EXPORTS — generar bundle ZIP — *L*
**Como** investigator, **exporto** un caso completo como ZIP con clips + manifest JSON.
Backend: zip de:
- Clips MP4 originales.
- Manifest con SHA256 de cada clip.
- Eventos JSON.
- Anotaciones.
- Custody log: quién accedió cuándo.
**Tests de aceptación**: bundle de 1GB se genera en <30s; abrir ZIP en otro ordenador permite reproducir clips.
**Estimación**: L (5d).
**Depende de**: BL-04.9.
---
# ÉPICA 5 — HEALTH & OPS
### `[BL-05.1]` Selftest engine en Cell — *L*
**Como** ops, **quiero** que el Cell ejecute un selftest cada 5min y publique resultados a MQTT.
Backend: `healthd` container con todos los chequeos de la vista Health, persiste último estado, publica a `hai/healthd/health`.
**Tests de aceptación**: matar Frigate y healthd reporta `frigate: DOWN` en <30s.
**Estimación**: L (5d).
**Depende de**: BL-00.4.
### `[BL-05.2]` Selftest engine en Router — *M*
**Como** ops, **lo mismo pero del lado router**: NTS, WAN, VLAN segregation, Tailscale.
Backend: ucode script en cron de 5min.
**Estimación**: M (3d).
**Depende de**: BL-02.1.
### `[BL-05.3]` Vista HEALTH agregada — *M*
**Como** ops, **veo** el resultado consolidado de los dos selftests con los 6 grupos.
UI: la vista Health prototipada.
Backend: `hai.healthcheck` une los dos.
**Estimación**: M (3d).
**Depende de**: BL-05.1, BL-05.2.
### `[BL-05.4]` Run all tests en directo — *M*
**Como** ops, **al pulsar el botón**, ejecuto un selftest fresh con animación de progreso.
Backend: SSE de progreso por test individual.
**Estimación**: M (2d).
**Depende de**: BL-05.3.
### `[BL-05.5]` Health history 24h — *M*
**Como** ops, **veo** la barra de histórico para entender patrones temporales.
Backend: persistencia de cada selftest run en SQLite del Cell con retention de 30 días.
**Estimación**: M (3d).
**Depende de**: BL-05.3.
### `[BL-05.6]` Quick-fix actions desde test failure — *L*
**Como** ops, **al ver un test rojo**, **pulso** un botón de acción contextual (PING X, RECONNECT BRIDGE, ...).
Backend: cada tipo de test tiene sus actions definidas, ejecutadas via RPC.
**Estimación**: L (4d).
**Depende de**: BL-05.4.
---
# ÉPICA 6 — HUB & FLEET
### `[BL-06.1]` Hub mínimo — broker MQTT + auth — *L*
**Como** ops, **necesito** un hub mínimo donde los routers se registren y publiquen estado.
Stack:
- Mosquitto en Hetzner bare-metal (Falkenstein).
- Postgres para fleet metadata.
- Keycloak para auth de operadores.
- mTLS con CA propia.
- Endpoint enrollment para tokens de fleet.
**Tests de aceptación**: dos routers de prueba pueden registrarse en el mismo hub y publicar en namespaces separados (`sites/X/...`, `sites/Y/...`).
**Estimación**: L (1 sem).
**Depende de**: BL-00.5.
### `[BL-06.2]` MQTT bridge router → hub — *M*
**Como** ops, **el router** publica selectivamente al hub según la policy de bridge.
UI: ya en MQTT panel.
Backend: `mosquitto.conf` con bridge sections; testing con cliente que verifica que `hai/+/snapshots/#` no llega al hub.
**Tests de aceptación**: matar el bridge, generar 100 eventos, restaurar, los 100 llegan al hub eventualmente.
**Estimación**: M (3d).
**Depende de**: BL-06.1.
### `[BL-06.3]` Sites Overview — fleet map — *L*
**Como** ops, **abro la consola del hub** y **veo** todos los sitios en un mapa con estado agregado.
UI: vista nueva, **distinta del index.html del router** — esto es la consola del hub. Probablemente comparte design tokens y componentes.
Backend: `GET /api/hub/sites` lista sitios + estado consolidado de health/alerts/cams.
**Tests de aceptación**: 10 sitios con estados variados se ven correctamente; click en uno lleva al index.html de ese site via Tailscale.
**Estimación**: L (1 sem).
**Depende de**: BL-06.1.
### `[BL-06.4]` Cross-site forensic search — *XL*
**Como** investigator, **lanzo** una query desde el hub que busca **en varios sitios** simultáneamente y agrega resultados.
Backend: hub mantiene índice consolidado de embeddings (Qdrant); cada sitio publica embeddings con su site_id; query se distribuye a sites relevantes via MQTT request-response, hub re-ranquea.
**Tests de aceptación**: query "vehículo placa L-7234 en cualquier sitio" devuelve hits de 2 sitios diferentes con timestamps correctos.
**Estimación**: XL (2 sem).
**Depende de**: BL-06.3, BL-04.4.
### `[BL-06.5]` Remote operate via Tailscale — *M*
**Como** ops, **desde el hub**, **abro** la consola de un sitio remoto con un click.
Backend: Tailscale ACLs configuradas para que el operador del hub tenga acceso al `:80` del router de cada sitio.
**Tests de aceptación**: click en site → tab abre `https://bl-lab-router.ts.net/` con auth ya pasada.
**Estimación**: M (3d).
**Depende de**: BL-06.3.
### `[BL-06.6]` Fleet provisioning de configuración — *L*
**Como** ops, **edito** la configuración base de la flota en un único sitio (el repo `fleet-config`) y **se propaga** a todos los routers.
Mecánica: cada router clona dos repos: el `site-config` (específico) y el `fleet-config` (común). Reconcile aplica primero fleet, luego site (overrides). Cambios en fleet-config llegan a todos en el siguiente fetch (5min).
**Tests de aceptación**: añadir una regla firewall en fleet-config se aplica a 10 sitios en <10min.
**Estimación**: L (5d).
**Depende de**: BL-02.8, BL-06.1.
---
# ÉPICA 7 — HARDENING
### `[BL-07.1]` Evidence chain — capa 1: hash + manifest + NTS — *L*
**Como** investigator, **cada clip generado** tiene un manifest firmado por el Cell con hash del fichero, timestamp NTS, modelo usado.
Backend:
- Frigate hook on_clip_done.
- Hash SHA256 stream.
- Manifest JSON con cam_id, sha256_clip, sha256_keyframe, ts_nts, model_sha, container_sha, prev_manifest_hash.
- Firma con clave del Cell (LUKS-protected file por ahora, TPM en BL-07.x).
**Tests de aceptación**: editar un clip cambia su SHA, manifest queda inválido, herramienta `blocao verify` lo detecta.
**Estimación**: L (5d).
**Depende de**: BL-04.4.
### `[BL-07.2]` `blocao verify` — herramienta CLI de verificación — *M*
**Como** investigator, **paso** un clip + manifest a un tercero y este puede verificar autenticidad sin necesitar acceso a Blocao.
Backend: CLI Go o Rust que toma clip + manifest + clave pública, valida SHA, firma, encadenamiento. Distribuible como single-binary.
**Tests de aceptación**: terceros sin acceso al sistema pueden ejecutar `blocao verify clip.mp4 manifest.json` y obtener "VALID" o "INVALID con razón".
**Estimación**: M (3d).
**Depende de**: BL-07.1.
### `[BL-07.3]` Hash chain de manifests + replicación al hub — *M*
**Como** investigator, **cada manifest** referencia el SHA del anterior, formando una cadena verificable; los manifests se replican al hub.
Backend: persistencia local + topic `hai/_evidence/manifest` que el bridge publica al hub para tener segunda copia.
**Estimación**: M (3d).
**Depende de**: BL-07.1, BL-06.2.
### `[BL-07.4]` Audit log de operador — *M*
**Como** investigator, **cada acción mía** queda registrada inmutablemente: query lanzada, clip visualizado, exportación, escalación.
Backend: append-only log en BD del Cell con firma del operador (FIDO2 para acciones críticas).
**Estimación**: M (3d).
**Depende de**: BL-04.8.
### `[BL-07.5]` Rebranding final + design tokens estables — *S*
**Como** team, **consolido** los tokens (paleta complementaria, tipos, espacios, radios) en un paquete versionado para reusar en el hub UI, web corporativa, decks.
Backend: `packages/design-tokens/` con `tokens.css`, `tokens.json`, `tokens.figma.json`.
**Estimación**: S (1d).
**Depende de**: nada.
### `[BL-07.6]` Documentación de patrones UI — *S*
**Como** team, **documento** las decisiones de diseño (rail+topbar+ctx-strip, asistentes full-screen, modos de color por dominio).
`docs/ui-patterns.md`.
**Estimación**: S (1d).
**Depende de**: BL-07.5.
---
# Stories transversales (no bloquean sprints)
### `[BL-X.1]` mDNS publishing del router como `bl-{site}-router.local` — *S*
**Depende de**: BL-01.2.
### `[BL-X.2]` Cell discovery via mDNS + MQTT announce — *M*
**Depende de**: BL-00.4, BL-02.1.
### `[BL-X.3]` First-time TLS cert: self-signed → Let's Encrypt vía Tailscale — *M*
**Depende de**: BL-01.5.
### `[BL-X.4]` HaLow radio support en Image Builder (cuando llegue hardware) — *L*
**Depende de**: BL-00.3.
### `[BL-X.5]` Multi-Cell coordination (preparación para Core) — *L*
**Depende de**: BL-04.4.
### `[BL-X.6]` Backup & Restore de configuración (los repos GitOps + BD operator) — *M*
**Depende de**: BL-02.8.
### `[BL-X.7]` Multi-operator + roles (RBAC) — *L*
**Depende de**: BL-06.1.
### `[BL-X.8]` Telegram/email/webhook notifications — *M*
**Depende de**: BL-05.1.
---
# Riesgos identificados
1. **Frigate custom build con RKNN** puede ser inestable. Mitigación: tener fallback CPU-only en parallel image.
2. **mTLS cert lifecycle** en routers desplegados — si el cert expira sin renovarse, el sitio queda desconectado del hub. Mitigación: renovación automática con margen de 90 días.
3. **Frigate API breaking changes** entre versiones (pasamos de 0.13 a 0.14 ya hubo cambios). Mitigación: pin de version en GitOps repo.
4. **GL.iNet / Banana Pi disponibilidad** del hardware del router — comprobar segundo proveedor.
5. **GDPR/AAIP compliance review** antes del primer cliente real — workstream legal aparte, no bloquea técnico.
6. **Coste hub** crece con número de sitios — definir tier de pricing antes de tener 50 sitios para que no se coma el margen.
---
# Notas para el sprint planning
**Sprint 0** debería ser puramente foundation (épica 0). Sin UI nueva.
**Sprint 1** mete el wizard funcional y un sitio se puede provisionar end-to-end aunque sea muy básico.
**Demo viable de cliente**: tras sprint 4 (épica 0+1+2+3+4) ya tienes una historia completa: desplegar router, descubrir cámaras, ver SCADA, hacer query forense. Sin hub, sin evidence chain. **Es la demo MVP**.
**Demo de cliente con flota**: tras sprint 6, ya tienes hub + multi-site.
**Demo legal-grade**: tras sprint 7, ya tienes evidence chain básica para defender el caso.
---
*Documento generado · pendiente de revisión por el team antes de carga en Linear / Jira.*