Files
pxe-server/webapp/templates/reports_todo.html
cproudlock a6f95fcca1 Webapp redesign phase 5: Blancco reports, report and to-do
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.
2026-10-14 14:00:54 -04:00

227 lines
15 KiB
HTML

{% extends "base.html" %}
{% import '_ui.html' as ui %}
{% block title %}Technician to-do - PXE Manager{% endblock %}
{# Technician to-do: the CMMC playbook steps the server cannot do itself.
Five lists; the ServiceNow tick list is the first. One failed wipe can be
in three of them (tick, destroy, cloud). COD recorded, Mark uploaded and
Resolved are compliance records: each asks to confirm, is audited, and
can be undone from the same list for 24 hours. #}
{% block extra_head %}
<style>
.td-tabs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); gap: 8px; }
.td-tab { display: flex; flex-direction: column; gap: 2px; padding: 12px 14px; border-radius: 10px; background: var(--surface);
border: 1px solid var(--line-2); color: var(--text); text-decoration: none; box-shadow: 0 1px 2px var(--shadow); }
.td-tab:hover { color: var(--text); border-color: var(--control); }
.td-tab .n { font-size: 22px; font-weight: 600; }
.td-tab .l { font-size: 12px; color: var(--muted); }
.sect { padding: 16px 24px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; border-top: 1px solid var(--line-2); scroll-margin-top: 16px; }
.sect:first-child { border-top: 0; }
.sect h2 { margin: 0; font-size: 15px; font-weight: 600; }
.tgrid { display: grid; grid-template-columns: 96px 104px minmax(0, 1fr) 200px 14px; align-items: center; gap: 0 14px;
padding: 11px 210px 11px 24px; font-size: 14px; min-height: 24px; }
.tgrid.nochev { grid-template-columns: 96px 104px minmax(0, 1fr) 200px; }
.tgrid.hw { grid-template-columns: minmax(0, 1fr) minmax(0, 260px) 110px 14px; }
.twhen { font-size: 13px; color: var(--muted); white-space: nowrap; }
.ui-row.done { background: var(--surface-2); }
.ui-row.done .tgrid { color: var(--muted); }
.tbody { padding: 2px 24px 14px 134px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; }
.note { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border-radius: 8px; background: var(--surface-2); font-size: 13px; line-height: 1.5; }
.kv { margin: 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 14px; }
.kv dt { color: var(--muted); font-weight: 400; } .kv dd { margin: 0; overflow-wrap: anywhere; }
.empty { padding: 4px 24px 14px; font-size: 13px; color: var(--muted); }
.ui-row > .act form { margin: 0; }
@container (max-width: 860px) {
.tgrid, .tgrid.nochev, .tgrid.hw { grid-template-columns: minmax(0, 1fr) auto 14px !important; padding: 11px 16px; gap: 2px 10px; }
.tgrid .twhen { grid-column: 1 / -1; }
.tgrid .c-model { grid-column: 1 / -1; grid-row: 3; }
.tgrid .trunc { white-space: normal; overflow-wrap: anywhere; }
.ui-row > .act { position: static; display: flex; justify-content: flex-end; padding: 0 16px 10px; }
.tbody { padding: 2px 16px 14px; }
.sect { padding: 14px 16px 8px; }
}
</style>
{% endblock %}
{% macro undo_form(kind, key, label) %}
<form method="post" action="{{ url_for('blancco_todo_mark', kind=kind, key=key) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="done" value="0">
<button type="submit" class="ui-btn ghost" aria-label="Undo: {{ label }}">Undo</button>
</form>
{% endmacro %}
{% macro attest(kind, key, label, confirm_text) %}
<form method="post" action="{{ url_for('blancco_todo_mark', kind=kind, key=key) }}" onsubmit='return confirm({{ confirm_text | tojson }});'>
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="ui-btn attest">{{ label }}...</button>
</form>
{% endmacro %}
{% block content %}
{% call ui.head('Technician to-do', 'Operate', 'The steps the server cannot do for you (' ~ site ~ '). One failed wipe can appear in up to three lists.') %}
{{ ui.chip(total ~ ' open', 'neutral' if total else 'ok') }}
{% endcall %}
<div class="ui-stack">
<nav class="td-tabs" aria-label="To-do lists">
<a class="td-tab" href="#sn"><span class="n">{{ todo.servicenow }}</span><span class="l">ServiceNow ticks</span></a>
<a class="td-tab" href="#fail"><span class="n" style="{{ 'color: var(--fail-text)' if todo.failed }}">{{ todo.failed | length }}</span><span class="l">Drives to destroy</span></a>
<a class="td-tab" href="#ev"><span class="n">{{ todo.evidence | length }}</span><span class="l">Evidence packs to upload</span></a>
<a class="td-tab" href="#cloud"><span class="n">{{ todo.not_in_cloud | length }}</span><span class="l">Not in Blancco cloud</span></a>
<a class="td-tab" href="#hw"><span class="n">{{ hardware | length }}</span><span class="l">Hardware gaps</span></a>
</nav>
<section class="panel ui-list" aria-label="To-do lists">
{# ---- 1. ServiceNow ---- #}
<div class="sect" id="sn"><h2>ServiceNow ticks</h2><span class="sub">Tick each asset's Onsite Bin Management RITM (ENT-045 / ENT-159).</span>
<div style="margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap" role="group" aria-label="Period">
{% for d in (7, 30, 90) %}<a class="fbtn" href="{{ url_for('blancco_todo', days=d, all='1' if include_done else None, _anchor='sn') }}"{% if days == d %} aria-current="true"{% endif %}>{{ d }} days</a>{% endfor %}
<a class="fbtn" href="{{ url_for('blancco_todo', days=days, all=None if include_done else '1', _anchor='sn') }}"{% if include_done %} aria-current="true"{% endif %}>Show ticked</a>
</div></div>
{% for it in sn_items %}
<div class="ui-row{{ ' done' if it.done }}">
<details>
<summary class="tgrid">
<span class="twhen mono">{{ it.when.strftime('%d %b') }}</span>
<span class="mono">{{ it.serial or '-' }}</span>
<span class="trunc c-model">{{ it.model or it.file }}</span>
<span>{{ ui.chip('Tick: Blancco Wipe failed', 'fail') if it.failed else ui.chip('Tick: Wiped', 'ok') }}</span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="tbody">
{% if it.failed %}
<div class="note"><span style="flex: 1">{{ it.note }}</span><button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ it.note }}">Copy note</button></div>
<div class="sub">Tick "Blancco Wipe failed" and paste this note into the RITM. The drive is also under Drives to destroy.</div>
{% else %}
<div>Open the RITM for <span class="mono">{{ it.serial }}</span>, tick "Wiped", then press Done.</div>
{% endif %}
<div class="sub" style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center">
{% if it.serial %}<button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ it.serial }}">Copy serial</button>{% endif %}
<a href="{{ url_for('blancco_view_report', filename=it.file) }}">Open report</a>
{% if it.cloud == 'failed' %}{{ ui.chip('Not in Blancco cloud', 'err') }}{% endif %}
</div>
</div>
</details>
<div class="act">
{% if it.done %}
<span class="sub">Ticked {{ it.done | local_dt }}</span>
<form method="post" action="{{ url_for('blancco_servicenow_mark', document_id=it.key, days=days, all='1' if include_done else None) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}"><input type="hidden" name="done" value="0">
<button type="submit" class="ui-btn ghost" aria-label="Undo tick for {{ it.serial }}">Undo</button></form>
{% else %}
<form method="post" action="{{ url_for('blancco_servicenow_mark', document_id=it.key, days=days, all='1' if include_done else None) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}"><input type="hidden" name="done" value="1">
<button type="submit" class="ui-btn pri" aria-label="Mark {{ it.serial }} ticked in ServiceNow">Done</button></form>
{% endif %}
</div>
</div>
{% else %}
<div class="empty">Nothing to tick in the last {{ days }} days.</div>
{% endfor %}
{# ---- 2. Drives to destroy ---- #}
<div class="sect" id="fail"><h2>Drives to destroy</h2><span class="sub">Failed wipes in the last 12 months without a recorded Certificate of Destruction (ENT-158 / ENT-159).</span></div>
{% for it in todo.failed %}
<div class="ui-row">
<details>
<summary class="tgrid">
<span class="twhen mono">{{ it.when.strftime('%d %b') }}</span>
<span class="mono">{{ it.serial or '-' }}</span>
<span class="trunc c-model">{{ it.model or it.file }}</span>
<span>{{ ui.chip('Drive to destroy', 'fail') }}</span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="tbody">
<div class="errbox">{{ it.note }}</div>
<div class="sub">Remove the drive, destroy it per site procedure, record the Certificate of Destruction on the RITM, then mark it. <a href="{{ url_for('blancco_view_report', filename=it.file) }}">Open report</a></div>
</div>
</details>
<div class="act">{{ attest('destroyed', it.key, 'COD recorded', 'Record that the drive in ' ~ (it.serial or it.file) ~ ' was destroyed and its Certificate of Destruction is on the RITM? This is logged in the audit log.') }}</div>
</div>
{% else %}
<div class="empty">No failed wipe waiting for destruction.</div>
{% endfor %}
{% for r in recent.destroyed %}
<div class="ui-row done"><div class="tgrid nochev"><span class="twhen">Marked</span><span class="mono" style="grid-column: span 2">{{ r.label }}</span><span class="sub">COD recorded {{ r.ts | local_dt }}</span></div>
<div class="act">{{ undo_form('destroyed', r.key, 'COD recorded for ' ~ r.label) }}</div></div>
{% endfor %}
{# ---- 3. Evidence packs ---- #}
<div class="sect" id="ev"><h2>Evidence packs to upload</h2><span class="sub">Built on the 1st of each month. Upload to the site's ENT-056 / ENT-158 folders in Box, then mark.</span></div>
{% for p in todo.evidence %}
<div class="ui-row"><div class="tgrid nochev"><span class="twhen mono">{{ p.month }}</span><span class="mono trunc" style="grid-column: span 2">{{ p.file }}</span><span></span></div>
<div class="act"><a class="ui-btn" href="{{ url_for('blancco_evidence_file', filename=p.file) }}">Download</a>
{{ attest('evidence_uploaded', p.month, 'Mark uploaded', 'Record that ' ~ p.file ~ ' is uploaded to Box? This is logged in the audit log.') }}</div></div>
{% else %}
<div class="empty">No pack waiting. The next one is built on the 1st.</div>
{% endfor %}
{% for r in recent.evidence_uploaded %}
<div class="ui-row done"><div class="tgrid nochev"><span class="twhen mono">{{ r.key }}</span><span style="grid-column: span 2">Evidence pack</span><span class="sub">Uploaded {{ r.ts | local_dt }}</span></div>
<div class="act">{{ undo_form('evidence_uploaded', r.key, 'evidence pack ' ~ r.key ~ ' uploaded') }}</div></div>
{% endfor %}
{# ---- 4. Not in the cloud ---- #}
<div class="sect" id="cloud"><h2>Not in Blancco cloud</h2><span class="sub">Blancco reported the upload failed. Upload the XML in the Blancco Management Portal, then mark (ENT-056).</span></div>
{% for it in todo.not_in_cloud %}
<div class="ui-row"><div class="tgrid nochev"><span class="twhen mono">{{ it.when.strftime('%d %b') }}</span><span class="mono">{{ it.serial or '-' }}</span><span class="trunc c-model">{{ it.model or it.file }}</span><span>{{ ui.chip('Upload failed', 'err') }}</span></div>
<div class="act"><a class="ui-btn" href="{{ url_for('blancco_download_report', filename=it.file) }}">Download XML</a>
{{ attest('cloud_resolved', it.key, 'Resolved', 'Record that the report for ' ~ (it.serial or it.file) ~ ' is uploaded to the Blancco Management Portal? This is logged in the audit log.') }}</div></div>
{% else %}
<div class="empty">Every report with a known upload status reached the cloud.</div>
{% endfor %}
{% for r in recent.cloud_resolved %}
<div class="ui-row done"><div class="tgrid nochev"><span class="twhen">Marked</span><span class="mono" style="grid-column: span 2">{{ r.label }}</span><span class="sub">Resolved {{ r.ts | local_dt }}</span></div>
<div class="act">{{ undo_form('cloud_resolved', r.key, 'cloud upload for ' ~ r.label) }}</div></div>
{% endfor %}
{# ---- 5. Hardware gaps ---- #}
<div class="sect" id="hw"><h2>Hardware gaps</h2><span class="sub">Models WinPE has seen here without a Win11 driver pack or a BIOS entry. <a href="{{ url_for('hardware_page') }}">Hardware coverage</a> shows every model.</span></div>
{% for r in hardware %}
{% set what = [] %}
{% if r.missing %}{% set _ = what.append('Driver pack: ' ~ (r.missing | join(', '))) %}{% endif %}
{% if r.bios.status in ('none', 'file-missing') %}{% set _ = what.append('BIOS: ' ~ ('no manifest entry' if r.bios.status == 'none' else r.bios.file ~ ' not on server')) %}{% endif %}
<div class="ui-row">
<details>
<summary class="tgrid hw">
<span class="trunc mono c-model">{{ r.model }}</span>
<span class="sub trunc">{{ what | join('; ') }}</span>
<span class="twhen">seen {{ r.last_seen | local_dt }}</span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="tbody"><dl class="kv">
<dt>Model (WMI)</dt><dd class="mono">{{ r.model }} <button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ r.model }}">Copy</button></dd>
<dt>System SKU</dt><dd class="mono">{{ r.skus | join(', ') or '-' }}</dd>
<dt>BIOS seen</dt><dd class="mono">{{ r.last_bios_version or '-' }}</dd>
<dt>Missing</dt><dd>{{ what | join('; ') }}</dd>
<dt>Serials</dt><dd class="mono">{{ r.serials | join(', ') or '-' }}</dd>
</dl></div>
</details>
<div class="act">
<a class="ui-btn" href="{{ url_for('dell_updates.page') }}">Find Dell pack</a>
{% call ui.more(r.model) %}
<form method="post" action="{{ url_for('hardware_dismiss') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="model" value="{{ r.model }}">
<input type="hidden" name="next" value="{{ url_for('blancco_todo') }}">
<button type="submit">Dismiss</button>
</form>
{% endcall %}
</div>
</div>
{% else %}
<div class="empty">Every model seen has a Win11 driver pack and a BIOS entry.</div>
{% endfor %}
</section>
<details class="panel" style="border-radius: 10px">
<summary class="disc" style="padding: 0 16px; min-height: 44px"><span class="chev" aria-hidden="true">&#9662;</span>What the attestation buttons record</summary>
<p style="margin: 0; padding: 0 16px 14px 42px; font-size: 13px; line-height: 1.55; color: var(--text-2)">COD recorded, Mark uploaded and
Resolved are compliance records (CMMC ENT-045, ENT-056, ENT-158, ENT-159). Each asks you to confirm, is written to the audit log with
your IP and the time, and can be undone from this page for 24 hours.</p>
</details>
</div>
{% endblock %}