# 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** ``, **cuando** ``, **veo / puedo / espero** ``. > *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 ` 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//announce` - `hai//health` - `hai//events/` - `hai//state/` - `hai/_registry/heartbeat/` - `_cmd/` - ...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//*` 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 '' -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:` 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.*