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:
cproudlock
2026-10-14 13:29:00 -04:00
parent 24b4d9df66
commit dfd57bef17
10 changed files with 108 additions and 21 deletions

View File

@@ -186,12 +186,14 @@
<div class="mt-3 laps-card" data-serial="{{ s.serial }}">
<div class="eyebrow mb-2">LAPS password QR (paste -> scan on bay - persists until cleared)</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<input type="text"
{# Masked: the tile is on a shared screen; Show reveals it while typing. #}
<input type="password"
class="form-control form-control-sm laps-input mono"
style="max-width: 22rem;"
placeholder="paste LAPS password from Intune portal here"
autocomplete="off"
value="{{ s.laps_password or '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary laps-show-btn" aria-pressed="false">Show</button>
<button type="button" class="btn btn-sm btn-primary laps-make-btn">{% if s.laps_password %}Update QR{% else %}Make QR{% endif %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary laps-clear-btn" {% if not s.laps_password %}style="display:none;"{% endif %}>Clear</button>
</div>

View File

@@ -39,8 +39,9 @@
</thead>
<tbody>
{% for p in packages %}
{% set is_active = active_ppkg and active_ppkg.SOURCE_PPKG and p.filename|lower == active_ppkg.SOURCE_PPKG|lower %}
<tr>
<td><span class="mono"><i class="bi bi-file-earmark-zip"></i> {{ p.filename }}</span></td>
<td><span class="mono"><i class="bi bi-file-earmark-zip"></i> {{ p.filename }}</span>{% if is_active %} <span class="badge badge-info">Active</span>{% endif %}</td>
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
<td>{{ p.modified | timestamp_fmt }}</td>
<td class="actions text-end text-nowrap">
@@ -48,11 +49,17 @@
class="btn btn-sm btn-outline-primary btn-row-action" title="Download">
<i class="bi bi-download"></i> Download
</a>
{% if is_active %}
<button type="button" class="btn btn-sm btn-outline-secondary btn-row-action" disabled>
<i class="bi bi-lock"></i> Active
</button>
{% else %}
<button type="button" class="btn btn-sm btn-outline-danger btn-row-action"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-filename="{{ p.filename }}" title="Delete">
<i class="bi bi-trash"></i> Delete
</button>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -22,6 +22,12 @@ function cancelImagingReload() {
<div>
<h1>Imaging Progress</h1>
<small class="text-light">Live via SSE (15s polling fallback). Client pushes &rarr; <code class="mono">/imaging/status</code>; log-inferred bays in yellow.</small>
{% if hidden %}
<div class="small mt-1">{{ hidden }} finished session{{ 's' if hidden != 1 }} older than {{ window_hours }} h hidden.
<a href="{{ url_for('imaging_dashboard', all='1') }}">Show all</a></div>
{% elif show_all %}
<div class="small mt-1">Showing finished sessions of every age. <a href="{{ url_for('imaging_dashboard') }}">Hide old finished sessions</a></div>
{% endif %}
</div>
<div class="header-actions">
<span class="status-indicator">
@@ -91,7 +97,7 @@ Content-Type: application/json
// server signals a state change. Preserves scroll, filter input value,
// and any LAPS input that the operator is actively editing.
(function() {
var TILES_URL = "{{ url_for('imaging_tiles_partial') }}";
var TILES_URL = "{{ url_for('imaging_tiles_partial', all='1' if show_all else None) }}";
var STREAM_URL = "{{ url_for('imaging_stream') }}";
var POLL_MS = 15000;
var lastHash = null;
@@ -308,6 +314,15 @@ document.addEventListener('click', function(e) {
} else if (e.target.classList.contains('laps-clear-btn')) {
card = e.target.closest('.laps-card');
if (card) clearLapsQR(card);
} else if (e.target.classList.contains('laps-show-btn')) {
card = e.target.closest('.laps-card');
var inp = card && card.querySelector('.laps-input');
if (inp) {
var show = inp.type === 'password';
inp.type = show ? 'text' : 'password';
e.target.textContent = show ? 'Hide' : 'Show';
e.target.setAttribute('aria-pressed', show ? 'true' : 'false');
}
}
});
document.addEventListener('keydown', function(e) {

View File

@@ -168,7 +168,17 @@ document.addEventListener('DOMContentLoaded', function() {
['importForm', 'uploadImportForm'].forEach(function(formId) {
var form = document.getElementById(formId);
if (form) {
form.addEventListener('submit', function() {
form.addEventListener('submit', function(e) {
// Import overwrites same-name files in the image and, from the
// upload folder, moves (consumes) the source. Make that explicit.
var src = form.querySelector('select[name="source"]');
var tgt = form.querySelector('select[name="target"]');
var what = formId === 'uploadImportForm'
? 'The files are MOVED out of the upload folder, so the source is used up.'
: 'The files are copied from the USB drive.';
var msg = 'Import ' + (src ? src.value : '') + ' into ' + (tgt ? tgt.options[tgt.selectedIndex].text : '') + '?\n\n'
+ what + '\nFiles with the same name in the image are overwritten. This takes several minutes.';
if (!confirm(msg)) { e.preventDefault(); return; }
var btn = form.querySelector('button[type="submit"]');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Importing...';

View File

@@ -43,9 +43,11 @@
{# Hero summary: state badge + standard + drive count + erasure datetime #}
{% set first = (data.erasures or [{}])[0] %}
{% set overall_state = first.state or 'Unknown' %}
{% set overall_tone = 'success' if overall_state == 'Successful' else 'danger' if overall_state else 'secondary' %}
{% set overall_stat = 'success' if overall_state == 'Successful' else 'danger' if overall_state else '' %}
{% set overall_state = result or 'Unknown' %}
{% set overall_tone = 'success' if overall_state == 'Successful' else 'danger' if result else 'secondary' %}
{% set overall_stat = 'success' if overall_state == 'Successful' else 'danger' if result else '' %}
{% set wiped = outcomes.values() | selectattr('ok') | list %}
{% set standards = wiped | map(attribute='standard') | reject('equalto', '') | unique | list %}
<div class="dashboard-grid mb-4">
<div class="stat-card {{ overall_stat }}">
<div class="label">Overall result</div>
@@ -55,11 +57,11 @@
</div>
<div class="stat-card">
<div class="label">Erasure standard</div>
<div class="value" style="font-size:1.05rem;">{{ first.erasure_standard_name or '-' }}</div>
<div class="value" style="font-size:1.05rem;">{{ standards | join(', ') if standards else (first.erasure_standard_name or '-') }}</div>
</div>
<div class="stat-card">
<div class="label">Drives erased</div>
<div class="value">{{ data.erasures | length }}</div>
<div class="value">{{ wiped | length }} of {{ outcomes | length }}</div>
</div>
<div class="stat-card">
<div class="label">Elapsed time</div>

View File

@@ -65,7 +65,7 @@
{% else %}<span class="text-muted">-</span>{% endif %}
</td>
<td class="text-end text-nowrap">
<form method="post" action="{{ url_for('blancco_servicenow_mark', document_id=it.document_id, all='1' if include_done else None) }}" style="display:inline;">
<form method="post" action="{{ url_for('blancco_servicenow_mark', document_id=it.key, all='1' if include_done else None) }}" style="display:inline;">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
{% if it.done %}
<input type="hidden" name="done" value="0">

View File

@@ -72,7 +72,7 @@
<td class="small wrap">{{ 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;">
<form method="post" action="{{ url_for('blancco_todo_mark', kind='destroyed', key=it.key) }}" 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>
@@ -103,7 +103,7 @@
<td class="mono text-nowrap">{{ it.when | local_dt }}</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;">
<form method="post" action="{{ url_for('blancco_todo_mark', kind='cloud_resolved', key=it.key) }}" 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>