Files
HAMeter/hameter/web/static/app.js
2026-03-06 12:25:27 -05:00

269 lines
8.9 KiB
JavaScript

/* 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();
}
});