261 lines
8.2 KiB
JavaScript
261 lines
8.2 KiB
JavaScript
/**
|
|
* DHCP Lease Manager — client-side logic.
|
|
* Uses Zoraxy's $.cjax() for CSRF-safe AJAX (jQuery available from parent frame).
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function showToast(message, type) {
|
|
var toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.className = 'toast ' + (type || '');
|
|
void toast.offsetWidth;
|
|
toast.classList.remove('hidden');
|
|
setTimeout(function () { toast.classList.add('hidden'); }, 3000);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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';
|
|
var hasHostname = lease.hostname && lease.hostname.trim() !== '';
|
|
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" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '" data-hostname="' + escAttr(lease.hostname || '') + '">' +
|
|
(isPermanent
|
|
? '<button class="btn edit-btn" data-action="edit">Edit</button>' +
|
|
'<button class="btn btn-danger" data-action="unpin">Unpin</button>' +
|
|
'<span class="edit-form" style="display:none">' +
|
|
'<input type="text" class="edit-hostname" placeholder="hostname" size="16" value="' + escAttr(lease.hostname || '') + '">' +
|
|
'<button class="btn btn-primary edit-confirm">OK</button>' +
|
|
'<button class="btn edit-cancel">✕</button>' +
|
|
'</span>'
|
|
: (hasHostname
|
|
? '<button class="btn btn-primary" data-action="pin">Pin</button>'
|
|
: '<button class="btn btn-primary pin-needs-name" data-action="pin-name">Pin</button>' +
|
|
'<span class="edit-form" style="display:none">' +
|
|
'<input type="text" class="edit-hostname" placeholder="hostname" size="16">' +
|
|
'<button class="btn btn-primary edit-confirm">OK</button>' +
|
|
'<button class="btn edit-cancel">✕</button>' +
|
|
'</span>'
|
|
)
|
|
) +
|
|
'</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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function loadLeases() {
|
|
$.get('./api/leases', function (data) {
|
|
renderLeases(data);
|
|
}).fail(function () {
|
|
showToast('Failed to load leases', 'error');
|
|
});
|
|
}
|
|
|
|
function pinLease(mac, ip, hostname) {
|
|
$.cjax({
|
|
url: './api/pin',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { mac: mac, ip: ip, hostname: hostname || '' },
|
|
success: function () {
|
|
showToast('Pinned ' + (hostname || mac), 'success');
|
|
loadLeases();
|
|
},
|
|
error: function (xhr) {
|
|
var msg = 'Pin failed';
|
|
try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {}
|
|
showToast(msg, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function unpinLease(mac) {
|
|
$.cjax({
|
|
url: './api/unpin',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { mac: mac },
|
|
success: function () {
|
|
showToast('Unpinned', 'success');
|
|
loadLeases();
|
|
},
|
|
error: function (xhr) {
|
|
var msg = 'Unpin failed';
|
|
try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {}
|
|
showToast(msg, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateHostname(mac, ip, hostname) {
|
|
// Re-pin with new hostname (server handles unpin + repin atomically)
|
|
$.cjax({
|
|
url: './api/pin',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { mac: mac, ip: ip, hostname: hostname },
|
|
success: function () {
|
|
showToast('Hostname updated', 'success');
|
|
loadLeases();
|
|
},
|
|
error: function (xhr) {
|
|
var msg = 'Update failed';
|
|
try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {}
|
|
showToast(msg, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function reloadDnsmasq() {
|
|
var btn = document.getElementById('reload-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Reloading...';
|
|
$.cjax({
|
|
url: './api/reload',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: {},
|
|
success: function (data) {
|
|
if (data.success) {
|
|
showToast('dnsmasq reloaded', 'success');
|
|
} else {
|
|
showToast(data.message || 'Reload failed', 'error');
|
|
}
|
|
loadLeases();
|
|
},
|
|
error: function (xhr) {
|
|
var msg = 'Reload failed';
|
|
try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {}
|
|
showToast(msg, 'error');
|
|
},
|
|
complete: function () {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Reload dnsmasq';
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Form helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function showEditForm(cell) {
|
|
var editBtn = cell.querySelector('.edit-btn');
|
|
var pinBtn = cell.querySelector('.pin-needs-name');
|
|
if (editBtn) editBtn.style.display = 'none';
|
|
if (pinBtn) pinBtn.style.display = 'none';
|
|
cell.querySelector('.edit-form').style.display = 'inline-flex';
|
|
var input = cell.querySelector('.edit-hostname');
|
|
input.focus();
|
|
input.select();
|
|
}
|
|
|
|
function hideEditForm(cell) {
|
|
cell.querySelector('.edit-form').style.display = 'none';
|
|
var editBtn = cell.querySelector('.edit-btn');
|
|
var pinBtn = cell.querySelector('.pin-needs-name');
|
|
if (editBtn) editBtn.style.display = 'inline-block';
|
|
if (pinBtn) pinBtn.style.display = 'inline-block';
|
|
}
|
|
|
|
function confirmEditForm(cell) {
|
|
var mac = cell.dataset.mac;
|
|
var ip = cell.dataset.ip;
|
|
var hostname = cell.querySelector('.edit-hostname').value.trim();
|
|
hideEditForm(cell);
|
|
updateHostname(mac, ip, hostname);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Event delegation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
document.getElementById('leases-body').addEventListener('click', function (e) {
|
|
var cell = e.target.closest('.actions-cell');
|
|
if (!cell) return;
|
|
|
|
var mac = cell.dataset.mac;
|
|
var ip = cell.dataset.ip;
|
|
var hostname = cell.dataset.hostname;
|
|
|
|
// Pin (one-click — hostname already exists)
|
|
if (e.target.closest('[data-action="pin"]')) {
|
|
pinLease(mac, ip, hostname);
|
|
}
|
|
|
|
// Pin (needs name — show form)
|
|
if (e.target.closest('[data-action="pin-name"]')) {
|
|
showEditForm(cell);
|
|
}
|
|
|
|
// Edit (pinned lease — show form to change hostname)
|
|
if (e.target.closest('[data-action="edit"]')) {
|
|
showEditForm(cell);
|
|
}
|
|
|
|
// Unpin
|
|
if (e.target.closest('[data-action="unpin"]')) {
|
|
unpinLease(mac);
|
|
}
|
|
|
|
// Confirm edit (OK button)
|
|
if (e.target.closest('.edit-confirm')) {
|
|
confirmEditForm(cell);
|
|
}
|
|
|
|
// Cancel edit (✕ button)
|
|
if (e.target.closest('.edit-cancel')) {
|
|
hideEditForm(cell);
|
|
}
|
|
});
|
|
|
|
document.getElementById('reload-btn').addEventListener('click', reloadDnsmasq);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Init
|
|
// ---------------------------------------------------------------------------
|
|
|
|
loadLeases();
|