/**
* 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 || {};
if (csrfToken) {
opts.headers['X-Zoraxy-Csrf'] = csrfToken;
}
if (opts.body && typeof opts.body === 'object') {
opts.body = JSON.stringify(opts.body);
opts.headers['Content-Type'] = 'application/json';
}
const res = await fetch(API_BASE + path, opts);
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || data.message || 'Request failed');
}
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 = '
| No active leases |
';
return;
}
var rows = leases.map(function (lease) {
var isPermanent = lease.status === 'permanent';
return (
'' +
'| ' + esc(lease.hostname || '--') + ' | ' +
'' + esc(lease.ip) + ' | ' +
'' + esc(lease.mac) + ' | ' +
'' +
'' +
esc(lease.status) +
'' +
' | ' +
'' +
(isPermanent
? ''
: ''
) +
' | ' +
'
'
);
}).join('');
tbody.innerHTML = rows;
}
function esc(s) {
if (!s) return '';
return s.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();