/* HAMeter Web UI - JavaScript */ // ---------------------------------------------------------------- // SSE (Server-Sent Events) connection // ---------------------------------------------------------------- let eventSource = null; function connectSSE() { if (eventSource) { eventSource.close(); } eventSource = new EventSource('/api/events'); eventSource.addEventListener('status', function(e) { const data = JSON.parse(e.data); updateStatus(data); }); eventSource.addEventListener('readings', function(e) { const data = JSON.parse(e.data); updateReadings(data); }); eventSource.addEventListener('costs', function(e) { const data = JSON.parse(e.data); updateCosts(data); }); eventSource.addEventListener('discovery', function(e) { const data = JSON.parse(e.data); if (typeof window._onDiscoveryUpdate === 'function') { window._onDiscoveryUpdate(data); } }); eventSource.addEventListener('logs', function(e) { const data = JSON.parse(e.data); if (typeof window._onLogUpdate === 'function') { window._onLogUpdate(data); } }); eventSource.onerror = function() { // Auto-reconnect is built into EventSource updateStatusDot('error'); }; } // ---------------------------------------------------------------- // Status updates // ---------------------------------------------------------------- function updateStatus(data) { updateStatusDot(data.status); // Update sidebar status const statusText = document.querySelector('.status-text'); if (statusText) { const labels = { 'unconfigured': 'Not Configured', 'stopped': 'Stopped', 'starting': 'Starting...', 'running': 'Running', 'restarting': 'Restarting...', 'discovery': 'Discovery Mode', 'error': 'Error', }; statusText.textContent = labels[data.status] || data.status; } } function updateStatusDot(status) { const dot = document.querySelector('.status-dot'); if (dot) { dot.className = 'status-dot ' + status; } } // ---------------------------------------------------------------- // Readings updates (dashboard) // ---------------------------------------------------------------- function updateReadings(readings) { for (const [meterId, data] of Object.entries(readings)) { const readingEl = document.querySelector('#reading-' + meterId + ' .reading-value'); if (readingEl) { readingEl.textContent = formatNumber(data.calibrated_consumption); } const rawEl = document.getElementById('raw-' + meterId); if (rawEl) { rawEl.textContent = formatNumber(data.raw_consumption); } const lastSeenEl = document.getElementById('lastseen-' + meterId); if (lastSeenEl) { lastSeenEl.textContent = data.timestamp || '--'; } const countEl = document.getElementById('count-' + meterId); if (countEl) { countEl.textContent = data.count || 0; } // Update cost display if present const costEl = document.getElementById('cost-' + meterId); if (costEl && data.cumulative_cost !== undefined) { costEl.textContent = '$' + Number(data.cumulative_cost).toFixed(2); } } } // ---------------------------------------------------------------- // Cost updates (dashboard) // ---------------------------------------------------------------- function updateCosts(costs) { for (const [meterId, data] of Object.entries(costs)) { const costEl = document.getElementById('cost-' + meterId); if (costEl) { costEl.textContent = '$' + Number(data.cumulative_cost).toFixed(2); } const billingStartEl = document.getElementById('billing-start-' + meterId); if (billingStartEl) { billingStartEl.textContent = data.billing_period_start || '--'; } const fixedEl = document.getElementById('fixed-charges-' + meterId); if (fixedEl) { fixedEl.textContent = '$' + Number(data.fixed_charges_applied).toFixed(2); } } } function formatNumber(n) { if (n === null || n === undefined) return '--'; return Number(n).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 4, }); } // ---------------------------------------------------------------- // Toast notifications // ---------------------------------------------------------------- function showToast(message, type) { type = type || 'info'; const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = 'toast toast-' + type; toast.textContent = message; container.appendChild(toast); setTimeout(function() { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(function() { toast.remove(); }, 300); }, 4000); } // ---------------------------------------------------------------- // Restart banner // ---------------------------------------------------------------- function showRestartBanner() { const banner = document.getElementById('restart-banner'); if (banner) { banner.classList.remove('hidden'); } else { // Create one dynamically if not on a page with one showToast('Pipeline restart required to apply changes', 'info'); } } // ---------------------------------------------------------------- // Pipeline control // ---------------------------------------------------------------- function restartPipeline() { if (!confirm('Restart the pipeline? Meter monitoring will briefly pause.')) return; fetch('/api/pipeline/restart', { method: 'POST' }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.ok) { showToast('Pipeline restarting...', 'info'); // Hide restart banner if visible const banner = document.getElementById('restart-banner'); if (banner) banner.classList.add('hidden'); } }) .catch(function(e) { showToast('Failed to restart: ' + e.message, 'error'); }); } // ---------------------------------------------------------------- // Cost actions // ---------------------------------------------------------------- function resetBillingPeriod(meterId) { if (!confirm('Reset the billing period for this meter? Cost will be set to $0.00.')) return; fetch('/api/costs/' + meterId + '/reset', { method: 'POST' }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.ok) { showToast('Billing period reset', 'success'); var costEl = document.getElementById('cost-' + meterId); if (costEl) costEl.textContent = '$0.00'; } else { showToast(data.error || 'Reset failed', 'error'); } }) .catch(function(e) { showToast('Failed: ' + e.message, 'error'); }); } function addFixedCharges(meterId) { if (!confirm('Add fixed charges to this meter?')) return; fetch('/api/costs/' + meterId + '/add-fixed', { method: 'POST' }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.ok) { showToast('Added $' + Number(data.fixed_added).toFixed(2) + ' fixed charges', 'success'); var costEl = document.getElementById('cost-' + meterId); if (costEl) costEl.textContent = '$' + Number(data.cumulative_cost).toFixed(2); } else { showToast(data.error || 'Failed to add charges', 'error'); } }) .catch(function(e) { showToast('Failed: ' + e.message, 'error'); }); } // ---------------------------------------------------------------- // Sidebar toggle (mobile) // ---------------------------------------------------------------- function toggleSidebar() { const sidebar = document.getElementById('sidebar'); if (sidebar) { sidebar.classList.toggle('open'); } } // ---------------------------------------------------------------- // Password toggle // ---------------------------------------------------------------- function togglePassword(inputId) { const input = document.getElementById(inputId); if (!input) return; if (input.type === 'password') { input.type = 'text'; } else { input.type = 'password'; } } // ---------------------------------------------------------------- // Init // ---------------------------------------------------------------- document.addEventListener('DOMContentLoaded', function() { // Only connect SSE on pages with the sidebar (not setup page) if (document.querySelector('.sidebar')) { connectSSE(); } });