Contrato de datos (consumido)¶
El contrato lo posee y congela nexrad-l3-pipeline: schema Postgres en db/pg_migrations/ de ese repo (migrado de D1, decisión 38 — db/migrations/ queda como referencia histórica) + convención de claves R2. Este proyecto lo lee y no lo cambia; cualquier cambio necesario es una migración negociada del lado del pipeline. Los contract tests de este repo (F1) asertan las formas de las que el viewer depende y fallan CI ante drift.
Esta página es la vista del consumidor: qué lee el viewer de cada tabla y qué asume. La fuente de verdad es el SQL del pipeline.
Convenciones globales¶
- Timestamps
TEXTISO-8601 UTC sin sufijo de zona (YYYY-MM-DDTHH:MM:SS), comparables lexicográficamente. - Retención: 72 h — el viewer nunca asume historia más allá de 3 días.
- Calibración raster:
físico = nivel · value_scale + value_offsetpara niveles ≥ 2; nivel 0 = below threshold (nodata), 1 = range folded.
Postgres¶
radars — catálogo dinámico¶
| Columna | Uso en el viewer |
|---|---|
site_id |
id de 3 chars del feed (AMX, JUA); clave de todas las consultas |
icao |
display (KAMX, TJUA) cuando existe — observado null en todo el feed del demo (jul-2026): el viewer cae a site_id |
lat, lon |
centrado de vista, círculo de cobertura, cálculos VWP |
height_m |
altitud de la antena en metros sobre el nivel del mar (ver aviso abajo) |
proj4 |
definición AEQD registrada tal cual con proj4.defs — el viewer no construye proyecciones |
last_seen_at |
frescura ("minutos desde el último scan"), semáforo de estado por radar |
Sin radares hardcodeados en ninguno de los dos proyectos: lo que hay en la tabla es lo que el viewer ofrece.
height_m cambió de unidad el 2026-11-05 (mismo nombre, otros valores)
El PDB del Level III trae la altura de antena en pies (ICD 2620001) y
el pipeline la guardaba sin convertir: la columna decía metros y contenía
pies (issue #8 del pipeline, arreglado + migración de datos
0003_radar_height_feet_to_m.sql). Desde esa fecha los valores son
metros de verdad y las filas viejas quedaron convertidas. Sin cambio de
schema: no hay drift que detectar, solo valores ~3,28 veces menores.
Las fixtures grabadas (server/dal/fixtures/radars.json) ya están en
metros, y zRadarRow acota height_m a [-100, 4000] como guardia de
regresión — ningún WSR-88D pasa de ~3250 m, así que una vuelta a pies
rompe CI en los radares de montaña (no en los costeros: 111 pies son
111 m plausibles).
El viewer hoy solo transporta la columna (DAL → tipo); ningún componente calcula con ella. Si algún día se usa para altura de haz o corrección de terreno, la unidad es metros.
products — descriptores mínimos¶
code (NEXRAD, p.ej. 153), mnemonic (N0B), unit, kind (raster | phenomena | vwp). Nombre display, categoría, rango y paleta no están aquí: viven en el catálogo estático del viewer keyed por code (decisión 5).
rasters — metadata de cada COG¶
Consultas del viewer: lista de datetimes por (site_id, product_code, día), más-cercano / siguiente / anterior a un instante (sobre vol_time, índice idx_rasters_lookup). Columnas usadas: r2_key (→ URL del COG), vol_time, value_scale/value_offset, max_level (recorte de leyenda), width/height/cell_m (extensión), el_angle, vcp.
phenomena — celdas, meso, TVS, granizo¶
Una fila por (site, vol_time, kind, cell_id) con lat/lon, azimuth_deg/range_km y attrs JSON. Consultas del viewer: por (site_id, vol_time) para el overlay del frame mostrado, por (site_id, cell_id) cross-volumen para las series de tendencia de los charts (el cell_id del RPG es estable entre volúmenes), y DISTINCT vol_time por (site_id, día UTC) como índice del join temporal (los fenómenos tienen vol_times propios; el frame mostrado se casa en cliente con el volumen más cercano — decisión 24).
Claves de attrs (parte del contrato)¶
Estado observado en el feed real (grabación de fixtures, jul-2026). La tabla canónica vive en db/README.md del pipeline; los tipos + parsers tolerantes del viewer en shared/contract/attrs.ts (stormCellAttrs()/mesoAttrs() — toda clave es opcional, un campo corrupto se descarta sin tumbar la fila):
kind |
Clave | Contenido |
|---|---|---|
storm_cell |
azran_nm |
[az_deg, range_nm] posición radar-céntrica |
storm_cell |
movement_deg, movement_kt |
vector de movimiento (movement_deg convención "desde") |
storm_cell |
new |
celda nueva en este volumen (sin tracks) |
storm_cell |
past, forecast |
arrays de posiciones [[x_km, y_km], …] AEQD de los packets 23/24 (SCIT); past reciente→viejo, forecast cercano→lejano — semántica deducida de las grabaciones, pendiente de confirmar con el experto (puerta M4; test canario en tests/unit/tracks.spec.ts) |
storm_cell |
dbz_max, dbz_max_height_kft |
reflectividad máxima y su altura, del GAB de NST — el GAB pagina de a 6 celdas: puede faltar |
meso |
radius_km, azran_nm, storm_id |
geometría del mesociclón y cell_id de la celda NST asociada (el cell_id de la fila meso es el ID del mesociclón, no la celda) |
meso |
strength_rank, msi, tvs |
rango de intensidad, MSI, flag TVS (bool) del tabular NMD — tvs es LA señal TVS del feed |
meso |
low_level_rv_kt, low_level_dv_kt, base_kft, depth_kft, depth_stmrel_pct, max_rv_kft, max_rv_kt, movement_deg, movement_kt |
resto del tabular NMD |
Fuera de alcance — el feed no lo distribuye (acordado con el pipeline, jul-2026): vil_kg_m2, top_kft (producto SS/62) y poh_pct/posh_pct/hail_size_in (HI/59) — esos productos no fluyen en el bucket de Unidata; el NST no los trae. VIL y echo top sí existen como rasters de grilla (DVL, EET). Si algún día aterrizan, attrs.ts suma claves opcionales sin migración. Las claves de la tabla están presentes en las fixtures grabadas (server/dal/fixtures/phenomena.json) que los contract tests y tests/unit/attrs.spec.ts validan.
vwp — perfiles de viento¶
Una fila por (site_id, vol_time, height_ft): wind_dir_deg, wind_speed_kt, rms_kt. El viewer deriva u/v en cliente; no hay componente vertical w en el contrato. Consultas: por (site_id, vol_time) para el perfil, y DISTINCT vol_time por (site_id, día UTC) como índice del join temporal (decisión 24).
wind_grids — viento GFS 10 m en grilla¶
Una fila por (site_id, valid_time, level): cycle_time, forecast_hour, model, r2_key → JSON u/v en R2 (gzip + immutable desde el edge — el ciclo y el nivel van en la key). Consulta del viewer: por (site_id, nivel, día UTC ± 2 h) como índice del join temporal (padding mayor que phen/vwp porque la tolerancia es 1 h). Schema del pipeline: db/pg_migrations/0001_init.sql (snapshot en tests/contract/schema/, drift check activo). Spec completa (keys R2, formato del fichero, cron) en Spec pipeline viento.
R2¶
- Convención de claves:
{site}/{mnemo}/{YYYY}/{MM}/{DD}/{site}_{mnemo}_{YYYYMMDD_HHMMSS}.tif— pero el viewer no construye claves: usarasters.r2_keyliteral. - Desde 2026-11-05 el pipeline admite un prefijo opcional de instancia (
R2_KEY_PREFIX, vacío en producción) que antepone{prefijo}/a toda clave R2 — rasters, mosaico, viento y rayos — para que varias instancias compartan bucket (LAMULA Jobs: una por job). Nada que cambiar aquí, justamente porque el viewer usar2_keyliteral y arma la URL como${base}/${r2_key}; el prefijo viaja dentro del valor. El único efecto visible es quer2_keypuede no empezar por elsite_idni, en el mosaico, pormosaic/— no asumir ninguna de las dos cosas. - COG: niveles crudos uint8, scale/offset también embebidos en el TIFF, CRS proj4 AEQD, overviews internos. Peor caso 3680×3680 (N0B) — bajo el cap de textura WebGL de 4096.
- Acceso: lectura pública (dominio custom o
r2.dev) con CORS habilitado para el origen del viewer y soporte de range requests (nativo en R2). El viewer descarga solo tiles/overviews del viewport.
Requisitos de setup fuera del código (una vez)¶
- CORS en el bucket R2 para el origen público del viewer (
https://webviewer.ladetec.com) ylocalhost:3000para desarrollo. Verificar tras la salida de Cloudflare Pages: la allowlist se escribió con los orígenes*.pages.dev; si el dominio del Swarm no entra en ella, todocog_urlfalla por CORS en producción y el mapa queda sin raster. - Acceso público de lectura al bucket (dominio custom recomendado sobre
r2.dev). - Credenciales Postgres (
NUXT_PG_HOST/NUXT_PG_PORT/NUXT_PG_DATABASE/NUXT_PG_USER/NUXT_PG_PASSWORD) del contenedor del viewer apuntando a la misma base que usa el pipeline — mismo Swarm, red interna, sin binding. - DNS del dominio del viewer (
webviewer.ladetec.com) apuntado (orange-cloud) al reverse proxy del Swarm. NUXT_PUBLIC_I18N_BASE_URL=https://webviewer.ladetec.comen el entorno del contenedor del viewer. Es variable de arranque, no de build (decisión 52): sin ella el SSR avisa en cada petición y las páginas salen sincanonicalnihreflang, que es lo que hace útil el prefijo/en/.
Puntos de coordinación abiertos con el pipeline¶
- ~~Extensión de
attrsde NST~~ — cerrado jul-2026: tracks (packets 23/24), dbz_max y el tabular NMD completo fluyen; VIL/top/granizo quedan fuera de alcance (el feed no distribuye SS/HI — barrido verificado por el pipeline). radars.icaollega null en todo el feed del demo — si el pipeline puede mapear ICAO desde su config, el display mejora sin tocar el viewer.- ~~Documentar claves de
attrsen el pipeline~~ — hecho: tabla canónica endb/README.mdde aquel repo. - ~~Acceso público de lectura + CORS del bucket R2 y
NUXT_PUBLIC_R2_BASE_URL~~ — hecho: bucket con CORS (GET/HEAD +Range, dominio custom +localhost:3000),cog_urlresuelve. - Confirmar con el experto la semántica de
past/forecast/movement_deg(orden y convención "desde") — parte de la puerta M4. - ~~Viento GFS 10 m~~ — cerrado jul-2026: el pipeline mergeó
0003_wind_grids.sqle ingesta activa; CORS (localhost:3000, pages.dev) y gzip del edge verificados contra el dominio custom el 2026-07-18. Queda del lado del viewer: re-grabar fixtures (incl.wind.jsonreal + bajar los JSON u/v golden) en la próxima re-grabación completa.
mosaic_domains / mosaic_domain_sites / mosaic_rasters — mosaico multi-radar¶
Compuesto multi-radar producido por el pipeline (0002_mosaic.sql, decisión 43). El viewer lo lee como un producto más, con tres diferencias que importan.
Rejilla temporal propia. slot_time son slots UTC fijos de 300 s, no los vol_time de ningún radar. Las consultas del viewer son las mismas que para rasters (día, más-cercano/siguiente/anterior) pero sobre esa columna.
Geometría por fila. mosaic_rasters trae proj4/width/height/cell_m además de mosaic_domains: añadir un radar a un dominio recalcula la malla, y un COG anterior tiene que seguir georreferenciándose con la suya. mosaic_domains describe la malla actual (para montar la vista); cada fila describe la suya (para pintar ese COG).
contributing es parte del contrato. JSON [{"site","vol_time","lag_s"}] con los radares que entraron en ese slot y su desfase contra el centro (negativo = volumen anterior al centro). Un slot se compone con los radares que haya dentro de tolerancia — un radar caído no apaga el mosaico — así que la cobertura varía entre frames. mosaic_domain_sites da los miembros del dominio; la resta son los ausentes, y eso es lo que pinta el overlay de cobertura. Sin ello, un hueco en el mapa es indistinguible de "no hay ecos".
| Columna | Uso en el viewer |
|---|---|
domain_id |
identidad del dominio; no es un site_id (ni 3 chars ni vive en radars) |
product_code |
código NEXRAD reusado — shared/products/<code> y su paleta valen sin cambios |
slot_time, slot_s |
timeline del mosaico |
r2_key |
→ URL del COG (prefijo mosaic/) |
value_scale, value_offset, max_level |
calibración canónica del producto, fija entre slots |
proj4, width, height, cell_m |
geometría de ese COG |
method |
weighted | lowest_beam |
contributing |
procedencia del slot |
N0G nunca aparece: la velocidad radial es relativa al radar y no es componible. Detalle del cálculo en docs/mosaico.md del pipeline.