const SensorDashboard = (function () { 'use strict'; const STORAGE_KEY = 'sensorView'; const MAX_SPARK_PTS = 30; // Map const devices = new Map(); // ---- Helpers ---- function esc(str) { return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function formatAge(timestamp) { if (!timestamp) return ''; const ts = typeof timestamp === 'string' ? new Date(timestamp).getTime() : Number(timestamp); const s = Math.floor((Date.now() - ts) / 1000); if (s < 10) return 'just now'; if (s < 60) return `${s}s ago`; return `${Math.floor(s / 60)}m ago`; } function isRecent(timestamp) { if (!timestamp) return false; const ts = typeof timestamp === 'string' ? new Date(timestamp).getTime() : Number(timestamp); return (Date.now() - ts) < 10000; } // ---- Primary value for sparkline ---- function getPrimary(msg) { if (msg.temperature !== undefined) return { value: msg.temperature, color: '#f59e0b' }; if (msg.pressure !== undefined) return { value: msg.pressure, color: '#a78bfa' }; if (msg.wind_speed !== undefined) return { value: msg.wind_speed, color: '#4aa3ff' }; return null; } function getFlashClass(msg) { return msg.temperature !== undefined ? 'sdb-card--flash-blue' : 'sdb-card--flash-purple'; } // ---- HTML builders ---- function buildReadingsHTML(msg) { // State-only device (no continuous numeric field) if (msg.state !== undefined && msg.temperature === undefined && msg.pressure === undefined && msg.wind_speed === undefined) { const raw = String(msg.state); const isOn = raw === '1' || raw === 'true' || raw === 'on' || raw === 'active'; return `
${esc(raw.toUpperCase())}
`; } const parts = []; if (msg.temperature !== undefined) parts.push({ val: msg.temperature, unit: `°${msg.temperature_unit || 'C'}`, label: 'Temp', color: '#f59e0b' }); if (msg.humidity !== undefined) parts.push({ val: msg.humidity, unit: '%', label: 'Humid', color: '#38bdf8' }); if (msg.pressure !== undefined) parts.push({ val: msg.pressure, unit: msg.pressure_unit || 'hPa', label: 'Press', color: '#a78bfa' }); if (msg.wind_speed !== undefined) parts.push({ val: msg.wind_speed, unit: msg.wind_unit || 'km/h', label: 'Wind', color: '#4aa3ff' }); if (msg.rain !== undefined) parts.push({ val: msg.rain, unit: msg.rain_unit || 'mm', label: 'Rain', color: '#38bdf8' }); if (parts.length === 0) return `
No numeric data
`; return parts.map(p => `
${esc(String(p.val))}
${esc(p.unit)}
${p.label}
`).join(''); } function buildSparklineHTML(history, color) { if (history.length < 2) return `
Collecting data…
`; const W = 120, H = 22, PAD = 2; const min = Math.min(...history); const max = Math.max(...history); const range = max - min || 1; const pts = history.map((v, i) => { const x = (i / (history.length - 1)) * (W - PAD * 2) + PAD; const y = H - PAD - ((v - min) / range) * (H - PAD * 2); return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); const last = pts.split(' ').pop().split(','); return ` `; } function buildCardHTML(msg, history, primaryColor) { const age = formatAge(msg.timestamp); const fresh = isRecent(msg.timestamp); const batLow = msg.battery === 'LOW'; const sparkHTML = history.length > 0 ? buildSparklineHTML(history, primaryColor || '#4aa3ff') : `
Waiting for data…
`; return `
${esc(msg.model || 'Unknown')}
ID ${esc(String(msg.id || 'N/A'))}${msg.channel ? ` · Ch ${esc(String(msg.channel))}` : ''}
${age}
${buildReadingsHTML(msg)}
${sparkHTML}
`; } // ---- Public: reading hook ---- function addReading(msg) { const key = `${msg.model || 'Unknown'}_${msg.id || msg.channel || '0'}`; const primary = getPrimary(msg); if (devices.has(key)) { const dev = devices.get(key); if (primary) { dev.history.push(primary.value); if (dev.history.length > MAX_SPARK_PTS) dev.history.shift(); dev.primaryColor = primary.color; } dev.card.innerHTML = buildCardHTML(msg, dev.history, dev.primaryColor); const cls = getFlashClass(msg); dev.card.classList.add(cls); setTimeout(() => dev.card.classList.remove(cls), 820); } else { const history = primary ? [primary.value] : []; const grid = document.getElementById('sensorDashboardGrid'); if (!grid) return; const card = document.createElement('div'); card.className = 'sdb-card sdb-card--new'; card.innerHTML = buildCardHTML(msg, history, primary ? primary.color : '#4aa3ff'); grid.insertBefore(card, grid.firstChild); setTimeout(() => card.classList.remove('sdb-card--new'), 2000); devices.set(key, { card, history, primaryColor: primary ? primary.color : '#4aa3ff' }); } } // ---- Show / hide / reset ---- function applyViewState(mode) { const view = document.getElementById('sensorDashboardView'); const output = document.getElementById('output'); if (mode === 'sensor') { const saved = localStorage.getItem(STORAGE_KEY) || 'dashboard'; const isDash = saved === 'dashboard'; if (view) view.style.display = isDash ? 'block' : 'none'; if (output) output.style.display = isDash ? 'none' : ''; _updateToggle(isDash); } else { if (view) view.style.display = 'none'; if (output) output.style.display = ''; } } function show() { localStorage.setItem(STORAGE_KEY, 'dashboard'); applyViewState('sensor'); } function hide() { localStorage.setItem(STORAGE_KEY, 'feed'); applyViewState('sensor'); } function _updateToggle(isDash) { document.getElementById('sensorToggleDash')?.classList.toggle('view-toggle-btn--active', isDash); document.getElementById('sensorToggleFeed')?.classList.toggle('view-toggle-btn--active', !isDash); } function reset() { devices.clear(); const grid = document.getElementById('sensorDashboardGrid'); if (grid) grid.innerHTML = ''; } return { addReading, show, hide, reset, applyViewState }; })();