Files
pxe-server/webapp/templates/reports_todo.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

231 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: 96px minmax(0, 1fr) auto 14px; }
.hw-miss { display: flex; gap: 6px; flex-wrap: nowrap; justify-content: flex-end; min-width: 0; }
.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, 366) %}<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 %}>{{ 'Year' if d == 366 else 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">{{ p.month | month_label }}</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">{{ r.key | month_label }}</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="twhen mono" title="Last seen {{ r.last_seen | local_dt }}">{{ r.last_seen | short_date }}</span>
<span class="trunc c-model">{{ r.model }}</span>
<span class="hw-miss">
{% if r.missing %}{{ ui.chip('No driver pack' if r.missing | length > 1 else 'No pack: ' ~ (r.missing[0] | replace('gea-', '')), 'err') }}{% endif %}
{% if r.bios.status == 'none' %}{{ ui.chip('No BIOS entry', 'warn') }}{% elif r.bios.status == 'file-missing' %}{{ ui.chip('BIOS file missing', 'err') }}{% endif %}
</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 %}