Download static/app.js from JoseAndresLopez/monitor-condiciones: direct link, hf CLI and curl.
- Browser
- Download file 6.67 kB
-
https://huggingface.co/spaces/JoseAndresLopez/monitor-condiciones/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/JoseAndresLopez/monitor-condiciones/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/JoseAndresLopez/monitor-condiciones/resolve/main/static/app.js
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). | |
| ============================================================ */ | |
| ; | |
| 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(() => {})); | |
| } | |