initial commit
This commit is contained in:
268
hameter/web/static/app.js
Normal file
268
hameter/web/static/app.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/* 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user