Saltar a contenido

Bitácora del proyecto

Historial de desarrollo de los tres portales, reconstruido a partir de las bitácoras originales de EMA Saladillo (docs/bitacora_ema_saladillo*.docx, hoy migradas acá) y del historial de commits de los tres repositorios. Desarrollado en sesiones colaborativas con Claude (Anthropic).

Marzo 2026 — Origen: EMA Saladillo

Contexto

El proyecto nace en el espacio curricular Laboratorio de Industrias, 7° Año Técnico Químico, EEST N°1 "Gral. Savio" (Saladillo), a cargo del Ing. Luciano Lamaita. La EEST N°1 tiene una Estación Meteorológica Automática (EMA) marca Tecmes que transmite por telemetría al Sistema Nacional de Información Hídrica (SNIH) del INA — el objetivo inicial fue acceder a esos datos de forma programática, independiente del sitio oficial. Durante el desarrollo se descubrieron tres estaciones adicionales con datos públicos en Saladillo (CFR, Defensa Civil, Clima Saladillo), lo que amplió el alcance hacia una red meteorológica local con análisis espacial.

v1 — Arquitectura inicial

Patrón ETL corrido localmente en una PC Windows: scrapers Python → Supabase (PostgreSQL) → dashboard HTML estático. Cada estación con un método de acceso distinto:

  • EET N°1: POST a MuestraDatos.aspx/LeerDatosActuales — el certificado SSL del servidor está vencido (verify=False), y el ID de estación correcto (284094) resultó ser la concatenación del código de red RMET (28) con el ID real (4094) — llamar solo con 4094 devolvía vacío.
  • CFR Saladillo: scraping HTML con BeautifulSoup.
  • Defensa Civil: sin ningún endpoint de datos — toda la información está superpuesta como texto sobre una imagen JPG de cámara Meteobridge. Se resolvió con recorte + OCR (Tesseract), ~95% de precisión.
  • Clima Saladillo: los datos se cargan por AJAX; el endpoint oculto (stationDataAjax.php) se descubrió inspeccionando el JavaScript de la página.

Deduplicación: hora_ar(), una función PostgreSQL IMMUTABLE que trunca el timestamp a la hora, permite un índice único y evita duplicados por múltiples ejecuciones del scraper.

21–22 de marzo — v2: cuarta estación y análisis de calidad de datos

  • Se suma EMA-CS (Clima Saladillo, barrio Falucho) y se establece la nomenclatura oficial EMA-XXX en todo el sistema.
  • Windows Task Scheduler fallaba con código 0x80070002 en todas las tareas: el Programador de Tareas corre en un contexto donde el PATH no incluye la instalación de Python del usuario — se resolvió usando la ruta completa del ejecutable.
  • Diagnóstico y limpieza de EMA-DC: los errores sistemáticos venían del OCR — la conversión a escala de grises pierde el texto blanco sobre fondos de color. Se resolvió extrayendo directamente los píxeles claros (R,G,B > 175) antes de binarizar, más una validación en dos capas (rango físico plausible + delta temporal máximo) que descarta valores imposibles antes de insertar. Se eliminaron a mano 5 temperaturas y 3 valores de lluvia erróneos ya acumulados.
  • Armonización temporal: nace la vista v_ema_armonizada (normaliza las 4 fuentes a una estructura común por hora) y v_temperatura_comparativa (pivotada, una columna por estación).
  • Análisis microclimático: con los primeros datos se observa que EMA-CS marca sistemáticamente +2 a +4°C de noche y -1 a -2°C de día respecto al promedio de las otras tres — hipótesis de isla de calor urbano (el barrio Falucho es zona urbana consolidada). También se detecta un offset de ~8 hPa en la presión de EMA-EET, sin explicación confirmada (¿presión absoluta vs. reducida a nivel del mar?).
  • Dashboard v4: pestaña de análisis espacial con interpolación IDW (Inverse Distance Weighting, p=2) e isolíneas por Marching Squares; modo comparativo rediseñado con tabla de diferencias por color.

23 de marzo — v3: GitHub Actions y primer deploy público

  • Migración de Windows Task Scheduler a GitHub Actions: el sistema dependía de una PC física encendida. Repo público creado (github.com/lemeit/ema-saladillo); credenciales de Supabase movidas a Secrets de GitHub (SUPA_URL, SUPA_KEY).
  • Dos cron schedules: scrapers horarios (5 * * * *) y ping diario a Supabase a las 11:05 UTC / 08:05 AR (evita que el plan gratuito pause el proyecto por inactividad).
  • Corrección de coordenadas: EMA-CFR y EMA-DC tenían las coordenadas intercambiadas desde el inicio del proyecto.
  • Primer dashboard público: lemeit.github.io/ema-saladillo (GitHub Pages, dashboard movido a docs/index.html por la convención de esa plataforma).

Agosto 2026 — Nace Aire Saladillo y se arma la red de 3 portales

19–20 de agosto — Aire Saladillo (aq.lemeit.ar)

Nuevo proyecto, purpleair-saladillo: sensores PurpleAir de calidad del aire (PM1.0/PM2.5/PM10) en escuelas y jardines, sobre Cloudflare D1 + Workers + Pages desde el día uno (arquitectura más moderna que la de EMA en ese momento, que todavía corría sobre Supabase). Rango temporal, bandas AQI, canales A/B de PM y export CSV desde el inicio.

21–24 de agosto — Diseño unificado y agua-saladillo

  • Etapa 1 de diseño unificado: nace design.lemeit.ar (lemeit-theme.css + lemeit-common.js) — paleta, tipografía (JetBrains Mono), selector de portales y footer versionado compartidos entre aq y ema.
  • Pestaña Mapa (Leaflet) con flip-cards y mini-gráfico en aq; canales A/B extendidos a PM1.0/PM10 (no solo PM2.5).
  • Migración de agua-saladillo: el dashboard de calidad del agua, que vivía como archivo suelto (docs/agua_saladillo.html) dentro de ema-saladillo, se muda a repo propio para evolucionar de forma independiente — tercer portal de la red.
  • agua-saladillo incorpora dataset completo (31 → 52 parámetros tras un merge posterior), normativa comparada CAA/PBA, y un primer GitHub Action de ingesta automática de protocolos PDF usando la API de Gemini para extraer JSON estructurado (con revisión humana obligatoria antes de mergear — los protocolos no tienen un formato de tabla único, así que la extracción declara su propio nivel de confianza).

