Arquitectura¶
Una sola aplicación Nuxt 3 desplegada en Cloudflare Pages (preset cloudflare-pages de Nitro). Las server routes (/server/api/*) corren como Pages Functions en el edge y leen D1 por binding; el cliente renderiza el mapa OpenLayers y decodifica los COG directamente desde R2. No hay backend propio que operar: el "backend" es la misma app.
Cloudflare Pages (un solo deploy)
┌─────────────────────────────────────────────────────────────┐
│ Nuxt 3 │
│ │
│ server routes (/api/*) cliente (navegador) │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ DAL adaptador live │ JSON │ Pinia stores │ │
│ │ binding D1 (lectura) │───────▶│ OpenLayers map │ │
│ │ claves R2 → URLs │ │ WebGLTileLayer + │ │
│ └──────────┬───────────┘ │ ol/source/GeoTIFF ────┼─┼──▶ R2 (COGs,
│ │ │ paleta = color ramp │ │ HTTP range,
│ ▼ │ proj4 AEQD por radar │ │ CORS)
│ Cloudflare D1 └────────────────────────┘ │
│ (compartida con │
│ nexrad-l3-pipeline, │
│ solo lectura aquí) │
└─────────────────────────────────────────────────────────────┘
Componentes¶
| Componente | Responsabilidad |
|---|---|
| DAL | Interfaz única de datos. Adaptador live: server routes → binding D1 + construcción de URLs R2. Adaptador fixture: mismas rutas servidas desde respuestas grabadas + COGs golden del repo, para CI determinista y desarrollo offline. Nada por encima del DAL sabe de dónde vienen los datos. |
Server routes (/server/api/*) |
Endpoints de lectura tipados (Zod/TS) sobre el contrato: radares, productos disponibles, lista de datetimes, raster más cercano/siguiente/anterior, fenómenos por volumen, series por cell_id, VWP, health. |
| Shell & routing | Rutas file-based de Nuxt, SSR del shell, estado deep-linkable, locale, chrome del layout. |
| Map core | Mapa OpenLayers, registro dinámico de proyecciones AEQD por radar (proj4.defs con la columna radars.proj4 tal cual + register), base Web Mercator con OSM, capa de cobertura del radar. |
| Raster renderer | WebGLTileLayer + ol/source/GeoTIFF leyendo el COG desde R2; paleta aplicada como expresión de estilo (color ramp) sobre niveles crudos con value_scale/value_offset; opacidad; valor físico bajo el cursor. |
| Timeline & animación | Lista de datetimes (ventana de retención 72 h), stepping siguiente/anterior/más-cercano, playback con prefetch de frames. |
| Storm engine (UI) | Overlay de celdas, tracks pasado/pronóstico, tabla ordenada por VIL, charts de tendencia (VIL/dBZ/top por volumen, construidos cliente-side como serie temporal por cell_id), markers meso/TVS/granizo. |
| Wind engine (UI) | VWP: canvas, tabla, barbas de viento. u/v se derivan de dir/speed en cliente. |
| Paletas & leyenda | Paletas versionadas en este repo (módulo TS por product_code: colores, stops, unidad, ticks); fuente única para el color ramp WebGL y el componente leyenda. |
| Mosaico | Vista compuesta multi-radar: N capas WebGLTile AEQD reproyectadas en GPU sobre la vista común. |
| i18n | @nuxtjs/i18n, catálogos es + en, locale en URL/preferencias. |
| Observabilidad | Logging estructurado cliente/servidor, endpoint de health (frescura por radar desde radars.last_seen_at), estados vacío/error explícitos — nada lanza excepción a la cara del usuario. |
Stack¶
| Capa | Elección | Nota |
|---|---|---|
| Framework | Nuxt 3 (Vue 3, Composition API, TypeScript) | preset Nitro cloudflare-pages |
| Estado / routing | Pinia + Vue Router (integrados en Nuxt) | |
| Estilos | Tailwind CSS | |
| Componentes | PrimeVue v4 (modo unstyled + Tailwind) | DataTable/DatePicker/Slider pesados en esta app |
| Mapa | OpenLayers ≥ 9 + proj4 | ol/source/GeoTIFF, WebGLTileLayer |
| i18n | @nuxtjs/i18n | es + en |
| Validación API | Zod en server routes | los tipos del contrato viven en un módulo compartido server/cliente |
| Tests | Vitest + @vue/test-utils + Playwright | visual-regression con goldens propios |
| Deploy | Cloudflare Pages vía wrangler-action en GitHub Actions |
preview deployments por PR |
Flujo de datos típico (una vista)¶
- Ruta
/{radar}/{product}/{datetime}(SSR resuelve shell + metadata inicial). - Cliente pide
/api/rasters/closest?site=AMX&product=153&t=…→ fila derastersconr2_key, calibración y dimensiones. ol/source/GeoTIFFabrehttps://<r2-público>/{r2_key}— descarga solo los tiles/overviews del viewport (range requests).- Estilo WebGL mapea nivel crudo → color con la paleta del producto; niveles 0 (nodata) y 1 (range folded) con tratamiento especial.
- Overlays (celdas, meso, TVS, VWP) llegan de
/api/phenomenay/api/vwppor(site, vol_time)del raster mostrado. - Timeline pide
/api/rasters/times?site&product&daypara poblar el slider; animación prefetchea los N frames siguientes.
Estado en URL¶
- Ruta:
/{locale?}/{radar}/{product}/{datetime}— lo compartible. - Query:
?overlays=cells,tracks,meso&base=osm&opacity=0.8— modificadores de vista. - localStorage: solo preferencias no compartibles (locale por defecto, última capa base).
Una URL pegada en otro navegador reproduce la vista exacta (mismo frame, mismos overlays).
Acceso a datos compartidos¶
- D1: binding de solo-lectura-por-disciplina — D1 no tiene roles; el contrato es que este proyecto jamás escribe y que las migraciones se aplican únicamente desde
db/del pipeline. Las server routes usan exclusivamenteSELECT. - R2: bucket expuesto con acceso público de lectura (dominio custom o
r2.dev) + CORS habilitado para el origen del viewer. El viewer construye URLs desderasters.r2_key; no lista el bucket ni conoce su estructura más allá de la convención documentada en el contrato.