Files
pxe-server/webapp/templates/updates_dell.html
cproudlock 24b4d9df66 Dell updates: say when the results predate the last apply
After an apply the tables still showed the check from before it, so a pack
just applied looked like it still needed updating until the next check.
2026-10-13 17:38:14 -04:00

447 lines
26 KiB
HTML

{% extends "base.html" %}
{% block title %}Dell Drivers and BIOS - PXE Server 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 %}
{%- 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 %}
{%- 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 %}
{%- 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; }
</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 %}
{% if prog.check_running %}
<form method="post" action="{{ url_for('dell_updates.cancel') }}" style="display:inline;">
<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>
</form>
{% else %}
<form method="post" action="{{ url_for('dell_updates.check') }}" style="display:inline;">
<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>
</form>
{% endif %}
</div>
</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>
</div>
{% if scan %}
<form method="post" action="{{ url_for('dell_updates.download') }}" id="dlForm"
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>
</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>
{# ---- 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>
</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>
</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>
</form>
{# ---- 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>
{% 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 %}
</div>
{% 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>
</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') }}"
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>
</div>
</form>
{% else %}
<p class="text-muted mb-0">Nothing staged. Tick rows above and use <strong>Download selected</strong>.</p>
{% endif %}
</div>
{# ---- 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>
{% 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>
{% 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>
{% 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"
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>
</form>
</div>
{% 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>
</div>
</div>
{% 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 %}
<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 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');
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 %}