21–22 de agosto — Armonización de EMA y migración a Cloudflare D1 (v4)

Sesión de alcance ampliado: unificar los tres proyectos sobre la misma infraestructura de Cloudflare.

  • Cloudflare Pages para EMA: dashboard movido de docs/index.html (convención GitHub Pages) a la raíz del repo (Cloudflare Pages no tiene esa restricción); publicado en emas.lemeit.ar. El deploy es manual vía wrangler pages deploy — a diferencia de index.html/api.html, no hay deploy automático por push en este proyecto.
  • Migración Supabase → Cloudflare D1: se exportó el historial completo (30.213 filas de las 4 tablas) y se importó a una tabla unificada mediciones (columna estacion). Se descartaron 9 filas de EMA-DC por un timestamp con error de OCR confirmado como dato corrupto real.
  • Worker de compatibilidad (ema-saladillo-api): expone D1 con el mismo formato de consulta que usaba PostgREST/Supabase (select=, order=, limit=, columna=eq.valor) — el único cambio necesario en el dashboard fue apuntar SUPA_URL al Worker, sin tocar la lógica de consultas.
  • Scrapers actualizados: los 4 scripts pasan a escribir en D1 vía un helper común (scrapers/d1_writer.py), mismo patrón que ya usaba purpleair-saladillo. Se detectaron y corrigieron 3.796 filas duplicadas en el historial migrado (el ignore-duplicates de Supabase nunca había funcionado realmente, porque las tablas viejas no tenían restricción única) y un 403 Forbidden causado por saltos de línea de sobra en los secrets de Cloudflare al pegarlos en GitHub (%0A en la URL de la API) — resuelto con .strip().
  • El workflow de scrapers se había roto en una sesión anterior de "armonización de repositorios" (el archivo terminó en github/workflows/, sin el punto) — restaurado a su ruta correcta.

23–28 de agosto — Paridad visual, AirGradient, mapas

  • Header y tabs homogeneizados entre los tres portales; footer compartido con logos de proveedor/institución.
  • AirGradient se suma como segundo proveedor de sensores en Aire Saladillo (además de PurpleAir), reutilizando las mismas tablas D1 (columna proveedor). Se agrega CO2 y luego NOx al dashboard.
  • Proxy de tiles CARTO: CARTO empezó a exigir API key para servir tiles de mapa; en vez de exponerla en el HTML público, los tres Workers pasan a actuar de proxy, agregando la key del lado del servidor (CARTO_API_KEY como secret de Wrangler) — patrón replicado igual en los tres portales el mismo día (28/08).
  • Modo claro por defecto en los tres portales (antes dependía del tema del sistema operativo).
  • Fix de overflow-x (el mapa Leaflet arrastraba toda la página al costado al hacer pan) — aplicado a los tres portales.

