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 %} -
| Timestamp | -Source | -Action | -Details | -|||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ parts[0] }} | - {% if meta|length == 2 %} -{{ meta[0].lstrip('[') }} | - {% set action_detail = meta[1].split(': ', 1) %} - {% if action_detail|length == 2 %} -{{ action_detail[0] }} | -{{ action_detail[1] }} | - {% else %} -{{ meta[1] }} | - {% endif %} - {% else %} -{{ parts[1] }} | - {% endif %} - {% else %} -{{ entry }} | - {% endif %} -||||||
{{ r.detail or '(no details)' }}
+{% if r.src %}From {{ r.src }} | {% endif %}{{ r.raw }}
+No audit log entries yet.
-Actions like image imports, unattend edits, and backup operations will be logged here.
+| Machine # | -Filename | -Size | -Last Modified | -Actions | -
|---|---|---|---|---|
| {{ b.machine }} | -{{ b.filename }} | -{{ "%.1f"|format(b.size / 1073741824) }} GB | -{{ b.modified | timestamp_fmt }} | -- - Download - - - | -
No backups found. Upload a Clonezilla backup .zip to get started.
+- 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. -
+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.
+| Filename | -Size | -Last Modified | -Actions | -
|---|---|---|---|
| {{ p.filename }}{% if is_active %} Active{% endif %} | -{{ "%.1f"|format(p.size / 1048576) }} MB | -{{ p.modified | timestamp_fmt }} | -- - Download - - {% if is_active %} - - {% else %} - - {% endif %} - | -
No enrollment packages found. Upload a .ppkg file to get started.
+- GCCH enrollment .ppkg provisioning packages are copied to - C:\Enrollment\ on the target machine after imaging. - At OOBE, connect to a network with internet, press Windows key 5 times, - then browse to C:\Enrollment\ and select the package. No USB stick needed. -
-- Naming convention: Use with-office.ppkg and - without-office.ppkg to match the WinPE enrollment menu options. -
-- Packages live in _shared/Applications/BPRT, which {{ mcl_users | join(', ') if mcl_users else 'no image' }} link{{ 's' if mcl_users|length == 1 }} to. - The bulk-enroll scripts pick the package by its first three name fields (tenant, environment, purpose) and fail - enrollment if two match, so an upload replaces the package with the same fields; the old one moves - to the archive below and can be restored. Every PC built from those images copies this folder while it images, so - upload, archive or restore only when no build is in progress. -
-- A whole new vendor drop (packages, Office zips and scripts from MediaCreator Lite) - goes through BPRT Sync instead: it uploads only the changed files from the work PC, checks - them and swaps the folder in one step, keeping the site's own scripts. -
- {% for l in mcl_links if l.state in ('copy', 'elsewhere') %} -Packages live in _shared/Applications/BPRT, which {{ mcl_users | join(', ') if mcl_users else 'no image' }} + link{{ 's' if mcl_users|length == 1 }} to. The bulk-enroll scripts pick the package by its first three name fields + (tenant, environment, purpose) and fail enrollment if two match, so an upload replaces the package with + the same fields; the old one moves to the archive below and can be restored. Every PC built from those images copies + this folder while it images, so upload, archive or restore only when no build is in progress.
+A whole new vendor drop (packages, Office zips and scripts from MediaCreator Lite) goes through + BPRT Sync instead: it uploads only the changed files from the work PC, checks them and + swaps the folder in one step, keeping the site's own scripts.
+| Tenant / purpose | Filename | Version | Size | Last Modified | |
|---|---|---|---|---|---|
| {{ p.prefix or '-' }} - {% if p.conflict %}Duplicate{% endif %} - {% if not p.prefix %}Ignored - {% elif not p.placeholder %}Used{% endif %} - | -{{ p.filename }} | -{{ p.version or '?' }} | -{{ "%.1f"|format(p.size / 1048576) }} MB | -{{ p.modified | timestamp_fmt }} | -- - | -
No MCL packages found in BPRT.
- {% endif %} - {% if mcl_archive %} -| Archived | Filename | Version | Size | |
|---|---|---|---|---|
| {{ p.batch }} | -{{ p.filename }} | -{{ p.version or '?' }} | -{{ "%.1f"|format(p.size / 1048576) }} MB | -- - | -
- Every model that has booted WinPE here, as reported by the machine itself (WMI model, Dell system SKU, - BIOS version), checked against each image's driver map and the WinPE BIOS manifest. A model missing a - Win11 driver pack or a BIOS entry also shows on the Technician To-Do until it is added or dismissed. -
- -+ Every model that has booted WinPE here, as the machine reported it (WMI model, Dell system SKU, BIOS version), + checked against each image's driver map and the WinPE BIOS manifest. A model missing a driver pack or a BIOS + entry also shows on the Technician to-do until it is added or dismissed. +
{% if rows %} -| Model (WMI) | -System SKU | -BIOS seen | - {% for image, os_label in images %}{{ image | replace('gea-', '') }} ({{ os_label }}) | {% endfor %} -BIOS update | -Last seen | -- |
|---|---|---|---|---|---|---|
|
- {{ r.model }}
- {% if r.manufacturer %} {{ r.manufacturer }} {% endif %}
- |
- {{ r.skus | join(', ') or '-' }} | -{{ r.last_bios_version or '-' }} | - {% for image, os_label in images %}{{ driver_badge(r.drivers[image]) }}{{ update_hint(hints.drivers, r.drivers[image].file) }} | {% endfor %} -{{ bios_badge(r.bios) }}{{ update_hint(hints.bios, r.bios.file) }} | -{{ r.last_seen | local_dt }} {{ r.count }} boot{{ 's' if r.count != 1 }} |
- - {% if r.gap %} - - {% endif %} - | -
No machines have reported yet.
-Models appear here the next time a machine boots WinPE from this server.
+| # | -Windows | -osId | -Status | -install.wim | -- |
|---|---|---|---|---|---|
| {{ loop.index }} | -
- {{ osv.productName or osv.marketingName or 'Windows' }}
- {{ osv.marketingName }}{% if osv.versionNumber %} | version {{ osv.versionNumber }}{% endif %}{% if osv.buildNumber %} | build {{ osv.buildNumber }}{% endif %}
- |
- {{ osv.id }} | -- {% if osv.isActive %}Active{% else %}Inactive{% endif %} - {% if os_selection is not none and osv.id | string == os_selection | string %}Selected{% endif %} - | -
- {{ r.dest or '(no DestinationDir)' }}
-
- {% if r.wim.exists %}On disk {{ r.wim.size | filesizeformat }}, modified {{ r.wim.mtime | local_dt }}
- {% else %}install.wim not on disk{% endif %}
- | index {{ osv.wimindex if osv.wimindex is defined else (r.entry.wimindex if r.entry.wimindex is defined else '-') }}
- {% if osv.wim and osv.wim.size and r.wim.exists and osv.wim.size != r.wim.size %}
- size differs from the JSON
- {% endif %}
-
- |
- - - | -
| No operating system entries. PESetup has nothing to apply. | |||||
|
+
+
+ + {{ loop.index }} + {{ osv.productName or osv.marketingName or 'Windows' }} {{ osv.versionNumber }} + {{ osv.id }} + + {% if osv.isActive %}{{ ui.chip('Active', 'ok') }}{% else %}{{ ui.chip('Inactive', 'off') }}{% endif %} + {% if os_selection is not none and osv.id | string == os_selection | string %}{{ ui.chip('Selected', 'w11', 'user_selections.json OperatingSystemSelection') }}{% endif %} + + {% if r.wim.exists %}{{ ui.chip('On disk', 'ok') }}{% else %}{{ ui.chip('Not on disk', 'err') }}{% endif %}{% if size_differs %}{{ ui.chip('Size differs', 'warn') }}{% endif %} + {{ r.dest or '(no DestinationDir)' }} + ++
+ {% call ui.more('operating system ' ~ loop.index) %}{% endcall %}
+
+ |
| No operating system entries. PESetup has nothing to apply. |
| # | -Package | -File | -osId | -Enabled | -On disk | -- |
|---|---|---|---|---|---|---|
| {{ loop.index }} | -
- {{ p.name or '(no name)' }}
- {% if p.comment %} {{ p.comment }} {% endif %}
- |
-
- {{ p.fileName or p.FileName or '' }}
- {{ p.destinationDir or p.DestinationDir or '' }}
- |
- {{ p.osId }} | -
-
-
-
- |
-
- {% if r.file.exists %}Yes {{ r.file.size | filesizeformat }}
- {% else %}No{% endif %}
- |
- - - | -
| No packages configured. | ||||||
|
+
+
+ + {{ loop.index }} + {{ p.name or '(no name)' }} + {{ fname }} + {{ p.osId }} + {% if p.enabled %}{{ ui.chip('Enabled', 'ok') }}{% else %}{{ ui.chip('Disabled', 'off') }}{% endif %} + {% if r.file.exists %}{{ ui.chip('On disk', 'ok') }}{% else %}{{ ui.chip('Missing', 'err') }}{% endif %} + ++
+
+
+
+
+ {% call ui.more('package ' ~ loop.index) %}{% endcall %}
+ |
| No packages configured. |
- Media build selection, not used while imaging.
+{% set n_none = models | selectattr('fam.entries', 'equalto', 0) | list | length %}
+
Media Creator Lite saved this list when the media was built: the driver families (Family ID) whose packs it
downloaded, and the OS it built for. scripts/Download-Drivers.ps1 and
Upload-Image.ps1 read it to choose which driver packs and OS to fetch and upload.
+
PESetup never reads this file: at imaging it matches the PC's WMI model against
HardwareDriver.json alone, so adding a model here does not
- give a PC drivers. The Driver entries column counts map entries with the same family.
+ give a PC drivers. Driver entries counts map entries with the same family.
+
+
+ Hardware models
+ {{ ui.chip(models | length, 'neutral') }}
+ {% if n_none %}{{ ui.chip(n_none ~ ' with no driver entry', 'warn') }}{% endif %}
+ Media build selection, not used while imaging
+
-
-
+
+
+
+
-
-
-
- {% for r in models %}
- #
- Model
- Family ID
- Driver entries
-
-
-
+ {% endfor %}
+ {{ loop.index }}
-
-
-
+
+
+
+ {% for r in models %}
+
+
+
+
+
- {% endfor %}
-
+
-
-
-
-
-
- {% if has_selections %}No hardware models selected.{% else %}No user_selections.json for this image.{% endif %}
+
+ {% if has_selections %}No hardware models selected.{% else %}No user_selections.json for this image.{% endif %} ' +
- ' ' +
- ' ' +
- ' New ' +
- '';
+ ' ';
tbody.insertBefore(tr, tbody.querySelector('.empty-row'));
rowsOf('ospModelTable').forEach(function (r, i) { r.querySelector('.order-num').textContent = i + 1; });
refreshEmpty(tbody.closest('table'));
@@ -391,6 +438,15 @@
});
});
+ var discardBtn = document.getElementById('ospDiscard');
+ if (discardBtn) {
+ discardBtn.addEventListener('click', function () {
+ if (!confirm('Discard the unsaved changes on this page?')) return;
+ dirty = {};
+ window.location.reload();
+ });
+ }
+
window.addEventListener('beforeunload', function (e) {
if (Object.keys(dirty).length) { e.preventDefault(); e.returnValue = ''; }
});
diff --git a/webapp/templates/image_overview.html b/webapp/templates/image_overview.html
index 973516e..1345df8 100644
--- a/webapp/templates/image_overview.html
+++ b/webapp/templates/image_overview.html
@@ -1,261 +1,313 @@
{% extends "base.html" %}
+{% import '_ui.html' as ui %}
{% block title %}{{ friendly_name }} - Overview{% endblock %}
+{# Overview of one image: what needs attention first (each item links to the
+ tab that fixes it), a calm row of counts, the control files one line each
+ (description in the row body), then compact lists of hardware gaps, newer
+ Dell packs and missing zips. Deep links into the Drivers tab use its
+ ?q=, ?os=, ?missing=1 and ?findings=1 filters. #}
+
{% block extra_head %}
{% endblock %}
{% block content %}
{% set header_actions %}
- Hardware Coverage
- Dell Updates
+ Hardware coverage
+ Dell updates
{% endset %}
{% include "_image_header.html" %}
{% set d = ov.drivers %}
{% set lint = ov.lint %}
-{# missing-zip findings have their own tile; bad_lint is what is wrong in the map itself #}
+{# missing-zip findings are counted on their own; bad_lint is what is wrong in the map itself #}
{% set bad_lint = lint.map_errors if lint.available else 0 %}
{% set os_row = ov.os.current %}
+{% set drv_url = image_tab_url(image_type, 'drivers') %}
+{% set os_url = image_tab_url(image_type, 'os') %}
-{# -------- tiles -------- #}
-Needs attention
+ {% if n_err %}{{ ui.chip(n_err ~ ' error' ~ ('s' if n_err != 1), 'err') }}{% endif %}
+ {% if attn | length > n_err %}{{ ui.chip((attn | length - n_err) ~ ' other', 'neutral') }}{% endif %}
- {% for icon, text, link, label in attn %}
-
{% else %}
- At a glance
-
+
-
- {% for f in ov.files %}
- File Last modified Size Backups
-
- {% endfor %}
-
-
- {{ f.name }}
-
-
- {% if f.exists %}{{ f.mtime | local_dt }}{% else %}Missing{% endif %}
-
- {% if f.exists %}{{ f.size | filesizeformat }}{% else %}-{% endif %}
- {{ f.backups or '-' }}
- Control files
+ {{ f.name }}
+ {% if f.exists %}{{ f.mtime | local_dt }}{% else %}{{ ui.chip('Missing', 'err') }}{% endif %}
+ {% if f.exists %}{{ f.size | filesizeformat }}{% else %}-{% endif %}
+ {{ f.backups or '-' }}
+
+
+