Files
pxe-server/webapp/templates/image_overview.html
cproudlock fd00d5ff89 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).
2026-10-14 14:29:03 -04:00

314 lines
19 KiB
HTML

{% 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 %}
<style>
/* Needs attention */
.ov-attn { list-style: none; margin: 0; padding: 0; }
.ov-attn li { display: grid; grid-template-columns: 74px minmax(0, 1fr) auto; gap: 4px 14px; align-items: start;
padding: 12px 24px; border-top: 1px solid var(--line); font-size: 14px; line-height: 1.5; }
.ov-attn li .chip { justify-self: start; margin-top: 1px; }
.ov-attn li a { white-space: nowrap; font-size: 13px; font-weight: 600; text-decoration: none; }
.ov-attn li a:hover { text-decoration: underline; }
.ov-clear { padding: 12px 24px 16px; border-top: 1px solid var(--line); font-size: 14px; color: var(--text-2); display: flex; gap: 10px; align-items: center; }
.ov-phead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 16px 24px 12px; }
.ov-phead h2 { margin: 0; font-size: 16px; font-weight: 600; }
.ov-phead .sp { margin-left: auto; font-size: 13px; }
/* Summary counts */
.ov-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
@container (max-width: 760px) { .ov-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ov-stat { display: flex; flex-direction: column; gap: 4px; padding: 16px 20px; text-decoration: none; color: var(--text);
border-top: 1px solid var(--line); border-left: 1px solid var(--line); margin: 0 0 -1px -1px; min-width: 0; }
a.ov-stat:hover { background: var(--surface-2); color: var(--text); }
.ov-stat .lbl { font-size: 13px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ov-stat .st { display: flex; flex-wrap: wrap; gap: 6px; }
.ov-stat .val { font-size: 24px; font-weight: 600; line-height: 1.2; }
.ov-stat .val.txt { font-size: 15px; line-height: 1.35; }
.ov-stat .det { font-size: 12px; color: var(--muted); }
.ov-stats-wrap { overflow: hidden; border-radius: 0 0 14px 14px; container-type: inline-size; }
/* Control files: one line each */
.cf-grid { display: grid; grid-template-columns: minmax(0, 1fr) 190px 80px 72px 14px; align-items: center; gap: 0 14px;
padding: 11px 100px 11px 24px; font-size: 14px; min-height: 24px; }
.cf-head { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; }
.cf-grid .r { text-align: right; white-space: nowrap; }
.cf-grid .when { white-space: nowrap; font-size: 13px; color: var(--text-2); }
.cf-body { padding: 0 24px 14px 24px; font-size: 13px; color: var(--text-2); display: flex; flex-direction: column; gap: 4px; }
.cf-body .mono { overflow-wrap: anywhere; color: var(--muted); }
.ui-row > .act { top: 8px; }
/* Compact lists */
.ov-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); gap: 22px; align-items: start; }
.cl { list-style: none; margin: 0; padding: 0; }
.cl li { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 0 12px; align-items: center; padding: 10px 24px; border-top: 1px solid var(--line); font-size: 14px; }
.cl li .when { font-size: 12px; color: var(--muted); white-space: nowrap; }
.cl li a.rowlink { color: var(--text); text-decoration: none; min-width: 0; }
.cl li a.rowlink:hover { color: var(--accent); }
.cl .pos { color: var(--muted); font-size: 12px; }
.cl-more { padding: 10px 24px 14px; border-top: 1px solid var(--line); font-size: 13px; }
.cl-empty { padding: 12px 24px 16px; border-top: 1px solid var(--line); font-size: 13px; color: var(--muted); }
@container (max-width: 760px) {
.cf-grid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 11px 90px 11px 16px; }
.cf-grid .c-size, .cf-grid .c-bak { display: none; }
.cf-head { display: none; }
.cf-body { padding: 0 16px 14px; }
}
@container (max-width: 480px) {
.cf-grid { grid-template-columns: minmax(0, 1fr) 14px; }
.cf-grid .when { grid-column: 1; grid-row: 2; }
.cf-grid .chev { grid-column: 2; grid-row: 1; }
}
@media (max-width: 600px) {
.ov-attn li { grid-template-columns: auto minmax(0, 1fr); padding: 12px 16px; }
.ov-attn li a { grid-column: 2; }
.ov-phead, .ov-clear, .cl li, .cl-more, .cl-empty { padding-left: 16px; padding-right: 16px; }
.cl li { grid-template-columns: minmax(0, 1fr) auto; }
.cl li .when { grid-column: 1; }
}
</style>
{% endblock %}
{% block content %}
{% set header_actions %}
<a href="{{ url_for('hardware_page') }}" class="ui-btn">Hardware coverage</a>
<a href="{{ url_for('dell_updates.page') }}" class="ui-btn">Dell updates</a>
{% endset %}
{% include "_image_header.html" %}
{% set d = ov.drivers %}
{% set lint = ov.lint %}
{# 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') %}
{# -------- needs attention: (kind, text, link, label) -------- #}
{% set attn = [] %}
{% if d.missing %}{% set _ = attn.append(('err', 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', missing=1), 'Drivers')) %}{% endif %}
{% if bad_lint %}{% set _ = attn.append(('err', 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', findings=1), 'Drivers')) %}{% endif %}
{% if not os_row %}{% set _ = attn.append(('err', 'OperatingSystem.json has no entry: PESetup has no Windows image to apply.', os_url, 'OS and Packages')) %}
{% elif not os_row.wim.exists %}{% set _ = attn.append(('err', 'install.wim is not on disk in ' ~ (os_row.dest or 'the configured folder (DestinationDir is empty)') ~ '.', os_url, 'OS and Packages')) %}{% endif %}
{% if ov.packages.missing %}{% set _ = attn.append(('warn', ov.packages.missing | length ~ ' enabled package file' ~ ('s are' if ov.packages.missing | length != 1 else ' is') ~ ' not on disk.', os_url, 'OS and Packages')) %}{% endif %}
{% if ov.gaps %}{% set _ = attn.append(('warn', 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(('warn', '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(('neutral', 'Deploy/Control/hw_drivers.json exists. PESetup never reads it; only HardwareDriver.json decides drivers.', drv_url, 'Drivers')) %}{% endif %}
{% if ov.dell.rows %}{% set _ = attn.append(('neutral', 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(('neutral', 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).', os_url, 'OS and Packages')) %}{% endif %}
{% set kind_label = {'err': 'Error', 'warn': 'Warning', 'neutral': 'Note'} %}
{% set n_err = attn | selectattr(0, 'equalto', 'err') | list | length %}
<div class="ui-stack">
<section class="panel" aria-labelledby="ov-attn-h">
<div class="ov-phead">
<h2 id="ov-attn-h">Needs attention</h2>
{% 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 %}
</div>
{% if attn %}
<ul class="ov-attn">
{% for kind, text, link, label in attn %}
<li>{{ ui.chip(kind_label[kind], kind) }}<span>{{ text }}</span><a href="{{ link }}">Open {{ label }}</a></li>
{% endfor %}
</ul>
{% else %}
<div class="ov-clear">{{ ui.chip('All clear', 'ok') }} Driver zips, install.wim and packages are all on disk and no seen model is missing drivers.</div>
{% endif %}
</section>
{# -------- summary counts -------- #}
<section class="panel" aria-labelledby="ov-sum-h">
<div class="ov-phead"><h2 id="ov-sum-h">At a glance</h2></div>
<div class="ov-stats-wrap"><div class="ov-stats">
{% for osn in ('Win11', 'Win10') %}
<a class="ov-stat" href="{{ image_tab_url(image_type, 'drivers', os=osn) }}">
<span class="lbl">{{ osn }} driver entries</span>
<span class="val">{{ d.by_os[osn] }}</span>
<span class="st">{{ ui.chip(osn, 'w11' if osn == 'Win11' else 'w10') }}{% if not d.total and osn == 'Win11' %} {{ ui.chip('Map empty', 'err') }}{% endif %}</span>
<span class="det">of {{ d.total }} entries{% if d.by_os.Other and osn == 'Win10' %}, {{ d.by_os.Other }} other{% endif %}</span>
</a>
{% endfor %}
<a class="ov-stat" href="{{ image_tab_url(image_type, 'drivers', missing=1) if d.missing else drv_url }}">
<span class="lbl">Zips missing</span>
<span class="val">{{ d.missing | length }}</span>
<span class="st">{{ ui.chip('Missing', 'err') if d.missing else ui.chip('OK', 'ok') }}</span>
<span class="det">{% if d.missing %}{{ d.missing | selectattr('os', 'equalto', 'Win11') | list | length }} of them Win11{% else %}every zip is on the share{% endif %}</span>
</a>
<a class="ov-stat" href="{{ image_tab_url(image_type, 'drivers', findings=1) if lint.available and lint.total else drv_url }}">
{% if lint.available %}
<span class="lbl">Map lint</span>
<span class="val">{{ lint.total }}</span>
<span class="st">{{ ui.chip('Errors', 'err') if bad_lint else (ui.chip('Findings', 'warn') if lint.total else ui.chip('Clean', 'ok')) }}</span>
<span class="det">{% for lv, n in lint.by_level | dictsort %}{{ n }} {{ lv | lower }}{% if not loop.last %}, {% endif %}{% else %}no findings{% endfor %}</span>
{% else %}
<span class="lbl">Map lint</span>
<span class="st">{{ ui.chip('Not available', 'off') }}</span>
<span class="det">Shown once the Drivers tab lint is installed</span>
{% endif %}
</a>
<a class="ov-stat" href="{{ os_url }}">
{% if os_row %}
<span class="lbl">Operating system</span>
<span class="val txt">{{ os_row.osv.productName or os_row.osv.marketingName }} {{ os_row.osv.versionNumber }}</span>
<span class="st">{{ ui.chip('On disk', 'ok') if os_row.wim.exists else ui.chip('wim missing', 'err') }}</span>
<span class="det">osId {{ os_row.osv.id }}{% if os_row.wim.exists %}, install.wim {{ (os_row.wim.size / 1073741824) | round(1) }} GB{% endif %}</span>
{% else %}
<span class="lbl">Operating system</span>
<span class="st">{{ ui.chip('None', 'err') }}</span>
<span class="det">OperatingSystem.json is empty or missing</span>
{% endif %}
</a>
<a class="ov-stat" href="{{ os_url }}">
<span class="lbl">Packages</span>
<span class="val">{{ ov.packages.enabled }} <span class="det">of {{ ov.packages.total }} enabled</span></span>
<span class="st">{{ ui.chip('File missing', 'warn') if ov.packages.missing else ui.chip('OK', 'ok') }}</span>
<span class="det">{% 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 %}</span>
</a>
<a class="ov-stat" href="{{ url_for('hardware_page') }}">
{% if ov.gaps_checked %}
<span class="lbl">Hardware gaps</span>
<span class="val">{{ ov.gaps | length }}</span>
<span class="st">{{ ui.chip('Gaps', 'warn') if ov.gaps else ui.chip('OK', 'ok') }}</span>
<span class="det">seen models with no working driver entry</span>
{% else %}
<span class="lbl">Hardware gaps</span>
<span class="st">{{ ui.chip('Not tracked', 'off') }}</span>
<span class="det">Hardware coverage checks the three GE images only</span>
{% endif %}
</a>
<a class="ov-stat" href="{{ url_for('dell_updates.page') }}">
{% if ov.dell.checked %}
<span class="lbl">Dell updates</span>
<span class="val">{{ ov.dell.rows | length }}</span>
<span class="st">{{ ui.chip('Newer packs', 'neutral') if ov.dell.rows else ui.chip('Current', 'ok') }}</span>
<span class="det">checked {{ ov.dell.checked | local_dt }}</span>
{% else %}
<span class="lbl">Dell updates</span>
<span class="st">{{ ui.chip('Not checked', 'off') }}</span>
<span class="det">Run a check on Updates, Dell drivers and BIOS</span>
{% endif %}
</a>
</div></div>
</section>
{# -------- control files -------- #}
<section class="panel ui-list" aria-labelledby="ov-cf-h">
<div class="ov-phead"><h2 id="ov-cf-h">Control files</h2></div>
<div class="cf-grid cf-head" aria-hidden="true"><span>File</span><span>Last modified</span><span class="r c-size">Size</span><span class="r c-bak">Backups</span><span></span></div>
{% for f in ov.files %}
<div class="ui-row">
<details>
<summary class="cf-grid">
<span class="tip" data-tip="{{ f.rel }}"><span class="trunc mono">{{ f.name }}</span></span>
<span class="when">{% if f.exists %}{{ f.mtime | local_dt }}{% else %}{{ ui.chip('Missing', 'err') }}{% endif %}</span>
<span class="r mono c-size">{% if f.exists %}{{ f.size | filesizeformat }}{% else %}-{% endif %}</span>
<span class="r mono c-bak">{{ f.backups or '-' }}</span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="cf-body">
<span>{{ f.what }}</span>
<span class="mono">{{ f.rel }}</span>
<span>{% if f.exists %}{{ f.size | filesizeformat }}, {% endif %}{{ f.backups }} backup{{ 's' if f.backups != 1 }} next to the file</span>
</div>
</details>
<div class="act"><a class="ui-btn sm" href="{{ image_tab_url(image_type, f.tab) }}" aria-label="Open {{ f.name }}">Open</a></div>
</div>
{% endfor %}
{% set saves = ov.last_saves | dictsort | selectattr(1) | list %}
{% if saves %}
<div class="hidden-bar">
<span>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 %}</span>
</div>
{% endif %}
</section>
<div class="ov-cols">
{# -------- coverage gaps -------- #}
<section class="panel" aria-labelledby="ov-gap-h">
<div class="ov-phead">
<h2 id="ov-gap-h">Hardware coverage gaps</h2>
{% if ov.gaps %}{{ ui.chip(ov.gaps | length, 'warn') }}{% endif %}
<a class="sp" href="{{ url_for('hardware_page') }}">Hardware coverage</a>
</div>
{% if not ov.gaps_checked %}
<div class="cl-empty">This image is not one of the images Hardware coverage checks.</div>
{% elif ov.gaps %}
<ul class="cl">
{% for g in ov.gaps[:8] %}
<li>
<a class="rowlink tip" href="{{ image_tab_url(image_type, 'drivers', q=g.model) }}" data-tip="{{ g.model }}{% if g.skus %} (SKU {{ g.skus | join(', ') }}){% endif %}{% if g.file %}: {{ g.file }}{% endif %}">
<span class="trunc mono">{{ g.model }}</span></a>
{% if g.status == 'no-entry' %}{{ ui.chip('No entry', 'err') }}{% else %}{{ ui.chip('Zip missing', 'err') }}{% endif %}
<span class="when">{{ g.last_seen | local_dt }}</span>
</li>
{% endfor %}
</ul>
{% if ov.gaps | length > 8 %}<div class="cl-more"><a href="{{ url_for('hardware_page') }}">{{ ov.gaps | length - 8 }} more on Hardware coverage</a></div>{% endif %}
{% else %}
<div class="cl-empty">Every model that has booted WinPE here matches a driver entry with its zip on disk.</div>
{% endif %}
</section>
{# -------- dell hints -------- #}
<section class="panel" aria-labelledby="ov-dell-h">
<div class="ov-phead">
<h2 id="ov-dell-h">Newer Dell driver packs</h2>
{% if ov.dell.rows %}{{ ui.chip(ov.dell.rows | length, 'neutral') }}{% endif %}
<a class="sp" href="{{ url_for('dell_updates.page') }}">Dell updates</a>
</div>
{% if ov.dell.rows %}
<ul class="cl">
{% for r in ov.dell.rows[:8] %}
<li>
<a class="rowlink tip" href="{{ image_tab_url(image_type, 'drivers', q=r.file) }}" data-tip="Entry {{ r.pos }}: {{ r.file }}{% if r.models %} ({{ r.models }}){% endif %}">
<span class="trunc"><span class="pos">{{ r.pos }}</span> <span class="mono">{{ r.file }}</span></span></a>
{{ ui.chip(r.ver or 'newer', 'neutral') }}
<span></span>
</li>
{% endfor %}
</ul>
{% if ov.dell.rows | length > 8 %}<div class="cl-more"><a href="{{ url_for('dell_updates.page') }}">{{ ov.dell.rows | length - 8 }} more on Dell updates</a></div>{% endif %}
{% elif ov.dell.checked %}
<div class="cl-empty">Every driver pack in this map is the newest Dell has (checked {{ ov.dell.checked | local_dt }}).</div>
{% else %}
<div class="cl-empty">Not checked yet. Run a check on Updates, Dell drivers and BIOS.</div>
{% endif %}
</section>
{# -------- missing zips -------- #}
{% if d.missing %}
<section class="panel" aria-labelledby="ov-miss-h">
<div class="ov-phead">
<h2 id="ov-miss-h">Zips not on the share</h2>
{{ ui.chip(d.missing | length, 'err') }}
<a class="sp" href="{{ image_tab_url(image_type, 'drivers', missing=1) }}">Drivers</a>
</div>
<ul class="cl">
{% for m in d.missing[:8] %}
<li>
<a class="rowlink tip" href="{{ image_tab_url(image_type, 'drivers', q=m.file) if m.file else image_tab_url(image_type, 'drivers', missing=1) }}" data-tip="Entry {{ m.pos }}: {{ m.file or '(no file name)' }}{% if m.models %} ({{ m.models }}){% endif %}">
<span class="trunc"><span class="pos">{{ m.pos }}</span> <span class="mono">{{ m.file or '(no file name)' }}</span></span></a>
{{ ui.chip(m.os, 'w11' if m.os == 'Win11' else 'w10') }}
<span></span>
</li>
{% endfor %}
</ul>
{% if d.missing | length > 8 %}<div class="cl-more"><a href="{{ image_tab_url(image_type, 'drivers', missing=1) }}">{{ d.missing | length - 8 }} more on the Drivers tab</a></div>{% endif %}
</section>
{% endif %}
</div>
</div>
{% endblock %}