Adding the new rewrite
This commit is contained in:
1 parent
4aca9f08d1
commit
9b48b01d1d
62 files changed
+4510
-1406
No files matched your search
@@ -0,0 +1,80 @@
|
||||
// Alerts page (DOM-safe)
|
||||
(async function () {
|
||||
function cell(text, cls) {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = text;
|
||||
if (cls) td.className = cls;
|
||||
return td;
|
||||
}
|
||||
|
||||
async function loadUps() {
|
||||
const r = await window.apiFetch('/api/ups');
|
||||
if (!r.ok) return;
|
||||
const list = await r.json();
|
||||
const sel = document.getElementById('filter-ups');
|
||||
list.forEach(u => {
|
||||
const o = document.createElement('option');
|
||||
o.value = u.name; o.textContent = u.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAlerts() {
|
||||
const severity = document.getElementById('filter-severity').value;
|
||||
const ups = document.getElementById('filter-ups').value;
|
||||
const view = document.getElementById('filter-view').value;
|
||||
const path = view === 'active' ? '/api/alerts/active' : '/api/alerts';
|
||||
const params = new URLSearchParams();
|
||||
if (severity) params.set('severity', severity);
|
||||
if (ups) params.set('ups', ups);
|
||||
if (view === 'history') params.set('days', '30');
|
||||
const url = path + (params.toString() ? '?' + params.toString() : '');
|
||||
const r = await window.apiFetch(url);
|
||||
const tbody = document.getElementById('alerts-tbody');
|
||||
tbody.textContent = '';
|
||||
if (!r.ok) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(cell('Failed to load')); tbody.appendChild(tr); return;
|
||||
}
|
||||
const rows = await r.json();
|
||||
if (!rows.length) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(cell('No alerts')); tbody.appendChild(tr); return;
|
||||
}
|
||||
rows.forEach(a => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(cell(new Date(a.ts * 1000).toLocaleString()));
|
||||
const sevCell = document.createElement('td');
|
||||
const sevSpan = document.createElement('span');
|
||||
sevSpan.className = 'sev ' + a.severity;
|
||||
sevSpan.textContent = a.severity;
|
||||
sevCell.appendChild(sevSpan);
|
||||
tr.appendChild(sevCell);
|
||||
tr.appendChild(cell(a.ups));
|
||||
tr.appendChild(cell(a.code || ''));
|
||||
tr.appendChild(cell(a.message));
|
||||
tr.appendChild(cell(a.acked_ts ? new Date(a.acked_ts * 1000).toLocaleString() : ''));
|
||||
const actionCell = document.createElement('td');
|
||||
if (!a.acked_ts) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-secondary ack-btn';
|
||||
btn.textContent = 'Ack';
|
||||
btn.dataset.id = a.id;
|
||||
btn.addEventListener('click', async () => {
|
||||
await window.apiFetch('/api/alerts/' + btn.dataset.id + '/ack', { method: 'POST' });
|
||||
loadAlerts();
|
||||
});
|
||||
actionCell.appendChild(btn);
|
||||
}
|
||||
tr.appendChild(actionCell);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('refresh-btn').addEventListener('click', loadAlerts);
|
||||
['filter-severity', 'filter-ups', 'filter-view'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('change', loadAlerts);
|
||||
});
|
||||
await loadUps();
|
||||
await loadAlerts();
|
||||
})();
|
||||
@@ -0,0 +1,33 @@
|
||||
// Shared client-side helpers: CSRF, logout, fetch wrapper
|
||||
(function () {
|
||||
function getCsrfToken() {
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (meta && meta.content) return meta.content;
|
||||
const m = document.cookie.match(/(?:^|;)\s*csrf_token=([^;]+)/);
|
||||
return m ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
|
||||
window.apiFetch = async function (url, opts = {}) {
|
||||
const method = (opts.method || 'GET').toUpperCase();
|
||||
const headers = Object.assign({ Accept: 'application/json' }, opts.headers || {});
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
headers['X-CSRF-Token'] = getCsrfToken();
|
||||
if (opts.body && !(opts.body instanceof FormData) && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
const resp = await fetch(url, Object.assign({ credentials: 'same-origin' }, opts, { headers }));
|
||||
if (resp.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('unauthorized');
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
document.addEventListener('click', async (e) => {
|
||||
if (e.target && e.target.id === 'logout-btn') {
|
||||
await window.apiFetch('/api/logout', { method: 'POST' });
|
||||
window.location.href = '/login';
|
||||
}
|
||||
});
|
||||
})();
|
||||
+121
-297
@@ -1,174 +1,113 @@
|
||||
// Configuration management JavaScript
|
||||
// UPS Configuration page (uses apiFetch for CSRF)
|
||||
|
||||
function el(tag, attrs, ...children) {
|
||||
const e = document.createElement(tag);
|
||||
if (attrs) Object.entries(attrs).forEach(([k, v]) => {
|
||||
if (k === 'class') e.className = v;
|
||||
else if (k === 'dataset') Object.assign(e.dataset, v);
|
||||
else if (k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2), v);
|
||||
else if (v !== null && v !== undefined) e.setAttribute(k, v);
|
||||
});
|
||||
children.forEach(c => {
|
||||
if (c == null) return;
|
||||
e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||
});
|
||||
return e;
|
||||
}
|
||||
|
||||
class ConfigManager {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
constructor() { this.confirmCallback = null; this.init(); }
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadUPSList();
|
||||
}
|
||||
init() { this.bindEvents(); this.loadUPSList(); }
|
||||
|
||||
bindEvents() {
|
||||
// Add UPS button
|
||||
document.getElementById('add-ups-btn').addEventListener('click', () => {
|
||||
this.showUPSModal();
|
||||
});
|
||||
|
||||
// Modal close buttons
|
||||
document.getElementById('add-ups-btn').addEventListener('click', () => this.showUPSModal());
|
||||
document.querySelectorAll('.close').forEach(close => {
|
||||
close.addEventListener('click', (e) => {
|
||||
this.closeModal(e.target.closest('.modal'));
|
||||
});
|
||||
close.addEventListener('click', (e) => this.closeModal(e.target.closest('.modal')));
|
||||
});
|
||||
|
||||
// UPS form submission
|
||||
document.getElementById('ups-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveUPS();
|
||||
e.preventDefault(); this.saveUPS();
|
||||
});
|
||||
|
||||
// Test connection button
|
||||
document.getElementById('test-connection-btn').addEventListener('click', () => {
|
||||
this.testConnection();
|
||||
});
|
||||
|
||||
// Cancel button
|
||||
document.getElementById('test-connection-btn').addEventListener('click', () => this.testConnection());
|
||||
document.getElementById('cancel-btn').addEventListener('click', () => {
|
||||
this.closeModal(document.getElementById('ups-modal'));
|
||||
});
|
||||
|
||||
// Confirmation modal buttons
|
||||
document.getElementById('confirm-yes').addEventListener('click', () => {
|
||||
if (this.confirmCallback) {
|
||||
this.confirmCallback();
|
||||
}
|
||||
if (this.confirmCallback) this.confirmCallback();
|
||||
this.closeModal(document.getElementById('confirm-modal'));
|
||||
});
|
||||
|
||||
document.getElementById('confirm-no').addEventListener('click', () => {
|
||||
this.closeModal(document.getElementById('confirm-modal'));
|
||||
});
|
||||
|
||||
// Click outside modal to close
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal')) {
|
||||
this.closeModal(e.target);
|
||||
}
|
||||
if (e.target.classList.contains('modal')) this.closeModal(e.target);
|
||||
});
|
||||
}
|
||||
|
||||
async loadUPSList() {
|
||||
try {
|
||||
const response = await fetch('/api/config/ups');
|
||||
const upsList = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(upsList.detail || 'Failed to load UPS configurations');
|
||||
}
|
||||
|
||||
this.renderUPSList(upsList);
|
||||
} catch (error) {
|
||||
this.showToast('Error loading UPS configurations: ' + error.message, 'error');
|
||||
}
|
||||
const resp = await window.apiFetch('/api/config/ups');
|
||||
const list = await resp.json();
|
||||
if (!resp.ok) throw new Error(list.detail || 'Failed to load');
|
||||
this.renderUPSList(list);
|
||||
} catch (err) { this.showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
renderUPSList(upsList) {
|
||||
renderUPSList(list) {
|
||||
const container = document.getElementById('ups-list');
|
||||
|
||||
if (upsList.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No UPS Configured</h3>
|
||||
<p>Get started by clicking "Add UPS" above to configure your first UPS monitoring.</p>
|
||||
</div>
|
||||
`;
|
||||
container.textContent = '';
|
||||
if (list.length === 0) {
|
||||
const empty = el('div', { class: 'empty-state' },
|
||||
el('h3', null, 'No UPS Configured'),
|
||||
el('p', null, 'Click "Add UPS" to configure your first UPS.'));
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = upsList.map(ups => this.renderUPSItem(ups)).join('');
|
||||
|
||||
// Bind action buttons
|
||||
container.querySelectorAll('.edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const upsName = e.target.dataset.upsName;
|
||||
this.editUPS(upsName);
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const upsName = e.target.dataset.upsName;
|
||||
this.confirmDelete(upsName);
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.test-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const upsName = e.target.dataset.upsName;
|
||||
this.testUPSConnection(upsName);
|
||||
});
|
||||
});
|
||||
list.forEach(ups => container.appendChild(this.renderUPSItem(ups)));
|
||||
}
|
||||
|
||||
renderUPSItem(ups) {
|
||||
const alertsEnabled = [
|
||||
ups.alert_loadpct_high && `Load: ${ups.alert_loadpct_high}%`,
|
||||
ups.alert_bcharge_low && `Battery: ${ups.alert_bcharge_low}%`,
|
||||
ups.alert_on_battery && 'On Battery',
|
||||
ups.alert_runtime_low_minutes && `Runtime: ${ups.alert_runtime_low_minutes}min`
|
||||
].filter(Boolean);
|
||||
|
||||
return `
|
||||
<div class="ups-item">
|
||||
<div class="ups-item-header">
|
||||
<div class="ups-item-title">${ups.name}</div>
|
||||
<div class="ups-item-actions">
|
||||
<button class="btn btn-secondary test-btn" data-ups-name="${ups.name}">Test</button>
|
||||
<button class="btn btn-primary edit-btn" data-ups-name="${ups.name}">Edit</button>
|
||||
<button class="btn btn-danger delete-btn" data-ups-name="${ups.name}">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ups-item-details">
|
||||
<div class="ups-detail">
|
||||
<div class="ups-detail-label">Host</div>
|
||||
<div class="ups-detail-value">${ups.host}:${ups.port}</div>
|
||||
</div>
|
||||
<div class="ups-detail">
|
||||
<div class="ups-detail-label">Polling Interval</div>
|
||||
<div class="ups-detail-value">${ups.interval_seconds}s</div>
|
||||
</div>
|
||||
<div class="ups-detail">
|
||||
<div class="ups-detail-label">Alerts</div>
|
||||
<div class="ups-detail-value">${alertsEnabled.length > 0 ? alertsEnabled.join(', ') : 'None'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const alerts = [];
|
||||
if (ups.alert_loadpct_high) alerts.push('Load: ' + ups.alert_loadpct_high + '%');
|
||||
if (ups.alert_bcharge_low) alerts.push('Battery: ' + ups.alert_bcharge_low + '%');
|
||||
if (ups.alert_on_battery) alerts.push('On Battery');
|
||||
if (ups.alert_runtime_low_minutes) alerts.push('Runtime: ' + ups.alert_runtime_low_minutes + 'min');
|
||||
if (ups.alert_itemp_high) alerts.push('Temp: ' + ups.alert_itemp_high + '°C');
|
||||
const testBtn = el('button', { class: 'btn btn-secondary', onclick: () => this.testUPSConnection(ups.name) }, 'Test');
|
||||
const editBtn = el('button', { class: 'btn btn-primary', onclick: () => this.editUPS(ups.name) }, 'Edit');
|
||||
const delBtn = el('button', { class: 'btn btn-danger', onclick: () => this.confirmDelete(ups.name) }, 'Delete');
|
||||
const item = el('div', { class: 'ups-item' },
|
||||
el('div', { class: 'ups-item-header' },
|
||||
el('div', { class: 'ups-item-title' }, ups.name),
|
||||
el('div', { class: 'ups-item-actions' }, testBtn, editBtn, delBtn)),
|
||||
el('div', { class: 'ups-item-details' },
|
||||
el('div', { class: 'ups-detail' },
|
||||
el('div', { class: 'ups-detail-label' }, 'Host'),
|
||||
el('div', { class: 'ups-detail-value' }, ups.host + ':' + ups.port)),
|
||||
el('div', { class: 'ups-detail' },
|
||||
el('div', { class: 'ups-detail-label' }, 'Polling Interval'),
|
||||
el('div', { class: 'ups-detail-value' }, ups.interval_seconds + 's')),
|
||||
el('div', { class: 'ups-detail' },
|
||||
el('div', { class: 'ups-detail-label' }, 'Alerts'),
|
||||
el('div', { class: 'ups-detail-value' }, alerts.length ? alerts.join(', ') : 'None'))));
|
||||
return item;
|
||||
}
|
||||
|
||||
showUPSModal(ups = null) {
|
||||
const modal = document.getElementById('ups-modal');
|
||||
const title = document.getElementById('modal-title');
|
||||
const form = document.getElementById('ups-form');
|
||||
|
||||
// Reset form
|
||||
form.reset();
|
||||
|
||||
if (ups) {
|
||||
// Edit mode
|
||||
title.textContent = 'Edit UPS';
|
||||
this.populateForm(ups);
|
||||
form.dataset.mode = 'edit';
|
||||
form.dataset.upsName = ups.name;
|
||||
} else {
|
||||
// Add mode
|
||||
title.textContent = 'Add UPS';
|
||||
form.dataset.mode = 'add';
|
||||
delete form.dataset.upsName;
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -179,222 +118,107 @@ class ConfigManager {
|
||||
document.getElementById('ups-interval').value = ups.interval_seconds || 30;
|
||||
document.getElementById('ups-loadpct').value = ups.alert_loadpct_high || '';
|
||||
document.getElementById('ups-bcharge').value = ups.alert_bcharge_low || '';
|
||||
document.getElementById('ups-onbattery').checked = ups.alert_on_battery || false;
|
||||
document.getElementById('ups-onbattery').checked = !!ups.alert_on_battery;
|
||||
document.getElementById('ups-runtime').value = ups.alert_runtime_low_minutes || '';
|
||||
const itemp = document.getElementById('ups-itemp');
|
||||
if (itemp) itemp.value = ups.alert_itemp_high || '';
|
||||
}
|
||||
|
||||
closeModal(modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
closeModal(modal) { modal.style.display = 'none'; }
|
||||
|
||||
async saveUPS() {
|
||||
const form = document.getElementById('ups-form');
|
||||
const formData = new FormData(form);
|
||||
const fd = new FormData(form);
|
||||
const mode = form.dataset.mode;
|
||||
const upsName = form.dataset.upsName;
|
||||
|
||||
const name = form.dataset.upsName;
|
||||
const data = {
|
||||
name: formData.get('name'),
|
||||
host: formData.get('host'),
|
||||
port: parseInt(formData.get('port')) || 3551,
|
||||
interval_seconds: parseInt(formData.get('interval_seconds')) || 30,
|
||||
alert_on_battery: formData.has('alert_on_battery')
|
||||
name: fd.get('name'),
|
||||
host: fd.get('host'),
|
||||
port: parseInt(fd.get('port')) || 3551,
|
||||
interval_seconds: parseInt(fd.get('interval_seconds')) || 30,
|
||||
alert_on_battery: fd.has('alert_on_battery'),
|
||||
};
|
||||
|
||||
// Add optional alert thresholds
|
||||
const loadpct = formData.get('alert_loadpct_high');
|
||||
if (loadpct) data.alert_loadpct_high = parseFloat(loadpct);
|
||||
|
||||
const bcharge = formData.get('alert_bcharge_low');
|
||||
if (bcharge) data.alert_bcharge_low = parseFloat(bcharge);
|
||||
|
||||
const runtime = formData.get('alert_runtime_low_minutes');
|
||||
if (runtime) data.alert_runtime_low_minutes = parseFloat(runtime);
|
||||
|
||||
const num = (k) => { const v = fd.get(k); if (v) data[k] = parseFloat(v); };
|
||||
num('alert_loadpct_high'); num('alert_bcharge_low');
|
||||
num('alert_runtime_low_minutes'); num('alert_itemp_high');
|
||||
try {
|
||||
let response;
|
||||
if (mode === 'edit') {
|
||||
response = await fetch(`/api/config/ups/${upsName}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
response = await fetch('/api/config/ups', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.detail || 'Failed to save UPS configuration');
|
||||
}
|
||||
|
||||
const url = mode === 'edit' ? '/api/config/ups/' + name : '/api/config/ups';
|
||||
const method = mode === 'edit' ? 'PUT' : 'POST';
|
||||
const resp = await window.apiFetch(url, { method, body: JSON.stringify(data) });
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) throw new Error(result.detail || 'Save failed');
|
||||
this.showToast(result.message, 'success');
|
||||
this.closeModal(document.getElementById('ups-modal'));
|
||||
this.loadUPSList();
|
||||
} catch (error) {
|
||||
this.showToast('Error saving UPS: ' + error.message, 'error');
|
||||
}
|
||||
} catch (err) { this.showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async editUPS(upsName) {
|
||||
async editUPS(name) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/ups/${upsName}`);
|
||||
const ups = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(ups.detail || 'Failed to load UPS configuration');
|
||||
}
|
||||
|
||||
const resp = await window.apiFetch('/api/config/ups/' + name);
|
||||
const ups = await resp.json();
|
||||
if (!resp.ok) throw new Error(ups.detail || 'Load failed');
|
||||
this.showUPSModal(ups);
|
||||
} catch (error) {
|
||||
this.showToast('Error loading UPS configuration: ' + error.message, 'error');
|
||||
}
|
||||
} catch (err) { this.showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
confirmDelete(upsName) {
|
||||
const modal = document.getElementById('confirm-modal');
|
||||
const message = document.getElementById('confirm-message');
|
||||
|
||||
message.textContent = `Are you sure you want to delete the UPS configuration "${upsName}"? This action cannot be undone.`;
|
||||
|
||||
this.confirmCallback = () => this.deleteUPS(upsName);
|
||||
modal.style.display = 'block';
|
||||
confirmDelete(name) {
|
||||
document.getElementById('confirm-message').textContent =
|
||||
'Delete UPS "' + name + '"? This cannot be undone.';
|
||||
this.confirmCallback = () => this.deleteUPS(name);
|
||||
document.getElementById('confirm-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
async deleteUPS(upsName) {
|
||||
async deleteUPS(name) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/ups/${upsName}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.detail || 'Failed to delete UPS configuration');
|
||||
}
|
||||
|
||||
const resp = await window.apiFetch('/api/config/ups/' + name, { method: 'DELETE' });
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) throw new Error(result.detail || 'Delete failed');
|
||||
this.showToast(result.message, 'success');
|
||||
this.loadUPSList();
|
||||
} catch (error) {
|
||||
this.showToast('Error deleting UPS: ' + error.message, 'error');
|
||||
}
|
||||
} catch (err) { this.showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
const form = document.getElementById('ups-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const fd = new FormData(form);
|
||||
const data = {
|
||||
name: formData.get('name') || 'test',
|
||||
host: formData.get('host'),
|
||||
port: parseInt(formData.get('port')) || 3551,
|
||||
name: fd.get('name') || 'test',
|
||||
host: fd.get('host'),
|
||||
port: parseInt(fd.get('port')) || 3551,
|
||||
interval_seconds: 30,
|
||||
alert_on_battery: false
|
||||
alert_on_battery: false,
|
||||
};
|
||||
|
||||
if (!data.host) {
|
||||
this.showToast('Host is required for connection test', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const testBtn = document.getElementById('test-connection-btn');
|
||||
const originalText = testBtn.textContent;
|
||||
testBtn.textContent = 'Testing...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
if (!data.host) { this.showToast('Host required', 'error'); return; }
|
||||
const btn = document.getElementById('test-connection-btn');
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'Testing...'; btn.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/config/ups/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
const resp = await window.apiFetch('/api/config/ups/test', {
|
||||
method: 'POST', body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
let msg;
|
||||
if (result.success) {
|
||||
msg = 'Connection successful';
|
||||
if (result.data && result.data.STATUS) {
|
||||
msg += ` (STATUS=${result.data.STATUS})`;
|
||||
}
|
||||
this.showToast(msg, 'success');
|
||||
} else {
|
||||
if (result.connectivity && !result.connectivity.ok) {
|
||||
msg = 'TCP connectivity failed: ' + (result.connectivity.error || 'unknown error');
|
||||
} else if (result.protocol && !result.protocol.ok) {
|
||||
msg = 'Protocol error after TCP success: ' + (result.protocol.error || 'unknown error');
|
||||
} else {
|
||||
msg = result.message || 'Connection failed';
|
||||
}
|
||||
this.showToast(msg, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showToast('Connection test failed: ' + error.message, 'error');
|
||||
} finally {
|
||||
testBtn.textContent = originalText;
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
const r = await resp.json();
|
||||
this.showToast(r.message || (r.success ? 'OK' : 'Failed'),
|
||||
r.success ? 'success' : 'error');
|
||||
} catch (err) { this.showToast('Test failed: ' + err.message, 'error'); }
|
||||
finally { btn.textContent = orig; btn.disabled = false; }
|
||||
}
|
||||
|
||||
async testUPSConnection(upsName) {
|
||||
const testBtn = document.querySelector(`[data-ups-name="${upsName}"].test-btn`);
|
||||
const originalText = testBtn.textContent;
|
||||
testBtn.textContent = 'Testing...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
async testUPSConnection(name) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/ups/${upsName}/test`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
let msg;
|
||||
if (result.success) {
|
||||
msg = `Connection to ${upsName} successful`;
|
||||
if (result.data && result.data.STATUS) {
|
||||
msg += ` (STATUS=${result.data.STATUS})`;
|
||||
}
|
||||
this.showToast(msg, 'success');
|
||||
} else {
|
||||
if (result.connectivity && !result.connectivity.ok) {
|
||||
msg = `TCP connectivity failed: ${result.connectivity.error}`;
|
||||
} else if (result.protocol && !result.protocol.ok) {
|
||||
msg = `Protocol error: ${result.protocol.error}`;
|
||||
} else {
|
||||
msg = result.message || 'Connection failed';
|
||||
}
|
||||
this.showToast(`Connection to ${upsName} failed: ${msg}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showToast(`Connection test failed: ${error.message}`, 'error');
|
||||
} finally {
|
||||
testBtn.textContent = originalText;
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
const resp = await window.apiFetch('/api/config/ups/' + name + '/test', { method: 'POST' });
|
||||
const r = await resp.json();
|
||||
this.showToast(r.message || (r.success ? 'OK' : 'Failed'),
|
||||
r.success ? 'success' : 'error');
|
||||
} catch (err) { this.showToast('Test failed: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
const toast = el('div', { class: 'toast ' + type }, message);
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 5000);
|
||||
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new ConfigManager();
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => { new ConfigManager(); });
|
||||
@@ -0,0 +1,56 @@
|
||||
// Events log page (DOM-safe)
|
||||
(async function () {
|
||||
function setCell(row, text) {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = text;
|
||||
row.appendChild(td);
|
||||
}
|
||||
|
||||
async function loadUps() {
|
||||
const r = await window.apiFetch('/api/ups');
|
||||
if (!r.ok) return;
|
||||
const list = await r.json();
|
||||
const sel = document.getElementById('filter-ups');
|
||||
list.forEach(u => {
|
||||
const o = document.createElement('option');
|
||||
o.value = u.name; o.textContent = u.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const ups = document.getElementById('filter-ups').value;
|
||||
const kind = document.getElementById('filter-kind').value;
|
||||
const params = new URLSearchParams();
|
||||
if (ups) params.set('ups', ups);
|
||||
if (kind) params.set('kind', kind);
|
||||
params.set('limit', '300');
|
||||
const r = await window.apiFetch('/api/events?' + params.toString());
|
||||
const tbody = document.getElementById('events-tbody');
|
||||
tbody.textContent = '';
|
||||
if (!r.ok) {
|
||||
const tr = document.createElement('tr');
|
||||
setCell(tr, 'Failed to load'); tbody.appendChild(tr); return;
|
||||
}
|
||||
const rows = await r.json();
|
||||
if (!rows.length) {
|
||||
const tr = document.createElement('tr');
|
||||
setCell(tr, 'No events'); tbody.appendChild(tr); return;
|
||||
}
|
||||
rows.forEach(e => {
|
||||
const tr = document.createElement('tr');
|
||||
setCell(tr, new Date(e.ts * 1000).toLocaleString());
|
||||
setCell(tr, e.ups);
|
||||
setCell(tr, e.type);
|
||||
setCell(tr, e.detail);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('refresh-btn').addEventListener('click', loadEvents);
|
||||
document.getElementById('filter-ups').addEventListener('change', loadEvents);
|
||||
document.getElementById('filter-kind').addEventListener('change', loadEvents);
|
||||
|
||||
await loadUps();
|
||||
await loadEvents();
|
||||
})();
|
||||
@@ -0,0 +1,79 @@
|
||||
// Settings page
|
||||
(async function () {
|
||||
async function loadUi() {
|
||||
const r = await window.apiFetch('/api/config/ui');
|
||||
if (!r.ok) return;
|
||||
const ui = await r.json();
|
||||
const form = document.getElementById('ui-form');
|
||||
Object.entries(ui).forEach(([k, v]) => {
|
||||
const el = form.elements[k];
|
||||
if (!el) return;
|
||||
if (el.type === 'checkbox') el.checked = !!v;
|
||||
else el.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSmtp() {
|
||||
const r = await window.apiFetch('/api/config/smtp');
|
||||
if (!r.ok) return;
|
||||
const s = await r.json();
|
||||
if (!s) return;
|
||||
const form = document.getElementById('smtp-form');
|
||||
Object.entries(s).forEach(([k, v]) => {
|
||||
const el = form.elements[k];
|
||||
if (!el) return;
|
||||
if (k === 'to_addrs' && Array.isArray(v)) el.value = v.join(', ');
|
||||
else if (el.type === 'checkbox') el.checked = !!v;
|
||||
else if (v !== null && v !== undefined) el.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('ui-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const f = e.target;
|
||||
const payload = {
|
||||
show_events: f.show_events.checked,
|
||||
show_energy: f.show_energy.checked,
|
||||
color_badges: f.color_badges.checked,
|
||||
enable_transfer_burst_alert: f.enable_transfer_burst_alert.checked,
|
||||
enable_voltage_deviation_alert: f.enable_voltage_deviation_alert.checked,
|
||||
energy_cost_per_kwh: parseFloat(f.energy_cost_per_kwh.value) || 0,
|
||||
};
|
||||
const r = await window.apiFetch('/api/config/ui', {
|
||||
method: 'PUT', body: JSON.stringify(payload),
|
||||
});
|
||||
alert(r.ok ? 'UI settings saved' : 'Save failed');
|
||||
});
|
||||
|
||||
document.getElementById('smtp-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const f = e.target;
|
||||
const to = f.to_addrs.value.split(',').map(s => s.trim()).filter(Boolean);
|
||||
const payload = {
|
||||
host: f.host.value,
|
||||
port: parseInt(f.port.value, 10),
|
||||
username: f.username.value || null,
|
||||
use_tls: f.use_tls.checked,
|
||||
use_ssl: f.use_ssl.checked,
|
||||
from_addr: f.from_addr.value || null,
|
||||
to_addrs: to,
|
||||
subject_prefix: f.subject_prefix.value || '[UPS]',
|
||||
silent_hours_start: f.silent_hours_start.value ? parseInt(f.silent_hours_start.value, 10) : null,
|
||||
silent_hours_end: f.silent_hours_end.value ? parseInt(f.silent_hours_end.value, 10) : null,
|
||||
daily_summary_hour: f.daily_summary_hour.value ? parseInt(f.daily_summary_hour.value, 10) : null,
|
||||
};
|
||||
const r = await window.apiFetch('/api/config/smtp', {
|
||||
method: 'PUT', body: JSON.stringify(payload),
|
||||
});
|
||||
alert(r.ok ? 'SMTP saved' : 'Save failed');
|
||||
});
|
||||
|
||||
document.getElementById('test-smtp-btn').addEventListener('click', async () => {
|
||||
const r = await window.apiFetch('/api/config/smtp/test', { method: 'POST' });
|
||||
const body = await r.json().catch(() => ({}));
|
||||
alert(r.ok ? 'Test email sent' : 'Failed: ' + (body.detail || 'unknown'));
|
||||
});
|
||||
|
||||
await loadUi();
|
||||
await loadSmtp();
|
||||
})();
|
||||
Reference in new issue
Block a user