- imaging: coerce client payload and stored session fields to str/int, drop lists/dicts; tiles template guards pctype/current_stage; POST rejects non-object bodies and non-string serials. - nic_status: normalise every check-file entry; load nic.conf as-is with a [nic] fallback; split interface= lists; keep the check's binding while fresh, resolve wildcard sockets from the dnsmasq pin (never True for an excluded NIC); mark vanished NICs down; include role bridges; stale or future-dated files downgrade to warnings and drop evidence. - Overview renders the network pill in its own try and shows 'Network: unknown' when status fails. - ipxe_update: read every Authenticode signature (all WIN_CERTIFICATE entries plus nested SPC_NESTED_SIGNATURE) with openssl; shim swap and install need both Microsoft UEFI CA 2011 and 2023; install re-reads each file (snponly ipxe.efi only); staged.json schema version, older records shown stale and refused; uhci counts as native USB. - backups describe their files; risky rollbacks need a confirm flag. - mac_folders: guarded listdir, cached describe, configured TFTP root, warning for a shim without ipxe.efi. - evidence pack: years 2000..2100, 400 days inclusive. - a11y role=group on quick ranges; progress bar stays full during 'shim'. - tests/test_review_fixes.py: 29 unittest cases incl. both real shims.
247 lines
16 KiB
HTML
247 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}Blancco reports - PXE Manager{% endblock %}
|
|
|
|
{# Blancco reports: one line per report, grouped by month (the erasure date
|
|
inside each report). Each row expands to its drives and every erasure
|
|
attempt; a failed Purge followed by a successful Clear on the same drive
|
|
counts as wiped and shows as "Successful, fallback". #}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.rgrid { display: grid; grid-template-columns: 116px 110px minmax(0, 1fr) 64px 160px 92px 96px 14px; align-items: center;
|
|
gap: 0 14px; padding: 11px 120px 11px 24px; font-size: 14px; min-height: 24px; }
|
|
.rhead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; }
|
|
.rwhen { font-size: 13px; color: var(--muted); white-space: nowrap; }
|
|
.month { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 14px 24px 10px; border-top: 1px solid var(--line-2); background: var(--surface-2); }
|
|
.month > summary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; width: 100%; }
|
|
.month h3 { margin: 0; font-size: 15px; font-weight: 600; }
|
|
.month .sp { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.month .sp form { margin: 0; }
|
|
details.mgroup > summary { list-style: none; }
|
|
.rbody { padding: 2px 24px 16px 150px; font-size: 13px; display: flex; flex-direction: column; gap: 10px; }
|
|
.drive { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; align-items: start; padding: 10px 12px; border: 1px solid var(--line-2); border-radius: 8px; }
|
|
.drive > div:first-child { min-width: 0; overflow-wrap: anywhere; }
|
|
.drive .attempts { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 4px; }
|
|
.att { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; }
|
|
.att .why { color: var(--fail-text); }
|
|
.rmeta { display: flex; flex-wrap: wrap; gap: 4px 18px; color: var(--muted); font-size: 12px; }
|
|
.rmeta .mono { overflow-wrap: anywhere; }
|
|
.rl-empty { padding: 28px 24px; text-align: center; color: var(--muted); border-top: 1px solid var(--line); }
|
|
@container (max-width: 1000px) {
|
|
.rgrid { grid-template-columns: 100px 104px minmax(0, 1fr) 160px 14px; }
|
|
.rgrid .c-drv, .rgrid .c-cloud, .rgrid .c-sn { display: none; }
|
|
.rbody { padding-left: 24px; }
|
|
}
|
|
@container (max-width: 620px) {
|
|
.rgrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 11px 16px; gap: 2px 10px; }
|
|
.rgrid .rwhen { grid-column: 1 / -1; }
|
|
.rgrid .c-model { grid-column: 1 / -1; grid-row: 3; }
|
|
.rhead { display: none; }
|
|
.ui-row > .act { position: static; display: flex; justify-content: flex-end; padding: 0 16px 10px; }
|
|
.rbody { padding: 2px 16px 14px; }
|
|
.month { padding: 12px 16px 8px; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% call ui.head('Blancco reports', 'Operate', total ~ ' report' ~ ('s' if total != 1) ~ ' on the share. Dates are the erasure date inside each report.') %}
|
|
<details class="more">
|
|
<summary class="ui-btn">Export</summary>
|
|
<form class="menu" method="get" action="{{ url_for('blancco_export_reports') }}" style="width: min(320px, calc(100vw - 32px)); padding: 14px; gap: 10px">
|
|
<label for="exportRange" class="sub">Reports from the last</label>
|
|
<select id="exportRange" name="range" class="field">
|
|
<option value="1m">1 month</option><option value="3m">3 months</option><option value="6m">6 months</option><option value="12m">12 months</option>
|
|
</select>
|
|
<div style="display: flex; gap: 8px">
|
|
<button type="submit" name="format" value="csv" class="ui-btn" title="One row per erased drive">Summary CSV</button>
|
|
<button type="submit" name="format" value="zip" class="ui-btn" title="Original XML/PDF reports plus summary.csv">Reports ZIP</button>
|
|
</div>
|
|
<div class="sub" style="white-space: normal">Dated by the timestamp inside each report.</div>
|
|
</form>
|
|
</details>
|
|
<details class="more">
|
|
<summary class="ui-btn">Evidence pack</summary>
|
|
<form class="menu" method="get" action="{{ url_for('blancco_evidence_pack') }}" style="width: min(380px, calc(100vw - 32px)); padding: 14px; gap: 10px">
|
|
<div class="sub" style="white-space: normal">ENT-056/158 evidence pack: every report erased between these dates (Eastern time, both days included).</div>
|
|
<div style="display: flex; gap: 8px; flex-wrap: wrap">
|
|
<label class="sub" style="display: flex; flex-direction: column; gap: 4px">From
|
|
<input id="evidenceFrom" type="date" name="from" value="{{ pack_from }}" class="field" required></label>
|
|
<label class="sub" style="display: flex; flex-direction: column; gap: 4px">To
|
|
<input id="evidenceTo" type="date" name="to" value="{{ pack_to }}" class="field" required></label>
|
|
</div>
|
|
<div style="display: flex; gap: 6px; flex-wrap: wrap" role="group" aria-label="Quick ranges">
|
|
<button type="button" class="fbtn" data-range="last-month">Last month</button>
|
|
<button type="button" class="fbtn" data-range="last-3-months">Last 3 months</button>
|
|
<button type="button" class="fbtn" data-range="this-month">This month</button>
|
|
<button type="button" class="fbtn" data-range="last-30">Last 30 days</button>
|
|
</div>
|
|
<button type="submit" class="ui-btn pri">Download</button>
|
|
<div class="sub" style="white-space: normal">Built automatically into <span class="mono">evidence\</span> on the reports share on the 1st.</div>
|
|
</form>
|
|
</details>
|
|
<a class="ui-btn pri" href="{{ url_for('blancco_todo') }}#sn">ServiceNow tick list <span class="chip c-neutral" style="font-size: 11px; padding: 1px 6px">{{ todo.servicenow }}</span></a>
|
|
{% endcall %}
|
|
|
|
<section class="panel ui-list" aria-labelledby="rl-h">
|
|
<h2 id="rl-h" class="vh">Reports by month</h2>
|
|
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 16px 24px">
|
|
<div role="search" style="flex: 1 1 220px; min-width: 160px; display: flex">
|
|
<label for="rq" class="vh">Search by system serial, model, drive serial or file name</label>
|
|
<input id="rq" type="search" class="field" style="flex: 1; min-width: 0" placeholder="System serial, model, drive serial or file">
|
|
</div>
|
|
<div role="group" aria-label="Show only" id="rfilters" style="display: flex; gap: 6px; flex-wrap: wrap">
|
|
<button type="button" class="fbtn" data-f="" aria-pressed="true">All</button>
|
|
<button type="button" class="fbtn" data-f="failed" aria-pressed="false">Failed {{ counts.failed }}</button>
|
|
<button type="button" class="fbtn" data-f="cloud" aria-pressed="false">Not in cloud {{ counts.cloud }}</button>
|
|
<button type="button" class="fbtn" data-f="tick" aria-pressed="false">To tick {{ counts.tick }}</button>
|
|
</div>
|
|
</div>
|
|
<div class="rgrid rhead" aria-hidden="true"><span>Erased</span><span>System serial</span><span>Model</span><span class="c-drv">Drives</span><span>Result</span><span class="c-cloud">Cloud</span><span class="c-sn">ServiceNow</span><span></span></div>
|
|
|
|
{% for g in groups %}
|
|
{% set open_month = loop.index <= 2 %}
|
|
<details class="mgroup" {% if open_month %}open{% endif %} data-month="{{ g.month }}">
|
|
<summary class="month">
|
|
<h3>{{ g.label }}</h3>
|
|
<span class="sub">{{ g.rows | length }} report{{ 's' if g.rows | length != 1 }}{% if g.failed %} | {{ g.failed }} failed{% endif %}</span>
|
|
<span class="sp">
|
|
{% if g.month == this_month %}<span class="sub">Evidence pack builds on the 1st</span>
|
|
{% elif g.uploaded %}{{ ui.chip('Evidence pack uploaded', 'ok') }}
|
|
{% elif g.pack %}{{ ui.chip('Evidence pack to upload', 'act') }}
|
|
<a class="ui-btn sm" href="{{ url_for('blancco_evidence_file', filename=g.pack) }}">Download pack</a>
|
|
{% endif %}
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</span>
|
|
</summary>
|
|
{% for r in g.rows %}
|
|
{% set failed = r.state == 'Failed' %}
|
|
<div class="ui-row rrow" data-f="{{ 'failed ' if failed }}{{ 'cloud ' if r.cloud == 'failed' }}{{ 'tick' if r.to_tick }}"
|
|
data-q="{{ (r.serial ~ ' ' ~ r.model ~ ' ' ~ r.filename ~ ' ' ~ (r.drives | map(attribute='serial') | join(' ')) ~ ' ' ~ (r.drives | map(attribute='model') | join(' '))) | lower }}">
|
|
<details>
|
|
<summary class="rgrid">
|
|
<span class="rwhen mono">{{ r.when.strftime('%d %b %-I:%M %p') }}</span>
|
|
<span class="mono">{{ r.serial or '-' }}</span>
|
|
<span class="tip c-model" data-tip="{{ r.model }}"><span class="trunc">{{ r.model or r.filename }}</span></span>
|
|
<span class="c-drv sub">{% if r.drives %}{{ r.drives | length }} drive{{ 's' if r.drives | length != 1 }}{% else %}{{ r.type }}{% endif %}</span>
|
|
<span>{% if r.state == 'Successful' %}{{ ui.chip('Successful, fallback' if r.fallback else 'Successful', 'ok') }}
|
|
{% elif failed %}{{ ui.chip('Failed', 'fail') }}
|
|
{% elif r.parse_error %}{{ ui.chip('Unreadable', 'err') }}
|
|
{% else %}<span class="sub">no erasure</span>{% endif %}</span>
|
|
<span class="c-cloud">{% if r.cloud == 'sent' %}{{ ui.chip('Sent', 'ok') }}{% elif r.cloud == 'failed' %}{{ ui.chip('Not sent', 'err') }}{% else %}<span class="sub">no status</span>{% endif %}</span>
|
|
<span class="c-sn">{% if r.ticked %}{{ ui.chip('Ticked', 'neutral') }}{% elif r.to_tick %}{{ ui.chip('To tick', 'act') }}{% elif r.key %}{{ ui.chip('Not ticked', 'off') }}{% endif %}</span>
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</summary>
|
|
<div class="rbody">
|
|
{% for d in r.drives %}
|
|
<div class="drive">
|
|
<div><strong>{{ d.model or 'Drive' }}</strong> <span class="mono">{{ d.serial }}</span></div>
|
|
<div class="sub">{% if d.capacity %}{{ ((d.capacity | int) / 1e9) | round(0) | int }} GB{% endif %}{% if d.interface %} | {{ d.interface }}{% endif %}</div>
|
|
<div class="attempts">
|
|
{% for a in d.attempts %}
|
|
<div class="att">{{ ui.chip('Erased' if a.ok else 'Failed', 'ok' if a.ok else 'fail') }}<span>{{ a.standard }}</span><span class="sub">{{ a.elapsed }}</span>{% if a.why %}<span class="why">{{ a.why }}</span>{% endif %}</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% if r.fallback %}<p class="sub" style="margin: 0">Counts as successful: every drive has a successful erasure; a failed attempt fell back to another standard.</p>{% endif %}
|
|
{% if failed %}<div class="notice"><span aria-hidden="true" style="font-weight: 700">!</span><span>A drive must be destroyed. This report is on the <a href="{{ url_for('blancco_todo') }}">to-do</a> for a Certificate of Destruction{% if r.cloud == 'failed' %} and for the Blancco cloud{% endif %}.</span></div>{% endif %}
|
|
{% if r.parse_error %}<div class="errbox">Could not read this report: {{ r.parse_error }}</div>{% endif %}
|
|
<div class="rmeta"><span>File <span class="mono">{{ r.filename }}</span></span>{% if r.document_id %}<span>Document <span class="mono">{{ r.document_id }}</span></span>{% endif %}<span>{{ (r.size or 0) | filesizeformat }}</span></div>
|
|
</div>
|
|
</details>
|
|
<div class="act">
|
|
{% if r.filename.lower().endswith('.xml') %}<a class="ui-btn" href="{{ url_for('blancco_view_report', filename=r.filename) }}">View</a>{% endif %}
|
|
{% call ui.more(r.serial or r.filename) %}
|
|
<a href="{{ url_for('blancco_download_report', filename=r.filename) }}">Download {{ r.type }}</a>
|
|
<button type="button" class="copy-btn" data-copy-text="{{ r.filename }}">Copy file name</button>
|
|
<div class="sep"></div>
|
|
<button type="button" class="del" data-bs-toggle="modal" data-bs-target="#deleteModal" data-filename="{{ r.filename }}">Delete report...</button>
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</details>
|
|
{% else %}
|
|
<div class="rl-empty">No erasure reports yet. They appear here after Blancco Drive Eraser completes a wipe and saves its report to <span class="mono">\\172.16.9.1\blancco-reports</span>.</div>
|
|
{% endfor %}
|
|
<div class="hidden-bar" id="rnomatch" hidden><span>No report matches.</span></div>
|
|
</section>
|
|
|
|
<!-- Delete confirmation -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteTitle">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form id="deleteForm" method="post">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="deleteTitle">Delete report</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Delete <strong id="deleteFilename" class="mono"></strong> from the share?</p>
|
|
<p class="muted mb-0">Erasure reports are compliance evidence. This cannot be undone.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn pri" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn danger">Delete</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
(function () {
|
|
var modal = document.getElementById('deleteModal');
|
|
modal.addEventListener('show.bs.modal', function (e) {
|
|
var filename = e.relatedTarget ? e.relatedTarget.getAttribute('data-filename') : '';
|
|
document.getElementById('deleteFilename').textContent = filename;
|
|
document.getElementById('deleteForm').action = "{{ url_for('blancco_delete_report', filename='__F__') }}".replace('__F__', encodeURIComponent(filename));
|
|
});
|
|
// Evidence pack quick ranges (dates in the browser's local day; the
|
|
// server reads them as Eastern-time days).
|
|
function iso(d) { return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2); }
|
|
document.querySelectorAll('[data-range]').forEach(function (b) {
|
|
b.addEventListener('click', function () {
|
|
var now = new Date(), from, to;
|
|
if (b.dataset.range === 'last-month') { from = new Date(now.getFullYear(), now.getMonth() - 1, 1); to = new Date(now.getFullYear(), now.getMonth(), 0); }
|
|
else if (b.dataset.range === 'last-3-months') { from = new Date(now.getFullYear(), now.getMonth() - 3, 1); to = new Date(now.getFullYear(), now.getMonth(), 0); }
|
|
else if (b.dataset.range === 'this-month') { from = new Date(now.getFullYear(), now.getMonth(), 1); to = now; }
|
|
else { to = now; from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29); }
|
|
document.getElementById('evidenceFrom').value = iso(from);
|
|
document.getElementById('evidenceTo').value = iso(to);
|
|
});
|
|
});
|
|
// Search and filters: rows hide; a month with no visible row hides; a
|
|
// search or filter opens the months so matches are seen.
|
|
var q = document.getElementById('rq'), f = '';
|
|
function apply() {
|
|
var needle = q.value.trim().toLowerCase(), any = false;
|
|
document.querySelectorAll('details.mgroup').forEach(function (g) {
|
|
var shown = 0;
|
|
g.querySelectorAll('.rrow').forEach(function (r) {
|
|
var ok = (!needle || r.getAttribute('data-q').indexOf(needle) >= 0) && (!f || r.getAttribute('data-f').indexOf(f) >= 0);
|
|
r.hidden = !ok; if (ok) shown++;
|
|
});
|
|
g.hidden = shown === 0;
|
|
if ((needle || f) && shown) g.open = true;
|
|
if (shown) any = true;
|
|
});
|
|
document.getElementById('rnomatch').hidden = any;
|
|
}
|
|
q.addEventListener('input', apply);
|
|
document.querySelectorAll('#rfilters .fbtn').forEach(function (b) {
|
|
b.addEventListener('click', function () {
|
|
f = b.getAttribute('data-f') || '';
|
|
document.querySelectorAll('#rfilters .fbtn').forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); });
|
|
apply();
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|