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.
160 lines
11 KiB
HTML
160 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}{{ filename }} - Blancco Report{% endblock %}
|
|
|
|
{% set meta = data.meta or {} %}
|
|
{% set company = data.company or {} %}
|
|
{% set hw = data.hardware or {} %}
|
|
{% set sysinfo = hw.system or {} %}
|
|
{% set bios = hw.bios or {} %}
|
|
{% set memsum = hw.memory or {} %}
|
|
|
|
{# Bytes to GB / TB in decimal units, as drive makers and the reports list use. #}
|
|
{% macro hbytes(n) -%}
|
|
{%- set n = (n | int) -%}
|
|
{%- if n >= 1000000000000 -%}
|
|
{{ "%.2f"|format(n / 1000000000000) }} TB
|
|
{%- elif n >= 1000000000 -%}
|
|
{{ "%.0f"|format(n / 1000000000) }} GB
|
|
{%- elif n >= 1000000 -%}
|
|
{{ "%.0f"|format(n / 1000000) }} MB
|
|
{%- else -%}
|
|
{{ n }} B
|
|
{%- endif -%}
|
|
{%- endmacro %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.rv-hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(170px, 100%), 1fr)); gap: 12px; }
|
|
.rv-stat { background: var(--surface); border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 2px var(--shadow); }
|
|
.rv-stat .k { font-size: 12px; color: var(--muted); }
|
|
.rv-stat .v { font-size: 18px; font-weight: 600; margin-top: 4px; }
|
|
.rv-cols { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(260px, 1fr); gap: 16px; align-items: start; }
|
|
@media (max-width: 1100px) { .rv-cols { grid-template-columns: 1fr; } }
|
|
.rv-drive { padding: 18px 20px; display: flex; flex-direction: column; gap: 12px; }
|
|
.rv-drive h2 { margin: 0; font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
|
|
.rv-att { border-top: 1px solid var(--line); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
.rv-steps { display: flex; gap: 3px; }
|
|
.rv-steps i { flex: 1; height: 8px; border-radius: 2px; background: var(--ok); }
|
|
.rv-steps i.bad { background: var(--fail); }
|
|
.rv-steps i.skip { background: var(--track-empty); }
|
|
.kv { margin: 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; font-size: 13px; }
|
|
.kv dt { color: var(--muted); font-weight: 400; } .kv dd { margin: 0; overflow-wrap: anywhere; }
|
|
.rv-side section { padding: 16px 18px; }
|
|
.rv-side h3 { margin: 0 0 10px; font-size: 14px; font-weight: 600; }
|
|
.rv-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.rv-table th { text-align: left; font-weight: 400; color: var(--muted); padding: 4px 8px 4px 0; }
|
|
.rv-table td { padding: 4px 8px 4px 0; border-top: 1px solid var(--line); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set n_drives = outcomes | length %}
|
|
{% set n_ok = outcomes.values() | selectattr('ok') | list | length %}
|
|
{% set standards = outcomes.values() | selectattr('ok') | map(attribute='standard') | reject('equalto', '') | unique | list %}
|
|
{% set first = (data.erasures or [{}])[0] %}
|
|
|
|
<header class="ui-head">
|
|
<div>
|
|
<a href="{{ url_for('blancco_reports') }}" class="eyebrow-t" style="text-decoration: none">Blancco reports{% if when %} / {{ when.strftime('%B %Y') }}{% endif %}</a>
|
|
<h1 style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px">
|
|
<span class="mono">{{ sysinfo.serial or filename }}</span>
|
|
<span style="font-size: 18px; font-weight: 400">{{ sysinfo.model }}</span>
|
|
{% if result == 'Successful' %}{{ ui.chip('Successful', 'ok') }}{% elif result == 'Failed' %}{{ ui.chip('Failed', 'fail') }}{% else %}{{ ui.chip('No erasure recorded', 'off') }}{% endif %}
|
|
</h1>
|
|
</div>
|
|
<div class="actions">
|
|
<a href="{{ url_for('blancco_download_report', filename=filename) }}" class="ui-btn">Download XML</a>
|
|
{% if note %}<button type="button" class="ui-btn pri copy-btn" data-copy-text="{{ note }}">Copy ServiceNow note</button>{% endif %}
|
|
</div>
|
|
</header>
|
|
|
|
<div class="ui-stack">
|
|
{% if failed and not destroyed %}
|
|
<div class="notice"><span aria-hidden="true" style="font-weight: 700">!</span><span>
|
|
<strong>{% for e in failed %}{{ (e.target or {}).model }} {{ (e.target or {}).serial }}{{ ', ' if not loop.last }}{% endfor %} {{ 'was' if failed | length == 1 else 'were' }} not erased.</strong>
|
|
Remove and destroy it, record the Certificate of Destruction on the RITM, then mark it on the <a href="{{ url_for('blancco_todo') }}#fail">to-do</a>.</span></div>
|
|
{% elif failed and destroyed %}
|
|
<div class="notice info">Certificate of Destruction recorded {{ destroyed.ts | local_dt }}.</div>
|
|
{% endif %}
|
|
|
|
<div class="rv-hero">
|
|
<div class="rv-stat"><div class="k">Result</div>
|
|
<div class="v" style="{{ 'color: var(--fail-text)' if result == 'Failed' }}">{% if n_drives %}{{ n_ok }} of {{ n_drives }} drive{{ 's' if n_drives != 1 }} wiped{% else %}No erasure{% endif %}</div></div>
|
|
<div class="rv-stat"><div class="k">Erased</div><div class="v">{{ when.strftime('%d %b %Y') if when else '-' }}</div><div class="sub">{{ when.strftime('%-I:%M %p %Z') if when else (meta.date or '') }}</div></div>
|
|
<div class="rv-stat"><div class="k">Standard</div><div class="v" style="font-size: 15px">{{ standards | join(', ') if standards else (first.erasure_standard_name or '-') }}</div></div>
|
|
<div class="rv-stat"><div class="k">Blancco cloud</div><div class="v">{{ 'Sent' if cloud == 'sent' else ('Not sent' if cloud == 'failed' else 'No status') }}</div></div>
|
|
<div class="rv-stat"><div class="k">ServiceNow</div><div class="v">{{ 'Ticked' if ticked else 'To tick' }}</div><div class="sub">{{ '"Blancco Wipe failed"' if note else '"Wiped"' }}{% if ticked %}, {{ ticked | local_dt }}{% endif %}</div></div>
|
|
</div>
|
|
|
|
<div class="rv-cols">
|
|
<div style="display: flex; flex-direction: column; gap: 14px">
|
|
{% set groups = {} %}
|
|
{% for er in data.erasures %}{% set t = er.target or {} %}{% set _ = groups.setdefault(t.serial or ('#' ~ loop.index0), []).append(er) %}{% endfor %}
|
|
{% for key, ers in groups.items() %}
|
|
{% set t = ers[0].target or {} %}
|
|
{% set ok = ers | selectattr('state', 'equalto', 'Successful') | list | length > 0 %}
|
|
<section class="panel rv-drive" aria-labelledby="dr{{ loop.index }}">
|
|
<div style="display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; align-items: center">
|
|
<h2 id="dr{{ loop.index }}">{{ t.vendor or '' }} {{ t.model or 'Drive' }} <span class="mono muted" style="font-weight: 400">{{ t.serial }}</span></h2>
|
|
{{ ui.chip('Erased' if ok else 'Not erased', 'ok' if ok else 'fail') }}
|
|
</div>
|
|
<div class="sub">{{ hbytes(t.capacity or 0) }}{% if t.interface_type %} | {{ t.interface_type }}{% endif %}{% if t.health %} | health {{ t.health }}{% endif %}</div>
|
|
{% for er in ers %}
|
|
{% set steps = (er.steps or {}).step %}
|
|
{% if steps is not iterable or steps is string or steps is mapping %}{% set steps = [steps] if steps else [] %}{% endif %}
|
|
{% set good = (er.state or '') | lower == 'successful' %}
|
|
<div class="rv-att">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: baseline">
|
|
<strong>Attempt {{ loop.index }}</strong><span>{{ er.erasure_standard_name or '-' }}</span><span class="sub mono">{{ er.elapsed_time or '' }}</span>
|
|
<span style="margin-left: auto">{{ ui.chip(er.state or 'Unknown', 'ok' if good else 'fail') }}</span>
|
|
</div>
|
|
{% if steps %}
|
|
<div class="rv-steps" role="img" aria-label="{{ steps | length }} steps">{% for st in steps %}<i class="{{ '' if st.state == 'completed' else ('bad' if st.state else 'skip') }}" title="{{ st.type }} {{ st.state }}"></i>{% endfor %}</div>
|
|
{% endif %}
|
|
{% if not good %}<div class="errbox">{{ blancco_failure_reason(er) }}</div>{% endif %}
|
|
<details><summary class="disc"><span class="chev" aria-hidden="true">▾</span>Steps and error counts</summary>
|
|
{% if steps %}
|
|
<table class="rv-table"><thead><tr><th>#</th><th>Step</th><th>Pattern</th><th>State</th><th>Elapsed</th><th>Errors</th></tr></thead><tbody>
|
|
{% for st in steps %}<tr><td>{{ st.number or loop.index }}</td><td>{{ st.type or '-' }}</td><td class="muted">{{ st.pattern or '-' }}</td>
|
|
<td>{{ st.state or '-' }}</td><td class="mono">{{ st.elapsed_time or '-' }}</td><td>{{ st.errors or 0 }}</td></tr>{% endfor %}
|
|
</tbody></table>
|
|
{% endif %}
|
|
<dl class="kv" style="margin-top: 8px"><dt>Errors</dt><dd>total {{ er.total_errors or 0 }}, read {{ er.read_errors or 0 }}, mismatch {{ er.mismatch_errors or 0 }}</dd>
|
|
<dt>Sectors</dt><dd>{{ er.processed_sectors or '-' }} of {{ t.sectors or '-' }}</dd>
|
|
<dt>Started</dt><dd class="mono">{{ er.start_time or '-' }}</dd><dt>Ended</dt><dd class="mono">{{ er.end_time or '-' }}</dd></dl>
|
|
</details>
|
|
</div>
|
|
{% endfor %}
|
|
</section>
|
|
{% else %}
|
|
<div class="notice">This report has no erasure entries.</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="panel rv-side" style="display: flex; flex-direction: column">
|
|
<section><h3>System</h3><dl class="kv">
|
|
<dt>Manufacturer</dt><dd>{{ sysinfo.manufacturer or '-' }}</dd><dt>Model</dt><dd>{{ sysinfo.model or '-' }}</dd>
|
|
<dt>Serial</dt><dd class="mono">{{ sysinfo.serial or '-' }}</dd><dt>Chassis</dt><dd>{{ sysinfo.chassis_type or '-' }}</dd>
|
|
<dt>BIOS</dt><dd>{{ bios.vendor or '-' }} {{ bios.version or '' }}</dd>
|
|
<dt>Boot</dt><dd>{{ sysinfo.bios_mode or '-' }}{% if sysinfo.secure_boot_state %}, Secure Boot {{ sysinfo.secure_boot_state }}{% endif %}</dd></dl></section>
|
|
<section style="border-top: 1px solid var(--line)"><h3>Customer</h3><dl class="kv">
|
|
<dt>Business</dt><dd>{{ company.business_name or '-' }}</dd><dt>Location</dt><dd>{{ company.business_location or '-' }}</dd>
|
|
<dt>Licence</dt><dd class="mono">{{ company.customer_license or '-' }}</dd>
|
|
{% for k, v in company.items() if k not in ['business_name', 'business_location', 'customer_license'] %}<dt>{{ k | replace('_', ' ') | capitalize }}</dt><dd>{{ v }}</dd>{% endfor %}</dl></section>
|
|
<details style="border-top: 1px solid var(--line)"><summary class="disc" style="padding: 0 18px; min-height: 44px"><span class="chev" aria-hidden="true">▾</span>Provenance</summary>
|
|
<dl class="kv" style="padding: 0 18px 16px">
|
|
<dt>Document</dt><dd><span class="mono">{{ meta.document_id or '-' }}</span>{% if meta.document_id %} <button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ meta.document_id }}">Copy</button>{% endif %}</dd>
|
|
<dt>Product</dt><dd>{{ meta.product_name }} {{ meta.product_version }}{% if meta.product_revision %} ({{ meta.product_revision }}){% endif %}</dd>
|
|
<dt>Generated</dt><dd class="mono">{{ meta.date or '-' }}</dd>
|
|
{% if data.license_consumption_ids %}<dt>Licence IDs</dt><dd class="mono">{{ data.license_consumption_ids | join(', ') }}</dd>{% endif %}
|
|
<dt>File</dt><dd class="mono">{{ filename }}</dd>
|
|
<dt>Integrity</dt><dd><span class="trunc mono" style="max-width: 100%">{{ meta.integrity or '-' }}</span>
|
|
{% if meta.integrity %}<button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ meta.integrity }}">Copy signature ({{ meta.integrity | length }} chars)</button>{% endif %}</dd>
|
|
</dl>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|