Sistema de inteligencia de riesgo de incendio forestal — provincia de Castellón
guaita(val.): atalaya, torre de vigía.
Índice de peligro diario por municipio calculado con el sistema FWI canadiense sobre datos abiertos, análisis de interfaz urbano-forestal a partir de cartografía catastral y PATFOR, y validación del índice contra el histórico real de incendios de la provincia.
⚠️ GUAITA no es un sistema de emergencias. No sustituye al 112 ni al boletín oficial PREVIFOC de la Generalitat Valenciana. Ante un incendio, llame al 112.
Visor coropleto de los 135 términos de Castellón por nivel de peligro (1 Bajo → 5 Extremo). Al clicar un municipio, un panel desglosa los tres componentes del índice (meteo, estructural, vulnerabilidad), los códigos FWI del día, la serie de 30 días y las banderas. La fecha del dato y el aviso de "no es un sistema de emergencias" están siempre visibles.
Limitaciones honestas (también en el propio visor → «Metodología y limitaciones»):
- Pesos de combustible sin calibrar — valores de partida (comportamiento publicado de Anderson); la calibración es la Fase 4.
f_tiempoincompleto sin EFFIS — mientras el WFS de perímetros no es accesible, solo los incendios semilla reducen el combustible; por eso el top-10 de hoy está encabezado por municipios de la Serra d'Espadà que ardieron en 2026 pero cuyo perímetro real aún no está cargado.comp_vulnerabprovisional — población normalizada (proxy débil: mide gente en casco urbano) + suelo protegido; se sustituye por el módulo IUF en v2.0.- El índice se calcula sobre meteo de reanálisis con ~5 días de latencia (no es tiempo real, T7); cada dato va etiquetado con su fecha.
Castellón ha sufrido cuatro eventos graves en cuatro años —Bejís (2022, ~19.000 ha), Les Useres (2022), Villanueva de Viver (2023, 4.700 ha) y la Serra d'Espadà (2026)— mientras los planes de prevención de demarcación sobre los que se planifica fueron redactados entre 2007 y 2013 y actualizados en 2013–2014.
Los 135 términos de la provincia en un visor MapLibre servido por teselas
vectoriales propias (ST_AsMVT desde PostGIS, sin pg_tileserv ni token de
Mapbox). Clic en un término muestra su nombre y comarca; el coropleto por nivel de
peligro llega en la Fase 3 sin rehacer la capa (las teselas ya declaran
promoteId: 'ine_code', ADR-06).
Captura del visor: pendiente de
docs/img/visor.png— se obtiene con el stack levantado y sembrado (make up && make seed), sobrehttp://localhost:5173.
Java 21 · Spring Boot 4.1 · PostgreSQL 16 + PostGIS 3.4 · React 19 + Vite · MapLibre GL · Docker Compose
cp .env.example .env # rellenar FIRMS_MAP_KEY y AEMET_API_KEY
make up
make seed # geodatos base (tarda; descarga varios GB)
make ingest # primera pasada de feedsVisor en http://localhost:5173, API en http://localhost:8080/api/v1.
El VPS puede compartir máquina con otra plataforma en producción (XPL0DAY). El
override docker-compose.vps.yml no toca el compose
base y añade el blindaje para que GUAITA —y en particular el backfill, que
escribe durante horas— no monopolice ni tumbe la máquina:
- Puertos remapeables por
.env(DB_PORT_HOST,API_PORT_HOST,WEB_PORT_HOST) para esquivar colisiones sin editar ficheros. - Límites de recursos (
mem_limit/cpus) endb(1 GB/1 CPU — PostGIS sobre una provincia cabe de sobra) yapi(1 GB/1 CPU — pico al parsear el JSON horario de un año × 135 municipios, ~43 MB). Defaults conservadores; ajústalos en.envtras mirarfree -hydocker stats. - Reinicio
unless-stoppeden los servicios de larga duración. - Rotación de logs (
max-size/max-file) en todos: sin ella el json-file de Docker crece sin límite y llena el disco (caída clásica en VPS pequeños).
# Levantar con el override
docker compose -f docker-compose.yml -f docker-compose.vps.yml up -d --wait
make seed # geodatos baseEl backfill NO corre en GitHub Actions (su BD es efímera: ver
docs/01, "Limitación operativa"). Corre en el VPS,
contra el volumen persistente, con una guardia de disco que aborta limpio
(BD consistente, reanudable) si el libre baja de DISK_MIN_FREE_GB (5 GB por
defecto), y que se niega a arrancar si el tramo no cabe con margen.
Los tramos intermedios solo ingieren meteo (finalize ausente); el tramo
final (finalize) calcula el FWI de los 135 sobre la serie completa, verifica
que la meteo está completa (aborta diciendo qué falta), corre las aserciones de
completitud y regenera el informe.
El lanzamiento va por ops/backfill.sh, con salvaguardas: un solo tramo a la
vez (rechaza lanzar otro con uno vivo), espera bloqueante, e intervalo mínimo
entre tramos (60 min; dos seguidos darían 429 de Open-Meteo). NO lances tramos a
mano en paralelo.
# Un tramo, desatendido (sobrevive a la desconexión SSH):
bash ops/backfill.sh tramo 2005 2008 # intermedio (solo meteo)
bash ops/backfill.sh espera # BLOQUEA hasta que acabe + veredicto
make backfill-check # valida lo ingerido (ver abajo)
# ...tras los tramos intermedios, el FINAL calcula FWI + aserciones + informe:
bash ops/backfill.sh tramo 2025 2026 true
bash ops/backfill.sh esperaEnsayo en seco (antes del T1 real). Valida la mecánica completa —guardas,
petición real, parseo, persistencia, backfill-check— sobre 1 año × 5
municipios, sin comprometer la serie y trivial de deshacer:
bash ops/backfill.sh tramo-dry # 5 municipios de control × año 2020
bash ops/backfill.sh tramo-dry # (a la vez: debe RECHAZARSE por concurrencia)
bash ops/backfill.sh espera
GUAITA_BACKFILL_EXPECT_MUNIS=5 make backfill-check # espera 5 municipios, no 135
docker stats guaita-backfill # (durante el tramo) pico de memoria
bash ops/backfill.sh dry-limpia # borra las filas del ensayo antes del T1El log del ensayo trae Open-Meteo 200: N KB: multiplica por ~27 (135/5) para
estimar el payload de un año-petición real. Con eso mides el tamaño y el pico de
memoria en vez de estimarlos.
Progreso, dos vías:
# a) Desde el log (último año procesado + disco libre):
grep -hE "meteo [0-9]{4} ->|GUARDIA DE DISCO" etl/reports/backfill-*.log | tail -n 3; \
df -h "${DISK_GUARD_HOST_PATH:-/}" | tail -1
# b) Desde la BD, sin leer el log (municipios cubiertos y rango ingerido):
docker compose -f docker-compose.yml -f docker-compose.vps.yml exec db \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
"select count(distinct ine_code) municipios, min(fecha), max(fecha), count(*) filas \
from meteo_municipio;"Memoria durante el tramo (riesgo de OOM al parsear el JSON horario; el heap va
capado con -Xmx, ExitOnOutOfMemoryError sale limpio si aun así revienta):
docker stats guaita-backfill # en vivo; Ctrl-C para salir (--no-stream = una foto)Reanudar tras un aborto/parada = relanzar el mismo tramo: es idempotente y retoma el estado desde la BD (que ahora persiste en el volumen del VPS).
La primera comprobación de correctitud NO puede esperar al tramo final: si el T1
ingiere con la hora equivocada o sin corrección altitudinal, hay que enterarse el
día 1, no el día 3 con la serie entera contaminada. Corre make backfill-check
tras CADA tramo, antes de lanzar el siguiente (sale con error si algo falla):
bash ops/backfill.sh tramo 2005 2008
bash ops/backfill.sh espera
make backfill-check # <- aquí, antes del T2Verifica sobre lo ingerido hasta ese momento: cobertura rectangular (135
municipios, todos con el mismo nº de días), elevacion_celda_m/delta_altitud_m
no NULL, rangos físicos (temp −20..50, HR 0..100, viento ≥ 0), estacionalidad
(un día de verano debe ser más cálido que uno de invierno; si no, hay un fallo de
índice horario / zona), y una tabla de los 6 municipios de control con celda,
altitud, delta y T/HR de un día de verano —Vistabella y Villahermosa con
corrección apreciable, los costeros casi nula—.
Terminado el backfill, un job @Scheduled (06:30 Europe/Madrid) ingiere cada
día la meteo nueva y recalcula el FWI. Recupera huecos: no procesa "hoy",
sino desde la última fecha con dato hasta el corte del archivo (~D-5); si el
servidor estuvo caído una semana, la siguiente pasada rellena los siete días. Si
Open-Meteo falla o los datos no validan, no escribe nada (ni ceros) y el hueco
lo recupera la pasada siguiente.
Está apagado por defecto; actívalo SOLO cuando el histórico esté completo (si no, generaría cadenas parciales):
# en .env: GUAITA_SCHEDULER_ENABLED=true (luego reinicia la api)
docker compose -f docker-compose.yml -f docker-compose.vps.yml up -d --wait api
# Métrica de retraso respecto a D-5 (si crece, algo va mal aunque el job no falle):
docker compose -f docker-compose.yml -f docker-compose.vps.yml exec db \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
"select (current_date - 5) - max(fecha) from meteo_municipio;"| Doc | Contenido |
|---|---|
| 00 | Visión, usuarios, alcance y no-alcance |
| 01 | Arquitectura y decisiones (ADR) |
| 02 | Fuentes, endpoints, licencias, trampas |
| 03 | Esquema PostGIS |
| 04 | Especificación del índice. Cerrada. |
| 05 | Módulo IUF |
| 06 | Contrato REST |
| 07 | Modelo de amenazas y RGPD |
| 08 | Plan por fases |
| 09 | Metodología de backtesting |
NASA FIRMS · AEMET OpenData · Copernicus EFFIS · PATFOR (Generalitat Valenciana) · CNIG/IGN · Dirección General del Catastro (INSPIRE).
MIT. Ver LICENSE.