/* ═══════════════════════════════════════════════════════════════ Smart Mirror — Dashboard JS Handles: clock, polling, chart, sensor cards, log, alerts ═══════════════════════════════════════════════════════════════ */ 'use strict'; /* ── Config injected by Flask via data-attribute ───────────────── */ const ROOT = document.getElementById('mirror-root'); const POLL_MS = parseInt(ROOT.dataset.pollMs || '800', 10); const SENSORS = JSON.parse(ROOT.dataset.sensors || '[]'); /* ── State ─────────────────────────────────────────────────────── */ let lastLogCount = 0; let alertsSeen = 0; let chartHistory = []; // first numeric channel only /* ══════════════════════════════════════════════════════════════════ CLOCK ══════════════════════════════════════════════════════════════════ */ function tickClock() { const now = new Date(); const hh = String(now.getHours()).padStart(2,'0'); const mm = String(now.getMinutes()).padStart(2,'0'); const ss = String(now.getSeconds()).padStart(2,'0'); const days = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag']; const months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; const dateStr = `${days[now.getDay()]}, ${now.getDate()}. ${months[now.getMonth()]} ${now.getFullYear()}`; document.getElementById('clock-time').textContent = `${hh}:${mm}:${ss}`; document.getElementById('clock-date').textContent = dateStr; } setInterval(tickClock, 1000); tickClock(); /* ══════════════════════════════════════════════════════════════════ CANVAS CHART ══════════════════════════════════════════════════════════════════ */ const canvas = document.getElementById('chart'); const ctx = canvas.getContext('2d'); function resizeCanvas() { const wrap = canvas.parentElement; canvas.width = wrap.clientWidth - 40; canvas.height = 140; } window.addEventListener('resize', () => { resizeCanvas(); drawChart(); }); resizeCanvas(); function drawChart() { const W = canvas.width, H = canvas.height; ctx.clearRect(0, 0, W, H); if (chartHistory.length < 2) { ctx.fillStyle = 'rgba(232,244,255,.08)'; ctx.font = `11px 'Share Tech Mono', monospace`; ctx.fillText('Warte auf Daten …', 8, H / 2 + 4); return; } const pts = chartHistory.slice(-80); const min = Math.min(...pts); const max = Math.max(...pts); const range = max - min || 1; const step = W / (pts.length - 1); const yPos = v => H - ((v - min) / range) * H * 0.82 - H * 0.09; /* grid lines */ ctx.strokeStyle = 'rgba(255,255,255,.04)'; ctx.lineWidth = 1; ctx.font = `9px 'Share Tech Mono', monospace`; ctx.fillStyle = 'rgba(232,244,255,.2)'; for (let i = 0; i <= 4; i++) { const y = H - (i / 4) * H * 0.82 - H * 0.09; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); ctx.fillText((min + (i / 4) * range).toFixed(2), 4, y - 3); } /* gradient fill */ const grad = ctx.createLinearGradient(0, 0, 0, H); grad.addColorStop(0, 'rgba(168,216,240,.18)'); grad.addColorStop(1, 'rgba(168,216,240,.00)'); ctx.beginPath(); pts.forEach((v, i) => { const x = i * step, y = yPos(v); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.lineTo((pts.length - 1) * step, H); ctx.lineTo(0, H); ctx.closePath(); ctx.fillStyle = grad; ctx.fill(); /* line */ ctx.beginPath(); pts.forEach((v, i) => { const x = i * step, y = yPos(v); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.strokeStyle = 'rgba(168,216,240,.85)'; ctx.lineWidth = 1.5; ctx.shadowColor = 'rgba(168,216,240,.4)'; ctx.shadowBlur = 8; ctx.stroke(); ctx.shadowBlur = 0; /* last point dot */ const lv = pts[pts.length - 1]; const lx = (pts.length - 1) * step; const ly = yPos(lv); ctx.beginPath(); ctx.arc(lx, ly, 3.5, 0, Math.PI * 2); ctx.fillStyle = '#a8d8f0'; ctx.shadowColor = '#a8d8f0'; ctx.shadowBlur = 12; ctx.fill(); ctx.shadowBlur = 0; } /* ══════════════════════════════════════════════════════════════════ SENSOR TILES ══════════════════════════════════════════════════════════════════ */ function updateSensorTiles(values) { SENSORS.forEach((s, i) => { const idx = s.field_index; if (idx >= values.length) return; const v = values[idx]; const tile = document.getElementById(`sc-${i}`); const valEl= document.getElementById(`sv-${i}`); if (!tile || !valEl) return; valEl.textContent = `${v.toFixed(2)} ${s.unit || ''}`; // Clear old badges tile.querySelectorAll('.sensor-alert-badge').forEach(b => b.remove()); tile.classList.remove('breach-high', 'breach-low'); const isHigh = s.notify_on_high && s.threshold_high != null && v > s.threshold_high; const isLow = s.notify_on_low && s.threshold_low != null && v < s.threshold_low; if (isHigh) { tile.classList.add('breach-high'); const b = document.createElement('span'); b.className = 'sensor-alert-badge'; b.textContent = '↑ Grenzwert überschritten'; tile.appendChild(b); } else if (isLow) { tile.classList.add('breach-low'); const b = document.createElement('span'); b.className = 'sensor-alert-badge low'; b.textContent = '↓ Grenzwert unterschritten'; tile.appendChild(b); } }); } /* ══════════════════════════════════════════════════════════════════ TABS ══════════════════════════════════════════════════════════════════ */ function switchTab(name) { document.querySelectorAll('.tab').forEach(t => { t.classList.toggle('active', t.dataset.tab === name); }); document.querySelectorAll('.tab-pane').forEach(p => { p.classList.toggle('active', p.id === `pane-${name}`); }); if (name === 'alerts') { const badge = document.getElementById('alert-badge'); badge.classList.remove('visible'); badge.textContent = ''; } } /* ══════════════════════════════════════════════════════════════════ MAIN POLL ══════════════════════════════════════════════════════════════════ */ async function poll() { try { const [dataRes, alertRes] = await Promise.all([ fetch('/api/data'), fetch('/api/alerts'), ]); const data = await dataRes.json(); const alerts = await alertRes.json(); /* ── Connection badge ── */ const badge = document.getElementById('conn-badge'); badge.className = `conn-badge glass ${data.status.connected ? 'ok' : 'err'}`; document.getElementById('conn-dot').className = 'conn-dot'; document.getElementById('conn-text').textContent = data.status.connected ? `${data.status.port} · ${data.status.baud} Bd` : 'Getrennt — warte …'; /* ── Stat cards ── */ document.getElementById('stat-total').textContent = data.status.total_lines; document.getElementById('stat-errors').textContent = data.status.errors; document.getElementById('stat-port').textContent = data.status.port; document.getElementById('stat-baud').textContent = `${data.status.baud} baud`; if (data.entries.length) { const latest = data.entries[data.entries.length - 1]; const disp = latest.values?.length ? latest.values[0].toFixed(3) : latest.raw.slice(0, 14); document.getElementById('stat-last').textContent = disp; document.getElementById('stat-ts').textContent = latest.ts.split('T')[1]; if (latest.values?.length) updateSensorTiles(latest.values); } /* ── Log (new entries only, prepended) ── */ const newEntries = data.entries.slice(-(data.entries.length - lastLogCount)); lastLogCount = data.entries.length; const logPane = document.getElementById('pane-log'); newEntries.forEach(e => { if (e.values?.length) chartHistory.push(e.values[0]); const row = document.createElement('div'); row.className = 'log-entry'; const numStr = e.values?.map(v => v.toFixed(3)).join(' ') || ''; row.innerHTML = `${e.ts.split('T')[1]}` + `${e.raw}` + (numStr ? `${numStr}` : ''); logPane.prepend(row); }); /* ── Alerts ── */ if (alerts.length > alertsSeen) { const newAlerts = alerts.slice(alertsSeen); alertsSeen = alerts.length; const alertPane = document.getElementById('pane-alerts'); if (alertsSeen === newAlerts.length) alertPane.innerHTML = ''; newAlerts.forEach(a => { const row = document.createElement('div'); row.className = 'alert-entry'; row.innerHTML = `` + `${a.ts.split('T')[1]}` + `${a.subject}`; alertPane.prepend(row); }); const alertBadge = document.getElementById('alert-badge'); if (!document.getElementById('pane-alerts').classList.contains('active')) { alertBadge.textContent = newAlerts.length > 9 ? '9+' : newAlerts.length; alertBadge.classList.add('visible'); } } drawChart(); } catch (err) { console.warn('[Mirror] Poll error:', err); } setTimeout(poll, POLL_MS); } /* ── Kick off ─────────────────────────────────────────────────── */ poll();