199 lines
6.3 KiB
JavaScript
199 lines
6.3 KiB
JavaScript
/**
|
|
* DHCP Lease Manager — client-side logic.
|
|
* All API URLs are relative to the page path, so the Zoraxy proxy handles routing.
|
|
*/
|
|
|
|
const API_BASE = './api';
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function showToast(message, type) {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.className = 'toast ' + type;
|
|
void toast.offsetWidth; // force reflow for transition
|
|
toast.classList.remove('hidden');
|
|
setTimeout(function () { toast.classList.add('hidden'); }, 3000);
|
|
}
|
|
|
|
/** Minimal fetch wrapper that includes the CSRF token. */
|
|
async function apiFetch(path, opts) {
|
|
opts = opts || {};
|
|
opts.headers = opts.headers || {};
|
|
var method = (opts.method || 'GET').toUpperCase();
|
|
var isPost = (method === 'POST' || method === 'PUT' || method === 'PATCH');
|
|
|
|
// Attach CSRF token as query param (Zoraxy may validate it there)
|
|
if (csrfToken && csrfToken !== '{{.csrfToken}}') {
|
|
var sep = path.indexOf('?') >= 0 ? '&' : '?';
|
|
path = path + sep + 'csrfToken=' + encodeURIComponent(csrfToken);
|
|
opts.headers['X-Zoraxy-Csrf'] = csrfToken;
|
|
}
|
|
|
|
if (isPost && opts.body && typeof opts.body === 'object') {
|
|
// Send as URL-encoded form data (Zoraxy's CSRF middleware parses forms, not JSON)
|
|
var formBody = new URLSearchParams();
|
|
for (var key in opts.body) {
|
|
if (opts.body.hasOwnProperty(key)) {
|
|
formBody.append(key, opts.body[key]);
|
|
}
|
|
}
|
|
formBody.append('csrfToken', csrfToken || '');
|
|
opts.body = formBody.toString();
|
|
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
}
|
|
var res = await fetch(API_BASE + path, opts);
|
|
var text = await res.text();
|
|
try {
|
|
var data = JSON.parse(text);
|
|
} catch (e) {
|
|
throw new Error('Unexpected response (status ' + res.status + '). Check plugin is running.');
|
|
}
|
|
if (!res.ok) {
|
|
throw new Error(data.error || data.message || 'Request failed (status ' + res.status + ')');
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function formatTime(t) {
|
|
var d = new Date(t);
|
|
if (isNaN(d.getTime())) return '--';
|
|
return d.toLocaleString();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Render
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function renderLeases(data) {
|
|
var tbody = document.getElementById('leases-body');
|
|
var leases = data.leases || [];
|
|
|
|
document.getElementById('active-count').textContent = data.active_count;
|
|
document.getElementById('pinned-count').textContent = data.pinned_count;
|
|
|
|
if (leases.length === 0) {
|
|
tbody.innerHTML = '<tr class="loading-row"><td colspan="5">No active leases</td></tr>';
|
|
return;
|
|
}
|
|
|
|
var rows = leases.map(function (lease) {
|
|
var isPermanent = lease.status === 'permanent';
|
|
return (
|
|
'<tr>' +
|
|
'<td>' + esc(lease.hostname || '--') + '</td>' +
|
|
'<td>' + esc(lease.ip) + '</td>' +
|
|
'<td class="mac-address">' + esc(lease.mac) + '</td>' +
|
|
'<td>' +
|
|
'<span class="status-badge ' + (isPermanent ? 'status-permanent' : 'status-active') + '">' +
|
|
esc(lease.status) +
|
|
'</span>' +
|
|
'</td>' +
|
|
'<td class="actions-cell">' +
|
|
(isPermanent
|
|
? '<button class="btn btn-danger" data-action="unpin" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '" data-hostname="' + escAttr(lease.hostname) + '">Unpin</button>'
|
|
: '<button class="btn btn-primary" data-action="pin" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '" data-hostname="' + escAttr(lease.hostname) + '">Pin</button>'
|
|
) +
|
|
'</td>' +
|
|
'</tr>'
|
|
);
|
|
}).join('');
|
|
|
|
tbody.innerHTML = rows;
|
|
}
|
|
|
|
function esc(s) {
|
|
if (!s) return '';
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
function escAttr(s) {
|
|
if (!s) return '';
|
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Actions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function loadLeases() {
|
|
try {
|
|
var data = await apiFetch('/leases');
|
|
renderLeases(data);
|
|
} catch (err) {
|
|
showToast('Failed to load leases: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function pinLease(mac, ip, hostname) {
|
|
try {
|
|
await apiFetch('/pin', {
|
|
method: 'POST',
|
|
body: { mac: mac, ip: ip, hostname: hostname || '' }
|
|
});
|
|
showToast('Pinned ' + (hostname || mac), 'success');
|
|
loadLeases();
|
|
} catch (err) {
|
|
showToast('Pin failed: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function unpinLease(mac, ip, hostname) {
|
|
try {
|
|
await apiFetch('/unpin', {
|
|
method: 'POST',
|
|
body: { mac: mac, ip: ip, hostname: hostname || '' }
|
|
});
|
|
showToast('Unpinned ' + (hostname || mac), 'success');
|
|
loadLeases();
|
|
} catch (err) {
|
|
showToast('Unpin failed: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function reloadDnsmasq() {
|
|
var btn = document.getElementById('reload-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Reloading...';
|
|
try {
|
|
await apiFetch('/reload', { method: 'POST' });
|
|
showToast('dnsmasq reloaded', 'success');
|
|
loadLeases();
|
|
} catch (err) {
|
|
showToast('Reload failed: ' + err.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Reload dnsmasq';
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Event delegation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
document.getElementById('leases-body').addEventListener('click', function (e) {
|
|
var btn = e.target.closest('button[data-action]');
|
|
if (!btn) return;
|
|
|
|
var action = btn.dataset.action;
|
|
var mac = btn.dataset.mac;
|
|
var ip = btn.dataset.ip;
|
|
var hostname = btn.dataset.hostname;
|
|
|
|
if (action === 'pin') {
|
|
pinLease(mac, ip, hostname);
|
|
} else if (action === 'unpin') {
|
|
unpinLease(mac, ip, hostname);
|
|
}
|
|
});
|
|
|
|
document.getElementById('reload-btn').addEventListener('click', reloadDnsmasq);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Init
|
|
// ---------------------------------------------------------------------------
|
|
|
|
loadLeases();
|