webapp: technician to-do for the manual CMMC steps; count fallbacks as wiped
Technician To-Do (sidebar link with a count, and a banner on every page while anything is open) lists the playbook steps the server cannot do: 1. wipes still to tick on the Onsite Bin RITM (ENT-045/159) 2. monthly evidence packs to upload to Box (ENT-056/158), Mark uploaded 3. drives that failed to wipe, to destroy (COD recorded) 4. reports Blancco could not upload to the cloud (Resolved) Marks are stored in the Blancco state file; the counts are cached for 60s and refreshed on every mark, ServiceNow tick and cloud-status post. Results are now judged per drive. Blancco keeps every attempt in the report, so a NIST Purge that failed followed by a successful Clear fallback on the same drive appeared as two entries, and the Reports page rolled the whole report up to Failed. 20 such wipes since May showed as Failed; they were wiped (the work instruction counts the fallback as a success). The CSV marks those rows "Fallback: ... then erased with ...". One genuine failure remains: HNMD1V3, Seagate Z500R4KG, 2026-08-11.
This commit is contained in:
@@ -565,17 +565,10 @@ def blancco_reports():
|
|||||||
sysinfo = (data.get("hardware") or {}).get("system") or {}
|
sysinfo = (data.get("hardware") or {}).get("system") or {}
|
||||||
serial = sysinfo.get("serial", "") or ""
|
serial = sysinfo.get("serial", "") or ""
|
||||||
model = sysinfo.get("model", "") or ""
|
model = sysinfo.get("model", "") or ""
|
||||||
# Overall erasure result: each erasure entry has its own
|
# Overall result per drive: Failed if any drive was not
|
||||||
# 'state' (Successful / Failed / ...). If any drive failed
|
# wiped. A failed Purge followed by a successful Clear
|
||||||
# the report rolls up to Failed; otherwise Successful.
|
# fallback on the same drive is a successful wipe.
|
||||||
erasures = data.get("erasures") or []
|
result = blancco_report.report_result(data)
|
||||||
states = [(e.get("state") or "").strip() for e in erasures if e.get("state")]
|
|
||||||
if not states:
|
|
||||||
result = ""
|
|
||||||
elif any(s.lower() != "successful" for s in states):
|
|
||||||
result = "Failed"
|
|
||||||
else:
|
|
||||||
result = "Successful"
|
|
||||||
cloud = blancco_evidence.cloud_status(
|
cloud = blancco_evidence.cloud_status(
|
||||||
state, (data.get("meta") or {}).get("document_id", ""))
|
state, (data.get("meta") or {}).get("document_id", ""))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -724,6 +717,33 @@ def blancco_servicenow_mark(document_id):
|
|||||||
return redirect(url_for("blancco_servicenow", all=request.args.get("all")))
|
return redirect(url_for("blancco_servicenow", all=request.args.get("all")))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/reports/todo")
|
||||||
|
def blancco_todo():
|
||||||
|
"""Technician to-do: the playbook steps the server cannot do itself."""
|
||||||
|
return render_template("reports_todo.html", todo=blancco_evidence.todo(),
|
||||||
|
site=config.BLANCCO_SITE)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/reports/todo/<kind>/<key>", methods=["POST"])
|
||||||
|
def blancco_todo_mark(kind, key):
|
||||||
|
done = request.form.get("done", "1") == "1"
|
||||||
|
try:
|
||||||
|
blancco_evidence.mark(kind, key, done)
|
||||||
|
except ValueError:
|
||||||
|
abort(400)
|
||||||
|
audit("REPORT_TODO", f"{kind} {key} {'done' if done else 'undone'}")
|
||||||
|
return redirect(url_for("blancco_todo"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/reports/evidence/file/<filename>")
|
||||||
|
def blancco_evidence_file(filename):
|
||||||
|
filename = secure_filename(filename)
|
||||||
|
fpath = os.path.join(config.BLANCCO_REPORTS, "evidence", filename)
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
abort(404)
|
||||||
|
return send_file(fpath, as_attachment=True)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/reports/download/<filename>")
|
@app.route("/reports/download/<filename>")
|
||||||
def blancco_download_report(filename):
|
def blancco_download_report(filename):
|
||||||
filename = secure_filename(filename)
|
filename = secure_filename(filename)
|
||||||
@@ -1481,6 +1501,7 @@ def inject_globals():
|
|||||||
return {
|
return {
|
||||||
"all_image_types": config.IMAGE_TYPES,
|
"all_image_types": config.IMAGE_TYPES,
|
||||||
"all_friendly_names": config.FRIENDLY_NAMES,
|
"all_friendly_names": config.FRIENDLY_NAMES,
|
||||||
|
"blancco_todo": blancco_evidence.todo_counts(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def record_cloud(document_id: str, sent: bool, serial: str = "", detail: str = "
|
|||||||
"serial": (serial or "")[:64], "detail": (detail or "")[:200],
|
"serial": (serial or "")[:64], "detail": (detail or "")[:200],
|
||||||
}
|
}
|
||||||
_save_state(state)
|
_save_state(state)
|
||||||
|
todo_invalidate()
|
||||||
|
|
||||||
|
|
||||||
def mark_servicenow(document_id: str, done: bool = True):
|
def mark_servicenow(document_id: str, done: bool = True):
|
||||||
@@ -78,6 +79,7 @@ def mark_servicenow(document_id: str, done: bool = True):
|
|||||||
else:
|
else:
|
||||||
state["servicenow"].pop(document_id, None)
|
state["servicenow"].pop(document_id, None)
|
||||||
_save_state(state)
|
_save_state(state)
|
||||||
|
todo_invalidate()
|
||||||
|
|
||||||
|
|
||||||
def cloud_status(state: dict, document_id: str) -> str:
|
def cloud_status(state: dict, document_id: str) -> str:
|
||||||
@@ -192,8 +194,11 @@ def servicenow_items(days: int = 30, include_done: bool = False) -> list[dict]:
|
|||||||
if isinstance(system, list):
|
if isinstance(system, list):
|
||||||
system = system[0] if system else {}
|
system = system[0] if system else {}
|
||||||
erasures = data.get("erasures") or []
|
erasures = data.get("erasures") or []
|
||||||
failed = [e for e in erasures
|
outcomes = blancco_report.drive_outcomes(data)
|
||||||
if (e.get("state") or "").strip().lower() != "successful"]
|
# A drive is failed only if none of its erasures succeeded (a Purge
|
||||||
|
# failure followed by a successful Clear fallback is a good wipe).
|
||||||
|
failed = [e for i, e in enumerate(erasures)
|
||||||
|
if not outcomes[blancco_report._drive_key(e, i)]["ok"]]
|
||||||
reasons = [blancco_report.failure_reason(e) for e in failed]
|
reasons = [blancco_report.failure_reason(e) for e in failed]
|
||||||
drives = ", ".join(
|
drives = ", ".join(
|
||||||
f"{(e.get('target') or {}).get('model', '')} {(e.get('target') or {}).get('serial', '')}".strip()
|
f"{(e.get('target') or {}).get('model', '')} {(e.get('target') or {}).get('serial', '')}".strip()
|
||||||
@@ -213,3 +218,78 @@ def servicenow_items(days: int = 30, include_done: bool = False) -> list[dict]:
|
|||||||
"done": done.get("ts") if done else None,
|
"done": done.get("ts") if done else None,
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
# --- technician to-do -------------------------------------------------------
|
||||||
|
# The parts of the playbook the server cannot do itself. Each kind is a dict
|
||||||
|
# in the state file keyed by month or document_id.
|
||||||
|
TODO_KINDS = {
|
||||||
|
"evidence_uploaded": re.compile(r"^20\d\d-(0[1-9]|1[0-2])$"), # month
|
||||||
|
"destroyed": _DOC_ID, # failed wipe: Certificate of Destruction recorded
|
||||||
|
"cloud_resolved": _DOC_ID, # report not in the cloud: uploaded by hand
|
||||||
|
}
|
||||||
|
_EVIDENCE_ZIP = re.compile(r"^ENT-056_.+_(20\d\d-\d\d)\.zip$")
|
||||||
|
|
||||||
|
|
||||||
|
def mark(kind: str, key: str, done: bool = True):
|
||||||
|
pattern = TODO_KINDS.get(kind)
|
||||||
|
if not pattern or not pattern.match(key or ""):
|
||||||
|
raise ValueError("bad to-do item")
|
||||||
|
state = load_state()
|
||||||
|
bucket = state.setdefault(kind, {})
|
||||||
|
if done:
|
||||||
|
bucket[key] = {"ts": _now()}
|
||||||
|
else:
|
||||||
|
bucket.pop(key, None)
|
||||||
|
_save_state(state)
|
||||||
|
todo_invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
def todo() -> dict:
|
||||||
|
"""Open technician tasks, newest first in each list."""
|
||||||
|
state = load_state()
|
||||||
|
for kind in TODO_KINDS:
|
||||||
|
state.setdefault(kind, {})
|
||||||
|
evidence_dir = os.path.join(config.BLANCCO_REPORTS, "evidence")
|
||||||
|
packs = []
|
||||||
|
if os.path.isdir(evidence_dir):
|
||||||
|
for f in sorted(os.listdir(evidence_dir), reverse=True):
|
||||||
|
m = _EVIDENCE_ZIP.match(f)
|
||||||
|
if m and m.group(1) not in state["evidence_uploaded"]:
|
||||||
|
packs.append({"month": m.group(1), "file": f})
|
||||||
|
year = servicenow_items(days=366, include_done=True)
|
||||||
|
failed = [i for i in year if i["failed"] and i["document_id"] not in state["destroyed"]]
|
||||||
|
not_in_cloud = [i for i in year
|
||||||
|
if i["cloud"] == "failed" and i["document_id"] not in state["cloud_resolved"]]
|
||||||
|
servicenow = sum(1 for i in year if not i["done"]
|
||||||
|
and i["when"] >= datetime.now(timezone.utc) - timedelta(days=30))
|
||||||
|
return {
|
||||||
|
"servicenow": servicenow,
|
||||||
|
"evidence": packs,
|
||||||
|
"failed": failed,
|
||||||
|
"not_in_cloud": not_in_cloud,
|
||||||
|
"total": servicenow + len(packs) + len(failed) + len(not_in_cloud),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_todo_cache = {"at": 0.0, "value": None}
|
||||||
|
|
||||||
|
|
||||||
|
def todo_counts(ttl: float = 60.0) -> dict:
|
||||||
|
"""todo() reduced to counts, cached: it is shown on every page."""
|
||||||
|
import time
|
||||||
|
now = time.monotonic()
|
||||||
|
if _todo_cache["value"] is None or now - _todo_cache["at"] > ttl:
|
||||||
|
try:
|
||||||
|
t = todo()
|
||||||
|
value = {"servicenow": t["servicenow"], "evidence": len(t["evidence"]),
|
||||||
|
"failed": len(t["failed"]), "not_in_cloud": len(t["not_in_cloud"]),
|
||||||
|
"total": t["total"]}
|
||||||
|
except Exception:
|
||||||
|
value = {"servicenow": 0, "evidence": 0, "failed": 0, "not_in_cloud": 0, "total": 0}
|
||||||
|
_todo_cache.update(at=now, value=value)
|
||||||
|
return _todo_cache["value"]
|
||||||
|
|
||||||
|
|
||||||
|
def todo_invalidate():
|
||||||
|
_todo_cache["value"] = None
|
||||||
|
|||||||
@@ -167,6 +167,41 @@ def failure_reason(er: dict) -> str:
|
|||||||
return "; ".join(parts) or f"state: {er.get('state') or 'unknown'}"
|
return "; ".join(parts) or f"state: {er.get('state') or 'unknown'}"
|
||||||
|
|
||||||
|
|
||||||
|
def _drive_key(er: dict, index: int) -> str:
|
||||||
|
target = er.get("target") or {}
|
||||||
|
if isinstance(target, list):
|
||||||
|
target = target[0] if target else {}
|
||||||
|
return target.get("serial") or f"#{index}"
|
||||||
|
|
||||||
|
|
||||||
|
def drive_outcomes(data: dict) -> dict:
|
||||||
|
"""Per drive: did any erasure on it succeed?
|
||||||
|
|
||||||
|
Blancco falls back to a weaker standard when one fails on a drive (e.g.
|
||||||
|
NIST 800-88 Purge fails, then Clear succeeds) and the report keeps both
|
||||||
|
entries. The drive is wiped if any of its erasures succeeded; that is
|
||||||
|
what the GE work instruction counts as a successful wipe.
|
||||||
|
Returns {drive_key: {"ok": bool, "standard": str}}.
|
||||||
|
"""
|
||||||
|
out = {}
|
||||||
|
for i, er in enumerate(data.get("erasures") or []):
|
||||||
|
key = _drive_key(er, i)
|
||||||
|
ok = (er.get("state") or "").strip().lower() == "successful"
|
||||||
|
cur = out.setdefault(key, {"ok": False, "standard": ""})
|
||||||
|
if ok and not cur["ok"]:
|
||||||
|
cur.update(ok=True, standard=er.get("erasure_standard_name", ""))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def report_result(data: dict) -> str:
|
||||||
|
""""Successful" when every drive was wiped, "Failed" if any drive was
|
||||||
|
not, "" when the report has no erasures."""
|
||||||
|
outcomes = drive_outcomes(data)
|
||||||
|
if not outcomes:
|
||||||
|
return ""
|
||||||
|
return "Successful" if all(o["ok"] for o in outcomes.values()) else "Failed"
|
||||||
|
|
||||||
|
|
||||||
def export_rows(filename: str, data: dict, when: str, cloud: str = "") -> list[dict]:
|
def export_rows(filename: str, data: dict, when: str, cloud: str = "") -> list[dict]:
|
||||||
"""One CSV row per erased drive; a report with no erasures still gets a row.
|
"""One CSV row per erased drive; a report with no erasures still gets a row.
|
||||||
|
|
||||||
@@ -191,7 +226,12 @@ def export_rows(filename: str, data: dict, when: str, cloud: str = "") -> list[d
|
|||||||
"cloud_upload": cloud,
|
"cloud_upload": cloud,
|
||||||
}
|
}
|
||||||
rows = []
|
rows = []
|
||||||
for er in data.get("erasures") or []:
|
outcomes = drive_outcomes(data)
|
||||||
|
for i, er in enumerate(data.get("erasures") or []):
|
||||||
|
reason = failure_reason(er)
|
||||||
|
outcome = outcomes.get(_drive_key(er, i)) or {}
|
||||||
|
if reason and outcome.get("ok"):
|
||||||
|
reason = f"Fallback: {reason}; then erased with {outcome.get('standard') or 'another standard'}"
|
||||||
target = er.get("target") or {}
|
target = er.get("target") or {}
|
||||||
if isinstance(target, list):
|
if isinstance(target, list):
|
||||||
target = target[0] if target else {}
|
target = target[0] if target else {}
|
||||||
@@ -211,6 +251,6 @@ def export_rows(filename: str, data: dict, when: str, cloud: str = "") -> list[d
|
|||||||
"end_time": er.get("end_time", ""),
|
"end_time": er.get("end_time", ""),
|
||||||
"elapsed_time": er.get("elapsed_time", ""),
|
"elapsed_time": er.get("elapsed_time", ""),
|
||||||
"total_errors": er.get("total_errors", ""),
|
"total_errors": er.get("total_errors", ""),
|
||||||
"failure_reason": failure_reason(er),
|
"failure_reason": reason,
|
||||||
})
|
})
|
||||||
return rows or [base]
|
return rows or [base]
|
||||||
|
|||||||
@@ -75,6 +75,13 @@
|
|||||||
<i class="bi bi-shield-check"></i> Blancco Reports
|
<i class="bi bi-shield-check"></i> Blancco Reports
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'blancco_todo' %}active{% endif %}"
|
||||||
|
href="{{ url_for('blancco_todo') }}">
|
||||||
|
<i class="bi bi-clipboard-check"></i> Technician To-Do
|
||||||
|
{% if blancco_todo.total %}<span class="badge badge-warning ms-1">{{ blancco_todo.total }}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
|
||||||
href="{{ url_for('enrollment') }}">
|
href="{{ url_for('enrollment') }}">
|
||||||
@@ -129,6 +136,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if blancco_todo.total and request.endpoint not in ('blancco_todo', 'imaging_tiles') %}
|
||||||
|
<div class="alert alert-warning d-flex flex-wrap align-items-center gap-2 py-2" role="alert">
|
||||||
|
<i class="bi bi-clipboard-check"></i>
|
||||||
|
<strong>Technician to-do:</strong>
|
||||||
|
{% set parts = [] %}
|
||||||
|
{% if blancco_todo.servicenow %}{% set _ = parts.append(blancco_todo.servicenow ~ ' wipe' ~ ('s' if blancco_todo.servicenow != 1) ~ ' to tick in ServiceNow') %}{% endif %}
|
||||||
|
{% if blancco_todo.evidence %}{% set _ = parts.append(blancco_todo.evidence ~ ' evidence pack' ~ ('s' if blancco_todo.evidence != 1) ~ ' to upload to Box') %}{% endif %}
|
||||||
|
{% if blancco_todo.failed %}{% set _ = parts.append(blancco_todo.failed ~ ' failed wipe' ~ ('s' if blancco_todo.failed != 1) ~ ' needing a Certificate of Destruction') %}{% endif %}
|
||||||
|
{% if blancco_todo.not_in_cloud %}{% set _ = parts.append(blancco_todo.not_in_cloud ~ ' report' ~ ('s' if blancco_todo.not_in_cloud != 1) ~ ' not in the Blancco cloud') %}{% endif %}
|
||||||
|
<span>{{ parts | join(' | ') }}</span>
|
||||||
|
<a href="{{ url_for('blancco_todo') }}" class="ms-auto pxe-btn pxe-btn-secondary btn-row-action">Open to-do</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
120
webapp/templates/reports_todo.html
Normal file
120
webapp/templates/reports_todo.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Technician To-Do - PXE Server Manager{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1><i class="bi bi-clipboard-check"></i> Technician To-Do</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<span class="badge badge-{{ 'warning' if todo.total else 'success' }} badge-lg">{{ todo.total }} open</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Blancco wipes, reports, evidence packs and cloud uploads are automatic. These are the
|
||||||
|
CMMC playbook steps that still need a technician ({{ site }}). Mark each one done here once finished.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{# 1. ServiceNow #}
|
||||||
|
<div class="section-card">
|
||||||
|
<div class="section-title">1. Tick wipes on the Onsite Bin Management RITM (ENT-045 / ENT-159)</div>
|
||||||
|
{% if todo.servicenow %}
|
||||||
|
<p class="mb-2"><strong>{{ todo.servicenow }}</strong> wipe{{ 's' if todo.servicenow != 1 }} from the last 30 days still to tick in ServiceNow.</p>
|
||||||
|
<a href="{{ url_for('blancco_servicenow') }}" class="pxe-btn pxe-btn-primary"><i class="bi bi-list-check"></i> Open the tick list</a>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">All caught up.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 2. Evidence packs #}
|
||||||
|
<div class="section-card">
|
||||||
|
<div class="section-title">2. Upload the monthly evidence pack to Box (ENT-056 / ENT-158)</div>
|
||||||
|
{% if todo.evidence %}
|
||||||
|
<p class="mb-2">Saved automatically on the 1st. Download it, upload it to the site's ENT-056 / ENT-158 evidence folders in Box, then mark it uploaded.</p>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Month</th><th>File</th><th class="text-end">Actions</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in todo.evidence %}
|
||||||
|
<tr>
|
||||||
|
<td class="mono">{{ p.month }}</td>
|
||||||
|
<td class="mono">{{ p.file }}</td>
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
<a href="{{ url_for('blancco_evidence_file', filename=p.file) }}" class="pxe-btn pxe-btn-secondary btn-row-action"><i class="bi bi-download"></i> Download</a>
|
||||||
|
<form method="post" action="{{ url_for('blancco_todo_mark', kind='evidence_uploaded', key=p.month) }}" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="pxe-btn pxe-btn-success btn-row-action">Mark uploaded</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">No pack waiting. The next one is saved on the 1st of next month.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 3. Failed wipes #}
|
||||||
|
<div class="section-card">
|
||||||
|
<div class="section-title">3. Destroy drives that failed to wipe (ENT-158 / ENT-159)</div>
|
||||||
|
{% if todo.failed %}
|
||||||
|
<p class="mb-2">Remove the drive, destroy it per site procedure, record the Certificate of Destruction and the destruction note on the RITM, then mark it here.</p>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Serial</th><th>Model</th><th>Wiped (UTC)</th><th>Failure</th><th class="text-end">Actions</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for it in todo.failed %}
|
||||||
|
<tr title="{{ it.file }}">
|
||||||
|
<td class="mono">{{ it.serial or '-' }}</td>
|
||||||
|
<td>{{ it.model or '-' }}</td>
|
||||||
|
<td class="mono text-nowrap">{{ it.when.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td class="small" style="max-width: 26rem;">{{ it.note }}</td>
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
<a href="{{ url_for('blancco_view_report', filename=it.file) }}" class="pxe-btn pxe-btn-secondary btn-row-action"><i class="bi bi-eye"></i> Report</a>
|
||||||
|
<form method="post" action="{{ url_for('blancco_todo_mark', kind='destroyed', key=it.document_id) }}" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="pxe-btn pxe-btn-success btn-row-action">COD recorded</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">No failed wipes waiting for destruction.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 4. Not in the cloud #}
|
||||||
|
<div class="section-card">
|
||||||
|
<div class="section-title">4. Reports that did not reach the Blancco cloud (ENT-056)</div>
|
||||||
|
{% if todo.not_in_cloud %}
|
||||||
|
<p class="mb-2">Auditors pull certificates from the Blancco Management Portal. Upload the report's XML there by hand, then mark it resolved.</p>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Serial</th><th>Model</th><th>Wiped (UTC)</th><th class="text-end">Actions</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for it in todo.not_in_cloud %}
|
||||||
|
<tr title="{{ it.file }}">
|
||||||
|
<td class="mono">{{ it.serial or '-' }}</td>
|
||||||
|
<td>{{ it.model or '-' }}</td>
|
||||||
|
<td class="mono text-nowrap">{{ it.when.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
<a href="{{ url_for('blancco_download_report', filename=it.file) }}" class="pxe-btn pxe-btn-secondary btn-row-action"><i class="bi bi-download"></i> XML</a>
|
||||||
|
<form method="post" action="{{ url_for('blancco_todo_mark', kind='cloud_resolved', key=it.document_id) }}" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="pxe-btn pxe-btn-success btn-row-action">Resolved</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">Every report with a known upload status reached the cloud.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user