JoseAndresLopez's picture
Subir aplicacion
4ab46a7 verified
Raw History Blame Contribute Delete
6.67 kB
/* ============================================================
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(() => {}));
}