- Unattend: a command's '...' menu stayed open after Move up/down and covered the next row's menu, so the next click could stage removing the wrong command. The menu now closes and focus returns to its button. - '...' menus close on an outside click, on Escape and when another opens (static/app.js), for every page. - Drivers: Remove asks to confirm again, as before the redesign. Long zip paths in a row body wrap (the page scrolled sideways at 390 px). - Rollback review returns to the page it was opened from (Dell page or Updates overview), for both Cancel and the rollback itself. - To-do: the ServiceNow period offers a year again (366 days). - Imaging floor: a search that matches a log-only bay opens that group; its open state survives the live refresh. 'Clear all' states the number of session files it deletes, hidden finished ones included. - Dashboard: the image key pattern is valid under the browser's v flag (client-side validation was silently skipped). - Import: allowed_import_source() compares real paths, so '<upload dir>/../..' is refused. This path traversal predates the redesign; the import moves the source tree, so it mattered.
536 lines
32 KiB
HTML
536 lines
32 KiB
HTML
{% extends "base.html" %}
|
|
{% 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_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_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_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>
|
|
.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 %}
|
|
{% 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 %}
|
|
|
|
<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="margin: 0">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="job" value="check">
|
|
<button type="submit" class="ui-btn">Stop check</button>
|
|
</form>
|
|
{% else %}
|
|
<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="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>
|
|
{% 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 %}
|
|
|
|
<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 ------------------------------------------------------ #}
|
|
{% 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>
|
|
<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 -------------------------------------------------------------- #}
|
|
{% 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>
|
|
<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>
|
|
</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 %}
|
|
<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 ---------------------------------------------------- #}
|
|
<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() }}">
|
|
{% 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>
|
|
{% 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 %}>Discard selected</button>
|
|
</div>
|
|
</form>
|
|
{% else %}
|
|
<div class="hidden-bar">Nothing staged. Tick rows above and use Download selected.</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{# ---- Preview ------------------------------------------------------------- #}
|
|
{% if preview %}
|
|
<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() %}
|
|
<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>
|
|
{% endfor %}
|
|
<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>
|
|
{% 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 %}
|
|
<a href="{{ url_for('dell_updates.page') }}" class="ui-btn">Cancel</a>
|
|
<button type="submit" class="ui-btn pri">Apply</button>
|
|
</form>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ---- History ------------------------------------------------------------- #}
|
|
{% if history %}
|
|
<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, back='dell') }}">Review rollback</a></div>{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</section>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if prog.check_running or prog.download_running %}
|
|
<script>
|
|
// Poll the background jobs; update progress in place, reload when they end.
|
|
(function () {
|
|
var URL = "{{ url_for('dell_updates.status') }}";
|
|
// A preview is a POST response: reloading it would resubmit the form,
|
|
// so there the poller only stops when the jobs end.
|
|
var RELOAD = {{ 'false' if preview else 'true' }};
|
|
function tick() {
|
|
fetch(URL, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (j) {
|
|
if (!j.check_running && !j.download_running) {
|
|
if (RELOAD) location.replace("{{ url_for('dell_updates.page') }}");
|
|
return;
|
|
}
|
|
var step = document.getElementById('checkStep');
|
|
if (step && j.check.step) step.textContent = j.check.step + '...';
|
|
Object.keys(j.download.items || {}).forEach(function (k) {
|
|
var it = j.download.items[k], cell = document.getElementById('st-' + k);
|
|
if (!cell) return;
|
|
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 + '%';
|
|
});
|
|
setTimeout(tick, 3000);
|
|
})
|
|
.catch(function () { setTimeout(tick, 6000); });
|
|
}
|
|
setTimeout(tick, 2000);
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %}
|