G26-Telemetry-Software/index.html
2025-12-04 09:36:09 +01:00

254 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Telemetría IoT - Sesiones</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* { box-sizing: border-box; }
body { margin:0; font-family: Arial, sans-serif; background: linear-gradient(135deg,#667eea,#764ba2); min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px; }
#app { background:#fff; width:100%; max-width:980px; border-radius:16px; padding:28px; box-shadow:0 20px 60px rgba(0,0,0,.25); }
h1 { margin:0 0 8px; color:#333; text-align:center; }
#estado { text-align:center; font-weight:700; padding:10px; border-radius:8px; margin:8px 0 14px; }
.activa { color:#155724; background:#d4edda; }
.inactiva { color:#721c24; background:#f8d7da; }
#peso { text-align:center; font-size:3.2rem; font-weight:800; color:#007bff; margin:6px 0 16px; letter-spacing:-1px; }
.grid { display:grid; grid-template-columns: repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin-bottom:16px; }
.card { background:#f7f7fb; border-radius:10px; padding:12px; text-align:center; }
.label { font-size:.85rem; color:#6c757d; }
.value { font-size:1.4rem; font-weight:700; color:#333; }
.controls { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-top:8px; }
button { background:#007bff; color:#fff; border:0; padding:12px 18px; border-radius:8px; font-weight:700; cursor:pointer; transition:.2s; }
button.stop { background:#dc3545; }
button.download { background:#28a745; }
button:hover { filter:brightness(.95); transform: translateY(-1px); }
#log { margin-top:16px; background:#f8f9fa; padding:12px; border-radius:8px; max-height:260px; overflow:auto; font-family: Consolas, monospace; font-size:.9rem; color:#495057; }
#conn { text-align:center; margin-top:6px; font-size:.9rem; color:#6c757d; }
</style>
</head>
<body>
<div id="app">
<h1>🔬 Telemetría IoT</h1>
<div id="estado" class="inactiva">❌ Sesión Inactiva</div>
<div id="peso">--- kg</div>
<div id="conn">Socket: desconocido</div>
<div class="grid">
<div class="card"><div class="label">Ventanas recibidas</div><div class="value" id="ventanas-count">0</div></div>
<div class="card"><div class="label">Muestras totales</div><div class="value" id="muestras-count">0</div></div>
<div class="card"><div class="label">Última actualización</div><div class="value" id="ultima-actualizacion">---</div></div>
</div>
<div class="controls">
<button onclick="iniciarSesion()">▶️ Iniciar sesión</button>
<button class="stop" onclick="finalizarSesion()">⏸️ Finalizar sesión</button>
<button class="download" onclick="descargarCSV()">📥 Descargar CSV</button>
</div>
<div id="log"></div>
</div>
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-database.js"></script>
<script>
const firebaseConfig = {
apiKey: "AIzaSyB0JaH3ZPXdj-fw2LmKq1DGCEjriJ8hmgc",
authDomain: "iot-formula-gades.firebaseapp.com",
databaseURL: "https://iot-formula-gades-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "iot-formula-gades",
storageBucket: "iot-formula-gades.firebasestorage.app",
messagingSenderId: "930129619937",
appId: "1:930129619937:web:473b802794abe593da40a0",
measurementId: "G-CWXQWQQE3R"
};
firebase.initializeApp(firebaseConfig);
const db = firebase.database();
const controlRef = db.ref('/control/sesion_activa');
const sesionesRef = db.ref('/sesiones');
const infoConnRef = db.ref('.info/connected'); // [web:263]
const estadoEl = document.getElementById('estado');
const connEl = document.getElementById('conn');
const pesoEl = document.getElementById('peso');
const ventanasCountEl = document.getElementById('ventanas-count');
const muestrasCountEl = document.getElementById('muestras-count');
const ultimaActualizacionEl = document.getElementById('ultima-actualizacion');
const logEl = document.getElementById('log');
let ultimaSesionID = null; // clave actual de sesión
let windowsRef = null; // referencia sin query
let windowsQuery = null; // query activa
let lastWindowId = null;
let prevLogId = null;
let prevSampleTs = null;
let ventanasRecibidas = 0;
let muestrasTotales = 0;
let todasLasMuestras = [];
function log(msg) {
const line = document.createElement('div');
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logEl.prepend(line);
while (logEl.children.length > 300) logEl.removeChild(logEl.lastChild);
}
infoConnRef.on('value', s => {
connEl.textContent = s.val() ? 'Socket: conectado' : 'Socket: desconectado';
});
function iniciarSesion() { controlRef.set(true); log('▶️ Iniciar sesión'); }
function finalizarSesion(){ controlRef.set(false); log('⏸️ Finalizar sesión'); }
function iniciarSesionUI() {
estadoEl.className = 'activa';
estadoEl.textContent = '✅ Sesión Activa';
pesoEl.style.color = '#28a745';
actualizarMetricas();
}
function finalizarSesionUI() {
estadoEl.className = 'inactiva';
estadoEl.textContent = '❌ Sesión Inactiva';
pesoEl.style.color = '#6c757d';
pesoEl.textContent = '--- kg';
actualizarMetricas();
}
function actualizarMetricas() {
ventanasCountEl.textContent = ventanasRecibidas;
muestrasCountEl.textContent = muestrasTotales;
ultimaActualizacionEl.textContent = new Date().toLocaleTimeString();
}
function resetEstadoSesion() {
ventanasRecibidas = 0; muestrasTotales = 0; todasLasMuestras = [];
lastWindowId = null; prevLogId = null; prevSampleTs = null;
actualizarMetricas();
}
// UI de sesión (no afecta a tracking)
controlRef.on('value', snap => {
const activo = !!snap.val();
if (activo) iniciarSesionUI(); else finalizarSesionUI();
});
// 1) Al cargar, engancharse a la última
sesionesRef.orderByKey().limitToLast(1).on('value', snap => {
let key = null; snap.forEach(ch => key = ch.key);
if (!key) return;
const keyNum = Number(key);
const curNum = ultimaSesionID ? Number(ultimaSesionID) : -1;
if (keyNum > curNum) attachToSession(key);
});
// 2) Y además detectar nuevas que aparezcan después
sesionesRef.on('child_added', snap => {
const key = snap.key;
const keyNum = Number(key);
const curNum = ultimaSesionID ? Number(ultimaSesionID) : -1;
if (keyNum > curNum) attachToSession(key);
});
function detachWindows() {
if (windowsQuery) { windowsQuery.off(); windowsQuery = null; }
if (windowsRef) { windowsRef.off(); windowsRef = null; }
resetEstadoSesion();
}
// Adjunta una sesión: hace un prefetch inicial y luego activa child_added
function attachToSession(sid) {
// Detach y reset
detachWindows();
ultimaSesionID = sid;
log(`📂 Sesión activa: ${sid}`);
windowsRef = db.ref(`/sesiones/${sid}/windows`);
// Prefetch: traer lo que ya exista ahora mismo (por si se escribió antes de adjuntar)
windowsRef.orderByKey().once('value', (snap) => {
const batch = [];
snap.forEach(ch => {
batch.push({ id: Number(ch.key), v: ch.val() });
});
batch.sort((a,b) => a.id - b.id);
for (const it of batch) {
if (lastWindowId === null || it.id > lastWindowId) {
processVentana(it.v, it.id);
}
}
// Ahora activar child_added para lo que venga a partir de aquí
windowsQuery = windowsRef.orderByKey().startAt(String((lastWindowId || 0) + 1)); // [web:263]
windowsQuery.on('child_added', (snap2) => {
const id = Number(snap2.key);
if (Number.isFinite(lastWindowId) && id <= lastWindowId) return;
processVentana(snap2.val(), id);
});
});
}
function processVentana(w, id) {
if (!w || !Array.isArray(w.muestras)) return;
if (prevLogId !== null && id > prevLogId + 1) {
log(`⚠️ Faltan ${id - prevLogId - 1} ventanas entre ${prevLogId} y ${id}`);
}
prevLogId = id;
const t0 = typeof w.t0_ms === 'number' ? w.t0_ms : 0;
const dt = typeof w.dt_ms === 'number' ? w.dt_ms : 500;
const numMuestras = w.muestras.length;
w.muestras.forEach((m, i) => {
const ts_ms = (typeof m.ts_ms === 'number') ? m.ts_ms : (t0 + i*dt);
const iso = new Date(ts_ms).toISOString();
const peso = (typeof m.peso === 'number') ? m.peso : parseFloat(m.peso);
todasLasMuestras.push([iso, ts_ms, id, peso]);
if (prevSampleTs !== null && ts_ms - prevSampleTs > 2000) {
const gap = ((ts_ms - prevSampleTs)/1000).toFixed(1);
log(`⚠️ Hueco temporal de ${gap}s antes de ventana ${id}`);
}
prevSampleTs = ts_ms;
});
// UI
const last = w.muestras[numMuestras - 1];
if (last && last.peso != null) {
const v = parseFloat(last.peso);
if (!Number.isNaN(v)) pesoEl.textContent = v.toFixed(3) + ' kg';
}
ventanasRecibidas++;
muestrasTotales = todasLasMuestras.length;
actualizarMetricas();
const lastTs = (typeof last?.ts_ms === 'number') ? last.ts_ms : (t0 + (numMuestras-1)*dt);
log(`📦 Ventana ${id} (+${numMuestras} muestras) @ ${new Date(lastTs).toISOString()}`);
lastWindowId = id;
}
function descargarCSV() {
if (!ultimaSesionID) { alert('⚠️ No hay sesión.'); return; }
if (todasLasMuestras.length === 0) { alert('⚠️ Sin datos.'); return; }
todasLasMuestras.sort((a,b) => a[1] - b[1]);
const BOM = "\uFEFF";
let csv = "sep=,\r\n";
csv += "Timestamp,Timestamp_ms,Window_ID,Peso_kg\r\n";
for (const r of todasLasMuestras) {
csv += [r[0], String(r[1]), String(r[2]), String(r[3])].join(',') + "\r\n";
}
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `sesion_${ultimaSesionID}.csv`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
log(`📥 CSV exportado (${todasLasMuestras.length} filas)`);
}
window.iniciarSesion = iniciarSesion;
window.finalizarSesion = finalizarSesion;
window.descargarCSV = descargarCSV;
</script>
</body>
</html>