Files
pxe-server/webapp/templates/image_overview.html
cproudlock 397063f987 Webapp: Updates pages, image page tabs with review-before-save, BPRT sync
Updates group:
- Dell drivers and BIOS: check Dell's catalogs against all three driver maps
  and models.txt, download with a shared speed cap, repack, preview, apply
  with backups and rollback.
- iPXE boot files: check GitHub for the signed Secure Boot set, verify the
  sha256 and each file's signer, install atomically, roll back. Includes the
  by-hand procedure.
- Blancco: upload a release, stage it, test-boot it from the menu, activate,
  roll back.
- BPRT sync: validate a batch uploaded from the MediaCreator Lite cache
  (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site
  overlay files kept.

Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and
Packages tabs. Every save shows a diff first, keeps the file's format, makes a
backup and can be undone. Driver lint runs inline.

Enrollment: upload standard/engineer MCL packages, one per tenant/purpose,
with archive and restore. Hardware coverage counts engineer as Win11.
2026-10-13 16:58:55 -04:00

262 lines
15 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ friendly_name }} - Overview{% endblock %}
{% block extra_head %}
<style>
.ov-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.ov-tile {
display: block; text-decoration: none; color: var(--text);
background: var(--bg-card); border: 1px solid var(--border); border-radius: 0.25rem;
border-left: 4px solid var(--border);
padding: 0.9rem 1rem; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
transition: border-color 0.15s;
}
a.ov-tile:hover { color: var(--text); border-color: var(--primary); }
.ov-tile.ok { border-left-color: var(--success); }
.ov-tile.warn { border-left-color: var(--warning); }
.ov-tile.bad { border-left-color: var(--danger); }
.ov-tile .label { font-size: 11px; color: var(--text-light); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
.ov-tile .value { font-size: 1.6rem; font-weight: 600; line-height: 1.2; margin: 0.25rem 0 0.15rem; }
.ov-tile .value.small-value { font-size: 0.95rem; line-height: 1.35; }
.ov-tile .detail { font-size: 12px; color: var(--text-light); }
.ov-tile.bad .value { color: var(--danger); }
.ov-tile.warn .value { color: var(--warning-dark); }
[data-theme="dark"] .ov-tile.warn .value { color: var(--warning); }
.ov-attn { list-style: none; padding: 0; margin: 0; }
.ov-attn li { display: flex; gap: 0.6rem; align-items: flex-start; padding: 0.55rem 1.25rem; border-bottom: 1px solid var(--border); font-size: 14px; }
.ov-attn li:last-child { border-bottom: none; }
.ov-attn .bi { margin-top: 0.15rem; }
.ov-attn .bi-x-octagon { color: var(--danger); }
.ov-attn .bi-exclamation-triangle { color: var(--warning); }
.ov-attn .bi-info-circle { color: var(--primary); }
.ov-attn a { margin-left: auto; white-space: nowrap; font-size: 13px; }
.ov-table td { vertical-align: top; white-space: normal; }
.ov-table .file-cell { overflow-wrap: anywhere; }
.ov-table .small { font-size: 12px; }
.ov-list-more { font-size: 12.5px; color: var(--text-light); padding: 0.5rem 1.25rem; }
</style>
{% endblock %}
{% block content %}
{% set header_actions %}
<a href="{{ url_for('hardware_page') }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-cpu"></i> Hardware Coverage</a>
<a href="{{ url_for('dell_updates.page') }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-motherboard"></i> Dell Updates</a>
{% 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 #}
{% set bad_lint = lint.map_errors if lint.available else 0 %}
{% set os_row = ov.os.current %}
{# -------- tiles -------- #}
<div class="ov-grid">
<a class="ov-tile {{ 'ok' if d.total else 'bad' }}" href="{{ image_tab_url(image_type, 'drivers') }}">
<div class="label">Driver entries</div>
<div class="value">{{ d.total }}</div>
<div class="detail">{{ d.by_os.Win11 }} Win11 | {{ d.by_os.Win10 }} Win10{% if d.by_os.Other %} | {{ d.by_os.Other }} other{% endif %}</div>
</a>
<a class="ov-tile {{ 'bad' if d.missing else 'ok' }}" href="{{ image_tab_url(image_type, 'drivers') }}">
<div class="label">Driver zips missing</div>
<div class="value">{{ d.missing | length }}</div>
<div class="detail">{% if d.missing %}{{ d.missing | selectattr('os', 'equalto', 'Win11') | list | length }} of them Win11{% else %}every entry's zip is on the share{% endif %}</div>
</a>
<a class="ov-tile {{ ('bad' if bad_lint else ('warn' if lint.total else 'ok')) if lint.available else '' }}" href="{{ image_tab_url(image_type, 'drivers') }}">
<div class="label">Driver map lint</div>
{% if lint.available %}
<div class="value">{{ lint.total }}</div>
<div class="detail">{% for lv, n in lint.by_level | dictsort %}{{ n }} {{ lv | lower }}{% if not loop.last %} | {% endif %}{% else %}no findings{% endfor %}</div>
{% else %}
<div class="value small-value text-muted">Not available</div>
<div class="detail">Shown once the Drivers tab lint is installed</div>
{% endif %}
</a>
<a class="ov-tile {{ ('ok' if os_row.wim.exists else 'bad') if os_row else 'bad' }}" href="{{ image_tab_url(image_type, 'os') }}">
<div class="label">Operating system</div>
{% if os_row %}
<div class="value small-value">{{ os_row.osv.productName or os_row.osv.marketingName }} {{ os_row.osv.versionNumber }}</div>
<div class="detail">osId {{ os_row.osv.id }} |
{% if os_row.wim.exists %}install.wim on disk ({{ (os_row.wim.size / 1073741824) | round(1) }} GB){% else %}<strong>install.wim not on disk</strong>{% endif %}</div>
{% else %}
<div class="value small-value">None</div>
<div class="detail">OperatingSystem.json is empty or missing</div>
{% endif %}
</a>
<a class="ov-tile {{ 'bad' if ov.packages.missing else 'ok' }}" href="{{ image_tab_url(image_type, 'os') }}">
<div class="label">Packages</div>
<div class="value">{{ ov.packages.enabled }}<span class="detail"> of {{ ov.packages.total }} enabled</span></div>
<div class="detail">{% if ov.packages.missing %}{{ ov.packages.missing | length }} enabled file{{ 's' if ov.packages.missing | length != 1 }} missing{% else %}all enabled files on disk{% endif %}</div>
</a>
<a class="ov-tile {{ ('bad' if ov.gaps else 'ok') if ov.gaps_checked else '' }}" href="{{ url_for('hardware_page') }}">
<div class="label">Hardware gaps</div>
{% if ov.gaps_checked %}
<div class="value">{{ ov.gaps | length }}</div>
<div class="detail">models seen by WinPE with no working driver entry here</div>
{% else %}
<div class="value small-value text-muted">Not tracked</div>
<div class="detail">Hardware Coverage checks the three GE images only</div>
{% endif %}
</a>
<a class="ov-tile {{ 'warn' if ov.dell.rows else ('ok' if ov.dell.checked else '') }}" href="{{ url_for('dell_updates.page') }}">
<div class="label">Dell updates</div>
{% if ov.dell.checked %}
<div class="value">{{ ov.dell.rows | length }}</div>
<div class="detail">newer driver pack{{ 's' if ov.dell.rows | length != 1 }} | checked {{ ov.dell.checked | local_dt }}</div>
{% else %}
<div class="value small-value text-muted">Not checked</div>
<div class="detail">Run a check on Updates, Dell Drivers and BIOS</div>
{% endif %}
</a>
</div>
{# -------- needs attention -------- #}
{% set attn = [] %}
{% if d.missing %}{% set _ = attn.append(('x-octagon', d.missing | length ~ ' driver entr' ~ ('ies point' if d.missing | length != 1 else 'y points') ~ ' at a zip that is not on the share. PESetup only warns, and the PC comes up without NIC or WiFi drivers.', image_tab_url(image_type, 'drivers'), 'Drivers')) %}{% endif %}
{% if bad_lint %}{% set _ = attn.append(('x-octagon', bad_lint ~ ' driver map lint error' ~ ('s' if bad_lint != 1) ~ ' in the entries themselves (a token or order PESetup will mis-match; see the findings on the Drivers tab).', image_tab_url(image_type, 'drivers'), 'Drivers')) %}{% endif %}
{% if not os_row %}{% set _ = attn.append(('x-octagon', 'OperatingSystem.json has no entry: PESetup has no Windows image to apply.', image_tab_url(image_type, 'os'), 'OS and Packages')) %}
{% elif not os_row.wim.exists %}{% set _ = attn.append(('x-octagon', 'install.wim is not on disk in ' ~ (os_row.dest or 'the configured folder (DestinationDir is empty)') ~ '.', image_tab_url(image_type, 'os'), 'OS and Packages')) %}{% endif %}
{% if ov.packages.missing %}{% set _ = attn.append(('exclamation-triangle', ov.packages.missing | length ~ ' enabled package file' ~ ('s are' if ov.packages.missing | length != 1 else ' is') ~ ' not on disk.', image_tab_url(image_type, 'os'), 'OS and Packages')) %}{% endif %}
{% if ov.gaps %}{% set _ = attn.append(('exclamation-triangle', ov.gaps | length ~ ' model' ~ ('s' if ov.gaps | length != 1) ~ ' booted WinPE here with no working driver entry in this image.', url_for('hardware_page'), 'Hardware Coverage')) %}{% endif %}
{% if ov.unattend_override %}{% set _ = attn.append(('exclamation-triangle', 'Deploy/Tools/GE/XML/FlatUnattendW10.xml exists. PESetup loads that file first, so the Unattend tab (which edits Deploy/FlatUnattendW10.xml) is not what imaging uses.', image_tab_url(image_type, 'unattend'), 'Unattend')) %}{% endif %}
{% if ov.hw_drivers_extra %}{% set _ = attn.append(('info-circle', 'Deploy/Control/hw_drivers.json exists. PESetup never reads it; only HardwareDriver.json decides drivers.', image_tab_url(image_type, 'drivers'), 'Drivers')) %}{% endif %}
{% if ov.dell.rows %}{% set _ = attn.append(('info-circle', ov.dell.rows | length ~ ' driver pack' ~ ('s have' if ov.dell.rows | length != 1 else ' has') ~ ' a newer Dell release (last check ' ~ (ov.dell.checked | local_dt) ~ ').', url_for('dell_updates.page'), 'Dell Updates')) %}{% endif %}
{% if ov.models.no_entry %}{% set _ = attn.append(('info-circle', ov.models.no_entry | length ~ ' hardware model selection' ~ ('s name' if ov.models.no_entry | length != 1 else ' names') ~ ' a driver family with no entry in HardwareDriver.json (media build selection only; imaging does not use it).', image_tab_url(image_type, 'os'), 'OS and Packages')) %}{% endif %}
<div class="card">
<div class="card-header">
<span class="d-flex align-items-center gap-2"><i class="bi bi-clipboard-pulse"></i> Needs attention</span>
<span class="badge {{ 'badge-danger' if attn | selectattr(0, 'equalto', 'x-octagon') | list else ('badge-warning' if attn else 'badge-success') }}">{{ attn | length or 'All clear' }}</span>
</div>
{% if attn %}
<ul class="ov-attn">
{% for icon, text, link, label in attn %}
<li><i class="bi bi-{{ icon }}"></i><span>{{ text }}</span><a href="{{ link }}">{{ label }} <i class="bi bi-chevron-right"></i></a></li>
{% endfor %}
</ul>
{% else %}
<div class="card-body text-muted">Driver zips, install.wim and packages are all on disk and no seen model is missing drivers.</div>
{% endif %}
</div>
{# -------- control files -------- #}
<div class="card">
<div class="card-header">
<span class="d-flex align-items-center gap-2"><i class="bi bi-folder2-open"></i> Control files</span>
</div>
<div class="table-container">
<table class="table mb-0 ov-table">
<thead><tr><th>File</th><th>Last modified</th><th class="text-end">Size</th><th class="text-end">Backups</th></tr></thead>
<tbody>
{% for f in ov.files %}
<tr>
<td>
<a href="{{ image_tab_url(image_type, f.tab) }}" class="mono">{{ f.name }}</a>
<div class="small text-muted">{{ f.what }}</div>
<div class="small text-muted mono file-cell">{{ f.rel }}</div>
</td>
<td class="text-nowrap">
{% if f.exists %}{{ f.mtime | local_dt }}{% else %}<span class="badge badge-danger">Missing</span>{% endif %}
</td>
<td class="text-end text-nowrap mono">{% if f.exists %}{{ f.size | filesizeformat }}{% else %}-{% endif %}</td>
<td class="text-end mono">{{ f.backups or '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% set saves = ov.last_saves | dictsort | selectattr(1) | list %}
{% if saves %}
<div class="card-footer small text-muted">
Last saves from this app:
{% for scope, rec in saves %}{{ {'drivers': 'Drivers', 'unattend': 'Unattend', 'os': 'OS and Packages'}[scope] }} {{ rec.ts | local_dt }} ({{ rec.files | map(attribute='name') | join(', ') }}){% if not loop.last %}; {% endif %}{% endfor %}
</div>
{% endif %}
</div>
<div class="row gx-4 gy-0">
<div class="col-12 col-xl-6">
{# -------- coverage gaps -------- #}
<div class="card">
<div class="card-header">
<span class="d-flex align-items-center gap-2"><i class="bi bi-cpu"></i> Hardware coverage gaps</span>
<a href="{{ url_for('hardware_page') }}" class="small fw-normal">Hardware Coverage</a>
</div>
{% if not ov.gaps_checked %}
<div class="card-body text-muted small">This image is not one of the images Hardware Coverage checks.</div>
{% elif ov.gaps %}
<div class="table-container">
<table class="table mb-0 ov-table">
<thead><tr><th>Model (WMI)</th><th>Problem</th><th>Last seen</th></tr></thead>
<tbody>
{% for g in ov.gaps[:8] %}
<tr>
<td class="mono">{{ g.model }}{% if g.skus %}<div class="small text-muted">SKU {{ g.skus | join(', ') }}</div>{% endif %}</td>
<td>{% if g.status == 'no-entry' %}<span class="badge badge-danger">No entry</span>{% else %}<span class="badge badge-danger" title="{{ g.file }}">Zip missing</span>{% endif %}</td>
<td class="text-nowrap small">{{ g.last_seen | local_dt }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if ov.gaps | length > 8 %}<div class="ov-list-more">and {{ ov.gaps | length - 8 }} more on Hardware Coverage</div>{% endif %}
{% else %}
<div class="card-body text-muted small">Every model that has booted WinPE here matches a driver entry with its zip on disk.</div>
{% endif %}
</div>
{# -------- dell hints -------- #}
{% if ov.dell.rows %}
<div class="card">
<div class="card-header">
<span class="d-flex align-items-center gap-2"><i class="bi bi-motherboard"></i> Newer Dell driver packs</span>
<a href="{{ url_for('dell_updates.page') }}" class="small fw-normal">Dell Updates</a>
</div>
<div class="table-container">
<table class="table mb-0 ov-table">
<thead><tr><th class="text-end">#</th><th>Current file</th><th>Dell has</th></tr></thead>
<tbody>
{% for r in ov.dell.rows[:8] %}
<tr>
<td class="text-end mono text-muted">{{ r.pos }}</td>
<td class="mono file-cell">{{ r.file }}<div class="small text-muted file-cell">{{ r.models }}</div></td>
<td><span class="badge badge-info">{{ r.ver or 'newer' }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if ov.dell.rows | length > 8 %}<div class="ov-list-more">and {{ ov.dell.rows | length - 8 }} more on Dell Updates</div>{% endif %}
</div>
{% endif %}
</div>
<div class="col-12 col-xl-6">
{# -------- missing zips -------- #}
{% if d.missing %}
<div class="card">
<div class="card-header">
<span class="d-flex align-items-center gap-2"><i class="bi bi-file-earmark-x"></i> Zips not on the share</span>
<a href="{{ image_tab_url(image_type, 'drivers') }}" class="small fw-normal">Drivers</a>
</div>
<div class="table-container">
<table class="table mb-0 ov-table">
<thead><tr><th class="text-end">#</th><th>File</th><th>OS</th></tr></thead>
<tbody>
{% for m in d.missing[:8] %}
<tr>
<td class="text-end mono text-muted">{{ m.pos }}</td>
<td class="mono file-cell">{{ m.file or '(no file name)' }}<div class="small text-muted file-cell">{{ m.models }}</div></td>
<td>{{ m.os }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if d.missing | length > 8 %}<div class="ov-list-more">and {{ d.missing | length - 8 }} more on the Drivers tab</div>{% endif %}
</div>
{% endif %}
</div>
</div>
{% endblock %}