Files
pxe-server/webapp/templates/reports.html
cproudlock 2dbaaded44 Webapp: evidence pack date range, US month labels, hardware gap rows
Evidence pack takes ?from=&to= (inclusive, Eastern time) with From/To
pickers and quick ranges; ZIP is named ENT-056_<site>_<from>_to_<to>.
Month keys show as 'September 2026'. Hardware gap rows on the to-do
use the shared one-line grid with chips for the missing pack or BIOS.
2026-10-14 15:08:36 -04:00

245 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" aria-label="Quick ranges">
<button type="button" class="fbtn" data-range="last-month">Last month</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">&#9662;</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">&#9662;</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 === '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 %}