- GET /updates/: one card per updater (Dell, iPXE, Blancco, BPRT) with
its state, a Check now / Open action and details in accordions; the
shared download cap is a small popover.
- Dell applies list: one line per apply titled by its first change
('Latitude 5350 pack A11 to A12', model looked up in the maps) and
'and n more'; the chip says how many written files are unchanged since
the apply. The body lists every change, skipped item, error and file.
- dell_updates.file_status(): per file an apply wrote, unchanged /
changed / missing by content hash, the same test rollback uses.
describe(), history_view() and rollback_preview() build on it.
- GET /updates/dell/rollback/<id>: review a rollback before doing it,
with the files that would be restored and a diff of each. The POST
route is unchanged; both forms can return to the overview.
196 lines
7.0 KiB
Python
196 lines
7.0 KiB
Python
"""Updates > Dell: Win11 driver packs and WinPE BIOS files from Dell's catalogs.
|
|
|
|
Check and download run as background jobs (systemd-run) that write progress
|
|
JSON the page polls; preview, apply, discard and rollback are quick renames
|
|
and small file rewrites, so they run inline. Logic lives in
|
|
services/dell_updates.py; this module is the route surface.
|
|
"""
|
|
|
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
|
|
|
from services import dell_updates, updates_common
|
|
from services.audit import audit
|
|
from views import save_speed_cap
|
|
|
|
bp = Blueprint("dell_updates", __name__, url_prefix="/updates/dell")
|
|
|
|
|
|
def _back():
|
|
return redirect(url_for("dell_updates.page"))
|
|
|
|
|
|
def _render(preview=None, preview_keys=()):
|
|
prog = dell_updates.progress()
|
|
items = sorted((prog["download"].get("items") or {}).values(),
|
|
key=lambda i: (i.get("status") == "applied", i.get("kind", ""), i.get("name", "")))
|
|
return render_template(
|
|
"updates_dell.html",
|
|
scan=dell_updates.last_scan(),
|
|
prog=prog,
|
|
staged=items,
|
|
history=dell_updates.history_view(15),
|
|
settings=updates_common.settings(),
|
|
preview=preview,
|
|
preview_keys=list(preview_keys),
|
|
)
|
|
|
|
|
|
@bp.route("/")
|
|
def page():
|
|
return _render()
|
|
|
|
|
|
@bp.route("/status.json")
|
|
def status():
|
|
prog = dell_updates.progress()
|
|
items = {k: {"status": v.get("status"), "percent": v.get("percent"), "error": v.get("error", "")}
|
|
for k, v in (prog["download"].get("items") or {}).items()}
|
|
return jsonify({
|
|
"check": {"state": prog["check"].get("state", ""), "step": prog["check"].get("step", ""),
|
|
"error": prog["check"].get("error", "")},
|
|
"download": {"state": prog["download"].get("state", ""), "items": items},
|
|
"check_running": prog["check_running"],
|
|
"download_running": prog["download_running"],
|
|
})
|
|
|
|
|
|
@bp.route("/check", methods=["POST"])
|
|
def check():
|
|
try:
|
|
dell_updates.start("check")
|
|
except Exception as ex:
|
|
flash(f"Check not started: {ex}", "danger")
|
|
return _back()
|
|
audit("DELL_CHECK", "catalog check started")
|
|
flash("Checking Dell's catalogs; this page updates when the scan is ready (a minute or two).", "info")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/download", methods=["POST"])
|
|
def download():
|
|
drivers = set(request.form.getlist("drv"))
|
|
picks = {}
|
|
for row_id in request.form.getlist("bios"):
|
|
try:
|
|
picks[row_id] = int(request.form.get(f"pick_{row_id}", "0"))
|
|
except ValueError:
|
|
picks[row_id] = 0
|
|
try:
|
|
count = dell_updates.select(drivers, picks)
|
|
dell_updates.start("download")
|
|
except Exception as ex:
|
|
flash(f"Download not started: {ex}", "danger")
|
|
return _back()
|
|
audit("DELL_DOWNLOAD_START", f"{count} item(s): {len(drivers)} driver row(s), {len(picks)} BIOS line(s)")
|
|
cap = updates_common.settings().get("max_mbps") or 0
|
|
flash(f"Downloading {count} item(s) at {'%g MB/s' % cap if cap else 'full speed'}. "
|
|
"Driver packs are hash checked and repacked here; nothing on the share changes until you apply.", "info")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/cancel", methods=["POST"])
|
|
def cancel():
|
|
job = request.form.get("job", "download")
|
|
if job == "check":
|
|
updates_common.stop_job(dell_updates.UNITS["check"])
|
|
audit("DELL_CHECK_CANCEL", "")
|
|
flash("Check stopped. The previous scan, if any, is still shown.", "warning")
|
|
else:
|
|
dell_updates.cancel_download()
|
|
audit("DELL_DOWNLOAD_CANCEL", "")
|
|
flash("Download stopped. Partial files are kept; downloading the same items again resumes them.", "warning")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/preview", methods=["POST"])
|
|
def preview():
|
|
keys = request.form.getlist("key")
|
|
if not keys:
|
|
flash("Select at least one staged item to apply.", "warning")
|
|
return _back()
|
|
return _render(preview=dell_updates.preview(keys), preview_keys=keys)
|
|
|
|
|
|
@bp.route("/apply", methods=["POST"])
|
|
def apply():
|
|
keys = request.form.getlist("key")
|
|
if not keys:
|
|
flash("Nothing selected to apply.", "warning")
|
|
return _back()
|
|
if updates_common.job_running(dell_updates.UNITS["download"]):
|
|
flash("A download is still running; apply when it has finished.", "danger")
|
|
return _back()
|
|
try:
|
|
report = dell_updates.apply(keys)
|
|
except Exception as ex:
|
|
flash(f"Apply failed: {ex}", "danger")
|
|
return _back()
|
|
parts = []
|
|
if report["drivers"]:
|
|
parts.append(f"{len(report['drivers'])} driver pack(s) replaced in the maps")
|
|
if report["bios"]:
|
|
parts.append(f"{len(report['bios'])} models.txt line(s) updated")
|
|
if parts:
|
|
flash("Applied: " + ", ".join(parts) + ". Old zips and BIOS files were kept; the maps and "
|
|
"models.txt were backed up first (Rollback in the history below).", "success")
|
|
for msg in report["skipped"]:
|
|
flash(f"Skipped {msg}.", "warning")
|
|
for msg in report["errors"]:
|
|
flash(f"Not applied: {msg}", "danger")
|
|
if not parts and not report["skipped"] and not report["errors"]:
|
|
flash("Nothing was applied (no selected item was ready).", "warning")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/discard", methods=["POST"])
|
|
def discard():
|
|
keys = request.form.getlist("key")
|
|
if updates_common.job_running(dell_updates.UNITS["download"]):
|
|
flash("A download is running; cancel it first.", "danger")
|
|
return _back()
|
|
gone = dell_updates.discard(keys)
|
|
if gone:
|
|
audit("DELL_DISCARD", ", ".join(gone))
|
|
flash(f"Removed {len(gone)} staged item(s) from the staging area.", "success")
|
|
else:
|
|
flash("Nothing removed.", "warning")
|
|
return _back()
|
|
|
|
|
|
@bp.route("/rollback/<apply_id>")
|
|
def rollback_review(apply_id):
|
|
"""Review a rollback before doing it: which files would be restored
|
|
(content unchanged since the apply) and the diff of each."""
|
|
try:
|
|
rec = dell_updates.rollback_preview(apply_id)
|
|
except ValueError:
|
|
flash("No apply with that id.", "warning")
|
|
return redirect(url_for("updates_overview"))
|
|
return render_template("updates_rollback.html", rec=rec)
|
|
|
|
|
|
@bp.route("/rollback", methods=["POST"])
|
|
def rollback():
|
|
apply_id = request.form.get("apply_id", "")
|
|
try:
|
|
restored, refused = dell_updates.rollback(apply_id)
|
|
except Exception as ex:
|
|
flash(f"Rollback failed: {ex}", "danger")
|
|
return _back()
|
|
if restored:
|
|
flash(f"Restored {len(restored)} file(s) from before apply {apply_id}. The new zips/BIOS files stay "
|
|
"on the share, unused.", "success")
|
|
for msg in refused:
|
|
flash(f"Not restored: {msg}.", "danger")
|
|
if request.form.get("next") == "overview":
|
|
return redirect(url_for("updates_overview"))
|
|
return _back()
|
|
|
|
|
|
@bp.route("/settings", methods=["POST"])
|
|
def save_settings():
|
|
save_speed_cap() # shared with the other Updates pages (views/__init__.py)
|
|
if request.form.get("next") == "overview":
|
|
return redirect(url_for("updates_overview"))
|
|
return _back()
|