269 lines
8.9 KiB
JavaScript
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();
|
|
}
|
|
});
|