BlackoutTracker · Cortes de luz y agua
Proyecto de la cátedra Sistemas de Información (UJAP, 2026). Hoy los vecinos se avisan de los cortes por WhatsApp; BlackoutTracker toma esa información, la estructura y la valida. Cada residente se registra una sola vez con un pin sobre su vivienda, y la API le asigna un sector con ST_Covers contra polígonos de PostGIS: seis sectores trazados sobre calles de OpenStreetMap. Después reporta cuándo empieza y cuándo termina cada corte, también sin conexión: el reporte espera en IndexedDB y se envía con la hora real del evento. El estado del sector se decide por consenso —3 residentes, en una ventana de 30 min para luz y de 180 para agua, todo configurable— y la duración típica de un corte se estima con la mediana de los ciclos cerrados, siempre junto al número de cortes en que se basa. Es un monorepo pnpm con una API en NestJS 11 + Drizzle, una PWA en React + Vite y un paquete de tipos compartido; Supabase solo aporta Postgres/PostGIS, Auth y Realtime.
Consenso sin árbitro
Como no hay moderador, el sistema decide solo cuándo empezó y cuándo terminó un corte, y dos reportes simultáneos del mismo sector podían abrir dos eventos. Solución: la decisión vive en funciones puras con una ventana deslizante que cuenta un voto por residente, con su marca más antigua. La hora del evento es la mediana de las N horas, así que un reloj desfasado o un vecino que avisa tarde no la mueven. Cada evaluación corre dentro de la transacción del reporte con un pg_advisory_xact_lock por (sector, servicio), y un índice único parcial permite un solo evento abierto por par.
Cortes que nadie cierra
Si nadie avisa que el servicio volvió, el evento queda abierto para siempre y el próximo corte se le suma; y si se le inventa un fin, la mediana recibe una duración que nadie observó. Solución: los reportes abiertos vencen a las 48 h. Si todos vencen sin que nadie cierre, el evento se marca incompleto con fin_en nulo (lo exige un CHECK), así sale del índice de eventos abiertos y queda fuera de la estimación sin filtros extra.
Red intermitente
Con la red intermitente, un envío puede cortarse después de que la API ya guardó el reporte, y el reintento lo duplicaría. Solución: el cliente genera el UUID del reporte y la base lo usa como clave primaria. El PUT es idempotente: con el mismo contenido devuelve el mismo reporte y con otro distinto da 409. La cola de Dexie se envía en orden cronológico y se detiene en el primer fallo reintentable, y un test de integración pasa los envíos por un proxy que corta peticiones al azar para verificar que no haya pérdidas ni duplicados.
Privacidad en la capa de datos
Ningún endpoint devuelve coordenadas de residentes, solo agregados por sector, y el mapa colorea polígonos en lugar de dibujar puntos. En Postgres, RLS está en deny-all y se aplica REVOKE a anon y authenticated en todas las tablas, salvo estado_sector, lo único que el front lee directo vía Supabase Realtime. Nest no emite tokens: verifica el JWT de Supabase con jose contra el JWKS.
Frontend con puertos y adaptadores
La PWA recibe 12 puertos (reloj, conexión, sesión, cola y uno por recurso de la API) que se arman en un solo archivo. Los adaptadores reales (HTTP, Dexie, Supabase) y los mocks son intercambiables, una suite de contrato corre contra los dos y ESLint hace cumplir la frontera. Así el modo demo es un conjunto de adaptadores y no condiciones repartidas por la app, y las pruebas E2E con Playwright corren en Docker sin tocar Supabase.