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).
225 lines
12 KiB
HTML
225 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}Hardware coverage - PXE Manager{% endblock %}
|
|
|
|
{# 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_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 new_chip(found, file) -%}
|
|
{% if file and file | lower in found %}
|
|
<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 %}
|
|
{% 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 '')) }}
|
|
|
|
<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 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">▾</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="hl-empty">
|
|
No machines have reported yet. Models appear here the next time a machine boots WinPE from this server.
|
|
</div>
|
|
{% endif %}
|
|
</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 %}
|