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).
This commit is contained in:
cproudlock
2026-10-14 14:29:03 -04:00
parent a6f95fcca1
commit fd00d5ff89
17 changed files with 3913 additions and 3130 deletions

View File

@@ -1,88 +1,224 @@
{% extends "base.html" %}
{% block title %}Hardware Coverage - PXE Server Manager{% endblock %}
{% import '_ui.html' as ui %}
{% block title %}Hardware coverage - PXE Manager{% endblock %}
{% macro driver_badge(d) -%}
{% if d.status == 'ok' %}<span class="badge badge-success" title="{{ d.file }}">OK</span>
{% elif d.status == 'no-entry' %}<span class="badge badge-danger" title="No HardwareDriver.json entry matches this model">Missing</span>
{% elif d.status == 'zip-missing' %}<span class="badge badge-danger" title="Entry {{ d.file }} matches, but the zip is not on the server">Zip missing</span>
{% else %}<span class="text-muted">-</span>{% endif %}
{# Hardware coverage: one line per model WinPE has reported. The line shows
the model, one coverage chip per image, the BIOS state and when it was
last seen; the body holds SKUs, serials, PC types, BIOS versions and the
driver files each image resolves to. #}
{% set drv_chip = {
'ok': ('OK', 'ok'),
'no-entry': ('Missing', 'err'),
'zip-missing': ('Zip missing', 'err'),
} %}
{# Chip for one image's driver status; the tip names the file or the reason. #}
{% macro driver_chip(d) -%}
{% set c = drv_chip.get(d.status) %}
{% if c %}
<span class="tip inl" data-tip="{% if d.status == 'ok' %}{{ d.file }}{% elif d.status == 'no-entry' %}No HardwareDriver.json entry matches this model{% else %}Entry {{ d.file }} matches, but the zip is not on the server{% endif %}">{{ ui.chip(c[0], c[1]) }}</span>
{%- else %}<span class="sub" title="Image not on this server">-</span>{% endif %}
{%- endmacro %}
{% macro bios_badge(b) -%}
{% if b.status == 'current' %}<span class="badge badge-success">{{ b.target }} (current)</span>
{% elif b.status == 'update' %}<span class="badge badge-info" title="{{ b.file }}">Flashes to {{ b.target }}</span>
{% elif b.status == 'file-missing' %}<span class="badge badge-danger" title="{{ b.file }} not in the BIOS folder">File missing</span>
{% else %}<span class="badge badge-danger">No BIOS entry</span>{% endif %}
{% macro bios_chip(b) -%}
{% if b.status == 'current' %}{{ ui.chip('Current ' ~ b.target, 'ok') }}
{% elif b.status == 'update' %}{{ ui.chip('Flashes to ' ~ b.target, 'neutral') }}
{% elif b.status == 'file-missing' %}{{ ui.chip('File missing', 'err') }}
{% else %}{{ ui.chip('No BIOS entry', 'err') }}{% endif %}
{%- endmacro %}
{# A newer Dell pack/BIOS for the file this model uses, from the last Updates > Dell scan. #}
{% macro update_hint(found, file) -%}
{% macro new_chip(found, file) -%}
{% if file and file | lower in found %}
<div class="mt-1"><a href="{{ url_for('dell_updates.page') }}" class="badge badge-info text-decoration-none" title="{{ file }}: Dell has {{ found[file | lower] or 'a newer version' }}. Open Updates to download it.">Update {{ found[file | lower] }}</a></div>
<a class="tip inl newlink" href="{{ url_for('dell_updates.page') }}" data-tip="{{ file }}: Dell has {{ found[file | lower] or 'a newer version' }}. Open Updates to download it.">{{ ui.chip('New', 'run') }}<span class="vh"> Dell version {{ found[file | lower] }}</span></a>
{% endif %}
{%- endmacro %}
{% block extra_head %}
<style>
.hgrid { display: grid; grid-template-columns: minmax(0, 1fr) 336px 170px 184px 14px; align-items: center;
gap: 0 14px; padding: 11px 72px 11px 24px; font-size: 14px; min-height: 24px; }
.hhead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; }
.hhead .cov span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.h-model { display: flex; align-items: center; gap: 8px; min-width: 0; }
.h-model .tip { flex: 0 1 auto; }
.h-model .mfr { flex: none; font-size: 12px; color: var(--muted); white-space: nowrap; }
.cov { display: grid; grid-template-columns: repeat(3, 104px); gap: 0 12px; align-items: center; }
.cov > span { display: flex; align-items: center; gap: 4px; min-width: 0; }
.hgrid .ilab { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.h-bios { display: flex; align-items: center; gap: 4px; min-width: 0; }
.h-seen { font-size: 13px; color: var(--muted); white-space: nowrap; }
.newlink { text-decoration: none; }
.newlink .chip { font-size: 11px; padding: 1px 6px; }
.hbody { padding: 2px 72px 16px 24px; font-size: 13px; display: flex; flex-direction: column; gap: 12px; }
.hfacts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: 10px 24px; margin: 0; }
.hfacts dt { font-size: 12px; font-weight: 400; color: var(--muted); }
.hfacts dd { margin: 0; overflow-wrap: anywhere; }
.hfiles { display: flex; flex-direction: column; gap: 6px; }
.hfile { display: grid; grid-template-columns: 190px 104px minmax(0, 1fr); gap: 4px 12px; align-items: baseline; }
.hfile .mono { overflow-wrap: anywhere; }
.hl-empty { padding: 28px 24px; text-align: center; color: var(--muted); border-top: 1px solid var(--line); }
.hintro { margin: 0; padding: 16px 24px 0; max-width: 80ch; }
@container (max-width: 1000px) {
.hgrid { grid-template-columns: minmax(0, 1fr) 336px 168px 14px; }
.hgrid .h-seen { display: none; }
}
@container (max-width: 760px) {
.hgrid { grid-template-columns: minmax(0, 1fr) 14px; padding: 12px 64px 12px 16px; gap: 6px 10px; }
.hhead { display: none; }
.hgrid .h-model { grid-column: 1; grid-row: 1; }
.hgrid .chev { grid-column: 2; grid-row: 1; }
.hgrid .cov, .hgrid .h-bios { grid-column: 1 / -1; }
.cov { display: flex; flex-wrap: wrap; gap: 6px 14px; }
.hgrid .ilab { position: static; width: auto; height: auto; overflow: visible; clip: auto; font-size: 12px; color: var(--muted); }
.hbody { padding: 2px 16px 14px; }
.hfile { grid-template-columns: minmax(0, 1fr) auto; }
.hfile .mono { grid-column: 1 / -1; }
.hintro { padding: 14px 16px 0; }
}
@container (min-width: 521px) and (max-width: 760px) {
.hgrid { grid-template-columns: auto minmax(0, 1fr) 14px; }
.hgrid .h-model { grid-column: 1 / 3; }
.hgrid .chev { grid-column: 3; }
.hgrid .cov { grid-column: 1; grid-row: 2; }
.hgrid .h-bios { grid-column: 2; grid-row: 2; padding-left: 14px; }
}
</style>
{% endblock %}
{% block content %}
<div class="page-header">
<h1><i class="bi bi-cpu"></i> Hardware Coverage</h1>
<div class="header-actions">
<span class="badge badge-secondary badge-lg">{{ rows | length }} model{{ 's' if rows | length != 1 }} seen</span>
</div>
</div>
{% set gaps = rows | selectattr('gap') | rejectattr('dismissed') | list %}
{% set dismissed = rows | selectattr('gap') | selectattr('dismissed') | list %}
{{ ui.head('Hardware coverage', 'Operate', (rows | length) ~ ' model' ~ ('s' if rows | length != 1) ~ ' seen' ~ (', ' ~ (gaps | length) ~ ' with open gaps' if gaps else '')) }}
<p class="text-muted">
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.
</p>
<div class="section-card">
<section class="panel ui-list" aria-labelledby="hl-h">
<h2 id="hl-h" class="vh">Models seen</h2>
<p class="sub hintro">
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.
</p>
{% if rows %}
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Model (WMI)</th>
<th>System SKU</th>
<th>BIOS seen</th>
{% for image, os_label in images %}<th title="{{ image }}">{{ image | replace('gea-', '') }} <span class="text-muted">({{ os_label }})</span></th>{% endfor %}
<th>BIOS update</th>
<th>Last seen</th>
<th class="text-end"></th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr title="Serials: {{ r.serials | join(', ') }}{% if r.pctypes %} | Types: {{ r.pctypes | join(', ') }}{% endif %}">
<td>
<span class="mono">{{ r.model }}</span>
{% if r.manufacturer %}<div class="small text-muted">{{ r.manufacturer }}</div>{% endif %}
</td>
<td class="mono">{{ r.skus | join(', ') or '-' }}</td>
<td class="mono">{{ r.last_bios_version or '-' }}</td>
{% for image, os_label in images %}<td>{{ driver_badge(r.drivers[image]) }}{{ update_hint(hints.drivers, r.drivers[image].file) }}</td>{% endfor %}
<td>{{ bios_badge(r.bios) }}{{ update_hint(hints.bios, r.bios.file) }}</td>
<td class="mono text-nowrap">{{ r.last_seen | local_dt }}<div class="small text-muted">{{ r.count }} boot{{ 's' if r.count != 1 }}</div></td>
<td class="text-end text-nowrap">
{% if r.gap %}
<form method="post" action="{{ url_for('hardware_dismiss') }}" style="display:inline;">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="model" value="{{ r.model }}">
<input type="hidden" name="done" value="{{ '0' if r.dismissed else '1' }}">
<button type="submit" class="pxe-btn pxe-btn-ghost btn-row-action">{{ 'Reopen' if r.dismissed else 'Dismiss' }}</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 16px 24px">
<div role="search" style="flex: 1 1 220px; min-width: 160px; display: flex">
<label for="hq" class="vh">Search by model, SKU, serial, PC type or driver file</label>
<input id="hq" type="search" class="field" style="flex: 1; min-width: 0" placeholder="Model, SKU, serial, PC type or file">
</div>
<div role="group" aria-label="Show only" id="hfilters" style="display: flex; gap: 6px; flex-wrap: wrap">
<button type="button" class="fbtn" data-f="" aria-pressed="true">All {{ rows | length }}</button>
<button type="button" class="fbtn" data-f="gap" aria-pressed="false">Gaps {{ gaps | length }}</button>
<button type="button" class="fbtn" data-f="dismissed" aria-pressed="false">Dismissed {{ dismissed | length }}</button>
</div>
</div>
<div class="hgrid hhead" aria-hidden="true">
<span>Model</span>
<span class="cov">{% for image, os_label in images %}<span title="{{ image }} ({{ os_label }})">{{ image | replace('gea-', '') }}</span>{% endfor %}</span>
<span>BIOS</span><span class="h-seen">Last seen</span><span></span>
</div>
{% for r in rows %}
<div class="ui-row hrow" data-f="{{ 'gap ' if r.gap and not r.dismissed }}{{ 'dismissed' if r.gap and r.dismissed }}"
data-q="{{ (r.model ~ ' ' ~ (r.manufacturer or '') ~ ' ' ~ (r.skus | join(' ')) ~ ' ' ~ (r.serials | join(' ')) ~ ' ' ~ (r.pctypes | join(' ')) ~ ' ' ~ (r.drivers.values() | map(attribute='file', default='') | join(' ')) ~ ' ' ~ (r.bios.file or '')) | lower }}">
<details>
<summary class="hgrid">
<span class="h-model">
<span class="tip" data-tip="{{ r.model }}"><span class="trunc" style="font-weight: 600">{{ r.model }}</span></span>
{% if r.manufacturer %}<span class="mfr">{{ r.manufacturer }}</span>{% endif %}
{% if r.gap and r.dismissed %}{{ ui.chip('Dismissed', 'off') }}{% endif %}
</span>
<span class="cov">
{% for image, os_label in images %}
<span><span class="ilab">{{ image | replace('gea-', '') }}</span>{{ driver_chip(r.drivers[image]) }}{{ new_chip(hints.drivers, r.drivers[image].file) }}</span>
{% endfor %}
</span>
<span class="h-bios"><span class="ilab">BIOS</span>{{ bios_chip(r.bios) }}{{ new_chip(hints.bios, r.bios.file) }}</span>
<span class="h-seen mono">{{ r.last_seen | local_dt }}</span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="hbody">
<dl class="hfacts">
<div><dt>Model (WMI)</dt><dd class="mono">{{ r.model }}</dd></div>
{% if r.manufacturer %}<div><dt>Manufacturer</dt><dd>{{ r.manufacturer }}</dd></div>{% endif %}
<div><dt>System SKU</dt><dd class="mono">{{ r.skus | join(', ') or '-' }}</dd></div>
<div><dt>Serials</dt><dd class="mono">{{ r.serials | join(', ') or '-' }}</dd></div>
<div><dt>PC types</dt><dd>{{ r.pctypes | join(', ') or '-' }}</dd></div>
<div><dt>BIOS versions seen</dt><dd class="mono">{{ r.bios_versions | join(', ') or '-' }}{% if r.last_bios_version %} <span class="sub">(last {{ r.last_bios_version }})</span>{% endif %}</dd></div>
<div><dt>Last seen</dt><dd>{{ r.last_seen | local_dt }}, {{ r.count }} boot{{ 's' if r.count != 1 }}</dd></div>
<div><dt>First seen</dt><dd>{{ r.first_seen | local_dt }}</dd></div>
</dl>
<div class="hfiles">
{% for image, os_label in images %}
{% set d = r.drivers[image] %}
<div class="hfile">
<span>{{ image }} <span class="sub">{{ os_label }}</span></span>
<span>{% if drv_chip.get(d.status) %}{{ ui.chip(drv_chip[d.status][0], drv_chip[d.status][1]) }}{% else %}<span class="sub">Not on server</span>{% endif %}</span>
<span>
{% if d.status == 'no-entry' %}No HardwareDriver.json entry matches this model.
{% elif d.status == 'zip-missing' %}<span class="mono">{{ d.file }}</span> matches, but the zip is not on the server.
{% elif d.file %}<span class="mono">{{ d.file }}</span>{% endif %}
{% if d.file and d.file | lower in hints.drivers %} <a href="{{ url_for('dell_updates.page') }}">Dell has {{ hints.drivers[d.file | lower] or 'a newer version' }}</a>{% endif %}
</span>
</div>
{% endfor %}
<div class="hfile">
<span>BIOS</span>
<span>{{ bios_chip(r.bios) }}</span>
<span>
{% if r.bios.status == 'none' %}No line in the WinPE BIOS manifest matches this model.
{% elif r.bios.status == 'file-missing' %}<span class="mono">{{ r.bios.file }}</span> is not in the BIOS folder.
{% elif r.bios.file %}<span class="mono">{{ r.bios.file }}</span>{% endif %}
{% if r.bios.file and r.bios.file | lower in hints.bios %} <a href="{{ url_for('dell_updates.page') }}">Dell has {{ hints.bios[r.bios.file | lower] or 'a newer version' }}</a>{% endif %}
</span>
</div>
</div>
</div>
</details>
{% if r.gap %}
<div class="act">
{% call ui.more(r.model) %}
<form method="post" action="{{ url_for('hardware_dismiss') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="model" value="{{ r.model }}">
<input type="hidden" name="done" value="{{ '0' if r.dismissed else '1' }}">
<button type="submit">{{ 'Reopen gap' if r.dismissed else 'Dismiss gap' }}</button>
</form>
{% endcall %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="hidden-bar" id="hnomatch" hidden><span>No model matches.</span></div>
{% else %}
<div class="text-center text-muted py-5">
<p class="mb-1">No machines have reported yet.</p>
<p class="small">Models appear here the next time a machine boots WinPE from this server.</p>
<div class="hl-empty">
No machines have reported yet. Models appear here the next time a machine boots WinPE from this server.
</div>
{% endif %}
</div>
</section>
{% endblock %}
{% block extra_scripts %}
<script>
(function () {
var q = document.getElementById('hq');
if (!q) return;
var f = '';
function apply() {
var needle = q.value.trim().toLowerCase(), any = false;
document.querySelectorAll('.hrow').forEach(function (r) {
var ok = (!needle || r.getAttribute('data-q').indexOf(needle) >= 0) && (!f || r.getAttribute('data-f').indexOf(f) >= 0);
r.hidden = !ok; if (ok) any = true;
});
document.getElementById('hnomatch').hidden = any;
}
q.addEventListener('input', apply);
document.querySelectorAll('#hfilters .fbtn').forEach(function (b) {
b.addEventListener('click', function () {
f = b.getAttribute('data-f') || '';
document.querySelectorAll('#hfilters .fbtn').forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); });
apply();
});
});
})();
</script>
{% endblock %}