feature: Add editable hostname and operating system
Release / Build and Push Docker Image (push) Successful in 4m56s

This commit is contained in:
Keith Solomon
2026-04-14 07:42:39 -05:00
parent 1d9e98872c
commit d10e533793
8 changed files with 192 additions and 7 deletions
+86 -2
View File
@@ -12,6 +12,8 @@ const scanProgressDetail = document.getElementById('scanProgressDetail');
let devices = [];
let selectedDeviceId = null;
let selectedDevice = null;
let saveInFlight = false;
function setStatus(msg) {
statusText.textContent = msg;
@@ -128,19 +130,40 @@ function renderDeviceList() {
}
function renderMachineInfo(d) {
selectedDevice = d;
machineInfoEl.classList.remove('empty');
machineInfoEl.innerHTML = `
<form id="machineInfoForm" class="machine-info-form">
<div class="info-grid">
<div class="info-card"><div class="label">Hostname</div><div class="value">${d.hostname || '-'}</div></div>
<div class="info-card info-card-edit">
<div class="label">Hostname</div>
<input id="hostnameInput" class="inline-input" type="text" value="${escapeAttr(d.hostname || '')}" placeholder="${escapeAttr(d.detected_hostname || 'Hostname')}" />
<div class="subvalue">Detected: ${escapeHtml(d.detected_hostname || '-')}</div>
</div>
<div class="info-card"><div class="label">IP Address</div><div class="value">${d.ip}</div></div>
<div class="info-card"><div class="label">MAC Address</div><div class="value">${d.mac || '-'}</div></div>
<div class="info-card"><div class="label">Vendor</div><div class="value">${d.vendor || '-'}</div></div>
<div class="info-card"><div class="label">Operating System</div><div class="value">${d.os_name || '-'}</div></div>
<div class="info-card info-card-edit">
<div class="label">Operating System</div>
<input id="osNameInput" class="inline-input" type="text" value="${escapeAttr(d.os_name || '')}" placeholder="${escapeAttr(d.detected_os_name || 'Operating system')}" />
<div class="subvalue">Detected: ${escapeHtml(d.detected_os_name || '-')}</div>
</div>
<div class="info-card"><div class="label">Status</div><div class="value">${d.is_active ? 'Active' : 'Not Seen in Last Scan'}</div></div>
<div class="info-card"><div class="label">First Seen</div><div class="value">${formatDate(d.first_seen)}</div></div>
<div class="info-card"><div class="label">Last Seen</div><div class="value">${formatDate(d.last_seen)}</div></div>
</div>
<div class="machine-info-actions">
<button id="saveDeviceBtn" type="submit" class="secondary-btn">Save Changes</button>
<button id="resetDeviceBtn" type="button" class="ghost-btn">Reset to Detected</button>
</div>
</form>
`;
const form = document.getElementById('machineInfoForm');
const resetBtn = document.getElementById('resetDeviceBtn');
form.addEventListener('submit', saveDeviceEdits);
resetBtn.addEventListener('click', resetDeviceEdits);
}
function renderPorts(ports) {
@@ -200,6 +223,67 @@ async function loadDevice(deviceId) {
renderPorts(d.ports || []);
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function escapeAttr(value) {
return escapeHtml(value);
}
async function saveDeviceEdits(event) {
event.preventDefault();
if (!selectedDevice || saveInFlight) return;
const hostname = document.getElementById('hostnameInput')?.value ?? '';
const osName = document.getElementById('osNameInput')?.value ?? '';
const saveBtn = document.getElementById('saveDeviceBtn');
saveInFlight = true;
if (saveBtn) saveBtn.disabled = true;
setStatus(`Saving edits for ${selectedDevice.ip}...`);
try {
const updated = await api(`/api/devices/${selectedDevice.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hostname, os_name: osName }),
});
selectedDevice = updated;
updateDeviceInList(updated);
renderMachineInfo(updated);
setStatus(`Saved hostname and operating system for ${updated.ip}.`);
} catch (err) {
setStatus(`Failed saving device changes: ${err.message}`);
} finally {
saveInFlight = false;
if (saveBtn) saveBtn.disabled = false;
}
}
function resetDeviceEdits() {
if (!selectedDevice) return;
const hostnameInput = document.getElementById('hostnameInput');
const osNameInput = document.getElementById('osNameInput');
if (hostnameInput) hostnameInput.value = '';
if (osNameInput) osNameInput.value = '';
setStatus('Manual overrides cleared in the form. Save to apply.');
}
function updateDeviceInList(updatedDevice) {
devices = devices.map((device) => (device.id === updatedDevice.id ? { ...device, ...updatedDevice } : device));
devices.sort((a, b) => {
if (a.is_active !== b.is_active) return b.is_active - a.is_active;
return compareIpNumeric(a, b);
});
renderDeviceList();
}
async function runScan() {
const subnet = subnetInput.value.trim();
if (!subnet) return;
+44
View File
@@ -81,6 +81,11 @@ button {
font-weight: 600;
}
button:disabled {
cursor: wait;
opacity: 0.75;
}
.scan-progress-card {
border: 1px solid var(--border);
border-radius: 10px;
@@ -206,6 +211,12 @@ button {
background: rgba(255,255,255,0.02);
}
.info-card-edit {
display: flex;
flex-direction: column;
gap: 8px;
}
.label {
color: var(--muted);
font-size: 0.78rem;
@@ -218,6 +229,38 @@ button {
word-break: break-word;
}
.subvalue {
color: var(--muted);
font-size: 0.78rem;
}
.inline-input {
width: 100%;
min-width: 0;
background: rgba(10, 17, 24, 0.75);
}
.machine-info-form {
display: grid;
gap: 12px;
}
.machine-info-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.secondary-btn {
background: linear-gradient(180deg, var(--accent), var(--accent-2));
}
.ghost-btn {
border: 1px solid var(--border);
background: rgba(255,255,255,0.03);
color: var(--text);
}
details.port {
border: 1px solid var(--border);
border-radius: 10px;
@@ -263,4 +306,5 @@ summary {
.toolbar { min-width: 0; }
.controls { flex-direction: column; align-items: stretch; }
input { min-width: 0; }
.machine-info-actions { flex-direction: column; }
}