Files
pxe-server/webapp/static/qr-render.js
cproudlock 69a1682a7f webapp: imaging UX overhaul + image management CRUD
Imaging dashboard
- services/imaging_log_tail.py: parses dnsmasq leases, Apache access log,
  Samba per-host log files, and dnsmasq syslog (DHCP/TFTP). Synthesizes
  inferred sessions keyed by MAC for bays that have only touched the boot
  chain but not yet pushed to /imaging/status. Active window 90 min.
- imaging_status.list_sessions() merges inferred sessions into the dashboard
  list. Real client-pushed sessions win for the same MAC.
- imaging_status: stage_history field tracks every stage transition (capped
  30); sidecar .log file per serial records every log_lines push uncapped
  (read_full_log() caps detail-page response to 1 MB).
- delete_session/delete_all_sessions clean up sidecar .log too.
- New SSE endpoint /imaging/stream emits a session-list hash every 5s.
  Client fetches /imaging/tiles (HTML partial) on hash change and swaps
  #imaging-tiles innerHTML. Polling fallback at 15s if SSE drops.
- Tile-swap preserves scroll, filter input, expanded state via localStorage,
  and any LAPS input the operator is mid-pasting (swap skipped when a
  laps-input is focused).
- imaging.html: removed 15s location.reload(). Added live-status dot in
  header (gray idle / green SSE connected / red SSE lost).
- _imaging_tiles.html: shared partial used by both /imaging full render and
  /imaging/tiles SSE refresh. Inferred bays render with yellow border +
  log-inferred badge + no progress bar (stage_index inference is coarse).
- imaging_detail.html (new): per-bay forensics page at /imaging/session/
  <serial>. Session metadata grid, stage timeline table, full sidecar log
  with truncation indicator, Copy-support-summary button. Linked from each
  client-pushed tile.
- qr-render.js exposes window.renderAllQRs() so the SSE swap can re-render
  Intune device-ID QRs in the swapped-in tiles.

Image management
- services/image_registry.py: JSON registry of image types at
  {SAMBA_SHARE}/image-registry.json. Bootstraps from baked-in
  config.IMAGE_TYPES on first run. create/clone/delete/rename_friendly
  mutate the file then call reload() which rewrites config.IMAGE_TYPES +
  config.FRIENDLY_NAMES in place. Sidebar reflects on next request.
- app.py routes: /images/new, /images/<t>/clone, /images/<t>/delete (with
  optional content-wipe checkbox), /images/<t>/rename.
- dashboard.html: + New image type button + Clone/Delete per row, all in
  Bootstrap modals with confirmation copy.
- Clone copies Deploy/ tree but preserves symlinks to shared dirs (Out-of-
  box Drivers, Operating Systems, Packages) so disk usage stays low.
- Delete with content checked unlinks symlinks (does not follow into shared
  dirs).

Driver / package upload + orphan adoption
- services/images.py: upload_driver, adopt_orphan, remove_orphans,
  upload_package. Filename sanitization blocks path traversal.
- app.py routes: /images/<t>/drivers/upload, /images/<t>/drivers/adopt,
  /images/<t>/drivers/orphans/delete, /images/<t>/packages/upload.
- image_config.html: Upload .zip button + modal on Drivers section. Orphan
  drivers card-footer rebuilt as interactive list with per-row Adopt inline
  form (family + destinationDir inputs) and bulk select+delete.
- Upload .zip on Packages section with optional destinationDir field that
  appends a packages.json entry.

Configuration
- config.py: new env vars DNSMASQ_LEASES, APACHE_ACCESS_LOG, SAMBA_LOG_DIR,
  DNSMASQ_SYSLOG for the log-tailer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:21:06 -04:00

48 lines
1.7 KiB
JavaScript

// QR render helper. Scans for any element with data-qr="<text>" and renders
// a Kazuhiko Arase qrcode-generator QR into it as inline <img>. Size is
// controlled via data-qr-size="N" (px square, default 96). Error-correction
// level via data-qr-ec="L|M|Q|H" (default M).
//
// The qrcode-generator lib (loaded before this script) exposes a global
// `qrcode()` factory: typeNumber 0 = auto, ec = 'L'|'M'|'Q'|'H'.
(function () {
function render(el) {
var text = el.getAttribute('data-qr') || '';
if (!text) return;
if (el.dataset.qrRendered === '1') return;
var size = parseInt(el.getAttribute('data-qr-size') || '96', 10);
var ec = el.getAttribute('data-qr-ec') || 'M';
try {
var qr = qrcode(0, ec);
qr.addData(text);
qr.make();
// createImgTag(cellSize, margin)
// 4-cell margin keeps the QR scannable per spec; cell size derived from
// requested pixel size and module count.
var modules = qr.getModuleCount();
var cellSize = Math.max(1, Math.floor(size / (modules + 8)));
el.innerHTML = qr.createImgTag(cellSize, 4);
el.dataset.qrRendered = '1';
el.title = 'Scan: ' + text;
} catch (e) {
el.textContent = '[QR error]';
}
}
function scan() {
var nodes = document.querySelectorAll('[data-qr]');
for (var i = 0; i < nodes.length; i++) render(nodes[i]);
}
// Exposed for callers that swap in new tiles dynamically (e.g. imaging
// dashboard SSE refresh). Idempotent because render() guards on
// dataset.qrRendered.
window.renderAllQRs = scan;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scan);
} else {
scan();
}
})();