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.
399 lines
24 KiB
HTML
399 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Blancco Updates - PXE Server Manager{% endblock %}
|
|
|
|
{% macro mb(n) -%}{{ "%.1f"|format((n or 0) / 1048576) }} MB{%- endmacro %}
|
|
|
|
{% macro release_cells(r) -%}
|
|
<td class="mono">{{ r.kernel or '-' }}</td>
|
|
<td class="mono">{{ r.prefs_version or '-' }}{% if r.vendor_prefs_version and r.vendor_prefs_version != r.prefs_version %}<div class="small text-muted">vendor {{ r.vendor_prefs_version }}</div>{% endif %}</td>
|
|
<td class="text-nowrap">{{ mb(r.size) }}</td>
|
|
{%- endmacro %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1><i class="bi bi-eraser"></i> Blancco Updates</h1>
|
|
<div class="header-actions">
|
|
<span class="badge badge-info badge-lg">Live: {{ current.version }}{% if current.inferred %} (installed by hand){% endif %}</span>
|
|
{% if test %}<span class="badge badge-warning badge-lg">Test entry: {{ test.version or '?' }}</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-muted">
|
|
Installs a new Blancco Drive Eraser release from the vendor ISO (or the vendor's USB image, <span class="mono">.img</span>
|
|
or <span class="mono">.img.gz</span>). Staging extracts the release next to the live one, applies the same changes as
|
|
<span class="mono">scripts/prepare-boot-tools.sh</span> (our preferences written into <span class="mono">config.img</span>,
|
|
the initramfs network patch), and a test entry in the PXE boot menu boots the staged copy. Only <strong>Activate</strong>
|
|
changes what the normal Blancco entry boots, and the files it replaces are kept as a backup you can roll back to.
|
|
The Ubuntu kernel, its initrd and the kmod tarball the UEFI path uses are not part of a release and are never touched.
|
|
</p>
|
|
|
|
{# ---- Live release -------------------------------------------------------- #}
|
|
<div class="section-card">
|
|
<div class="section-title"><i class="bi bi-hdd-network"></i> Live release</div>
|
|
<p class="small text-muted mb-2">Served from <span class="mono">{{ current.path }}</span>.</p>
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead><tr><th>Version</th><th>Blancco kernel</th><th>Preferences</th><th>Size</th><th>Rootfs date</th><th>Installed</th></tr></thead>
|
|
<tbody>
|
|
<tr>
|
|
<td class="mono">{{ current.version }}
|
|
{% if current.inferred %}<div class="small text-muted">from the preferences in config.img</div>{% endif %}
|
|
{% if current.changed_by_hand %}<div class="small text-warning">files changed outside this page since the last activate or roll back</div>{% endif %}</td>
|
|
{{ release_cells(current) }}
|
|
<td class="mono text-nowrap">{{ current.rootfs_mtime | timestamp_fmt if current.rootfs_mtime else '-' }}</td>
|
|
<td class="small">
|
|
{% if current.activated %}{{ current.activated | local_dt }}
|
|
{% if current.restored_from %}<div class="text-muted">rolled back to {{ current.restored_from }}</div>
|
|
{% elif current.source %}<div class="text-muted">from {{ current.source }}</div>{% endif %}
|
|
{% else %}<span class="text-muted">by hand (before this page)</span>{% endif %}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if current.missing %}
|
|
<div class="alert alert-warning mt-2 mb-0">Missing from the live folder: <span class="mono">{{ current.missing | join(', ') }}</span></div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ---- Job ----------------------------------------------------------------- #}
|
|
{% if job.state %}
|
|
<div class="section-card" id="blanccoJob" data-state="{{ job.state }}">
|
|
<div class="section-title"><i class="bi bi-gear"></i> Staging {{ job.source or '' }}</div>
|
|
{% if job_running %}
|
|
<div class="d-flex align-items-center gap-2 mb-1">
|
|
<span class="spinner-border spinner-border-sm" role="status"></span>
|
|
<span id="blanccoJobStep">{{ job.step or 'Starting' }}</span>
|
|
<span id="blanccoJobMsg" class="small text-muted">{{ job.message or '' }}</span>
|
|
</div>
|
|
<div class="progress mb-2" style="height: 0.5rem;">
|
|
<div id="blanccoJobBar" class="progress-bar" style="width: {{ job.percent or 0 }}%;"></div>
|
|
</div>
|
|
<form method="post" action="{{ url_for('blancco_update.cancel') }}" class="mb-2"
|
|
onsubmit="return confirm('Stop staging and remove the partial files?');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-ghost pxe-btn-sm"><i class="bi bi-x-circle"></i> Cancel</button>
|
|
</form>
|
|
{% elif job.state == 'done' %}
|
|
<div class="alert alert-success mb-2">{{ job.message }} <span class="small text-muted">({{ job.finished | local_dt }})</span></div>
|
|
{% elif job.state == 'blocked' %}
|
|
<div class="alert alert-warning mb-2"><strong>Staging stopped: preferences need merging.</strong> {{ job.message }}</div>
|
|
{% elif job.state == 'cancelled' %}
|
|
<div class="alert alert-warning mb-2">Staging was cancelled.</div>
|
|
{% else %}
|
|
<div class="alert alert-danger mb-2">{{ job.message or 'Staging failed.' }}</div>
|
|
{% endif %}
|
|
{% if job.log %}
|
|
<details {% if job_running or job.state in ('failed', 'blocked') %}open{% endif %}>
|
|
<summary class="small text-muted" style="cursor:pointer;">Job log</summary>
|
|
<pre id="blanccoJobLog" class="mono small mb-0" style="white-space: pre-wrap; max-height: 16rem; overflow:auto;">{{ job.log | join('\n') }}</pre>
|
|
</details>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ---- Get a release ------------------------------------------------------- #}
|
|
<div class="section-card">
|
|
<div class="section-title"><i class="bi bi-upload"></i> Release files</div>
|
|
<form id="blanccoUpload" method="post" action="{{ url_for('blancco_update.upload') }}" enctype="multipart/form-data"
|
|
class="d-flex flex-wrap gap-2 align-items-end mb-2">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="form-field mb-0" style="min-width: min(22rem, 100%);">
|
|
<label for="blanccoFile" class="form-label">Vendor ISO or USB image</label>
|
|
<input type="file" class="form-control" id="blanccoFile" name="release_file" accept=".iso,.img,.gz" required>
|
|
<div class="form-text">About 1 GB; streamed to <span class="mono">{{ upload_dir }}</span>.</div>
|
|
</div>
|
|
<button type="submit" class="pxe-btn pxe-btn-primary"><i class="bi bi-upload"></i> Upload</button>
|
|
</form>
|
|
<div id="blanccoUploadProgress" class="mb-3" style="display:none;">
|
|
<div class="small mb-1" id="blanccoUploadText">Uploading...</div>
|
|
<div class="progress" style="height: 0.5rem;"><div id="blanccoUploadBar" class="progress-bar" style="width: 0%;"></div></div>
|
|
</div>
|
|
<p class="small text-muted">
|
|
Files copied in by hand are listed too, from
|
|
{% for key, folder in source_dirs %}<span class="mono">{{ folder }}</span>{{ ', ' if not loop.last }}{% endfor %}
|
|
(<span class="mono">.iso</span>, <span class="mono">.img</span>, <span class="mono">.img.gz</span>, <span class="mono">.iso.gz</span>).
|
|
</p>
|
|
{% if sources %}
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead><tr><th>File</th><th>Folder</th><th>Size</th><th>Modified</th><th class="text-end"></th></tr></thead>
|
|
<tbody>
|
|
{% for s in sources %}
|
|
<tr>
|
|
<td class="mono">{{ s.name }}</td>
|
|
<td class="small text-muted">{{ s.key }}</td>
|
|
<td class="text-nowrap">{{ mb(s.size) }}</td>
|
|
<td class="mono text-nowrap">{{ s.modified | timestamp_fmt }}</td>
|
|
<td class="text-end text-nowrap">
|
|
<form method="post" action="{{ url_for('blancco_update.stage') }}" style="display:inline;">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="source" value="{{ s.key }}:{{ s.name }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-primary btn-row-action" {% if job_running %}disabled{% endif %}>
|
|
<i class="bi bi-box-seam"></i> Stage</button>
|
|
</form>
|
|
{% if s.deletable %}
|
|
<form method="post" action="{{ url_for('blancco_update.delete_upload', name=s.name) }}" style="display:inline;"
|
|
onsubmit="return confirm({{ ('Delete ' ~ s.name ~ '?') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-ghost btn-row-action"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted mb-0">No release files yet. Upload the vendor ISO or USB image above.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ---- Staged -------------------------------------------------------------- #}
|
|
<div class="section-card">
|
|
<div class="section-title"><i class="bi bi-box-seam"></i> Staged releases</div>
|
|
{% if staged %}
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead><tr><th>Version</th><th>Blancco kernel</th><th>Preferences</th><th>Size</th><th>Status</th><th>Staged</th><th class="text-end"></th></tr></thead>
|
|
<tbody>
|
|
{% for s in staged %}
|
|
<tr>
|
|
<td class="mono">{{ s.name }}{% if s.in_menu %} <span class="badge badge-warning">in boot menu</span>{% endif %}</td>
|
|
{{ release_cells(s) }}
|
|
<td>
|
|
{% if s.status == 'ready' %}<span class="badge badge-success">Ready</span>
|
|
{% if s.merged_prefs %}<div class="small text-muted">merged preferences</div>{% endif %}
|
|
{% elif s.status == 'blocked' %}<span class="badge badge-danger">Needs preferences merge</span>
|
|
{% elif s.status == 'failed' %}<span class="badge badge-danger">Failed</span>
|
|
{% if s.error %}<div class="small text-danger wrap">{{ s.error }}</div>{% endif %}
|
|
{% else %}<span class="badge badge-secondary">{{ s.status }}</span>{% endif %}
|
|
</td>
|
|
<td class="small text-nowrap">{{ s.staged | local_dt }}<div class="text-muted">{{ s.source or '' }}</div></td>
|
|
<td class="text-end text-nowrap">
|
|
{% if s.status == 'ready' %}
|
|
{% if not s.in_menu %}
|
|
<form method="post" action="{{ url_for('blancco_update.test_add', version=s.name) }}" style="display:inline;"
|
|
onsubmit="return confirm({{ ('Add a test entry for ' ~ s.name ~ ' to the PXE boot menu' ~ (' (replacing the one for ' ~ (test.version or '?') ~ ')' if test else '') ~ '? The menu is backed up first.') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-secondary btn-row-action"><i class="bi bi-list-check"></i> Add test entry</button>
|
|
</form>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('blancco_update.activate', version=s.name) }}" style="display:inline;"
|
|
onsubmit="return confirm({{ ('Make Blancco ' ~ s.name ~ ' the live release? Every Blancco boot from now on gets it. The current files (' ~ current.version ~ ') are backed up first and can be rolled back.') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-primary btn-row-action"><i class="bi bi-check2-circle"></i> Activate</button>
|
|
</form>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('blancco_update.delete_staged', version=s.name) }}" style="display:inline;"
|
|
onsubmit="return confirm({{ ('Delete staged ' ~ s.name ~ '?') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-ghost btn-row-action" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% if s.status == 'blocked' %}
|
|
<tr>
|
|
<td colspan="7" class="wrap">
|
|
<div class="alert alert-warning mb-2">{{ s.blocked }}</div>
|
|
<ol class="small mb-2">
|
|
<li><a href="{{ url_for('blancco_update.vendor_prefs', version=s.name) }}">Download the vendor preferences.xml of {{ s.name }}</a>.</li>
|
|
<li>Carry our settings across from <span class="mono">playbook/blancco-preferences.xml</span> (report share
|
|
<span class="mono">{{ server }}</span>/<span class="mono">blancco-reports</span>, <span class="mono">auto_backup</span>, WiFi,
|
|
screensaver, business location and the rest), keeping the new file's <span class="mono"><version></span> block and any new settings.</li>
|
|
<li>Upload the merged file here; staging then finishes. Commit it to the repo as <span class="mono">playbook/blancco-preferences.xml</span>.</li>
|
|
</ol>
|
|
<form method="post" action="{{ url_for('blancco_update.merged_prefs', version=s.name) }}" enctype="multipart/form-data"
|
|
class="d-flex flex-wrap gap-2 align-items-end">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="form-field mb-0" style="min-width: min(20rem, 100%);">
|
|
<label class="form-label" for="prefs-{{ loop.index }}">Merged preferences.xml</label>
|
|
<input type="file" class="form-control" id="prefs-{{ loop.index }}" name="prefs_file" accept=".xml" required>
|
|
</div>
|
|
<button type="submit" class="pxe-btn pxe-btn-primary"><i class="bi bi-upload"></i> Upload and finish staging</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted mb-0">Nothing staged.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ---- Test entry ---------------------------------------------------------- #}
|
|
<div class="section-card">
|
|
<div class="section-title"><i class="bi bi-list-check"></i> Test entry in the PXE boot menu</div>
|
|
{% if test %}
|
|
<p class="mb-2">
|
|
The menu has <span class="mono">{{ test.item or 'a blancco_staged entry' }}</span>, booting the staged
|
|
<strong class="mono">{{ test.version or '?' }}</strong> from <span class="mono">/blancco/staged/{{ test.version }}/</span>.
|
|
</p>
|
|
{% if test.efi is false %}
|
|
<div class="alert alert-warning">The entry's UEFI branch is off: when it was added, the UEFI initrd did not read
|
|
<span class="mono">blancco.base=</span>, so a UEFI test boot would have run the LIVE release. A UEFI boot of the entry
|
|
says so and returns to the menu. After rebuilding the initrd, add the test entry again.</div>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('blancco_update.test_remove') }}"
|
|
onsubmit="return confirm('Remove the test entry from the PXE boot menu? The menu is backed up first.');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-secondary"><i class="bi bi-x-circle"></i> Remove test entry</button>
|
|
</form>
|
|
{% else %}
|
|
<p class="text-muted">No test entry. <strong>Add test entry</strong> on a staged release puts one under the normal Blancco entry.</p>
|
|
{% endif %}
|
|
<ul class="small mt-3 mb-0">
|
|
<li><strong>Legacy BIOS:</strong> the entry boots Blancco's own kernel from the staged folder
|
|
(<span class="mono">archiso_http_srv</span> pointed at it). Works with any initrd.</li>
|
|
<li><strong>UEFI:</strong> the entry copies the live <span class="mono">:blancco_efi</span> kernel line (Ubuntu kernel,
|
|
Secure Boot as the normal entry) and adds <span class="mono">blancco.base=/blancco/staged/<version></span>, which
|
|
the init in <span class="mono">{{ initrd.file or 'kexec-initrd' }}</span> must understand:
|
|
{% if initrd.state == 'yes' %}<span class="badge badge-success">supported</span>
|
|
{% elif initrd.state == 'no' %}<span class="badge badge-danger">not supported</span>
|
|
<div class="text-danger">This initrd predates <span class="mono">blancco.base=</span>: booted, it would run the LIVE release, so a test
|
|
entry added now has its UEFI branch off (legacy BIOS still works). Rebuild the initrd with the current
|
|
<span class="mono">playbook/blancco-init.sh</span>, then add the entry again (the boot screen shows "release under test"
|
|
when it works).</div>
|
|
{% else %}<span class="badge badge-secondary">unknown</span> <span class="text-muted">{{ initrd.detail }}</span>{% endif %}</li>
|
|
<li>Add, replace and remove edit only the test entry's lines; each edit saves
|
|
<span class="mono">GetPxeScript.aspx.bak-<time>-pre-blancco-staged*</span> next to the menu first. Activating the
|
|
version under test removes its entry.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{# ---- Backups ------------------------------------------------------------- #}
|
|
<div class="section-card">
|
|
<div class="section-title"><i class="bi bi-clock-history"></i> Backups</div>
|
|
<p class="small text-muted mb-2">Folders <span class="mono">{{ current.path }}/backup-*</span>. A rollback first saves what is live, so it can be undone the same way;
|
|
the restored backup's folder is then released, because its files are the live files.</p>
|
|
{% if backups %}
|
|
<div class="table-container">
|
|
<table class="data-table">
|
|
<thead><tr><th>Backup</th><th>Version</th><th>Blancco kernel</th><th>Preferences</th><th>Size</th><th>Saved</th><th class="text-end"></th></tr></thead>
|
|
<tbody>
|
|
{% for b in backups %}
|
|
<tr>
|
|
<td class="mono">{{ b.name }}{% if b.reason %}<div class="small text-muted">{{ b.reason }}</div>{% endif %}</td>
|
|
<td class="mono">{{ b.version }}</td>
|
|
{{ release_cells(b) }}
|
|
<td class="mono text-nowrap">{{ b.created | local_dt if b.created is string else b.created | timestamp_fmt }}</td>
|
|
<td class="text-end text-nowrap">
|
|
{% if b.complete %}
|
|
<form method="post" action="{{ url_for('blancco_update.rollback', name=b.name) }}" style="display:inline;"
|
|
onsubmit="return confirm({{ ('Put ' ~ b.name ~ ' back live? What is live now (' ~ current.version ~ ') is backed up first.') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-secondary btn-row-action"><i class="bi bi-arrow-counterclockwise"></i> Roll back</button>
|
|
</form>
|
|
{% else %}
|
|
<span class="badge badge-secondary" title="Missing {{ b.missing | join(', ') }}">Incomplete</span>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('blancco_update.delete_backup', name=b.name) }}" style="display:inline;"
|
|
onsubmit="return confirm({{ ('Delete ' ~ b.name ~ '? It cannot be rolled back to afterwards.') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="pxe-btn pxe-btn-ghost btn-row-action" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="form-text">Each backup holds one release, about 1 GB. Taking one is instant (hard links of the live files before they are replaced).</div>
|
|
{% else %}
|
|
<p class="text-muted mb-0">No backups yet. Activating a release saves the one it replaces here.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ---- How it works -------------------------------------------------------- #}
|
|
<div class="pxe-card">
|
|
<h3 class="pxe-card-title"><i class="bi bi-info-circle"></i> What a release changes</h3>
|
|
<p class="small mb-2">
|
|
Replaced in <span class="mono">{{ current.path }}</span>: <span class="mono">arch/x86_64/airootfs.sfs</span>,
|
|
<span class="mono">vmlinuz-bde-linux</span>, <span class="mono">initramfs-bde-linux.img</span> (copytoram network patch),
|
|
<span class="mono">intel-ucode.img</span>, <span class="mono">amd-ucode.img</span>, <span class="mono">config.img</span>
|
|
(vendor cpio with our preferences written in at the same size), <span class="mono">preferences.xml</span>,
|
|
<span class="mono">config-clean.xml</span> (the licence container from <span class="mono">config.img</span>) and, when the
|
|
ISO has one, <span class="mono">blancco-shim.efi</span>. Never touched: <span class="mono">vmlinuz-ubuntu*</span>,
|
|
<span class="mono">kexec-initrd*</span>, <span class="mono">kmod*.tar.gz</span>, <span class="mono">grubx64.efi</span>,
|
|
<span class="mono">shimx64.efi</span>, <span class="mono">blancco-progress.py</span>.
|
|
</p>
|
|
<p class="small mb-0">
|
|
Our preferences come from the live <span class="mono">preferences.xml</span> (the playbook deploys
|
|
<span class="mono">playbook/blancco-preferences.xml</span> there). They are a merge onto one vendor release's file,
|
|
so when a release's <span class="mono"><version></span> differs, staging stops until a merged file is uploaded.
|
|
After activating such a release, commit the merged file to the repo, or the next playbook run puts the old one back.
|
|
</p>
|
|
<p class="small mb-0 mt-2">
|
|
A playbook run also copies <span class="mono">boot-tools/blancco/*</span> from the USB over the live files. After activating
|
|
a release here, refresh that copy (<span class="mono">scripts/prepare-boot-tools.sh</span> with the new ISO), or a playbook run
|
|
puts the old release back.
|
|
</p>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
(function () {
|
|
var csrf = document.querySelector('meta[name="csrf-token"]').content;
|
|
|
|
// Upload: stream the raw file (no multipart parse, no copy in /tmp) with a progress bar.
|
|
var form = document.getElementById('blanccoUpload');
|
|
form.addEventListener('submit', function (ev) {
|
|
var input = document.getElementById('blanccoFile');
|
|
if (!input.files.length || !window.XMLHttpRequest) { return; }
|
|
ev.preventDefault();
|
|
var file = input.files[0];
|
|
var box = document.getElementById('blanccoUploadProgress');
|
|
var bar = document.getElementById('blanccoUploadBar');
|
|
var text = document.getElementById('blanccoUploadText');
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('POST', form.action + '?name=' + encodeURIComponent(file.name));
|
|
xhr.setRequestHeader('X-CSRF-Token', csrf);
|
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
|
xhr.upload.onprogress = function (e) {
|
|
if (!e.lengthComputable) { return; }
|
|
var pct = 100 * e.loaded / e.total;
|
|
bar.style.width = pct.toFixed(1) + '%';
|
|
text.textContent = 'Uploading ' + file.name + ': ' + Math.round(e.loaded / 1048576) + ' of ' +
|
|
Math.round(e.total / 1048576) + ' MB';
|
|
};
|
|
xhr.onload = function () {
|
|
var r = {};
|
|
try { r = JSON.parse(xhr.responseText); } catch (e) {}
|
|
if (xhr.status === 200 && r.ok) { location.reload(); return; }
|
|
text.textContent = 'Upload failed: ' + (r.error || ('HTTP ' + xhr.status));
|
|
bar.classList.add('bg-danger');
|
|
};
|
|
xhr.onerror = function () { text.textContent = 'Upload failed: connection lost.'; bar.classList.add('bg-danger'); };
|
|
box.style.display = '';
|
|
form.querySelector('button[type=submit]').disabled = true;
|
|
xhr.send(file);
|
|
});
|
|
|
|
// Follow a running staging job; reload when it ends.
|
|
var job = document.getElementById('blanccoJob');
|
|
if (!job || job.dataset.state !== 'running') { return; }
|
|
var URL = '{{ url_for("blancco_update.status") }}';
|
|
function tick() {
|
|
fetch(URL, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (d) {
|
|
if (d.state !== 'running') { location.reload(); return; }
|
|
document.getElementById('blanccoJobStep').textContent = d.step || '';
|
|
document.getElementById('blanccoJobMsg').textContent = d.message || '';
|
|
document.getElementById('blanccoJobBar').style.width = (d.percent || 0) + '%';
|
|
var log = document.getElementById('blanccoJobLog');
|
|
if (log && d.log) { log.textContent = d.log.join('\n'); }
|
|
setTimeout(tick, 2000);
|
|
})
|
|
.catch(function () { setTimeout(tick, 5000); });
|
|
}
|
|
setTimeout(tick, 1500);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|