Webapp: fix six live bugs found in the page inventory
- Report view: the hero summarised only the first erasure, so a drive whose Purge failed and Clear fallback succeeded showed Failed while the list said Successful. It now uses the per-drive outcome (report_result) and lists the standards that wiped each drive. - To-do and ServiceNow: reports with no usable document id could never be ticked or cleared (400). They are now tracked by file name (file:<name>). - Imaging: the LAPS password field is masked, with a Show/Hide toggle. - Imaging: succeeded sessions not updated for ACTIVE_WINDOW_HOURS are hidden from the dashboard (failed and in-progress always stay), with Show all. - Enrollment: the active SFLD package can no longer be deleted, which left ppkg.conf pointing at a missing file; its row is marked Active. - Import: confirm before importing, saying upload-folder sources are moved and same-name files in the image are overwritten.
This commit is contained in:
@@ -30,6 +30,15 @@ import config
|
||||
from services import blancco_report
|
||||
|
||||
_DOC_ID = re.compile(r"^[0-9a-fA-F-]{36}$")
|
||||
# Reports without a usable document id (older or hand-made ones) are tracked
|
||||
# by file name instead, so they can still be ticked and cleared.
|
||||
_FILE_KEY = re.compile(r"^file:[A-Za-z0-9._-]{1,200}$")
|
||||
_REPORT_KEY = re.compile(f"{_DOC_ID.pattern}|{_FILE_KEY.pattern}")
|
||||
|
||||
|
||||
def report_key(document_id: str, filename: str) -> str:
|
||||
"""The state-file key for one report: its document id, else "file:<name>"."""
|
||||
return document_id if _DOC_ID.match(document_id or "") else f"file:{filename}"
|
||||
|
||||
|
||||
# --- state ------------------------------------------------------------------
|
||||
@@ -71,7 +80,7 @@ def record_cloud(document_id: str, sent: bool, serial: str = "", detail: str = "
|
||||
|
||||
|
||||
def mark_servicenow(document_id: str, done: bool = True):
|
||||
if not _DOC_ID.match(document_id or ""):
|
||||
if not _REPORT_KEY.match(document_id or ""):
|
||||
raise ValueError("bad document_id")
|
||||
state = load_state()
|
||||
if done:
|
||||
@@ -127,6 +136,7 @@ def collect(since=None, until=None) -> list[dict]:
|
||||
"data": data,
|
||||
"document_id": (data.get("meta") or {}).get("document_id", ""),
|
||||
})
|
||||
out[-1]["key"] = report_key(out[-1]["document_id"], f)
|
||||
return out
|
||||
|
||||
|
||||
@@ -193,7 +203,7 @@ def servicenow_items(days: int = 30, include_done: bool = False) -> list[dict]:
|
||||
state = load_state()
|
||||
items = []
|
||||
for r in reversed(collect(since)):
|
||||
done = state["servicenow"].get(r["document_id"])
|
||||
done = state["servicenow"].get(r["key"])
|
||||
if done and not include_done:
|
||||
continue
|
||||
data = r["data"]
|
||||
@@ -212,6 +222,7 @@ def servicenow_items(days: int = 30, include_done: bool = False) -> list[dict]:
|
||||
for e in failed)
|
||||
items.append({
|
||||
"document_id": r["document_id"],
|
||||
"key": r["key"],
|
||||
"file": r["file"],
|
||||
"when": r["when"].astimezone(_tz()),
|
||||
"serial": system.get("serial", ""),
|
||||
@@ -232,8 +243,8 @@ def servicenow_items(days: int = 30, include_done: bool = False) -> list[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
|
||||
"destroyed": _REPORT_KEY, # failed wipe: Certificate of Destruction recorded
|
||||
"cloud_resolved": _REPORT_KEY, # report not in the cloud: uploaded by hand
|
||||
}
|
||||
_EVIDENCE_ZIP = re.compile(r"^ENT-056_.+_(20\d\d-\d\d)\.zip$")
|
||||
|
||||
@@ -265,9 +276,9 @@ def todo() -> dict:
|
||||
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"]]
|
||||
failed = [i for i in year if i["failed"] and i["key"] 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"]]
|
||||
if i["cloud"] == "failed" and i["key"] 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 {
|
||||
|
||||
@@ -15,7 +15,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
import config
|
||||
@@ -220,6 +220,29 @@ def list_sessions(include_inferred: bool = True) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def hide_stale(sessions: list[dict], hours: float = ACTIVE_WINDOW_HOURS) -> tuple[list[dict], int]:
|
||||
"""Drop succeeded sessions not updated for `hours` from the dashboard list.
|
||||
|
||||
Failed and in-progress sessions always stay: a failure must be seen and
|
||||
cleared by someone. Returns (kept, hidden_count).
|
||||
"""
|
||||
cutoff = datetime.now().astimezone() - timedelta(hours=hours)
|
||||
kept, hidden = [], 0
|
||||
for s in sessions:
|
||||
if s.get("status") == "succeeded":
|
||||
try:
|
||||
when = datetime.fromisoformat(s.get("last_updated") or "")
|
||||
if when.tzinfo is None:
|
||||
when = when.astimezone()
|
||||
except ValueError:
|
||||
when = None
|
||||
if when is not None and when < cutoff:
|
||||
hidden += 1
|
||||
continue
|
||||
kept.append(s)
|
||||
return kept, hidden
|
||||
|
||||
|
||||
def get_session(serial: str) -> Optional[dict]:
|
||||
path = _path_for(serial)
|
||||
if not os.path.isfile(path):
|
||||
|
||||
Reference in New Issue
Block a user