/* ============================================================ Monitor de Condiciones de Observación — cliente Analiza los datos enteramente en el navegador (Pyodide + pandas/numpy), sin backend. Las mini-gráficas son SVG a mano (sin librerías). ============================================================ */ "use strict"; const ESTADO = { ok: { titulo: "Apto para observar", sub: "Condiciones dentro de límites." }, aviso: { titulo: "Precaución", sub: "Condiciones a vigilar de cerca." }, critico: { titulo: "No apto", sub: "Fuera de límites: valorar cerrar la cúpula." }, }; const ORDEN_SENSORES = ["humedad", "viento", "margen_rocio", "temp_ambiente", "punto_rocio", "presion"]; const $ = (s) => document.querySelector(s); function reloj() { return new Date().toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" }); } /* -------- Mini-gráfica SVG -------- */ function sparkline(valores, anomalias, aviso, critico, color) { const w = 160, h = 42, pad = 3; const min = Math.min(...valores), max = Math.max(...valores); const rango = (max - min) || 1; const x = (i) => pad + (i / (valores.length - 1)) * (w - 2 * pad); const y = (v) => h - pad - ((v - min) / rango) * (h - 2 * pad); const pts = valores.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" "); const linea = (val, col) => (val != null && val >= min && val <= max) ? `` : ""; const marcas = anomalias.map((a, i) => a ? `` : "").join(""); return ` ${linea(aviso, "#E0A83E")}${linea(critico, "#E5484D")} ${marcas}`; } function pintar(data) { document.body.dataset.nivel = data.nivel_global; const info = ESTADO[data.nivel_global]; $("#reloj").textContent = `${data.fuente} · ${reloj()}`; $("#estadoTitulo").textContent = info.titulo; $("#estadoSub").textContent = data.n_incidencias > 0 ? `${data.n_incidencias} aviso(s) · ${info.sub}` : info.sub; $("#fuenteNombre").textContent = data.fuente || ""; $("#nMuestras").textContent = data.n_muestras; $("#avisos").textContent = (data.avisos && data.avisos.length) ? data.avisos.join(" · ") : ""; // Avisos / incidencias const cont = $("#incidencias"); $("#contadorInc").textContent = data.n_incidencias; if (!data.incidencias.length) { cont.innerHTML = `

Ninguno. Condiciones aptas para observar.

`; } else { cont.innerHTML = data.incidencias.map((i) => { const eta = i.eta_h ? `límite en ~${(+i.eta_h).toFixed(1)} h` : ""; const etq = (i.sensor === "margen_rocio") ? "margen de rocío" : i.sensor.replace(/_/g, " "); return `
${etq} ${(+i.valor).toFixed(2)} ${i.unidad}
${eta}

Motivo: ${i.causa}

Acción: ${i.accion}

`; }).join(""); } // Sensores const sens = $("#sensores"); sens.innerHTML = ORDEN_SENSORES.filter((s) => data.sensores[s]).map((s) => { const d = data.sensores[s]; const color = d.nivel === "critico" ? "#E5484D" : d.nivel === "aviso" ? "#E0A83E" : "#3FB68B"; return `
${d.etiqueta}
${d.actual} ${d.unidad}
${sparkline(d.valores, d.anomalias, d.aviso, d.critico, color)}
`; }).join(""); $("#informe").textContent = data.informe; } /* -------- Motor de análisis en el navegador (Pyodide) -------- */ let pyodideListo = null; async function iniciarPyodide() { if (pyodideListo) return pyodideListo; pyodideListo = (async () => { const pyodide = await loadPyodide(); await pyodide.loadPackage(["numpy", "pandas"]); for (const nombre of ["conditions_core.py", "logic.py"]) { const texto = await (await fetch(`./${nombre}`)).text(); pyodide.FS.writeFile(nombre, texto); } await pyodide.runPythonAsync("import logic"); return pyodide; })(); return pyodideListo; } async function analizarTexto(csvTexto, nombre) { const pyodide = await iniciarPyodide(); pyodide.globals.set("_csv_texto", csvTexto); pyodide.globals.set("_nombre", nombre); const resultadoJson = nombre === null ? pyodide.runPython("logic.analizar_muestra(_csv_texto)") : pyodide.runPython("logic.analizar_subida(_csv_texto, _nombre)"); return JSON.parse(resultadoJson); } async function cargarMuestra() { marcarFuente("btnMuestra"); $("#estadoTitulo").textContent = "Cargando…"; try { const csvTexto = await (await fetch("./data/muestra_condiciones.csv")).text(); const data = await analizarTexto(csvTexto, null); if (data.error) throw new Error(data.error); pintar(data); } catch (e) { fallo(); } } async function subirCsv(file) { $("#estadoTitulo").textContent = "Analizando CSV…"; try { const csvTexto = await file.text(); const data = await analizarTexto(csvTexto, file.name); if (data.error) throw new Error(data.error); marcarFuente(null); pintar(data); } catch (e) { $("#estadoTitulo").textContent = "CSV no válido"; $("#estadoSub").textContent = e.message || "Revisa el fichero e inténtalo de nuevo."; } } function marcarFuente(id) { $("#btnMuestra").setAttribute("aria-pressed", id === "btnMuestra"); } function fallo() { $("#estadoTitulo").textContent = "Sin datos"; $("#estadoSub").textContent = "No se pudo cargar. Pulsa Actualizar."; } $("#btnMuestra").addEventListener("click", cargarMuestra); $("#btnActualizar").addEventListener("click", () => { const f = $("#fileCsv").files[0]; f ? subirCsv(f) : cargarMuestra(); }); $("#fileCsv").addEventListener("change", (e) => { if (e.target.files[0]) subirCsv(e.target.files[0]); }); cargarMuestra(); if ("serviceWorker" in navigator) { window.addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {})); }