Skip to content

Repository files navigation

GUAITA

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.


Demo pública

https://guaita.xpl0day.com

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_tiempo incompleto 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_vulnerab provisional — 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.

Por qué

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.

Visor

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), sobre http://localhost:5173.

Stack

Java 21 · Spring Boot 4.1 · PostgreSQL 16 + PostGIS 3.4 · React 19 + Vite · MapLibre GL · Docker Compose

Arranque

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 feeds

Visor en http://localhost:5173, API en http://localhost:8080/api/v1.

Despliegue en VPS

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) en db (1 GB/1 CPU — PostGIS sobre una provincia cabe de sobra) y api (1 GB/1 CPU — pico al parsear el JSON horario de un año × 135 municipios, ~43 MB). Defaults conservadores; ajústalos en .env tras mirar free -h y docker stats.
  • Reinicio unless-stopped en 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 base

Backfill histórico desatendido

El 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 espera

Ensayo 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 T1

El 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).

Validación temprana — make backfill-check

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 T2

Verifica 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—.

Operación diaria (tras el backfill)

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;"

Documentación

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

Datos y atribución

NASA FIRMS · AEMET OpenData · Copernicus EFFIS · PATFOR (Generalitat Valenciana) · CNIG/IGN · Dirección General del Catastro (INSPIRE).

Licencia

MIT. Ver LICENSE.

About

Sistema de inteligencia de riesgo de incendio forestal para la provincia de Castellón: índice de peligro diario con el FWI canadiense, auditoría de interfaz urbano-forestal (Catastro × PATFOR) y validación histórica por backtesting. Java 21 · Spring Boot 4.1 · PostgreSQL/PostGIS · React + MapLibre.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages