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:
@@ -1,411 +1,500 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dell Drivers and BIOS - PXE Server Manager{% endblock %}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% block title %}Dell drivers and BIOS - PXE Manager{% endblock %}
|
||||
|
||||
{% macro size(n) -%}
|
||||
{%- if not n -%}-{%- elif n >= 1000000000 -%}{{ '%.2f' | format(n / 1000000000) }} GB{%- elif n >= 1000000 -%}{{ '%.0f' | format(n / 1000000) }} MB{%- else -%}{{ '%.0f' | format(n / 1000) }} KB{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro drv_badge(s) -%}
|
||||
{% if s == 'update' %}<span class="badge badge-warning">Update</span>
|
||||
{% elif s == 'current' %}<span class="badge badge-success">Current</span>
|
||||
{% elif s == 'no-match' %}<span class="badge badge-secondary" title="No Win11 pack in Dell's catalog matches these model names">No match</span>
|
||||
{% elif s == 'unknown-version' %}<span class="badge badge-info" title="The current file name has no A-number to compare">Unknown version</span>
|
||||
{% else %}<span class="badge badge-secondary">{{ s }}</span>{% endif %}
|
||||
{% macro drv_chip(r) -%}
|
||||
{% if r.status == 'update' and r.warnings %}{{ ui.chip('Update: decide', 'warn') }}
|
||||
{% elif r.status == 'update' %}{{ ui.chip('Update', 'act') }}
|
||||
{% elif r.status == 'current' %}{{ ui.chip('Current', 'ok') }}
|
||||
{% elif r.status == 'no-match' %}{{ ui.chip('No match', 'off') }}
|
||||
{% elif r.status == 'unknown-version' %}{{ ui.chip('Unknown version', 'neutral') }}
|
||||
{% else %}{{ ui.chip(r.status, 'neutral') }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro bios_badge(s) -%}
|
||||
{% if s == 'update' %}<span class="badge badge-warning">Update</span>
|
||||
{% elif s == 'current' %}<span class="badge badge-success">Current</span>
|
||||
{% elif s == 'different-file-family' %}<span class="badge badge-danger" title="Dell's latest BIOS for these models is a different file than ours">Different file</span>
|
||||
{% elif s == 'ambiguous' %}<span class="badge badge-danger" title="Several BIOS file families match this line">Ambiguous</span>
|
||||
{% elif s == 'no-match' %}<span class="badge badge-secondary" title="No Dell model in the catalog index matches this line">No match</span>
|
||||
{% elif s == 'error' %}<span class="badge badge-danger">Catalog error</span>
|
||||
{% else %}<span class="badge badge-secondary">{{ s }}</span>{% endif %}
|
||||
{% macro bios_chip(s) -%}
|
||||
{% if s == 'update' %}{{ ui.chip('Update', 'act') }}
|
||||
{% elif s == 'current' %}{{ ui.chip('Current', 'ok') }}
|
||||
{% elif s == 'different-file-family' %}{{ ui.chip('Different file', 'err') }}
|
||||
{% elif s == 'ambiguous' %}{{ ui.chip('Ambiguous', 'err') }}
|
||||
{% elif s == 'no-match' %}{{ ui.chip('No match', 'off') }}
|
||||
{% elif s == 'error' %}{{ ui.chip('Catalog error', 'err') }}
|
||||
{% else %}{{ ui.chip(s, 'neutral') }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro stage_badge(s) -%}
|
||||
{% if s == 'ready' %}<span class="badge badge-success">Ready</span>
|
||||
{% elif s == 'applied' %}<span class="badge badge-primary">Applied</span>
|
||||
{% elif s in ('error', 'interrupted', 'cancelled') %}<span class="badge badge-danger">{{ s | capitalize }}</span>
|
||||
{% else %}<span class="badge badge-info">{{ s | capitalize }}</span>{% endif %}
|
||||
{% macro stage_chip(s) -%}
|
||||
{% if s == 'ready' %}{{ ui.chip('Ready', 'ok') }}
|
||||
{% elif s == 'applied' %}{{ ui.chip('Applied', 'neutral') }}
|
||||
{% elif s in ('error', 'interrupted', 'cancelled') %}{{ ui.chip(s | capitalize, 'err') }}
|
||||
{% else %}{{ ui.chip(s | capitalize, 'run') }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{# One driver-pack row. The checkbox sits outside <summary> so ticking it
|
||||
never toggles the row. #}
|
||||
{% macro drv_row(r) -%}
|
||||
<div class="ui-row dl-row" data-status="{{ r.status }}">
|
||||
{% if r.pack and r.status != 'current' or r.pack and r.warnings %}
|
||||
<div class="pick"><input type="checkbox" class="form-check-input" name="drv" value="{{ r.id }}" id="pick-{{ r.id }}" {% if r.default %}checked{% endif %}>
|
||||
<label for="pick-{{ r.id }}" class="vh">Download the pack for {{ r.tokens | join(', ') }}</label></div>
|
||||
{% endif %}
|
||||
<details>
|
||||
<summary class="dgrid">
|
||||
<span class="tip" data-tip="{{ r.tokens | join(', ') }}"><span class="trunc">{{ r.tokens | join(', ') }}</span></span>
|
||||
<span class="mono c-chg">{% if r.pack %}{{ r.cur_ver or '?' }} to {{ r.pack.ver }}{% else %}{{ r.cur_ver or '-' }}{% endif %}</span>
|
||||
{% set imgs = r.images | map('replace', 'gea-', '') | join(', ') %}
|
||||
<span class="tip c-img" data-tip="{{ imgs }}"><span class="trunc sub">{{ imgs }}</span></span>
|
||||
<span>{{ drv_chip(r) }}</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="dbody">
|
||||
{% for w in r.warnings %}<div class="notice">{{ w }}</div>{% endfor %}
|
||||
<dl class="kv">
|
||||
<dt>Current file</dt><dd class="mono">{{ r.current }}</dd>
|
||||
{% if r.pack %}
|
||||
<dt>Dell pack</dt><dd><span class="mono">{{ r.pack.name }}</span><div class="sub">{{ r.pack.ver }}, {{ r.pack.date }}, {{ size(r.pack.size) }}</div></dd>
|
||||
<dt>Stages as</dt><dd class="mono">{{ r.pack.zip }}</dd>
|
||||
<dt>Pack covers</dt><dd>{{ r.pack.models | join(', ') }}</dd>
|
||||
{% endif %}
|
||||
<dt>Models (WMI)</dt><dd>{{ r.tokens | join(', ') }}</dd>
|
||||
<dt>Images</dt><dd>{{ r.images | join(', ') }}</dd>
|
||||
</dl>
|
||||
{% for n in r.notes %}<div class="sub">{{ n }}</div>{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro bios_row(r) -%}
|
||||
<div class="ui-row dl-row" data-status="{{ r.status }}">
|
||||
{% if r.candidates and (r.status != 'current') %}
|
||||
<div class="pick"><input type="checkbox" class="form-check-input" name="bios" value="{{ r.id }}" id="pick-{{ r.id }}" {% if r.default %}checked{% endif %}>
|
||||
<label for="pick-{{ r.id }}" class="vh">Download the BIOS for line {{ r.sub }}</label></div>
|
||||
{% endif %}
|
||||
<details>
|
||||
<summary class="dgrid">
|
||||
<span class="tip" data-tip="{{ r.line }}"><span class="trunc mono">{{ r.sub }}</span></span>
|
||||
<span class="mono c-chg">{% if r.candidates %}{{ r.cur_ver }} to {{ r.candidates[0].version }}{% if r.candidates | length > 1 %} (+{{ r.candidates | length - 1 }}){% endif %}{% else %}{{ r.cur_ver or '-' }}{% endif %}</span>
|
||||
<span class="tip c-img" data-tip="{{ r.current }}"><span class="trunc sub mono">{{ r.current }}</span></span>
|
||||
<span>{{ bios_chip(r.status) }}</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="dbody">
|
||||
{% for w in r.warnings %}<div class="notice">{{ w }}</div>{% endfor %}
|
||||
<dl class="kv">
|
||||
<dt>Manifest line</dt><dd class="mono">{{ r.line }}</dd>
|
||||
<dt>Current file</dt><dd><span class="mono">{{ r.current }}</span> <span class="sub">{{ r.cur_ver }}</span></dd>
|
||||
<dt>Dell latest</dt>
|
||||
<dd>
|
||||
{% if r.candidates | length > 1 %}
|
||||
<label for="pick_{{ r.id }}" class="vh">BIOS file for line {{ r.sub }}</label>
|
||||
<select class="field" id="pick_{{ r.id }}" name="pick_{{ r.id }}" style="max-width: 100%">
|
||||
{% for c in r.candidates %}<option value="{{ loop.index0 }}">{{ c.file }} ({{ c.version }}, {{ c.date }}) - {{ c.models | join(', ') | truncate(70) }}</option>{% endfor %}
|
||||
</select>
|
||||
{% elif r.candidates %}
|
||||
{% set c = r.candidates[0] %}
|
||||
<span class="mono">{{ c.file }}</span>
|
||||
<div class="sub">{{ c.version }}, {{ c.date }}, {{ size(c.size) }}; {{ c.models | join(', ') }}</div>
|
||||
<div class="sub mono" style="overflow-wrap: anywhere">sha256 {{ c.sha256 }}</div>
|
||||
{% else %}<span class="muted">none found</span>{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
{% for n in r.notes %}<div class="sub">{{ n }}</div>{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Long Dell file names wrap instead of pushing Status off the page. */
|
||||
.dell-updates td.wrap { min-width: 9rem; max-width: 22rem; }
|
||||
.sec-h { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 8px 16px; padding: 18px 24px 12px; }
|
||||
.sec-h .sub { margin-top: 2px; }
|
||||
summary.sec-h { flex-wrap: nowrap; }
|
||||
.state-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 6px; }
|
||||
.strip { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 12px 20px; font-size: 13px; }
|
||||
.strip form { margin: 0 0 0 auto; }
|
||||
.pulse { width: 10px; height: 10px; border-radius: 5px; background: var(--run); flex: none; animation: pulse 1.4s ease-in-out infinite; }
|
||||
@keyframes pulse { 50% { opacity: .35; } }
|
||||
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }
|
||||
.dl-row > .pick { position: absolute; left: 20px; top: 13px; z-index: 1; }
|
||||
.dl-row > .pick input { width: 18px; height: 18px; margin: 0; }
|
||||
.dgrid { display: grid; grid-template-columns: minmax(0, 1fr) 150px minmax(0, 200px) 130px 14px; align-items: center; gap: 0 16px;
|
||||
padding: 12px 24px 12px 56px; font-size: 14px; min-height: 24px; }
|
||||
.dgrid .c-chg { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; }
|
||||
.dhead { padding-top: 4px; padding-bottom: 6px; font-size: 12px; }
|
||||
.dbody { padding: 2px 24px 16px 56px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.kv { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 4px 16px; margin: 0; }
|
||||
.kv dt { font-weight: 400; color: var(--muted); }
|
||||
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
details.fold { border-top: 1px solid var(--line); }
|
||||
details.fold > summary { padding: 0 24px; min-height: 44px; }
|
||||
.list-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 14px 24px; border-top: 1px solid var(--line); }
|
||||
.list-foot .sub { flex: 1 1 260px; }
|
||||
.sgrid { display: grid; grid-template-columns: minmax(0, 1fr) 150px 80px 170px 14px; align-items: center; gap: 0 16px;
|
||||
padding: 12px 24px 12px 56px; font-size: 14px; min-height: 24px; }
|
||||
.stat { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.bar { flex: 1 1 auto; height: 6px; border-radius: 3px; background: var(--track-empty); overflow: hidden; min-width: 30px; }
|
||||
.bar > i { display: block; height: 100%; background: var(--run); }
|
||||
.pv { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.pv li { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 4px 10px; align-items: baseline; padding: 8px 12px; border: 1px solid var(--line-2); border-radius: 8px; font-size: 13px; }
|
||||
.pv li .mono { overflow-wrap: anywhere; min-width: 0; }
|
||||
.pv .old { color: var(--del-text); } .pv .new { color: var(--add-text); }
|
||||
.pv .why { grid-column: 1 / -1; }
|
||||
.agrid { display: grid; grid-template-columns: 170px minmax(0, 1fr) 130px 14px; align-items: center; gap: 0 16px;
|
||||
padding: 12px 180px 12px 24px; font-size: 14px; min-height: 24px; }
|
||||
.abody { padding: 2px 24px 16px 210px; font-size: 13px; line-height: 1.55; display: flex; flex-direction: column; gap: 8px; }
|
||||
.agrid .when { white-space: nowrap; }
|
||||
.abody ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
||||
.abody li { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; }
|
||||
.abody .mono { overflow-wrap: anywhere; min-width: 0; }
|
||||
.mgrid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); gap: 4px 16px; padding: 10px 24px; border-top: 1px solid var(--line); font-size: 13px; }
|
||||
.mgrid .mono { overflow-wrap: anywhere; }
|
||||
@container (max-width: 860px) {
|
||||
.dgrid { grid-template-columns: minmax(0, 1fr) 150px auto 14px; }
|
||||
.dgrid .c-img, .dhead .c-img { display: none; }
|
||||
.sgrid { grid-template-columns: minmax(0, 1fr) 70px 190px 14px; }
|
||||
.sgrid .c-kind { display: none; }
|
||||
.mgrid { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
@container (max-width: 560px) {
|
||||
.dgrid, .sgrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 12px 16px 12px 50px; gap: 4px 10px; }
|
||||
.dgrid .c-chg { grid-column: 1 / 2; grid-row: 2; font-size: 12px; color: var(--muted); }
|
||||
.sgrid .c-size { display: none; }
|
||||
.sgrid .stat { grid-column: 1 / -1; grid-row: 2; }
|
||||
.dhead { display: none; }
|
||||
.dl-row > .pick { left: 16px; }
|
||||
.dbody { padding: 2px 16px 14px; }
|
||||
.kv { grid-template-columns: minmax(0, 1fr); }
|
||||
.kv dd { margin-bottom: 6px; }
|
||||
.agrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 12px 16px; }
|
||||
.agrid .when { grid-column: 1 / -1; }
|
||||
.ui-row > .act { position: static; padding: 0 16px 10px; display: flex; justify-content: flex-end; }
|
||||
.abody { padding: 2px 16px 14px; }
|
||||
.sec-h, .list-foot { padding-left: 16px; padding-right: 16px; }
|
||||
.pv li { grid-template-columns: minmax(0, 1fr); }
|
||||
.mgrid { padding: 10px 16px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dell-updates">
|
||||
{% set chk = prog.check %}
|
||||
{% set dl = prog.download %}
|
||||
{% set n_drv = scan.drivers | selectattr('status', 'equalto', 'update') | list | length if scan else 0 %}
|
||||
{% set n_bios = scan.bios | selectattr('status', 'equalto', 'update') | list | length if scan else 0 %}
|
||||
<div class="page-header">
|
||||
<h1><i class="bi bi-motherboard"></i> Dell Drivers and BIOS</h1>
|
||||
<div class="header-actions">
|
||||
{% if scan %}
|
||||
{% if n_drv or n_bios %}<span class="badge badge-warning badge-lg">{{ n_drv }} driver / {{ n_bios }} BIOS update{{ 's' if n_drv + n_bios != 1 }}</span>
|
||||
{% else %}<span class="badge badge-success badge-lg">Up to date</span>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<header class="ui-head">
|
||||
<div>
|
||||
<div class="eyebrow-t">Updates</div>
|
||||
<h1>Dell drivers and BIOS</h1>
|
||||
<div class="state-line">
|
||||
{% if prog.check_running %}{{ ui.chip('Checking', 'run') }}
|
||||
{% elif not scan %}{{ ui.chip('Not checked yet', 'off') }}
|
||||
{% elif n_drv or n_bios %}{{ ui.chip(n_drv ~ ' driver / ' ~ n_bios ~ ' BIOS update' ~ ('s' if n_drv + n_bios != 1), 'act') }}
|
||||
{% else %}{{ ui.chip('Up to date', 'ok') }}{% endif %}
|
||||
{% if scan %}<span class="sub">Checked {{ scan.checked | local_dt }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<details class="more">
|
||||
<summary class="fbtn" style="background: var(--surface); min-height: 36px">Speed cap
|
||||
<strong class="mono">{{ ('%g MB/s' | format(settings.max_mbps)) if settings.max_mbps else 'none' }}</strong><span class="chev" aria-hidden="true">▾</span></summary>
|
||||
<form class="menu" method="post" action="{{ url_for('dell_updates.save_settings') }}" style="width: min(300px, calc(100vw - 32px)); padding: 14px; gap: 10px">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<label for="maxMbps" style="font-size: 13px; font-weight: 600">Download speed cap (MB/s)</label>
|
||||
<div style="display: flex; gap: 8px"><input id="maxMbps" name="max_mbps" type="number" min="0" max="1000" step="0.5"
|
||||
value="{{ '%g' | format(settings.max_mbps) }}" aria-describedby="cap-help" class="field mono" style="width: 90px">
|
||||
<button type="submit" class="ui-btn pri">Save</button></div>
|
||||
<span id="cap-help" class="sub" style="white-space: normal">Shared by every download on the Updates pages; 0 means no cap. The site link is about 400 Mbps (50 MB/s).</span>
|
||||
</form>
|
||||
</details>
|
||||
{% if prog.check_running %}
|
||||
<form method="post" action="{{ url_for('dell_updates.cancel') }}" style="display:inline;">
|
||||
<form method="post" action="{{ url_for('dell_updates.cancel') }}" style="margin: 0">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="job" value="check">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost"><i class="bi bi-x-circle"></i> Stop check</button>
|
||||
<button type="submit" class="ui-btn">Stop check</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('dell_updates.check') }}" style="display:inline;">
|
||||
<form method="post" action="{{ url_for('dell_updates.check') }}" style="margin: 0">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-secondary" {% if prog.download_running %}disabled title="A download is running"{% endif %}>
|
||||
<i class="bi bi-arrow-repeat"></i> Check for updates
|
||||
</button>
|
||||
<button type="submit" class="ui-btn pri" {% if prog.download_running %}disabled title="A download is running"{% endif %}>Check now</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ui-stack">
|
||||
|
||||
{# ---- Progress and job outcomes ------------------------------------------ #}
|
||||
{% if prog.check_running %}
|
||||
<div class="panel strip" id="checkJob" role="status">
|
||||
<span class="pulse" aria-hidden="true"></span>
|
||||
<span>Checking Dell's catalogs: <span id="checkStep">{{ chk.step or 'Starting' }}...</span></span>
|
||||
</div>
|
||||
|
||||
<p class="text-muted">
|
||||
Compares every Win11 entry in the three images' <span class="mono">HardwareDriver.json</span> and every line of the WinPE
|
||||
BIOS manifest <span class="mono">models.txt</span> with Dell's current catalogs. Downloads are checked against Dell's sha256,
|
||||
driver packs are repacked into the zip PESetup expects, and nothing on the share changes until you preview and apply.
|
||||
Applying puts the new file next to the one it replaces, rewrites the maps and <span class="mono">models.txt</span> (backed
|
||||
up first, never reordered) and never deletes old zips or BIOS files.
|
||||
</p>
|
||||
|
||||
{# ---- Catalogs, check job, speed cap ------------------------------------- #}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-cloud-arrow-down"></i> Dell catalogs</div>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-8">
|
||||
{% if prog.check_running %}
|
||||
<div id="checkJob">
|
||||
<div class="small mb-1"><span class="spinner-border spinner-border-sm"></span>
|
||||
<span id="checkStep">{{ chk.step or 'Starting' }}...</span></div>
|
||||
</div>
|
||||
{% elif chk.state == 'error' %}
|
||||
<div class="alert alert-danger py-2 mb-2">Last check failed: {{ chk.error }}</div>
|
||||
{% elif chk.state == 'interrupted' %}
|
||||
<div class="alert alert-warning py-2 mb-2">The last check stopped before it finished. Run it again.</div>
|
||||
{% endif %}
|
||||
{% if scan %}
|
||||
<div>Last check: <strong>{{ scan.checked | local_dt }}</strong></div>
|
||||
{% if history and history[0].at and scan.checked and history[0].at > scan.checked %}
|
||||
<div class="alert alert-warning py-2 my-2"><i class="bi bi-info-circle"></i>
|
||||
These results are from before the last apply ({{ history[0].at | local_dt }}), so rows it changed can still
|
||||
show as updates. Run <strong>Check for updates</strong> again to refresh them.</div>
|
||||
{% endif %}
|
||||
<div class="small text-muted">
|
||||
{% set cats = scan.catalogs or {} %}
|
||||
Driver pack catalog dated <span class="mono">{{ cats.drivers | local_dt }}</span>,
|
||||
catalog index dated <span class="mono">{{ cats.index | local_dt }}</span>.
|
||||
Maps read: {{ (scan.images or []) | join(', ') or 'none' }}.
|
||||
{% if scan.cab_errors %}<span class="text-danger">{{ scan.cab_errors | length }} per-model catalog(s) failed to load.</span>{% endif %}
|
||||
</div>
|
||||
{% for e in scan.map_errors %}<div class="small text-danger"><i class="bi bi-exclamation-triangle"></i> {{ e }}</div>{% endfor %}
|
||||
{% elif not prog.check_running %}
|
||||
<p class="text-muted mb-0">Not checked yet. <strong>Check for updates</strong> fetches Dell's two catalog files (under 1 MB)
|
||||
and the small per-model BIOS catalogs it needs, then compares.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-4 d-flex justify-content-lg-end">
|
||||
<form method="post" action="{{ url_for('dell_updates.save_settings') }}" class="d-flex gap-2 align-items-end">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-field mb-0">
|
||||
<label for="maxMbps" class="form-label">Speed cap (MB/s)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="maxMbps" name="max_mbps"
|
||||
min="0" max="1000" step="0.5" value="{{ '%g'|format(settings.max_mbps) }}" style="width: 7rem;">
|
||||
</div>
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost pxe-btn-sm" title="Shared by every download on the Updates pages; 0 = no cap. The site link is about 400 Mbps (50 MB/s).">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% elif chk.state == 'error' %}
|
||||
<div class="errbox">Last check failed: {{ chk.error }}</div>
|
||||
{% elif chk.state == 'interrupted' %}
|
||||
<div class="notice">The last check stopped before it finished. Run it again.</div>
|
||||
{% endif %}
|
||||
{% if prog.download_running %}
|
||||
<div class="panel strip" role="status">
|
||||
<span class="pulse" aria-hidden="true"></span>
|
||||
<span>Downloading at {{ '%g MB/s' | format(settings.max_mbps) if settings.max_mbps else 'full speed' }}. Progress per item is under <a href="#staged">Staged downloads</a>; you can leave this page.</span>
|
||||
<form method="post" action="{{ url_for('dell_updates.cancel') }}"
|
||||
onsubmit="return confirm('Stop the download? Partial files are kept and resume next time.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="job" value="download">
|
||||
<button type="submit" class="ui-btn sm">Cancel download</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if scan and history and history[0].at and scan.checked and history[0].at > scan.checked %}
|
||||
<div class="notice">These results are from before the last apply ({{ history[0].at | local_dt }}), so rows it changed can still
|
||||
show as updates. Run Check now again to refresh them.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if scan %}
|
||||
<form method="post" action="{{ url_for('dell_updates.download') }}" id="dlForm"
|
||||
<details>
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>How Dell updates work</summary>
|
||||
<p class="sub" style="margin: 4px 0 0; max-width: 900px; line-height: 1.6">
|
||||
Compares every Win11 entry in the three images' <span class="mono">HardwareDriver.json</span> and every line of the WinPE
|
||||
BIOS manifest <span class="mono">models.txt</span> with Dell's current catalogs. Downloads are checked against Dell's sha256,
|
||||
driver packs are repacked into the zip PESetup expects, and nothing on the share changes until you preview and apply.
|
||||
Applying puts the new file next to the one it replaces, rewrites the maps and <span class="mono">models.txt</span> (backed
|
||||
up first, never reordered) and never deletes old zips or BIOS files.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
{% if not scan %}
|
||||
{% if not prog.check_running %}
|
||||
<section class="panel" style="padding: 22px 24px">
|
||||
<p class="muted" style="margin: 0">Not checked yet. Check now fetches Dell's two catalog files (under 1 MB) and the small
|
||||
per-model BIOS catalogs it needs, then compares.</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set cats = scan.catalogs or {} %}
|
||||
{% for e in scan.map_errors %}<div class="notice bad">{{ e }}</div>{% endfor %}
|
||||
{% if scan.cab_errors %}<div class="notice bad">{{ scan.cab_errors | length }} per-model catalog(s) failed to load; their BIOS lines show as Catalog error.</div>{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('dell_updates.download') }}" id="dlForm" class="ui-stack"
|
||||
onsubmit="var n=this.querySelectorAll('input[name=drv]:checked,input[name=bios]:checked').length; if(!n){alert('Select at least one row.');return false;} return confirm('Download ' + n + ' selected row(s) from downloads.dell.com at the speed cap? Driver packs are 1 to 4 GB each.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
{# ---- Driver packs ------------------------------------------------------ #}
|
||||
<div class="section-card">
|
||||
<div class="section-title d-flex flex-wrap align-items-center gap-2">
|
||||
<span><i class="bi bi-hdd-network"></i> Win11 driver packs</span>
|
||||
<span class="badge badge-secondary">{{ scan.drivers | length }}</span>
|
||||
<label class="small text-muted ms-auto"><input type="checkbox" class="form-check-input js-show-current" data-table="drvTable"> Show current rows</label>
|
||||
{% set cur_d = scan.drivers | selectattr('status', 'equalto', 'current') | list %}
|
||||
<section class="panel ui-list" id="drvTable" aria-labelledby="drv-h">
|
||||
<div class="sec-h">
|
||||
<div><h2 id="drv-h" class="sec">Win11 driver packs</h2>
|
||||
<div class="sub">One row per zip in use; standard and engineer share their zips. Updates are ticked unless a warning needs a decision.</div></div>
|
||||
<span class="sub">{{ scan.drivers | length }} pack{{ 's' if scan.drivers | length != 1 }}</span>
|
||||
</div>
|
||||
<p class="small text-muted">
|
||||
One row per zip in use; standard and engineer share their zips. Updates are ticked unless a warning needs a decision.
|
||||
An entry whose current pack carries NVIDIA drivers is skipped at apply time if the Dell pack has no NVIDIA INFs.
|
||||
</p>
|
||||
<div class="table-container">
|
||||
<table class="data-table" id="drvTable">
|
||||
<thead>
|
||||
<tr><th></th><th>Models (WMI)</th><th>Images</th><th>Current file</th><th>Dell pack</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in scan.drivers %}
|
||||
<tr data-status="{{ r.status }}">
|
||||
<td>{% if r.pack and r.status != 'current' or r.pack and r.warnings %}<input type="checkbox" class="form-check-input" name="drv" value="{{ r.id }}" {% if r.default %}checked{% endif %}>{% endif %}</td>
|
||||
<td class="small wrap">{{ r.tokens | join(', ') }}</td>
|
||||
<td class="small" style="white-space: normal; min-width: 6rem;">{% for i in r.images %}{{ i | replace('gea-', '') }}{% if not loop.last %}, {% endif %}{% endfor %}</td>
|
||||
<td class="wrap"><span class="mono small">{{ r.current }}</span>{% if r.cur_ver %} <span class="badge badge-secondary">{{ r.cur_ver }}</span>{% endif %}</td>
|
||||
<td class="wrap">
|
||||
{% if r.pack %}
|
||||
<span class="mono small" title="{{ r.pack.models | join(', ') }}">{{ r.pack.name }}</span>
|
||||
<div class="small text-muted">{{ r.pack.ver }}, {{ r.pack.date }}, {{ size(r.pack.size) }}, stages as <span class="mono">{{ r.pack.zip }}</span></div>
|
||||
{% else %}<span class="text-muted">-</span>{% endif %}
|
||||
</td>
|
||||
<td class="wrap">{{ drv_badge(r.status) }}
|
||||
{% for w in r.warnings %}<div class="small text-danger mt-1"><i class="bi bi-exclamation-triangle"></i> {{ w }}</div>{% endfor %}
|
||||
{% for n in r.notes %}<div class="small text-muted mt-1">{{ n }}</div>{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dgrid dhead ui-rowhead" aria-hidden="true"><span>Models</span><span>Version</span><span class="c-img">Images</span><span>Status</span><span></span></div>
|
||||
{% for r in scan.drivers if r.status != 'current' %}{{ drv_row(r) }}{% endfor %}
|
||||
{% if cur_d %}
|
||||
<details class="fold">
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>Show {{ cur_d | length }} current pack{{ 's' if cur_d | length != 1 }}</summary>
|
||||
{% for r in cur_d %}{{ drv_row(r) }}{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
<div class="hidden-bar">An entry whose current pack carries NVIDIA drivers is skipped at apply time if the Dell pack has no NVIDIA INFs.</div>
|
||||
</section>
|
||||
|
||||
{# ---- BIOS -------------------------------------------------------------- #}
|
||||
<div class="section-card">
|
||||
<div class="section-title d-flex flex-wrap align-items-center gap-2">
|
||||
<span><i class="bi bi-cpu"></i> BIOS (models.txt)</span>
|
||||
<span class="badge badge-secondary">{{ scan.bios | length }}</span>
|
||||
<label class="small text-muted ms-auto"><input type="checkbox" class="form-check-input js-show-current" data-table="biosTable"> Show current rows</label>
|
||||
{% set cur_b = scan.bios | selectattr('status', 'equalto', 'current') | list %}
|
||||
<section class="panel ui-list" id="biosTable" aria-labelledby="bios-h">
|
||||
<div class="sec-h">
|
||||
<div><h2 id="bios-h" class="sec">BIOS (models.txt)</h2>
|
||||
<div class="sub">One row per manifest line. check-bios.cmd flashes the file of the first line whose text is in the machine's model name.</div></div>
|
||||
<span class="sub">{{ scan.bios | length }} line{{ 's' if scan.bios | length != 1 }}</span>
|
||||
</div>
|
||||
<p class="small text-muted">
|
||||
One row per manifest line. check-bios.cmd flashes the file of the FIRST line whose text is in the machine's model name,
|
||||
so each Dell model is credited to the first line that catches it. Applying changes only the file and version fields
|
||||
of the ticked lines; their order and the comments stay as they are.
|
||||
</p>
|
||||
<div class="table-container">
|
||||
<table class="data-table" id="biosTable">
|
||||
<thead>
|
||||
<tr><th></th><th>Line</th><th>Current file</th><th>Dell latest</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in scan.bios %}
|
||||
<tr data-status="{{ r.status }}">
|
||||
<td>{% if r.candidates and (r.status != 'current') %}<input type="checkbox" class="form-check-input" name="bios" value="{{ r.id }}" {% if r.default %}checked{% endif %}>{% endif %}</td>
|
||||
<td class="mono small text-nowrap">{{ r.sub }}</td>
|
||||
<td class="wrap"><span class="mono small">{{ r.current }}</span> <span class="badge badge-secondary">{{ r.cur_ver }}</span></td>
|
||||
<td class="wrap">
|
||||
{% if r.candidates | length > 1 %}
|
||||
<select class="form-control form-control-sm" name="pick_{{ r.id }}" style="max-width: 28rem;">
|
||||
{% for c in r.candidates %}<option value="{{ loop.index0 }}">{{ c.file }} ({{ c.version }}, {{ c.date }}) - {{ c.models | join(', ') | truncate(70) }}</option>{% endfor %}
|
||||
</select>
|
||||
{% elif r.candidates %}
|
||||
{% set c = r.candidates[0] %}
|
||||
<span class="mono small" title="sha256 {{ c.sha256 }}">{{ c.file }}</span>
|
||||
<div class="small text-muted">{{ c.version }}, {{ c.date }}, {{ size(c.size) }}; {{ c.models | join(', ') | truncate(90) }}</div>
|
||||
{% else %}<span class="text-muted">-</span>{% endif %}
|
||||
</td>
|
||||
<td class="wrap">{{ bios_badge(r.status) }}
|
||||
{% for w in r.warnings %}<div class="small text-danger mt-1"><i class="bi bi-exclamation-triangle"></i> {{ w }}</div>{% endfor %}
|
||||
{% for n in r.notes %}<div class="small text-muted mt-1">{{ n }}</div>{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="dgrid dhead ui-rowhead" aria-hidden="true"><span>Line</span><span>Version</span><span class="c-img">Current file</span><span>Status</span><span></span></div>
|
||||
{% for r in scan.bios if r.status != 'current' %}{{ bios_row(r) }}{% endfor %}
|
||||
{% if cur_b %}
|
||||
<details class="fold">
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>Show {{ cur_b | length }} current line{{ 's' if cur_b | length != 1 }}</summary>
|
||||
{% for r in cur_b %}{{ bios_row(r) }}{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
<div class="hidden-bar">Each Dell model is credited to the first line that catches it. Applying changes only the file and version fields of the ticked lines; their order and the comments stay as they are.</div>
|
||||
<div class="list-foot">
|
||||
<button type="submit" class="ui-btn pri" {% if prog.download_running or prog.check_running %}disabled{% endif %}>Download selected</button>
|
||||
<span class="sub">Ticked driver packs and BIOS lines. Runs in the background at {{ '%g MB/s' | format(settings.max_mbps) if settings.max_mbps else 'full speed (no cap)' }};
|
||||
you can leave this page. Already staged items are not fetched again.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-4">
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary" {% if prog.download_running or prog.check_running %}disabled{% endif %}>
|
||||
<i class="bi bi-download"></i> Download selected
|
||||
</button>
|
||||
<span class="small text-muted">Runs in the background at {{ '%g MB/s' | format(settings.max_mbps) if settings.max_mbps else 'full speed (no cap)' }};
|
||||
you can leave this page. Already staged items are not fetched again.</span>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<div class="sub">Driver pack catalog dated <span class="mono">{{ cats.drivers | local_dt }}</span>, catalog index dated
|
||||
<span class="mono">{{ cats.index | local_dt }}</span>. Maps read: {{ (scan.images or []) | join(', ') or 'none' }}.</div>
|
||||
|
||||
{# ---- Missing coverage ---------------------------------------------------- #}
|
||||
{% if scan.missing %}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-question-diamond"></i> Seen models without coverage <span class="badge badge-secondary">{{ scan.missing | length }}</span></div>
|
||||
<p class="small text-muted">Machines that booted WinPE here with no Win11 driver entry or no BIOS line. For information only:
|
||||
new entries are added by hand (see Hardware Coverage).</p>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Model</th><th>SKU</th><th>Missing</th><th>Dell pack that would cover it</th><th>Dell BIOS</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in scan.missing %}
|
||||
<tr>
|
||||
<td class="mono small">{{ m.model }}</td>
|
||||
<td class="mono small">{{ m.skus | join(', ') or '-' }}</td>
|
||||
<td class="small wrap">{% if m.no_drivers %}drivers ({{ m.no_drivers | map('replace', 'gea-', '') | join(', ') }}){% endif %}{% if m.no_drivers and m.no_bios %}, {% endif %}{% if m.no_bios %}BIOS line{% endif %}</td>
|
||||
<td class="small wrap">{% if m.pack %}<span class="mono">{{ m.pack.name }}</span> ({{ m.pack.ver }}, {{ size(m.pack.size) }}){% elif m.no_drivers %}<span class="text-muted">none found</span>{% else %}-{% endif %}</td>
|
||||
<td class="small wrap">{% if m.bios %}<span class="mono">{{ m.bios.file }}</span> ({{ m.bios.version }}){% elif m.no_bios %}<span class="text-muted">none found</span>{% else %}-{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<section class="panel ui-list" aria-labelledby="miss-h">
|
||||
<details>
|
||||
<summary class="sec-h" style="align-items: center">
|
||||
<div><h2 id="miss-h" class="sec" style="font-size: 15px">Seen models without coverage</h2>
|
||||
<div class="sub">For information only: new entries are added by hand (see Hardware coverage).</div></div>
|
||||
<span style="display: flex; align-items: center; gap: 10px">{{ ui.chip(scan.missing | length ~ ' model' ~ ('s' if scan.missing | length != 1), 'neutral') }}<span class="chev" aria-hidden="true">▾</span></span>
|
||||
</summary>
|
||||
{% for m in scan.missing %}
|
||||
<div class="mgrid">
|
||||
<div><span class="mono">{{ m.model }}</span><div class="sub">SKU {{ m.skus | join(', ') or '-' }}. Missing
|
||||
{% if m.no_drivers %}drivers ({{ m.no_drivers | map('replace', 'gea-', '') | join(', ') }}){% endif %}{% if m.no_drivers and m.no_bios %}, {% endif %}{% if m.no_bios %}BIOS line{% endif %}</div></div>
|
||||
<div><span class="sub">Dell pack</span><div>{% if m.pack %}<span class="mono">{{ m.pack.name }}</span> ({{ m.pack.ver }}, {{ size(m.pack.size) }}){% elif m.no_drivers %}<span class="muted">none found</span>{% else %}-{% endif %}</div></div>
|
||||
<div><span class="sub">Dell BIOS</span><div>{% if m.bios %}<span class="mono">{{ m.bios.file }}</span> ({{ m.bios.version }}){% elif m.no_bios %}<span class="muted">none found</span>{% else %}-{% endif %}</div></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# ---- Staged downloads ---------------------------------------------------- #}
|
||||
<div class="section-card" id="staged">
|
||||
<div class="section-title d-flex flex-wrap align-items-center gap-2">
|
||||
<span><i class="bi bi-box-seam"></i> Staged downloads</span>
|
||||
{% if dl.state %}<span class="small text-muted">last run {{ dl.state }}{% if dl.finished %} {{ dl.finished | local_dt }}{% endif %}{% if dl.error %}: {{ dl.error }}{% endif %}</span>{% endif %}
|
||||
{% if prog.download_running %}
|
||||
<form method="post" action="{{ url_for('dell_updates.cancel') }}" class="ms-auto"
|
||||
onsubmit="return confirm('Stop the download? Partial files are kept and resume next time.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="job" value="download">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost pxe-btn-sm"><i class="bi bi-x-circle"></i> Cancel download</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<section class="panel ui-list" id="staged" aria-labelledby="st-h">
|
||||
<div class="sec-h">
|
||||
<div><h2 id="st-h" class="sec">Staged downloads</h2>
|
||||
<div class="sub">Tick what to apply, then preview. Nothing on the share changes until you apply.</div></div>
|
||||
{% if dl.state %}<span class="sub">Last run {{ dl.state }}{% if dl.finished %} {{ dl.finished | local_dt }}{% endif %}</span>{% endif %}
|
||||
</div>
|
||||
{% if dl.error %}<div class="errbox" style="margin: 0 24px 12px">{{ dl.error }}</div>{% endif %}
|
||||
{% if staged %}
|
||||
<form method="post" action="{{ url_for('dell_updates.preview') }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th></th><th>Item</th><th>Replaces</th><th>Size</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for it in staged %}
|
||||
<tr>
|
||||
<td>{% if it.status in ('ready', 'error', 'cancelled', 'interrupted', 'applied') %}<input type="checkbox" class="form-check-input" name="key" value="{{ it.key }}" {% if it.status == 'ready' %}checked{% endif %}>{% endif %}</td>
|
||||
<td class="wrap">
|
||||
<span class="mono small">{{ it.zip if it.kind == 'driver' else it.name }}</span>
|
||||
<div class="small text-muted">{{ 'Driver pack' if it.kind == 'driver' else 'BIOS' }} {{ it.ver }}{% if it.kind == 'driver' %} from <span class="mono">{{ it.name }}</span>{% endif %}
|
||||
{% if it.infs %}; {{ it.infs }} INFs, {{ it.nvidia_infs }} NVIDIA{% endif %}</div>
|
||||
</td>
|
||||
<td class="small wrap">
|
||||
{% if it.kind == 'driver' %}{% for c in it.replaces %}<div class="mono">{{ c }}</div>{% endfor %}
|
||||
{% else %}{% for ln in it.lines %}<div class="mono">{{ ln.sub }}</div>{% endfor %}{% endif %}
|
||||
</td>
|
||||
<td class="small text-nowrap">{{ size(it.size_file or it.size) }}</td>
|
||||
<td id="st-{{ it.key }}" class="wrap" style="min-width: 10rem;">
|
||||
{{ stage_badge(it.status) }}
|
||||
{% if it.status in ('queued', 'downloading', 'verified', 'extracting', 'zipping') %}
|
||||
<div class="progress mt-1" style="height: 6px;"><div class="progress-bar" style="width: {{ it.percent or 0 }}%;"></div></div>
|
||||
<div class="small text-muted js-pct">{{ it.percent if it.percent is not none else 0 }}%</div>
|
||||
{% endif %}
|
||||
{% if it.error %}<div class="small text-danger">{{ it.error }}</div>{% endif %}
|
||||
{% if it.applied %}<div class="small text-muted">apply {{ it.applied }}</div>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for it in staged %}
|
||||
{% set label = it.zip if it.kind == 'driver' else it.name %}
|
||||
<div class="ui-row dl-row">
|
||||
{% if it.status in ('ready', 'error', 'cancelled', 'interrupted', 'applied') %}
|
||||
<div class="pick"><input type="checkbox" class="form-check-input" name="key" value="{{ it.key }}" id="key-{{ it.key }}" {% if it.status == 'ready' %}checked{% endif %}>
|
||||
<label for="key-{{ it.key }}" class="vh">Select {{ label }}</label></div>
|
||||
{% endif %}
|
||||
<details {% if it.error %}open{% endif %}>
|
||||
<summary class="sgrid">
|
||||
<span class="tip" data-tip="{{ label }}"><span class="trunc mono">{{ label }}</span></span>
|
||||
<span class="sub c-kind">{{ 'Driver pack' if it.kind == 'driver' else 'BIOS' }} {{ it.ver }}</span>
|
||||
<span class="sub c-size">{{ size(it.size_file or it.size) }}</span>
|
||||
<span id="st-{{ it.key }}" class="stat">
|
||||
{{ stage_chip(it.status) }}
|
||||
{% if it.status in ('queued', 'downloading', 'verified', 'extracting', 'zipping') %}
|
||||
<span class="bar" aria-hidden="true"><i style="width: {{ it.percent or 0 }}%"></i></span>
|
||||
<span class="sub js-pct">{{ it.percent if it.percent is not none else 0 }}%</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="dbody">
|
||||
{% if it.error %}<div class="errbox">{{ it.error }}</div>{% endif %}
|
||||
<dl class="kv">
|
||||
{% if it.kind == 'driver' %}
|
||||
<dt>Dell file</dt><dd class="mono">{{ it.name }}</dd>
|
||||
<dt>Replaces</dt><dd>{% for c in it.replaces %}<div class="mono">{{ c }}</div>{% endfor %}</dd>
|
||||
{% if it.infs %}<dt>Contents</dt><dd>{{ it.infs }} INFs, {{ it.nvidia_infs }} NVIDIA</dd>{% endif %}
|
||||
{% else %}
|
||||
<dt>BIOS lines</dt><dd>{% for ln in it.lines %}<div class="mono">{{ ln.sub }}</div>{% endfor %}</dd>
|
||||
{% endif %}
|
||||
<dt>Version</dt><dd>{{ it.ver }}</dd>
|
||||
{% if it.applied %}<dt>Applied</dt><dd class="mono">apply {{ it.applied }}</dd>{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary" {% if prog.download_running %}disabled{% endif %}><i class="bi bi-eye"></i> Preview apply</button>
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost" formaction="{{ url_for('dell_updates.discard') }}"
|
||||
{% endfor %}
|
||||
<div class="list-foot">
|
||||
<button type="submit" class="ui-btn pri" {% if prog.download_running %}disabled{% endif %}>Preview apply</button>
|
||||
<button type="submit" class="ui-btn danger" formaction="{{ url_for('dell_updates.discard') }}"
|
||||
onclick="return confirm('Delete the ticked items from the staging area? Files on the share are not touched.');"
|
||||
{% if prog.download_running %}disabled{% endif %}><i class="bi bi-trash"></i> Discard selected</button>
|
||||
{% if prog.download_running %}disabled{% endif %}>Discard selected</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Nothing staged. Tick rows above and use <strong>Download selected</strong>.</p>
|
||||
<div class="hidden-bar">Nothing staged. Tick rows above and use Download selected.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ---- Preview ------------------------------------------------------------- #}
|
||||
{% if preview %}
|
||||
<div class="section-card" id="preview">
|
||||
<div class="section-title"><i class="bi bi-file-diff"></i> Preview: what Apply will change</div>
|
||||
<section class="panel" id="preview" aria-labelledby="pv-h" style="padding: 18px 24px; display: flex; flex-direction: column; gap: 12px">
|
||||
<h2 id="pv-h" class="sec">Preview: what Apply will change</h2>
|
||||
{% for image, rows in preview.drivers.items() %}
|
||||
<h6 class="mt-2">{{ image }} <span class="text-muted small">HardwareDriver.json</span></h6>
|
||||
{% if rows %}
|
||||
<div class="table-container">
|
||||
<table class="table table-sm mb-2">
|
||||
<thead><tr><th>Entry models</th><th>Old file</th><th></th><th>New file</th><th>Folder</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr><td class="small wrap">{{ r.models }}</td><td class="mono small wrap">{{ r.old }}</td><td class="text-muted">to</td>
|
||||
<td class="mono small wrap">{{ r.new }}</td><td class="mono small text-muted wrap">{{ r.dir }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<div style="font-weight: 600; margin-bottom: 6px">{{ image }} <span class="sub mono">HardwareDriver.json</span></div>
|
||||
{% if rows %}
|
||||
<ul class="pv">
|
||||
{% for r in rows %}
|
||||
<li><span class="mono old">{{ r.old }}</span><span class="sub">to</span><span class="mono new">{{ r.new }}</span>
|
||||
<span class="sub why">{{ r.models }} | <span class="mono">{{ r.dir }}</span></span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}<div class="sub">No change.</div>{% endif %}
|
||||
</div>
|
||||
{% else %}<p class="small text-muted">No change.</p>{% endif %}
|
||||
{% endfor %}
|
||||
<h6 class="mt-3">models.txt</h6>
|
||||
{% if preview.bios %}
|
||||
<div class="table-container">
|
||||
<table class="table table-sm mb-2">
|
||||
<thead><tr><th>Old line</th><th></th><th>New line</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in preview.bios %}<tr><td class="mono small wrap">{{ b.old }}</td><td class="text-muted">to</td><td class="mono small wrap">{{ b.new }}</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<div style="font-weight: 600; margin-bottom: 6px">models.txt</div>
|
||||
{% if preview.bios %}
|
||||
<ul class="pv">{% for b in preview.bios %}<li><span class="mono old">{{ b.old }}</span><span class="sub">to</span><span class="mono new">{{ b.new }}</span></li>{% endfor %}</ul>
|
||||
{% else %}<div class="sub">No change.</div>{% endif %}
|
||||
</div>
|
||||
{% else %}<p class="small text-muted">No change.</p>{% endif %}
|
||||
{% for e in preview.errors %}<div class="small text-danger"><i class="bi bi-exclamation-triangle"></i> {{ e }}</div>{% endfor %}
|
||||
{% for s in preview.skipped %}<div class="small text-warning"><i class="bi bi-exclamation-triangle"></i> Will skip {{ s }}</div>{% endfor %}
|
||||
{% if preview.missing %}<div class="small text-muted">Not ready, ignored: {{ preview.missing | join(', ') }}</div>{% endif %}
|
||||
<form method="post" action="{{ url_for('dell_updates.apply') }}" class="mt-3"
|
||||
{% if preview.errors %}<div class="errbox">{{ preview.errors | join('; ') }}</div>{% endif %}
|
||||
{% for s in preview.skipped %}<div class="notice">Will skip {{ s }}</div>{% endfor %}
|
||||
{% if preview.missing %}<div class="sub">Not ready, ignored: {{ preview.missing | join(', ') }}</div>{% endif %}
|
||||
<form method="post" action="{{ url_for('dell_updates.apply') }}" style="display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; margin: 0"
|
||||
onsubmit="return confirm('Apply these changes to the live share? The maps and models.txt are backed up first; old files are kept.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
{% for k in preview_keys %}<input type="hidden" name="key" value="{{ k }}">{% endfor %}
|
||||
<button type="submit" class="pxe-btn pxe-btn-success"><i class="bi bi-check2-circle"></i> Apply</button>
|
||||
<a href="{{ url_for('dell_updates.page') }}" class="pxe-btn pxe-btn-ghost">Cancel</a>
|
||||
<a href="{{ url_for('dell_updates.page') }}" class="ui-btn">Cancel</a>
|
||||
<button type="submit" class="ui-btn pri">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# ---- History ------------------------------------------------------------- #}
|
||||
{% if history %}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-clock-history"></i> Applied</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>When</th><th>Changes</th><th>Backups</th><th class="text-end"></th></tr></thead>
|
||||
<tbody>
|
||||
{% for h in history %}
|
||||
<tr>
|
||||
<td class="mono small text-nowrap">{{ h.at | local_dt }}<div class="text-muted">{{ h.id }}</div></td>
|
||||
<td class="small wrap">
|
||||
{% for d in h.drivers %}<div><span class="mono">{{ d.old }}</span> to <span class="mono">{{ d.new }}</span></div>{% endfor %}
|
||||
{% for b in h.bios %}<div><span class="mono">{{ b.new }}</span></div>{% endfor %}
|
||||
{% for s in h.skipped %}<div class="text-warning">skipped {{ s }}</div>{% endfor %}
|
||||
{% for e in h.errors %}<div class="text-danger">{{ e }}</div>{% endfor %}
|
||||
</td>
|
||||
<td class="small wrap">{% for p, b in h.backups.items() %}<div class="mono">{{ b.rsplit('/', 1)[-1] }}</div>{% endfor %}</td>
|
||||
<td class="text-end text-nowrap">
|
||||
{% if h.rolled_back %}<span class="badge badge-secondary">Rolled back</span>
|
||||
{% elif h.backups %}
|
||||
<form method="post" action="{{ url_for('dell_updates.rollback') }}" style="display:inline;"
|
||||
onsubmit="return confirm('Put the maps and models.txt back as they were before {{ h.id }}? Files changed again since are left alone. The new zips and BIOS files stay on the share.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="apply_id" value="{{ h.id }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost btn-row-action"><i class="bi bi-arrow-counterclockwise"></i> Rollback</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="panel ui-list" aria-labelledby="h-h">
|
||||
<div class="sec-h"><div><h2 id="h-h" class="sec">Applied</h2>
|
||||
<div class="sub">Rollback opens a review page first; it restores only files nobody has changed since the apply.</div></div></div>
|
||||
{% for h in history %}
|
||||
{% set can = not h.rolled_back and h.unchanged > 0 %}
|
||||
<div class="ui-row">
|
||||
<details>
|
||||
<summary class="agrid">
|
||||
<span class="mono sub when">{{ h.at | local_dt }}</span>
|
||||
<span class="tip" data-tip="{{ h.title }}"><span class="trunc">{{ h.title }}</span></span>
|
||||
<span>
|
||||
{% if h.rolled_back %}{{ ui.chip('Rolled back', 'neutral') }}
|
||||
{% elif h.errors and not h.total %}{{ ui.chip('Apply failed', 'err') }}
|
||||
{% elif not h.total %}{{ ui.chip('Nothing written', 'off') }}
|
||||
{% elif h.unchanged == h.total %}{{ ui.chip(h.unchanged ~ '/' ~ h.total ~ ' unchanged', 'ok') }}
|
||||
{% elif h.unchanged %}{{ ui.chip(h.unchanged ~ '/' ~ h.total ~ ' unchanged', 'warn') }}
|
||||
{% else %}{{ ui.chip('0/' ~ h.total ~ ' unchanged', 'off') }}{% endif %}
|
||||
</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="abody">
|
||||
<ul>{% for c in h.changes %}<li>{{ c }}</li>{% endfor %}
|
||||
{% for d in h.drivers %}<li class="sub"><span class="mono">{{ d.old }}</span> to <span class="mono">{{ d.new }}</span></li>{% endfor %}</ul>
|
||||
{% if h.skipped %}<div class="notice">Skipped: {{ h.skipped | join('; ') }}</div>{% endif %}
|
||||
{% if h.errors %}<div class="errbox">{{ h.errors | join('; ') }}</div>{% endif %}
|
||||
{% if h.files %}
|
||||
<ul>
|
||||
{% for f in h.files %}
|
||||
<li><span class="mono">{{ f.label }}</span>
|
||||
{% if f.state == 'unchanged' %}<span class="muted">unchanged since this apply</span>
|
||||
{% elif f.state == 'changed' %}<span>content differs from what this apply wrote{% if f.mtime %} (last modified {{ f.mtime | local_dt }}){% endif %}</span>
|
||||
{% else %}<span style="color: var(--fail-text)">missing</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if h.backups %}<div class="sub">Backups: {% for p, b in h.backups.items() %}<span class="mono">{{ b.rsplit('/', 1)[-1] }}</span>{{ ', ' if not loop.last }}{% endfor %}</div>{% endif %}
|
||||
{% if h.rolled_back %}<div class="sub">Rolled back {{ h.rolled_back | local_dt }}.</div>{% endif %}
|
||||
<div class="sub">Apply <span class="mono">{{ h.id }}</span>.</div>
|
||||
</div>
|
||||
</details>
|
||||
{% if can %}<div class="act"><a class="ui-btn" href="{{ url_for('dell_updates.rollback_review', apply_id=h.id) }}">Review rollback</a></div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
// Hide rows that need nothing unless asked; without JS every row shows.
|
||||
(function () {
|
||||
var boxes = document.querySelectorAll('.js-show-current');
|
||||
function paint(box) {
|
||||
var t = document.getElementById(box.dataset.table);
|
||||
if (!t) return;
|
||||
t.querySelectorAll('tbody tr[data-status=current]').forEach(function (tr) {
|
||||
var tick = tr.querySelector('input[type=checkbox]');
|
||||
if (!tick || !tick.checked) tr.style.display = box.checked ? '' : 'none';
|
||||
});
|
||||
}
|
||||
boxes.forEach(function (b) { b.addEventListener('change', function () { paint(b); }); paint(b); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
{% if prog.check_running or prog.download_running %}
|
||||
@@ -429,9 +518,9 @@
|
||||
Object.keys(j.download.items || {}).forEach(function (k) {
|
||||
var it = j.download.items[k], cell = document.getElementById('st-' + k);
|
||||
if (!cell) return;
|
||||
var badge = cell.querySelector('.badge');
|
||||
if (badge) badge.textContent = it.status.charAt(0).toUpperCase() + it.status.slice(1);
|
||||
var bar = cell.querySelector('.progress-bar'), pct = cell.querySelector('.js-pct');
|
||||
var chip = cell.querySelector('.chip');
|
||||
if (chip && it.status) chip.textContent = it.status.charAt(0).toUpperCase() + it.status.slice(1);
|
||||
var bar = cell.querySelector('.bar > i'), pct = cell.querySelector('.js-pct');
|
||||
if (bar && it.percent != null) bar.style.width = it.percent + '%';
|
||||
if (pct && it.percent != null) pct.textContent = it.percent + '%';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user