From fd00d5ff89c4f535651bbda41bbe3006453378b3 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 14 Oct 2026 14:29:03 -0400 Subject: [PATCH] Webapp redesign phase 6: the remaining pages Restyled to the same row pattern (one line, details in an expandable body, one action plus a menu, tokens for light and dark): - hardware coverage, audit log (family chips, destructive actions in red, client-side filters, first 300 rows), Clonezilla backups, enrollment packages (active package marked and undeletable, MCL flags with a visible legend) - image Overview, OS and packages, Unattend (sticky save bar), Import - Dell, iPXE, Blancco and BPRT update pages; Dell history links to the rollback review instead of rolling back on one click - boot menu editor (tabs, rows with a menu, lint chips that jump to the line, tokens instead of hard-coded dark colours) and the imaging session detail page Fixes found on the way: - BPRT batch Activate never asked to confirm: a hidden input named 'confirm' shadowed window.confirm in the inline handler, so the form submitted without a prompt. It now calls window.confirm. - Backup upload refused when a backup with that name exists, unless 'Replace the existing backup' is ticked (it overwrote silently). - app.js: em dashes in comments replaced (plain ASCII). --- webapp/app.py | 4 + webapp/static/app.js | 4 +- webapp/static/unattend-editor.js | 10 +- webapp/templates/audit.html | 216 ++++-- webapp/templates/backups.html | 163 ++--- webapp/templates/enrollment.html | 474 +++++++------ webapp/templates/hardware.html | 274 ++++++-- webapp/templates/image_os.html | 474 +++++++------ webapp/templates/image_overview.html | 496 +++++++------- webapp/templates/imaging_detail.html | 327 ++++----- webapp/templates/import.html | 297 ++++----- webapp/templates/startnet_editor.html | 923 +++++++++++++------------- webapp/templates/unattend_editor.html | 498 +++++++------- webapp/templates/updates_blancco.html | 658 ++++++++++-------- webapp/templates/updates_bprt.html | 850 +++++++++++++----------- webapp/templates/updates_dell.html | 755 +++++++++++---------- webapp/templates/updates_ipxe.html | 620 +++++++++-------- 17 files changed, 3913 insertions(+), 3130 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index dc0318f..8e78cc3 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -560,6 +560,10 @@ def clonezilla_upload(): os.makedirs(config.CLONEZILLA_SHARE, exist_ok=True) dest = os.path.join(config.CLONEZILLA_SHARE, filename) + # A backup is a whole disk image: never replace one by accident. + if os.path.exists(dest) and request.form.get("overwrite") != "1": + flash(f"{filename} already exists. Tick Replace the existing backup to overwrite it.", "danger") + return redirect(url_for("clonezilla_backups")) f.save(dest) audit("BACKUP_UPLOAD", filename) flash(f"Uploaded {filename} successfully.", "success") diff --git a/webapp/static/app.js b/webapp/static/app.js index 3d2c2b1..1173978 100644 --- a/webapp/static/app.js +++ b/webapp/static/app.js @@ -203,7 +203,7 @@ document.addEventListener('DOMContentLoaded', function () { }); // ----------------------------------------------------------------------- - // AutoLogon Enabled toggle — keep hidden input in sync + // AutoLogon Enabled toggle - keep hidden input in sync // ----------------------------------------------------------------------- var autologonToggle = document.getElementById('autologonEnabledToggle'); if (autologonToggle) { @@ -216,7 +216,7 @@ document.addEventListener('DOMContentLoaded', function () { } // ----------------------------------------------------------------------- - // OOBE toggle switches — keep hidden input in sync + // OOBE toggle switches - keep hidden input in sync // ----------------------------------------------------------------------- document.querySelectorAll('.oobe-toggle').forEach(function (cb) { cb.addEventListener('change', function () { diff --git a/webapp/static/unattend-editor.js b/webapp/static/unattend-editor.js index 3ed768e..a7ec94d 100644 --- a/webapp/static/unattend-editor.js +++ b/webapp/static/unattend-editor.js @@ -167,6 +167,8 @@ var rd = rawDirty(); var head = document.querySelector('.image-head'); if (head) head.classList.toggle('dirty', fd || rd); + var page = document.getElementById('unaPage'); + if (page) page.classList.toggle('dirty', fd || rd); if (reviewBtn) reviewBtn.disabled = !(fd || rd); var now = sectionState(f); var groups = {}; @@ -415,7 +417,7 @@ return box; } var bad = lint.new_errors > 0; - var a = el('div', 'alert mb-0 ' + (bad ? 'alert-danger' : 'alert-warning')); + var a = el('div', 'notice una-lintbox' + (bad ? ' bad' : '')); a.appendChild(el('strong', '', bad ? 'This change adds ' + lint.new_errors + ' lint error' + (lint.new_errors === 1 ? '' : 's') + ' and will not be saved. One error invalidates the whole answer file for its pass.' @@ -423,8 +425,8 @@ var ul = el('ul', 'una-lint-list'); lint.findings.forEach(function (f) { var li = el('li'); - li.appendChild(el('span', 'badge ' + (f.level === 'ERROR' ? 'badge-danger' : 'badge-warning'), f.level)); - if (f['new']) li.appendChild(el('span', 'badge badge-info ms-1', 'new')); + li.appendChild(el('span', 'chip ' + (f.level === 'ERROR' ? 'c-err' : 'c-warn'), f.level)); + if (f['new']) li.appendChild(el('span', 'chip c-edit', 'new')); li.appendChild(document.createTextNode(' ' + f.message)); ul.appendChild(li); }); @@ -462,7 +464,7 @@ if (!r.ok) { out.innerHTML = ''; fail(r.data.error || 'Preview failed.'); return; } var n = PxeImage.renderDiff(out, r.data.files); if ((mode === 'form' && rd) || (mode === 'raw' && fd)) { - out.insertBefore(el('div', 'alert alert-info', + out.insertBefore(el('div', 'notice info', (mode === 'form' ? 'Your Raw XML edits are' : 'Your Form edits are') + ' not part of this save and are lost when the page reloads after it.'), out.firstChild); } diff --git a/webapp/templates/audit.html b/webapp/templates/audit.html index 9711606..bf0eecb 100644 --- a/webapp/templates/audit.html +++ b/webapp/templates/audit.html @@ -1,60 +1,176 @@ {% extends "base.html" %} -{% block title %}Audit Log - PXE Server Manager{% endblock %} +{% import '_ui.html' as ui %} +{% block title %}Audit log - PXE Manager{% endblock %} + +{# Audit log, newest first. One line per entry: time, source, action chip and + the details truncated with a tip; the full details are in the row body. + Chips are coloured by action family; destructive actions use c-err. The + whole log is rendered; the script shows the first 300 matches and a + "Show more" control. #} + +{% set families = [ + ('images', 'Images', 'w11'), + ('drivers', 'Drivers and BIOS', 'off'), + ('enrollment', 'Enrollment', 'ok'), + ('blancco', 'Blancco', 'neutral'), + ('imaging', 'Imaging', 'w10'), + ('boot', 'Boot files', 'neutral'), + ('backups', 'Backups', 'w10'), + ('other', 'Other', 'neutral'), +] %} +{% set fam_kind = {} %}{% for k, l, c in families %}{% set _ = fam_kind.update({k: c}) %}{% endfor %} + +{% macro family(a) -%} +{%- if a.startswith('IMAGING_') -%}imaging +{%- elif a.startswith(('IMAGE_', 'UNATTEND_', 'SHOPFLOOR_MENU', 'STARTNET', 'PACKAGE_')) -%}images +{%- elif a.startswith(('DRIVER_', 'DELL_', 'HARDWARE_', 'BIOS_')) -%}drivers +{%- elif a.startswith(('ENROLLMENT_', 'PPKG_', 'MCL_', 'BPRT_')) -%}enrollment +{%- elif a.startswith(('BLANCCO_', 'REPORT_')) -%}blancco +{%- elif a.startswith('IPXE_') -%}boot +{%- elif a.startswith('BACKUP_') -%}backups +{%- else -%}other{%- endif -%} +{%- endmacro %} + +{% block extra_head %} + +{% endblock %} {% block content %} - +{{ ui.head('Audit log', 'Tools', (entries | length) ~ ' entr' ~ ('ies' if entries | length != 1 else 'y') ~ ', newest first.') }} -
-
Activity History
+
+

