Updates group: - Dell drivers and BIOS: check Dell's catalogs against all three driver maps and models.txt, download with a shared speed cap, repack, preview, apply with backups and rollback. - iPXE boot files: check GitHub for the signed Secure Boot set, verify the sha256 and each file's signer, install atomically, roll back. Includes the by-hand procedure. - Blancco: upload a release, stage it, test-boot it from the menu, activate, roll back. - BPRT sync: validate a batch uploaded from the MediaCreator Lite cache (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site overlay files kept. Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and Packages tabs. Every save shows a diff first, keeps the file's format, makes a backup and can be undone. Driver lint runs inline. Enrollment: upload standard/engineer MCL packages, one per tenant/purpose, with archive and restore. Hardware coverage counts engineer as Win11.
442 lines
25 KiB
HTML
442 lines
25 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>
|
|
<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 %}
|