Files
pxe-server/webapp/static/app.js
cproudlock 0b86255711 Webapp redesign phase 2: Overview and imaging floor
Imaging floor (/imaging, /imaging/tiles):
- One line per PC: status chip, serial with machine number, hostname
  and IP, stage text with a segmented stage track, one action plus a
  '...' menu. Everything else (description, error, timestamps, Intune
  ID + QR, Set category / ARTS links, LAPS QR, log tail) is in the row's
  expandable body.
- Order: failed, needs action (stage 7), in progress, done; log-only
  bays fold into one collapsed group. imaging_status.floor_state() and
  floor_order() compute this.
- Status filter buttons with live counts next to the text filter. SSE
  refresh, expanded-row memory and LAPS handling are unchanged.
- Stage text is never truncated; BIOS updates and erasing are bold.

Overview (/):
- Services popover, technician to-do line, the six most urgent floor
  rows (?limit=6 partial, refreshed every 15 s), and image cards with
  lint warnings, map errors and missing zips as separate chips. New,
  clone, delete and unattend stay on the cards' menus.

Copy buttons are now handled once in static/app.js, with a fallback for
plain-http pages where navigator.clipboard is unavailable.
2026-10-14 13:44:07 -04:00

497 lines
22 KiB
JavaScript

/**
* PXE Server Manager - Frontend JavaScript
*
* Handles:
* - Add/remove rows in driver paths, specialize commands, first-logon commands
* - Drag-to-reorder and up/down buttons for command lists
* - Switching between Form editor and Raw XML views
* - AJAX and form-based save
*/
document.addEventListener('DOMContentLoaded', function () {
// -----------------------------------------------------------------------
// Utility: renumber the "Order" column in a table body
// -----------------------------------------------------------------------
function renumberRows(tbody) {
var rows = tbody.querySelectorAll('tr');
rows.forEach(function (row, idx) {
var orderCell = row.querySelector('.order-num');
if (orderCell) {
orderCell.textContent = idx + 1;
}
});
}
// -----------------------------------------------------------------------
// Utility: hide "empty" message if rows exist, show if none
// -----------------------------------------------------------------------
function toggleEmpty(tableId, emptyId) {
var tbody = document.querySelector('#' + tableId + ' tbody');
var emptyEl = document.getElementById(emptyId);
if (!tbody || !emptyEl) return;
emptyEl.style.display = tbody.querySelectorAll('tr').length > 0 ? 'none' : '';
}
// -----------------------------------------------------------------------
// Remove row handler (delegated)
// -----------------------------------------------------------------------
document.addEventListener('click', function (e) {
var btn = e.target.closest('.remove-row');
if (!btn) return;
var row = btn.closest('tr');
var tbody = row.parentElement;
row.remove();
renumberRows(tbody);
// Determine which table we are in and toggle empty message
var table = tbody.closest('table');
if (table) {
if (table.id === 'driverPathsTable') toggleEmpty('driverPathsTable', 'driverPathsEmpty');
if (table.id === 'specCmdTable') toggleEmpty('specCmdTable', 'specCmdEmpty');
if (table.id === 'flCmdTable') toggleEmpty('flCmdTable', 'flCmdEmpty');
}
});
// -----------------------------------------------------------------------
// Move-up / Move-down handlers (delegated)
// -----------------------------------------------------------------------
document.addEventListener('click', function (e) {
var btn = e.target.closest('.move-up');
if (btn) {
var row = btn.closest('tr');
var prev = row.previousElementSibling;
if (prev) {
row.parentElement.insertBefore(row, prev);
renumberRows(row.parentElement);
}
return;
}
btn = e.target.closest('.move-down');
if (btn) {
var row = btn.closest('tr');
var next = row.nextElementSibling;
if (next) {
row.parentElement.insertBefore(next, row);
renumberRows(row.parentElement);
}
}
});
// -----------------------------------------------------------------------
// Add Driver Path
// -----------------------------------------------------------------------
var addDriverPathBtn = document.getElementById('addDriverPath');
if (addDriverPathBtn) {
addDriverPathBtn.addEventListener('click', function () {
var tbody = document.querySelector('#driverPathsTable tbody');
var idx = tbody.querySelectorAll('tr').length + 1;
var tr = document.createElement('tr');
tr.innerHTML =
'<td class="order-num">' + idx + '</td>' +
'<td><input type="text" class="form-control form-control-sm" name="driver_path[]" value="" placeholder="e.g. C:\\Drivers\\Network"></td>' +
'<td><button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button></td>';
tbody.appendChild(tr);
toggleEmpty('driverPathsTable', 'driverPathsEmpty');
tr.querySelector('input').focus();
});
}
// -----------------------------------------------------------------------
// Add Specialize Command
// -----------------------------------------------------------------------
var addSpecCmdBtn = document.getElementById('addSpecCmd');
if (addSpecCmdBtn) {
addSpecCmdBtn.addEventListener('click', function () {
var tbody = document.querySelector('#specCmdTable tbody');
var idx = tbody.querySelectorAll('tr').length + 1;
var tr = document.createElement('tr');
tr.setAttribute('draggable', 'true');
tr.innerHTML =
'<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>' +
'<td class="order-num">' + idx + '</td>' +
'<td><input type="text" class="form-control form-control-sm" name="spec_cmd_path[]" value="" placeholder="Command path"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="spec_cmd_desc[]" value="" placeholder="Description"></td>' +
'<td class="text-nowrap">' +
'<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up"><i class="bi bi-arrow-up"></i></button> ' +
'<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down"><i class="bi bi-arrow-down"></i></button> ' +
'<button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button>' +
'</td>';
tbody.appendChild(tr);
initDragForRow(tr);
toggleEmpty('specCmdTable', 'specCmdEmpty');
tr.querySelector('input[name="spec_cmd_path[]"]').focus();
});
}
// -----------------------------------------------------------------------
// Add First Logon Command
// -----------------------------------------------------------------------
var addFlCmdBtn = document.getElementById('addFlCmd');
if (addFlCmdBtn) {
addFlCmdBtn.addEventListener('click', function () {
var tbody = document.querySelector('#flCmdTable tbody');
var idx = tbody.querySelectorAll('tr').length + 1;
var tr = document.createElement('tr');
tr.setAttribute('draggable', 'true');
tr.innerHTML =
'<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>' +
'<td class="order-num">' + idx + '</td>' +
'<td><input type="text" class="form-control form-control-sm" name="fl_cmd_commandline[]" value="" placeholder="Command line"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="fl_cmd_desc[]" value="" placeholder="Description"></td>' +
'<td class="text-nowrap">' +
'<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up"><i class="bi bi-arrow-up"></i></button> ' +
'<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down"><i class="bi bi-arrow-down"></i></button> ' +
'<button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button>' +
'</td>';
tbody.appendChild(tr);
initDragForRow(tr);
toggleEmpty('flCmdTable', 'flCmdEmpty');
tr.querySelector('input[name="fl_cmd_commandline[]"]').focus();
});
}
// -----------------------------------------------------------------------
// Add User Account
// -----------------------------------------------------------------------
var addUserAccountBtn = document.getElementById('addUserAccount');
if (addUserAccountBtn) {
addUserAccountBtn.addEventListener('click', function () {
var tbody = document.querySelector('#userAccountsTable tbody');
var idx = tbody.querySelectorAll('tr').length;
var tr = document.createElement('tr');
tr.innerHTML =
'<td class="order-num">' + (idx + 1) + '</td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_name_' + idx + '" value="" placeholder="Username"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_password_' + idx + '" value="" placeholder="Password"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_group_' + idx + '" value="Administrators"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_display_' + idx + '" value="" placeholder="Display Name"></td>' +
'<td>' +
'<input type="hidden" name="account_plaintext_' + idx + '" value="true">' +
'<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">Remove</button>' +
'</td>';
tbody.appendChild(tr);
var emptyEl = document.getElementById('userAccountsEmpty');
if (emptyEl) emptyEl.style.display = 'none';
tr.querySelector('input[name^="account_name_"]').focus();
});
}
// -----------------------------------------------------------------------
// Remove User Account row + renumber indices (delegated)
// -----------------------------------------------------------------------
document.addEventListener('click', function (e) {
var btn = e.target.closest('.remove-account-row');
if (!btn) return;
var row = btn.closest('tr');
var tbody = row.parentElement;
row.remove();
// Renumber order and field name indices
var rows = tbody.querySelectorAll('tr');
rows.forEach(function (r, i) {
var orderCell = r.querySelector('.order-num');
if (orderCell) orderCell.textContent = i + 1;
// Rename inputs to match new index
r.querySelectorAll('input').forEach(function (inp) {
var name = inp.getAttribute('name');
if (name) {
inp.setAttribute('name', name.replace(/_\d+$/, '_' + i));
}
});
});
var emptyEl = document.getElementById('userAccountsEmpty');
if (emptyEl) emptyEl.style.display = rows.length > 0 ? 'none' : '';
});
// -----------------------------------------------------------------------
// AutoLogon Enabled toggle — keep hidden input in sync
// -----------------------------------------------------------------------
var autologonToggle = document.getElementById('autologonEnabledToggle');
if (autologonToggle) {
autologonToggle.addEventListener('change', function () {
var hidden = document.getElementById('autologon_enabled_val');
if (hidden) {
hidden.value = this.checked ? 'true' : 'false';
}
});
}
// -----------------------------------------------------------------------
// OOBE toggle switches — keep hidden input in sync
// -----------------------------------------------------------------------
document.querySelectorAll('.oobe-toggle').forEach(function (cb) {
cb.addEventListener('change', function () {
var hiddenId = this.getAttribute('data-field') + '_val';
var hidden = document.getElementById(hiddenId);
if (hidden) {
hidden.value = this.checked ? 'true' : 'false';
}
});
});
// -----------------------------------------------------------------------
// Drag-and-drop reorder for command tables
// -----------------------------------------------------------------------
var dragSrcRow = null;
function initDragForRow(row) {
row.addEventListener('dragstart', function (e) {
dragSrcRow = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // required for Firefox
});
row.addEventListener('dragover', function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
return false;
});
row.addEventListener('dragenter', function (e) {
this.style.borderTop = '2px solid #0d6efd';
});
row.addEventListener('dragleave', function (e) {
this.style.borderTop = '';
});
row.addEventListener('drop', function (e) {
e.stopPropagation();
this.style.borderTop = '';
if (dragSrcRow !== this) {
var tbody = this.parentElement;
tbody.insertBefore(dragSrcRow, this);
renumberRows(tbody);
}
return false;
});
row.addEventListener('dragend', function () {
this.classList.remove('dragging');
// clean up all borders
this.parentElement.querySelectorAll('tr').forEach(function (r) {
r.style.borderTop = '';
});
});
}
// Initialize drag on existing rows
document.querySelectorAll('.command-table tbody tr[draggable="true"]').forEach(initDragForRow);
// -----------------------------------------------------------------------
// Save: Form view
// -----------------------------------------------------------------------
var saveFormBtn = document.getElementById('saveFormBtn');
if (saveFormBtn) {
saveFormBtn.addEventListener('click', function () {
var activeTab = document.querySelector('.editor-tabs .nav-link.active');
var form = document.getElementById('unattendForm');
var modeInput = document.getElementById('saveMode');
if (activeTab && activeTab.id === 'raw-tab') {
modeInput.value = 'raw';
} else {
modeInput.value = 'form';
}
form.submit();
});
}
// -----------------------------------------------------------------------
// Save: Raw XML via AJAX
// -----------------------------------------------------------------------
var saveRawBtn = document.getElementById('saveRawBtn');
if (saveRawBtn) {
saveRawBtn.addEventListener('click', function () {
var xmlContent = document.getElementById('rawXmlEditor').value;
var url = window.PXE_API_URL;
if (!url) {
alert('API URL not configured.');
return;
}
saveRawBtn.disabled = true;
saveRawBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving...';
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ raw_xml: xmlContent })
})
.then(function (resp) { return resp.json(); })
.then(function (data) {
if (data.error) {
showToast('Error: ' + data.error, 'danger');
} else {
showToast('Raw XML saved successfully.', 'success');
}
})
.catch(function (err) {
showToast('Network error: ' + err.message, 'danger');
})
.finally(function () {
saveRawBtn.disabled = false;
saveRawBtn.innerHTML = '<i class="bi bi-floppy me-1"></i> Save Raw XML';
});
});
}
// -----------------------------------------------------------------------
// Toast notification helper
// -----------------------------------------------------------------------
function showToast(message, type) {
// Create a Bootstrap alert at the top of main content
var container = document.querySelector('.main-content');
var alert = document.createElement('div');
alert.className = 'alert alert-' + (type || 'info') + ' alert-dismissible fade show';
alert.setAttribute('role', 'alert');
alert.innerHTML = message +
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>';
container.insertBefore(alert, container.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(function () {
if (alert.parentElement) {
alert.classList.remove('show');
setTimeout(function () { alert.remove(); }, 200);
}
}, 5000);
}
// -----------------------------------------------------------------------
// Initial empty-state check (in case page loaded with data)
// -----------------------------------------------------------------------
toggleEmpty('driverPathsTable', 'driverPathsEmpty');
toggleEmpty('specCmdTable', 'specCmdEmpty');
toggleEmpty('flCmdTable', 'flCmdEmpty');
toggleEmpty('userAccountsTable', 'userAccountsEmpty');
// =======================================================================
// Image Configuration Editor handlers
// =======================================================================
// -----------------------------------------------------------------------
// Generic: collect table rows into JSON and submit form
// -----------------------------------------------------------------------
function collectAndSubmit(tableId, hiddenId, formId, extractor) {
var tbody = document.querySelector('#' + tableId + ' tbody');
if (!tbody) return;
var rows = tbody.querySelectorAll('tr');
var data = [];
rows.forEach(function (row) {
var item = extractor(row);
if (item) data.push(item);
});
document.getElementById(hiddenId).value = JSON.stringify(data);
document.getElementById(formId).submit();
}
// Helper: extract JSON from data-json attr, stripping internal _fields
function extractDataJson(row) {
var raw = row.getAttribute('data-json');
if (!raw) return null;
try {
var obj = JSON.parse(raw);
Object.keys(obj).forEach(function (k) {
if (k.charAt(0) === '_') delete obj[k];
});
return obj;
} catch (e) { return null; }
}
// -----------------------------------------------------------------------
// Save: Hardware Models
// -----------------------------------------------------------------------
var saveHwModelsBtn = document.getElementById('saveHwModels');
if (saveHwModelsBtn) {
saveHwModelsBtn.addEventListener('click', function () {
collectAndSubmit('hwModelsTable', 'hwModelsData', 'hwModelsForm', function (row) {
var m = row.querySelector('[data-field="Model"]');
var f = row.querySelector('[data-field="Id"]');
if (!m || !f) return null;
return { Model: m.value, Id: f.value };
});
});
}
// -----------------------------------------------------------------------
// Save: Drivers
// -----------------------------------------------------------------------
var saveDriversBtn = document.getElementById('saveDrivers');
if (saveDriversBtn) {
saveDriversBtn.addEventListener('click', function () {
collectAndSubmit('driversTable', 'driversData', 'driversForm', extractDataJson);
});
}
// -----------------------------------------------------------------------
// Save: Operating Systems
// -----------------------------------------------------------------------
var saveOsBtn = document.getElementById('saveOs');
if (saveOsBtn) {
saveOsBtn.addEventListener('click', function () {
collectAndSubmit('osTable', 'osData', 'osForm', extractDataJson);
});
}
// -----------------------------------------------------------------------
// Save: Packages
// -----------------------------------------------------------------------
var savePackagesBtn = document.getElementById('savePackages');
if (savePackagesBtn) {
savePackagesBtn.addEventListener('click', function () {
collectAndSubmit('packagesTable', 'packagesData', 'packagesForm', extractDataJson);
});
}
// -----------------------------------------------------------------------
// Add Hardware Model row
// -----------------------------------------------------------------------
var addHwModelBtn = document.getElementById('addHwModel');
if (addHwModelBtn) {
addHwModelBtn.addEventListener('click', function () {
var tbody = document.querySelector('#hwModelsTable tbody');
var idx = tbody.querySelectorAll('tr').length + 1;
var tr = document.createElement('tr');
tr.innerHTML =
'<td class="order-num">' + idx + '</td>' +
'<td><input type="text" class="form-control form-control-sm" data-field="Model" value="" placeholder="Model name"></td>' +
'<td><input type="text" class="form-control form-control-sm" data-field="Id" value="" placeholder="Driver Family ID"></td>' +
'<td><span class="badge bg-secondary badge-disk">New</span></td>' +
'<td><button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button></td>';
tbody.appendChild(tr);
var empty = document.getElementById('hwModelsEmpty');
if (empty) empty.style.display = 'none';
tr.querySelector('input').focus();
});
}
});
// Copy buttons: any .copy-btn[data-copy-text], on every page. The LAN is
// plain http, so navigator.clipboard is often unavailable; fall back to a
// hidden textarea + execCommand. The button briefly reads "Copied".
document.addEventListener('click', function (e) {
var btn = e.target.closest && e.target.closest('.copy-btn');
if (!btn) return;
var text = btn.getAttribute('data-copy-text') || '';
if (!text) return;
var done = function (ok) {
var orig = btn.getAttribute('data-orig-label') || btn.textContent;
btn.setAttribute('data-orig-label', orig);
btn.textContent = ok ? 'Copied' : 'Copy failed';
setTimeout(function () { btn.textContent = orig; }, 1400);
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); });
return;
}
var ta = document.createElement('textarea');
ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (err) { ok = false; }
document.body.removeChild(ta);
done(ok);
});