Activity history

{% if entries %} -
- - - - - - - - - - - {% for entry in entries %} - - {% set parts = entry.split(' ', 1) %} - {% if parts|length == 2 %} - {% set meta = parts[1].split('] ', 1) %} - - {% if meta|length == 2 %} - - {% set action_detail = meta[1].split(': ', 1) %} - {% if action_detail|length == 2 %} - - - {% else %} - - {% endif %} - {% else %} - - {% endif %} - {% else %} - - {% endif %} - - {% endfor %} - -
TimestampSourceActionDetails
{{ parts[0] }}{{ meta[0].lstrip('[') }}{{ action_detail[0] }}{{ action_detail[1] }}{{ meta[1] }}{{ parts[1] }}{{ entry }}
+ {# Parse once, so the filter pills can show counts. #} + {% set rows = [] %} + {% for entry in entries %} + {% set parts = entry.split(' ', 1) %} + {% set r = {'raw': entry, 'when': '', 'src': '', 'action': '', 'detail': entry} %} + {% if parts | length == 2 %} + {% set _ = r.update({'when': parts[0].split(',')[0], 'detail': parts[1]}) %} + {% set meta = parts[1].split('] ', 1) %} + {% if meta | length == 2 %} + {% set _ = r.update({'src': meta[0].lstrip('['), 'detail': meta[1]}) %} + {% set ad = meta[1].split(': ', 1) %} + {% if ad | length == 2 %}{% set _ = r.update({'action': ad[0], 'detail': ad[1]}) %}{% endif %} + {% endif %} + {% endif %} + {% set fam = family(r.action) if r.action else 'other' %} + {% set _ = r.update({'fam': fam, 'bad': r.action and ('DELETE' in r.action or 'ROLLBACK' in r.action or 'REMOVE' in r.action)}) %} + {% set _ = rows.append(r) %} + {% endfor %} + {% set bad_count = rows | selectattr('bad') | list | length %} + +
+
+ + +
+
+ + {% if bad_count %}{% endif %} + {% for k, label, c in families %} + {% set n = rows | selectattr('fam', 'equalto', k) | list | length %} + {% if n %}{% endif %} + {% endfor %} +
+
+ + + {% for r in rows %} +
+
+ + {{ r.when or '-' }} + {{ r.src or '-' }} + {% if r.action %}{{ ui.chip(r.action, 'err' if r.bad else fam_kind[r.fam]) }}{% else %}-{% endif %} + {{ r.detail or '-' }} + + +
+

{{ r.detail or '(no details)' }}

+

{% if r.src %}From {{ r.src }} | {% endif %}{{ r.raw }}

+
+
+
+ {% endfor %} + + {% else %} -
- -

No audit log entries yet.

-

Actions like image imports, unattend edits, and backup operations will be logged here.

+
+ No audit log entries yet. Image imports, unattend edits, backup operations and other changes are logged here.
{% endif %} -
+
+{% endblock %} + +{% block extra_scripts %} + {% endblock %} diff --git a/webapp/templates/backups.html b/webapp/templates/backups.html index 9a91c6c..4aec16e 100644 --- a/webapp/templates/backups.html +++ b/webapp/templates/backups.html @@ -1,100 +1,107 @@ {% extends "base.html" %} -{% block title %}Clonezilla Backups - PXE Server Manager{% endblock %} +{% import '_ui.html' as ui %} +{% block title %}Clonezilla backups - PXE Manager{% endblock %} + +{# Clonezilla backups: one line per zip on the share (machine number, file, + size, date). Download is the row's action; Delete is in the menu and opens + the confirm modal. #} + +{% block extra_head %} + +{% endblock %} {% block content %} - +{% call ui.head('Clonezilla backups', 'Tools', (backups | length) ~ ' backup' ~ ('s' if backups | length != 1) ~ ' on the share') %} + +{% endcall %} -
-
- - Machine Backups - {{ backups|length }} -
-
- {% if backups %} -
- - - - - - - - - - - - {% for b in backups %} - - - - - - - - {% endfor %} - -
Machine #FilenameSizeLast ModifiedActions
{{ b.machine }}{{ b.filename }}{{ "%.1f"|format(b.size / 1073741824) }} GB{{ b.modified | timestamp_fmt }} - - Download - - -
+
+
+

Machine backups

+ {% if backups %} + + {% for b in backups %} +
+
+ {{ b.machine }} + {{ b.filename }} + {{ "%.1f"|format(b.size / 1073741824) }} GB + {{ b.modified | timestamp_fmt }}
- {% else %} -
- -

No backups found. Upload a Clonezilla backup .zip to get started.

+
+ Download + {% call ui.more(b.machine) %} + +
+ + {% endcall %}
- {% endif %}
-
+ {% endfor %} + {% else %} +
No backups found. Upload a Clonezilla backup .zip to get started.
+ {% endif %} +
-
-
Backup Naming Convention
-

- Name backup files with the machine number (e.g., 6501.zip). - The Samba share \\pxe-server\clonezilla is also available on the network for direct Clonezilla save/restore operations. -

+
+

Naming

+

Name backup files with the machine number, for example 6501.zip. The machine column is the file name without .zip.

+

The Samba share \\pxe-server\clonezilla is also on the network for direct Clonezilla save and restore.

+
-