File size: 6,674 Bytes
4ab46a7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | /* ============================================================
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)
? `<line x1="0" y1="${y(val).toFixed(1)}" x2="${w}" y2="${y(val).toFixed(1)}"
stroke="${col}" stroke-width="1" stroke-dasharray="3 3" opacity="0.5"/>` : "";
const marcas = anomalias.map((a, i) => a
? `<circle cx="${x(i).toFixed(1)}" cy="${y(valores[i]).toFixed(1)}" r="2.3"
fill="none" stroke="#E5484D" stroke-width="1.2"/>` : "").join("");
return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" role="img" aria-label="tendencia">
${linea(aviso, "#E0A83E")}${linea(critico, "#E5484D")}
<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.6"
stroke-linejoin="round" stroke-linecap="round"/>${marcas}</svg>`;
}
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 = `<p class="vacio">Ninguno. Condiciones aptas para observar.</p>`;
} else {
cont.innerHTML = data.incidencias.map((i) => {
const eta = i.eta_h ? `<span class="incidencia__eta">límite en ~${(+i.eta_h).toFixed(1)} h</span>` : "";
const etq = (i.sensor === "margen_rocio") ? "margen de rocío" : i.sensor.replace(/_/g, " ");
return `<article class="incidencia" data-nivel="${i.nivel}">
<div class="incidencia__cab">
<span class="incidencia__sensor">${etq}</span>
<span class="incidencia__valor">${(+i.valor).toFixed(2)} ${i.unidad}</span>
</div>${eta}
<p><b>Motivo:</b> ${i.causa}</p>
<p><b>Acción:</b> ${i.accion}</p>
</article>`;
}).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 `<div class="sensor" data-nivel="${d.nivel}">
<div class="sensor__top"><span class="sensor__nom">${d.etiqueta}</span></div>
<div class="sensor__val">${d.actual}<small> ${d.unidad}</small></div>
${sparkline(d.valores, d.anomalias, d.aviso, d.critico, color)}
</div>`;
}).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(() => {}));
}
|