Reports (/reports):
- Grouped by month of the erasure date inside each report (not the file
date), newest first; older months fold. Each month header shows the
evidence pack state (builds on the 1st, to upload with Download, or
uploaded).
- One line per report: erasure date, system serial, model, drives,
result ('Successful, fallback' when a failed attempt fell back to
another standard), cloud and ServiceNow state. The row body lists every
drive and erasure attempt with the failure reason.
- Search by system serial, model, drive serial or file; filters for
failed, not in cloud and to tick. 'To tick' uses the to-do's 30-day
window; older unticked reports read 'Not ticked'.
- Export and the evidence pack download moved into header popovers.
Report (/reports/view/<file>):
- Header with serial, model and the per-drive result; a notice names
the drive to destroy until its COD is recorded; Copy ServiceNow note.
- One card per drive with every attempt, a step track and the failure
reason; steps and error counts fold. System, customer and provenance
(integrity signature folded, copyable) on the side. Sizes in decimal
units, as on the list.
- blancco_evidence.failure_note() is shared with the ServiceNow list.
To-do (/reports/todo):
- The ServiceNow tick list is now its first section (period and show
ticked filters); /reports/servicenow redirects there.
- Five counters; one line per item with details in the row body.
- COD recorded, Mark uploaded and Resolved confirm first (the attribute
quoting is safe for JSON) and can be undone for 24 hours from the
page. The header total includes hardware gaps, matching the sidebar.
222 lines
14 KiB
HTML
222 lines
14 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(320px, calc(100vw - 32px)); padding: 14px; gap: 10px">
|
|
<label for="evidenceMonth" class="sub">ENT-056/158 evidence pack for</label>
|
|
<input id="evidenceMonth" type="month" name="month" value="{{ last_month }}" class="field" required>
|
|
<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));
|
|
});
|
|
// 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 %}
|