29–31 de agosto — API pública, fixes y esta wiki

  • Fix: scrapers/supabase_ping.py (EMA) tenía un NameError — SUPA_URL/SUPA_KEY solo existían como valores anidados dentro del diccionario PROYECTOS, nunca como nombres a nivel de módulo, lo que hacía fallar el job ping de GitHub Actions apenas arrancaba.
  • Aire Saladillo: Mapa pasa a ser la pestaña por defecto (antes era Actuales); fix de un bug de apilamiento donde el dropdown "Portales" quedaba parcialmente tapado por el mapa Leaflet (los paneles internos de Leaflet tienen z-index de hasta 650–700 y, sin un contexto de apilamiento propio en #mapa-container, competían directamente contra el header) — resuelto dándole al contenedor del mapa su propio position: relative; z-index: 1.
  • API pública en Aire Saladillo y EMA Saladillo: los endpoints de lectura que ya alimentaban cada dashboard (/api/sensores, /api/ultimas, /api/historico en aq; /rest/v1/mediciones_* y las vistas armonizadas en ema) se documentaron y ampliaron para uso de terceros — sin necesidad de infraestructura nueva, ya eran de lectura pública con CORS abierto:
    • desde/hasta como rango de fechas absoluto en UTC, alternativa a la ventana relativa (range/horas) que ya existía.
    • &formato=csv en todos los endpoints de lectura, para descargar CSV directo sin parsear JSON.
    • Página de documentación (api.html) publicada en cada portal — aq.lemeit.ar/api.html y emas.lemeit.ar/api.html.
  • Corrección de dominio: se detectó y corrigió una referencia equivocada a ema.lemeit.ar (dominio inexistente) en vez del dominio real emas.lemeit.ar, introducida en la documentación nueva de la API de EMA.
  • Esta wiki: se arma ambiental-wiki, siguiendo el mismo patrón que ya usa DVBA (wiki-src/ con MkDocs Material, build automático vía GitHub Actions), para tener documentación técnica y bitácora unificadas de los tres portales en un solo lugar — hasta acá solo existía para EMA, como 4 documentos Word sueltos (bitacora_ema_saladillo_v1..v4.docx, migrados acá).

Septiembre 2026 — Admin de visitas, enlaces a la wiki, PWA de Aire Saladillo, rediseño de Historial/Gráfico y tema retro de la wiki

  • Administración de visitas (Aire Saladillo): nueva tabla visitas en D1 (timestamp, ruta, país vía request.cf, user-agent/referrer), escrita en segundo plano vía ctx.waitUntil para no demorar la respuesta real. Dos endpoints de solo lectura (/api/admin/visitas, /api/admin/resumen) protegidos por un secret ADMIN_KEY enviado como header X-Admin-Key — mismo esquema que ya usaba X-Ingest-Key en /api/ingest-ahora — en vez de ?key=... en la URL, para no dejar la clave pegada en el historial del navegador ni en logs de acceso/proxies intermedios (como consecuencia, los endpoints admin ya no se pueden abrir pegando la URL en el navegador, hace falta curl o similar que pueda mandar headers). Se sumó scripts/ver-visitas.ps1 para consultarlas desde PowerShell sin pelearse con que ahí curl es un alias de Invoke-WebRequest con una sintaxis de headers distinta a la del curl real (hace falta curl.exe o Invoke-WebRequest -Headers @{...}).
  • Enlaces cruzados a la wiki: el footer compartido (lemeit-common.js, un solo cambio actualiza los tres portales) y la sección "Acerca de" de cada uno pasan a enlazar a wiki.lemeit.ar — hasta ahora la wiki no se mencionaba desde ningún portal.
    • De paso se investigó por qué la guía de DVBA "se abre como una app" en vez de como página web: la PWA instalada de DVBA tiene "scope": "./" en su manifest.json, que servido desde /DVBA/ en GitHub Pages cubre toda la ruta — incluida /DVBA/wiki/ — así que la app instalada "adopta" también los links de esa wiki.
  • Historial (Aire Saladillo) — reporte configurable y export PDF: se agregó filtro de rango de fechas personalizado (desde/hasta, ya soportado por la API), filtro de parámetros por checkboxes y, más adelante en la misma tanda, columnas de PM1.0 y PM10 (ambos proveedores ya las reportaban, no se mostraban). Se sumó primero una vista "Reporte" en pantalla —toggle Clásica/Reporte, texto plano monoespaciado estilo resumen climatológico NOAA (ejemplo real visto en chajarialdia.com.ar/estacion), con fondo papel fijo independiente del tema claro/oscuro del resto del sitio— y luego exportación real a PDF con jsPDF: texto seleccionable en fuente Courier (no una captura de pantalla), con paginación automática y repetición de encabezados de columna en cada página nueva.
    • Fix: la carga de jsPDF desde cdnjs falló en el primer despliegue (probablemente un bloqueador de contenido del navegador) y tiraba una excepción sin manejar (window.jspdf indefinido) al hacer click en "Descargar PDF". Se agregó un script de respaldo a jsDelivr si cdnjs falla, y un aviso claro al usuario si ninguna de las dos CDN está disponible, en vez de romper la página.
    • Simplificación: la vista "Reporte" en pantalla, tras uso real, resultó redundante — el PDF ya se veía siempre en blanco/formato reporte, así que tener además un toggle Clásica/Reporte en pantalla (con el fondo invertido respecto de lo esperado) sumaba una opción de más. Se sacó el toggle: la tabla en pantalla usa siempre el formato normal del sitio, y el estilo reporte quedó reservado exclusivamente al PDF. Se agregó accent-color a los checkboxes del sitio (el azul por defecto del navegador desentonaba con la paleta), y se cambió "Estación" por "Sitio" en el PDF y demás lugares que nombran la ubicación de un sensor, para no chocar con "estación meteorológica" del proyecto hermano EMA. El PDF, además de descargarse, ahora también se abre en una pestaña nueva (blob URL, visor nativo del navegador) dentro del mismo click, para poder verlo sin ir a buscar el archivo descargado.
  • Gráfico (Aire Saladillo) — comparación de sitios: nuevo modo ("Comparar sitios") donde se elige un parámetro — no varios, para no mezclar unidades en el mismo eje — y varios sitios a la vez, cada uno dibujado como una línea de color distinto sobre el mismo eje Y. Los sitios no miden todos en el mismo instante exacto (cada uno hace su propio polling), así que el eje de tiempo se arma uniendo las marcas de todos los sitios elegidos, con spanGaps para no cortar las líneas en los huecos propios de cada uno — una limitación conocida, a mejorar a futuro con un agrupado por intervalos redondeados.
  • PWA instalable (Aire Saladillo): el dashboard pasa a poder instalarse como app — manifest.json, íconos (incluido uno "maskable" para Android, con el contenido confinado a la zona segura central ya que el sistema operativo le aplica su propia máscara/forma) y un service worker (sw.js) que cachea únicamente el "app shell" (HTML, manifest, íconos). Los datos en vivo (API propia) y las librerías de CDN (Chart.js, jsPDF, Leaflet, fuentes, design.lemeit.ar) se excluyen deliberadamente del cacheo y van siempre directo a red, para no quedar con lecturas ni versiones de librerías desactualizadas al abrir la app sin señal. Al abrirse instalada — detectado vía matchMedia("(display-mode: standalone)") o navigator.standalone (Safari/iOS, que no soporta bien esa media query) — o con ?pwa=1 en la URL (el propio start_url del manifest, útil también para probarlo a mano desde el navegador), arranca directo en la pestaña "Actuales": los relojes tipo velocímetro de AQI son la portada más útil en el celular, a diferencia del navegador de escritorio normal, que sigue abriendo en "Mapa". Mismo tratamiento planeado a futuro para EMA Saladillo.
  • Gráfico (Aire Saladillo) — zoom por selección: se sumó chartjs-plugin-zoom (+ hammerjs para gestos táctiles) al Gráfico, tanto en el histórico de un sitio como en "Comparar sitios": arrastre del mouse para zoom por selección ("box zoom"), rueda para acercar/alejar, pellizco táctil en celular, y un botón "Reset zoom" que aparece solo cuando hay zoom aplicado. Se cargan con el mismo patrón de doble CDN con respaldo (jsDelivr primero, cdnjs si el primero falla) ya usado para jsPDF.
    • Fix: en el primer despliegue, arrastrar para seleccionar terminaba haciendo zoom hacia el lado contrario de donde se arrastraba — y peor cuantos más zooms anidados (zoom sobre un zoom ya aplicado) se hacían seguidos. La causa: el paneo del gráfico (mover la vista arrastrando) y el zoom por selección estaban atados al mismo gesto — arrastre simple del mouse, sin modificador — y competían entre sí; el paneo corría el eje mientras se armaba el rectángulo de selección, así que el resultado final quedaba desplazado respecto de lo que realmente se había seleccionado. Se resolvió exigiendo Ctrl+arrastre para panear, dejando el arrastre simple exclusivamente para la selección de zoom. De paso se cambió el eje X de una escala de categorías (labels de texto ya formateados) a una escala numérica (linear, con los horarios mostrados igual que antes vía un callback de ticks), porque chartjs-plugin-zoom no soporta bien los zooms anidados sobre un eje de categorías — se verificó con una prueba de 3 zooms sucesivos que antes del fix terminaban en una franja incorrecta y después quedan correctamente anidados uno dentro del otro.
  • Roadmap documentado: expansión planeada a 4 sensores nuevos de aire (2 en Saladillo, 2 en el partido de 25 de Mayo — una escuela urbana y una rural en cada partido), y la necesidad de sumar una estación EMA en 25 de Mayo para que la red meteorológica cubra la misma región. Esto implica revisar a futuro el nombre "EMA Saladillo" (la "S" deja de ser literal) — ver la sección Roadmap de Aire Saladillo y de EMA Saladillo.
  • Tema retro/minimalista para esta wiki: se reemplazó el tema Material genérico (teal, fuente del sistema) por una piel propia (lemeit-retro.css) que reusa la paleta y tipografía de los tres dashboards — JetBrains Mono en todo, naranja #D9691F, fondos cálidos claro/oscuro —, con encabezados en mayúsculas, esquinas rectas y letra más chica. De paso se sacó el contador de estrellas/forks del header (ruido para una wiki chica, aunque el link al repo se conserva) y se reemplazó el ícono aislado de GitHub del footer por una línea de texto con el sitio, el stack usado (MkDocs Material, Cloudflare Pages, GitHub Actions) y el link al repo.

15 de septiembre — EMA-25C vía Weather Underground, rebrand a EMAS y renombre del repo

  • Quinta estación, EMA-25C (25 de Mayo): se suma 25Clima (25clima.ar, operada por un tercero — N-TecLab / SS Desarrollos, parte del medio digital 25 Digital), la primera estación de la red fuera del partido de Saladillo. No siendo una estación propia del proyecto, se integra vía la API pública de Weather Underground, donde 25Clima ya publica sus datos bajo el station ID IDEMAY14.
    • El obstáculo: Weather Underground solo habilita la generación de una API key de lectura si la cuenta tiene al menos un dispositivo propio "activo" (con datos reales recientes) — un dispositivo simplemente registrado, sin datos, no alcanza. Sin una estación física propia para registrar, se resolvió activando un dispositivo placeholder con datos reales de un sensor ya existente en la red (tools/subir_a_wu.py, sube lecturas de un sensor PurpleAir vía el protocolo clásico de upload de WU, convertidas a unidades imperiales). Es dato ambiental genuino, no un truco para eludir la restricción.
    • Nuevo scraper scrapers/wu_25demayo.py: consulta /v2/pws/observations/current de la API v2 de WU y mapea la respuesta a los 10 parámetros ya usados por el resto de las estaciones (Temperatura, Sensación Térmica, Punto de Rocío, Humedad, Presión, Velocidad Viento, Ráfaga, Dirección del Viento, Lluvia Diaria, Radiación Solar), guardando en D1 bajo EMA-25C. Sumado al cron horario existente (scrapers.yml).
    • Worker de compatibilidad: TABLE_MAP, ARMONIZADA_NOMBRE y CANON_MAP se extienden con mediciones_25c/EMA-25C/m25; se corrige además la regex de rutas (/^\/rest\/v1\/([a-z_]+)$/ → con 0-9), que no matcheaba mediciones_25c por tener un dígito.
    • Dashboard: la estación se integra en el selector, la comparativa (ahora 5 columnas/cards), el panel de detalle en "Estaciones", la tarjeta "Estaciones de la red" en "Acerca de" y el mapa — pero a propósito no en la interpolación espacial (ESP_FUENTES, mapa de calor): a ~80 km del resto del cluster, sumarla ahí daría un gradiente sin sentido geográfico.
    • Queda pendiente contactar al operador de 25Clima para confirmar su carácter permanente en el dashboard público — se decidió integrar igual mientras tanto, dejándolo explícito en el texto público como estación de terceros "en incorporación".
  • Rebrand: "EMA Saladillo" → "EMAS": con una estación fuera de Saladillo, la "S" del nombre deja de ser literal — pasa a leerse como el plural de "EMA" y el proyecto se presenta como regional. Se actualiza en index.html, api.html, README, el switcher de portales (lemeit-common.js) y esta wiki. De paso se saca la referencia a "Laboratorio de Industrias, 7° año Técnico Químico" del texto público del sitio (footer y "Acerca de") — el proyecto arrancó ahí pero ya no es un proyecto de esa materia en particular; se reemplaza por lenguaje genérico ("proyecto educativo") y, en la bio del autor, el mismo criterio que ya usaba Aire Saladillo ("Docente de Física y Química").
  • Renombre del repositorio: lemeit/ema-saladillo pasa a lemeit/emas en GitHub (rename manual del usuario, que conserva un redirect automático de GitHub para git y para la web) y la carpeta local homónima. Se actualizan los links propios al repo en index.html y en 3 archivos de esta wiki — se dejan sin tocar las menciones históricas de esta misma bitácora, que documentan el nombre tal como era en su momento.
  • Fix de documentación: la wiki documentaba aq.lemeit.ar/api/* como base URL de la API de Monitoreo Ambiental Escolar, pero esa ruta nunca estuvo conectada al Worker en Cloudflare Pages (sin [routes] ni _redirects) — confirmado pidiendo la URL directo y viendo que devolvía el index.html de la SPA en vez de JSON. Se corrige a la URL real del Worker (purpleair-saladillo-api.fisicai-eureka-01.workers.dev), que index.html/api.html ya usaban correctamente.
  • Bug propio, panel de EMA-25C perdido en silencio: el panel de detalle de la nueva estación en la pestaña "Estaciones" se armó en un script de edición que nunca llegó a guardar el archivo (terminaba sin el write final) — el script no tiraba error, así que el cambio se creyó aplicado durante buena parte de la sesión hasta que un pedido de revisión del usuario lo puso en evidencia. Recordatorio: verificar el resultado en disco, no solo que el script haya corrido sin errores.
  • PWA y navegación entre portales: cambiar de portal desde la app instalada (ej. de aq a emas) navega a un dominio distinto dentro de la misma ventana, y el sistema lo trata como "salida de la app instalada" (barra mínima con URL y una X para volver) — comportamiento estándar de Chromium/Windows para PWAs ante una navegación fuera de su scope, no un bug de un portal en particular. Se probó abrir el link con target="_blank" para evitarlo, pero eso hace que el cambio de portal abra una pestaña de navegador aparte, peor experiencia que quedarse "atrapado" con la barra de salida — se revirtió a la navegación original.
  • Logo de proveedor solo donde corresponde: el pie de página compartido (lm-footer) mostraba el logo de la última estación seleccionada incluso en pestañas generales sin relación a una estación puntual (ej. "Acerca de" mostrando el logo de Tecmes por defecto). Se simplifica: el pie compartido ya no muestra logos de proveedor en ninguna pestaña, y quedan exclusivamente en la pestaña "Estaciones" (panel de cada una) y en la tarjeta "Estaciones de la red" de "Acerca de".
  • Logo de 25Clima: sin logo propio disponible en un formato limpio, se usa un recorte del isologo tal como lo publica 25Digital (el medio del que 25Clima es un producto) en sus propias notas — estética más amateur que los logos de las otras 4 estaciones, pero es lo que hay, y es la atribución real a la fuente.

16 de septiembre — Mapas multi-partido, procedencia del Collective y materiales para la reunión del 16/9

  • Mapas centrados en ambos partidos: tanto en emas como en aq, el mapa arrancaba centrado en Saladillo con zoom fijo (13), dejando la estación/futuros sensores de 25 de Mayo fuera de vista por defecto — pensado para la reunión del 16/9, con gente de ambos partidos presente, se cambia a fitBounds de Leaflet. En emas, sobre las coordenadas fijas de las 5 estaciones; en aq, sobre el centroide de los sensores realmente desplegados más un punto de referencia en 25 de Mayo (mismo que usa EMA-25C), así el encuadre se sigue ajustando solo a medida que se sumen sensores reales ahí.
  • Fix de contexto de apilamiento en emas: el mapa Leaflet de la pestaña "Mapa" tapaba el desplegable de portales al abrirla — mismo bug que ya se había resuelto antes en aq (position:relative + z-index en el contenedor del mapa, para que los paneles internos de Leaflet no escapen de su propio contexto de apilamiento). Se aplica el mismo fix.
  • Logos de estaciones en emas, desproporcionados: al agregar logos reales para las 4 estaciones que todavía mostraban un emoji (TECMES, CFR, Defensa Civil, Clima Saladillo), se los limitó solo por ancho (max-width) — para logos cuadrados eso los renderizaba mucho más altos que el de 25Clima (un recorte horizontal), quedando desproporcionados entre sí. Se pasa a limitar por alto (max-height) con object-fit:contain.
  • Radiación solar faltante en histórico/gráfico: CFR, Clima Saladillo y EMA-25C sí reportan radiación solar (está en su params), pero faltaba en paramHistorial — el objeto que puebla los selectores de parámetro de Histórico y Gráfico — así que no aparecía como opción para esas 3 estaciones pese a tener el dato.
  • Procedencia de los 4 PurpleAir Flex: hasta ahora no figuraba en ningún lado (ni el README de purpleair-saladillo ni la presentación institucional) que esas unidades fueron una donación de PurpleAir Collective, convocatoria de mitad de 2024 en base a una propuesta de necesidad presentada por el autor — el proyecto quedó en 2° puesto de esa ronda. Se documenta en el README del repo y en la presentación (slide de antecedentes).
  • Sensor Clarity Node-S (EP N°15) fuera de servicio: dejó de enviar datos y debe reemplazarse. Es un sensor de un tercero, externo al proyecto (se muestra en el mapa institucional solo como referencia de contexto) — se actualiza su estado de "activo" a "fuera de servicio" en el mapa de la presentación (color e ícono distintos, y la referencia/leyenda correspondiente).
  • Materiales para la reunión del 16/9: se arma un slide de especificaciones técnicas (PurpleAir Flex y AirGradient Open Air, con fotos y una nota de adaptación al caso argentino sobre el cargador) para insertar en la presentación institucional, y una hoja de especificaciones + requerimientos aparte en Word, para entregar impresa al grupo. Ambas, junto con el plan de despliegue y la presentación, se guardan en docs/ del repo purpleair-saladillo (público), para poder linkearlas directamente desde la presentación en vez de solo adjuntarlas sueltas.
  • plan_despliegue.docx — vuelta a la versión imprimible: la versión con fotos (pensada como acompañamiento visual de la presentación) se descarta a favor de la versión original de texto y tablas, mejor para imprimir. Se actualiza el estado de las 5 escuelas (4 confirmadas y el Colegio Secundario Madre Teresa en proceso de reubicación, no la sede original) y el nombre completo de la EES N°2 "Manuel Pardal", para que coincida con el resto de los materiales de la reunión.
  • Fix del mapa institucional (v3): el recuadro del sensor Clarity fuera de servicio quedaba superpuesto sobre la línea "Colegio Secundario Madre Teresa (en reubicación)" del recuadro "Ciudad de Saladillo" — se reposiciona en un hueco libre del mapa (regenerado desde render_map_v3.py), sin tocar el resto de las 18 diapositivas. De paso se corrige el link de la hoja de especificaciones en el slide 6, apuntando ahora a raw.githubusercontent.com en vez de la vista de blob de GitHub (esta última no tiene visor propio para .docx: siempre cae en una pantalla intermedia con "View raw").
  • Renombre de repos para homogeneizar nombres: purpleair-saladillo → lemeit-aq y emas → lemeit-emas (rename manual del usuario en GitHub, mismo criterio de redirect automático que el rename anterior de ema-saladillo → emas). Se actualizan los links propios en el README.md de ambos repos y de agua-saladillo, en las fichas de proyecto de aboutme, y en esta wiki (01-aire-saladillo.md, 02-ema-saladillo.md, 03-agua-saladillo.md, index.md y el README.md del repo) — se dejan sin tocar, como siempre, las menciones históricas de esta misma bitácora. El Worker de la API (purpleair-saladillo-api...workers.dev) y la base D1 (purpleair-saladillo) quedan con su nombre actual: son recursos de Cloudflare independientes del nombre del repo, y renombrarlos cambiaría la URL en vivo de la API — queda como una decisión aparte, más delicada.
  • lemeit-aq — SQL suelto ordenado en d1/: schema.sql y los 7 migration_00X_*.sql vivían sueltos en la raíz del repo; se mueven a una carpeta d1/, mismo patrón que ya usa agua-saladillo (d1/schema.sql). Se actualizan las rutas en los comentarios de los propios archivos de migración (el comando wrangler d1 execute se corre parado en worker/, así que pasan a --file=../d1/migration_00X...sql) y las menciones en el README.md.
  • Tercer rename: agua-saladillo → lemeit-wq: mismo criterio que los dos anteriores, para que los cuatro repos de la red (lemeit-aq, lemeit-emas, lemeit-wq, lemeit-wiki) compartan el prefijo lemeit-. Se actualizan los links propios en lemeit-emas/README.md, lemeit-aq/README.md (mención genérica a los repos hermanos), la ficha de proyecto en aboutme y esta wiki (03-agua-saladillo.md, index.md, README.md y profe-lemeit/index.md) — de nuevo sin tocar las menciones históricas de esta bitácora ni los recursos de Cloudflare (agua-saladillo-api, la base D1 agua-saladillo).
  • api.lemeit.ar compartido entre proyectos, arranque en lemeit-aq: antes de que los 2 sensores nuevos empiecen a mandar datos, se decide un dominio único (api.lemeit.ar) para las tres APIs de la red ambiental (aq, emas, wq), con un prefijo de ruta por proyecto (/aq/, /emas/, /wq/) en vez de un dominio propio por API — usando Cloudflare Routes (no Custom Domains: una Route sí permite que varios Workers convivan en el mismo hostname por patrón de ruta, un Custom Domain ata todo el hostname a un solo Worker). Se arranca por etapas: hoy se libera api.lemeit.ar (ver punto siguiente, Eureka Tutor) y se suma la Route api.lemeit.ar/aq/* al Worker de lemeit-aq — aditivo, el *.workers.dev de siempre sigue respondiendo igual, sin corte. emas (Worker ya en producción) y wq (Worker a crear) quedan para una etapa posterior. El Worker despoja el prefijo /aq de la URL en la primera línea del fetch(), antes de cualquier matching de rutas existente, para no tocar el resto de la lógica.
  • Eureka Tutor cede api.lemeit.ar: era el único uso previo del dominio (Worker eureka-proxy, sin relación con la red ambiental) — se muda a api-eureka.lemeit.ar para liberar el dominio compartido de arriba. Cambio de código (WORKER_URL en index.html, comentario del allowlist de orígenes en src/worker.js) y de documentación (README.md, diagrama también replicado en esta wiki).

17 de septiembre — Un solo dominio para los 3 portales (app.lemeit.ar), colores de marca por sitio

  • Título corto en el header de Aire Saladillo para pantallas chicas: "Monitoreo Ambiental Escolar" se cortaba con "..." junto al logo y las luces de estado en el celular. Se agregan dos <span> dentro del mismo <h1> (título completo / "Aire Escolar", el mismo nombre corto que ya usa el switcher de portales) alternados por @media (max-width:640px), en vez de achicar la tipografía hasta que entre.
  • Logo AQ → AE, y color de marca por sitio: se elige reemplazar la sigla "AQ" (Air Quality, en inglés, inconsistente con el resto del sitio en español) por AE (Aire Escolar), y se le da un color propio en vez de heredar el naranja --accent de toda la UI — verde-azulado #009688, el mismo que ya usaban los badges de shields.io del README (.lm-chip-value). Se documenta como token de marca aparte en lemeit-theme.css (--lm-site-aq), separado de --lm-chip-value aunque hoy compartan valor, para no atar el significado de un componente de badges genérico a la identidad de un sitio puntual. Se regeneran favicon, apple-touch-icon y los 3 íconos de la PWA (192/512/maskable) en el color nuevo, y se actualiza el logo aq.svg de esta misma wiki.
  • EMAS también suma su propio color de marca: hasta ahora el favicon de EMAS (#5B8DEF, azul) no salía de ninguna paleta compartida — color suelto, distinto además del naranja --accent que sigue usando el chip del header. Se reemplaza por un celeste de la misma familia tonal que el verde de AQ (#0097A7, token --lm-site-emas en lemeit-theme.css), aplicado al chip del header, el favicon y el logo ema.svg de esta wiki.
  • Un solo dominio para los 3 portales (app.lemeit.ar): retomando lo anotado ayer (cambiar de portal desde la app instalada abre una barra de navegador en vez de quedarse "adentro" — comportamiento estándar de Chromium ante una navegación fuera del scope de la PWA, no un bug puntual), se arma un Worker nuevo (lemeit-gateway, repo gateway) que hace de reverse-proxy: app.lemeit.ar/aq/*, /emas/* y /wq/* sirven el contenido de cada Pages (purpleair-saladillo, ema-saladillo, agua-saladillo respectivamente, pedido directo a su *.pages.dev para no depender de los dominios que pasan a redirigir) bajo un solo origen. Mismo Worker, mismas 4 Routes de Cloudflare, también redirige (301) los 3 subdominios viejos (aq/emas/wq.lemeit.ar) hacia app.lemeit.ar — aq.lemeit.ar ya se había divulgado en una reunión, así que redirige desde el día 1; los otros dos se suman igual por consistencia. El switcher de portales (lemeit-common.js, SITES) pasa a apuntar a las URLs nuevas, y el manifest.json de Aire Saladillo amplía su scope de "./" (solo /aq/) a "/" (todo app.lemeit.ar) — con eso, cambiar de portal desde la app instalada queda contenido dentro de la misma ventana en vez de saltar al navegador. Migración escalonada para no cortar el sitio ya divulgado: Worker deployado y probado primero con los subdominios viejos todavía sirviendo directo, recién después se les sacó el Custom Domain de Pages uno por uno. Por ahora la app instalable sigue siendo una sola (Aire Escolar, la única con manifest.json/service worker) — EMAS y WQ todavía no tienen el suyo.
  • Header mobile de EMAS roto por el selector de estaciones: el selector de 5 estaciones + botón de comparativa vivía en la misma fila que el estado de conexión, el switcher de portales y el botón de tema — en pantallas chicas esa fila no entraba, y al no poder achicarse (flex-shrink:0) terminaba tapando o empujando fuera de la pantalla al resto de los controles. Se lo saca de esa fila y pasa a su propia fila completa debajo del header en mobile, con scroll horizontal — mismo criterio ya usado en el .nav de pestañas del propio sitio —, dejando la fila de arriba (logo, título, estado, switcher, tema) siempre visible y sin competir por espacio.
  • Sacar el Custom Domain de Pages borra el registro DNS, no solo el vínculo: al pasar emas.lemeit.ar y wq.lemeit.ar del Custom Domain de su proyecto de Pages a la Route del gateway, ambos quedaron sin resolver (NXDOMAIN, confirmado con nslookup contra 1.1.1.1 para descartar caché local) — a diferencia de aq.lemeit.ar, que por algún motivo conservó su registro. Cloudflare gestiona (crea y borra) el registro DNS de un Custom Domain de Pages como parte del propio vínculo: sacarlo no deja un registro "suelto" apuntando a otro lado, lo elimina directamente. Se resuelve igual que ya se había resuelto para api.lemeit.ar/app.lemeit.ar: un registro A dummy (IP no ruteable 192.0.2.1, Proxied) por cada subdominio afectado.
  • La API de EMAS nunca vivió en realidad en emas.lemeit.ar: al probar https://app.lemeit.ar/emas/rest/v1/v_temperatura_comparativa?horas=48 (la URL vieja, ya redirigida) se encontró que abre el dashboard en vez de devolver JSON. Investigando el porqué: el propio index.html del dashboard llama a la API por SUPA_URL = "https://ema-saladillo-api.fisicai-eureka-01.workers.dev" (el *.workers.dev directo) y api.html ya documentaba esa misma URL como Base URL — la única fuente que decía https://emas.lemeit.ar era esta wiki. Mismo caso ya visto con AQ (la wiki documentaba una ruta que nunca estuvo conectada al Worker real). Se resuelve de raíz igual que se hizo con AQ: se suma una Route de Cloudflare al Worker de EMAS (api.lemeit.ar/emas/*), con el prefijo /emas sacándose una sola vez al entrar; index.html y api.html pasan a usar esa URL nueva como base, y el *.workers.dev original sigue andando igual de respaldo.
  • Mismo problema en WQ, resuelto igual: revisando si el caso de EMAS se repetía, se confirma que sí — wq.lemeit.ar/api/coords tampoco sirvió nunca datos reales (el Worker de WQ tampoco tenía Route propia, y index.html ya llamaba directo a agua-saladillo-api...workers.dev). Se suma la Route api.lemeit.ar/wq/* al Worker de WQ, mismo patrón — con esto los tres portales ya tienen su API bajo api.lemeit.ar/<portal>.
  • Barrida de links cruzados a app.lemeit.ar: pedido explícito de revisar todos los links de los tres portales para que sean coherentes con la consolidación de dominio. Se actualizan los enlaces "Acerca de" de cada portal (a los otros dos hermanos), los api.html (link de atribución y Base URL), los README.md de los 4 repos (lemeit-aq, lemeit-emas, lemeit-wq, lemeit-design), las 3 fichas de proyecto de aboutme, y 03-agua-saladillo.md de esta wiki — de aq/emas/wq.lemeit.ar a app.lemeit.ar/aq, /emas, /wq. Los links viejos seguían funcionando (redirigen), esto es prolijidad — salvo el punto anterior (WQ), que sí era un fix real. lemeit-design de paso suma la sección de colores --lm-site-* al README.

Otros ajustes (sin fecha registrada)

  • Página "Fuentes, datos y licencias" (creditos.html) en Aire Escolar: nueva página que documenta de dónde salen el hardware y los datos de referencia del proyecto (PurpleAir, AirGradient, OpenAQ) y bajo qué licencia se usan cada uno, enlazada desde "Acerca de". Pensada tanto para cualquier visitante como para los propios proveedores (los sensores PurpleAir fueron una donación de PurpleAir Collective). Redactada a partir de las páginas públicas de cada proveedor.
  • Documentación de la API y "Fuentes, datos y licencias" pasan de páginas sueltas a pestañas del portal: el contenido de api.html y de creditos.html se integró a index.html como secciones más (?tab=api y ?tab=creditos). Los dos archivos quedaron como simples redirecciones (<meta http-equiv="refresh"> más location.replace, que conserva el #hash) para no romper los links externos que ya existían, entre ellos los de esta wiki. Los links a app.lemeit.ar/aq/api.html y creditos.html siguen funcionando, pero conviene apuntar directo a app.lemeit.ar/aq/?tab=api y ?tab=creditos. La viñeta anterior sobre creditos.html describe la página tal como era cuando todavía era un archivo aparte.
  • Bug real, universal: redirect de Cloudflare Pages perdía el prefijo de sección en el gateway: creditos.html daba 404 al abrirla desde app.lemeit.ar/aq/ pese a estar bien deployada — y resultó que api.html tenía el mismo problema en los tres portales, sin haberse notado antes. Causa: Cloudflare Pages redirige automáticamente /archivo.html → /archivo (sin extensión) con un header Location relativo a la raíz del propio proyecto de Pages, sin ninguna noción del prefijo /aq, /emas o /wq que agrega el Worker lemeit-gateway de por medio. El gateway devolvía ese Location tal cual, así que el navegador (que le había pegado a app.lemeit.ar) lo resolvía contra app.lemeit.ar directo, perdiendo el prefijo y cayendo en el 404 propio del gateway. Se arregla reescribiendo el Location en gateway/src/index.js cuando el upstream contesta un 3xx hacia su propio origen, volviendo a anteponerle el prefijo de sección.
  • Logos de proveedor en "Acerca de" de EMAS, desalineados en celular: el logo de EMA-25C (25Clima) es más ancho que los otros 4, y la caja que lo contiene (.about-est-logo) solo tenía la altura fija (height:22px, width:auto en la imagen) — así que esa fila quedaba más ancha que el resto y se notaba desalineada, sobre todo en pantallas chicas. Se le da a la caja un tamaño fijo (ancho y alto) para las 5 estaciones, con cada logo achicándose (object-fit:contain) para entrar sin recortarse.
  • De nuevo el DNS borrado al sacar el Custom Domain, esta vez en aq.lemeit.ar: mismo gotcha ya documentado para emas/wq (ver más arriba), pero con más demora — aq.lemeit.ar había conservado su registro DNS en el momento de sacarle el Custom Domain, y dejó de resolver (NXDOMAIN) recién más tarde. Mismo arreglo: registro A dummy (192.0.2.1, Proxied) agregado a mano en el DNS de lemeit.ar.

Octubre 2026 — Aire Escolar: primeros sensores en escuelas

5–6 de octubre — Código propio por sensor y alta de los dos primeros sensores nuevos

  • Código propio del proyecto (AE-NN): convención independiente del sensor_index de PurpleAir. AE-00 es el primer sensor de la red (anterior al proyecto) y AE-01 en adelante se asignan por orden de instalación. Se descartó un código con ciudad y escuela (tipo AE-25M-EEA-01): esos datos ya viven en la base (institución, coordenadas) y el código tiene que sobrevivir si un sensor cambia de escuela.
  • Migración 008 (d1/migration_008_codigo.sql): columna codigo en sensores con índice único (admite NULL). La fecha de alta usa fecha_instalacion, que existía desde el esquema original y nunca se había cargado. /api/sensores ya devolvía ambas por usar SELECT *; se amplió el SELECT de /api/ultimas con s.codigo y s.fecha_instalacion. La migración se ejecutó desde la consola web de D1 (sin acceso a wrangler esa noche), una sentencia por vez.
  • AE-01 y AE-02: dos PA-II-FLEX de los 4 donados por PurpleAir Collective, registrados en PurpleAir con las coordenadas de cada escuela: AE-01 (298687, Esc. de Educación Artística N°1 "Lola Mora", 25 de Mayo) y AE-02 (298701, EES N°2 "Manuel Pardal", Saladillo). Se agregaron a SENSOR_INDEXES y, tras un disparo de cron-job.org, aparecieron las dos filas con nombre y coordenadas correctas. Después se completaron codigo, fecha_instalacion, institucion y notas (CUE y clave provincial) con UPDATE desde la consola de D1, y el portal los mostró en el mapa con el nombre de la escuela en el título y el código debajo. La prueba se hizo con los sensores todavía en interior, antes de instalarlos.
  • AE-00: el sensor 198617 (Colegio Secundario Madre Teresa, hoy en reubicación) se renombró solo en D1, sin volver a registrarlo en PurpleAir: PurpleAir exige registrar de nuevo el sensor para cambiar valores, y eso le asignaría un sensor_index nuevo y cortaría la continuidad del historial. En PurpleAir conserva su nombre original; el Worker no lo pisa, porque el ON CONFLICT de upsertSensorMetadata solo actualiza latitud y longitud.
  • Aprendizaje (secret): SENSOR_INDEXES es un secret, y Cloudflare no permite leerlo ni agregarle un valor, solo reemplazarlo entero (en el dashboard: Rotate, y luego Deploy; Save version solo guarda sin activar). Hay que recuperar los índices ya activos con una consulta a sensores y pegar la lista completa.
  • Aprendizaje (WiFi): un sensor que ya fue configurado antes en una red se conecta a esa red y no abre su portal de configuración mientras la encuentre.
  • Decisión: se posterga el uso de los Data Processors de PurpleAir (reenvío de datos por POST al Worker, que bajaría el consumo de puntos de la API); por ahora sigue la ingesta por consulta con cron-job.org como único disparador.
  • Pendiente: borrar de lecturas las mediciones de prueba de AE-01 y AE-02 tomadas en interior, una vez que cada sensor reporte desde su escuela (los timestamps están en UTC); descargar el historial de AE-00 anterior a la ingesta (el posterior sale de /api/historico en CSV); mostrar codigo en index.html (hoy el portal muestra nombre); asignar AE-03 y AE-04.

28 de octubre de 2026 — AQ: AE-03, inventario y activación de versión

  • AE-03 — Instituto Niño Jesús: PurpleAir PA-II-FLEX, sensor_index = 298691, device ID fc:f5:c4:ae:ca:d6. El titular confirmó registro en D1 y visibilidad en el portal; un SELECT directo confirmó una lectura reciente, sin consultar la API pública que registra visitas.
  • Incidente y solución informados por el titular: Rotate de SENSOR_INDEXES creó una versión nueva del Worker, pero faltaba promoverla/desplegarla. Rotar por sí solo no activó esa versión. Se verificó después un deployment al 100 %, sin reconstruir valores de secretos ni acreditar su correspondencia con el HEAD.
  • Código propio completado: con autorización explícita del titular se asignó codigo = 'AE-03' únicamente al sensor 298691, que tenía el campo vacío. D1 informó una fila modificada y el SELECT posterior confirmó código, nombre e institución. No se cambiaron lecturas ni otros sensores. codigo es la etiqueta del proyecto; no reemplaza al sensor_index.
  • Inventario: AE-00 (198617, Madre Teresa, en reubicación según antecedente); AE-01 (298687, Lola Mora, instalación física pendiente según última información); AE-02 (298701, Manuel Pardal, operativo y con lectura reciente); AE-03 (298691, Niño Jesús); AE-04 reservado para Del Valle, sin índice asignado. D1 conserva AirGradient 900002 y 900003 sin código y con lecturas antiguas: no se acredita conexión actual.
  • Documentación versionada: PR #19 fusionado en lemeit-aq, commit 2b3032976e49c5e75f2f4153f6803c522af3ecff, con inventario al corte y procedimiento seguro y reversión. Ese corte precede a la asignación de código registrada aquí.
  • Alta segura: respaldar privadamente la lista completa autorizada antes de reemplazar el secreto; D1 ayuda a contrastarla, pero no permite reconstruir por sí sola la lista activa. Comprobar versión efectiva, ingesta y D1; API y dashboard generan visitas y requieren autorización bajo una restricción estricta de solo lectura. Nunca revelar secretos. El comportamiento depende de la herramienta: algunas operaciones de secretos despliegan inmediatamente; otras solo crean versión.
  • Alcance: no se rotaron secretos, ejecutaron ingestas ni desplegó el Worker en esta intervención. La única escritura productiva autorizada fue completar el código de AE-03. No se acredita cobertura automática AirGradient (AQ-05), migración 009 ni despliegue del HEAD. El dashboard actual muestra nombre; mostrar codigo directamente sigue pendiente.

Notas sobre las fuentes de esta bitácora

  • El detalle línea por línea de cada sesión (comandos exactos, mensajes de error completos, capturas) queda en el historial de commits de cada repositorio (git log) y, para EMA hasta agosto 2026, en los .docx originales dentro de ema-saladillo/docs/.
  • Esta página se actualiza manualmente después de cada sesión de trabajo relevante — no hay automatización todavía que la genere desde los commits.