Files
pxe-server/webapp/static/image-pages.js
cproudlock 397063f987 Webapp: Updates pages, image page tabs with review-before-save, BPRT sync
Updates group:
- Dell drivers and BIOS: check Dell's catalogs against all three driver maps
  and models.txt, download with a shared speed cap, repack, preview, apply
  with backups and rollback.
- iPXE boot files: check GitHub for the signed Secure Boot set, verify the
  sha256 and each file's signer, install atomically, roll back. Includes the
  by-hand procedure.
- Blancco: upload a release, stage it, test-boot it from the menu, activate,
  roll back.
- BPRT sync: validate a batch uploaded from the MediaCreator Lite cache
  (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site
  overlay files kept.

Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and
Packages tabs. Every save shows a diff first, keeps the file's format, makes a
backup and can be undone. Driver lint runs inline.

Enrollment: upload standard/engineer MCL packages, one per tenant/purpose,
with archive and restore. Hardware coverage counts engineer as Win11.
2026-10-13 16:58:55 -04:00

115 lines
4.7 KiB
JavaScript

/**
* image-pages.js - helpers shared by the GE Aerospace image tabs.
* Loaded by templates/_image_header.html. Exposes window.PxeImage:
*
* PxeImage.postJSON(url, body) POST JSON with the CSRF header;
* resolves {ok, status, data}.
* PxeImage.renderDiff(el, files) draw [{name, changed, diff, reformat}]
* (services/control_files.public) into el.
* PxeImage.reviewAndSave(opts) preview -> diff modal -> save -> reload.
* opts: previewUrl, saveUrl, body (object), title (optional),
* onSaved(data) (optional; default reloads the page).
*/
(function () {
'use strict';
function csrfToken() {
var m = document.querySelector('meta[name="csrf-token"]');
return m ? m.getAttribute('content') : '';
}
function postJSON(url, body) {
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken() },
body: JSON.stringify(body || {}),
credentials: 'same-origin'
}).then(function (resp) {
return resp.json().catch(function () { return { error: 'HTTP ' + resp.status }; })
.then(function (data) { return { ok: resp.ok, status: resp.status, data: data }; });
});
}
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text !== undefined) e.textContent = text;
return e;
}
function renderDiff(container, files) {
container.innerHTML = '';
var changed = (files || []).filter(function (f) { return f.changed; });
if (!changed.length) {
container.appendChild(el('div', 'alert alert-info mb-0', 'No changes: the files already hold exactly this.'));
return 0;
}
changed.forEach(function (f) {
var box = el('div', 'diff-file');
var head = el('div', 'diff-file-head');
head.appendChild(el('i', 'bi bi-file-earmark-diff'));
head.appendChild(el('span', 'mono', f.name));
if (!f.existed) head.appendChild(el('span', 'badge badge-info', 'new file'));
if (f.reformat) {
var w = el('span', 'badge badge-warning', 'whitespace also changes');
w.title = 'The current file is not laid out the way it will be written; ' +
'unchanged lines may show as changed.';
head.appendChild(w);
}
box.appendChild(head);
var pre = el('pre', 'diff-view');
String(f.diff || '').split('\n').forEach(function (line, i, all) {
if (i === all.length - 1 && line === '') return;
var c = '';
if (line.indexOf('+++') === 0 || line.indexOf('---') === 0) c = 'd-meta';
else if (line.indexOf('@@') === 0) c = 'd-hunk';
else if (line.charAt(0) === '+') c = 'd-add';
else if (line.charAt(0) === '-') c = 'd-del';
pre.appendChild(el('span', c, line || ' '));
});
box.appendChild(pre);
container.appendChild(box);
});
return changed.length;
}
function reviewAndSave(opts) {
var modalEl = document.getElementById('imageReviewModal');
var body = document.getElementById('imageReviewBody');
var err = document.getElementById('imageReviewError');
var confirmBtn = document.getElementById('imageReviewConfirm');
var title = document.getElementById('imageReviewTitle');
if (!modalEl || !window.bootstrap) return;
title.textContent = opts.title || 'Review changes';
body.innerHTML = '<div class="text-muted py-3">Building the preview...</div>';
err.classList.add('d-none');
confirmBtn.disabled = true;
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
function fail(msg) {
err.textContent = msg;
err.classList.remove('d-none');
}
postJSON(opts.previewUrl, opts.body).then(function (r) {
if (!r.ok) { body.innerHTML = ''; fail(r.data.error || 'Preview failed.'); return; }
var n = renderDiff(body, r.data.files);
var expect = {};
(r.data.files || []).forEach(function (f) { expect[f.name] = f.sha_before; });
confirmBtn.disabled = n === 0;
confirmBtn.onclick = function () {
confirmBtn.disabled = true;
var payload = Object.assign({}, opts.body, { expect: expect });
postJSON(opts.saveUrl, payload).then(function (s) {
if (!s.ok) { fail(s.data.error || 'Save failed.'); confirmBtn.disabled = false; return; }
if (opts.onSaved) opts.onSaved(s.data);
else window.location.reload();
}).catch(function (e) { fail('Save failed: ' + e); confirmBtn.disabled = false; });
};
}).catch(function (e) { body.innerHTML = ''; fail('Preview failed: ' + e); });
}
window.PxeImage = { postJSON: postJSON, renderDiff: renderDiff, reviewAndSave: reviewAndSave };
})();