webapp: imaging progress dashboard + serial column on reports list
Adds end-to-end progress tracking for PXE imaging sessions and surfaces
each Blancco report's BIOS serial in the report list.
webapp:
* services/imaging_status.py - JSON-per-serial state store under
IMAGING_DIR (default /var/log/pxe-imaging). Atomic write via
tempfile + rename. log_tail capped at 50 lines. Merges partial
updates so clients can post just the current_stage tick.
* config.py - new IMAGING_DIR env-overridable path.
* services/csrf.py - explicit exempt list for machine-to-machine
endpoints; /imaging/status is the first entry. Air-gapped LAN;
trust-by-network for client posts.
* app.py - four new routes:
GET /imaging dashboard (renders all sessions)
POST /imaging/status client status push (JSON body)
GET /imaging/<serial>.json raw session JSON for ad-hoc polling
POST /imaging/delete/<s> clear a session from the dashboard
Also parses each Blancco XML in the /reports list to surface
system.serial + system.model columns.
* templates/imaging.html - Bootstrap dashboard with per-session
cards (state badge, progress bar, stage idx/total, mac, elapsed,
log tail). meta http-equiv refresh=5 for auto-tick.
* templates/base.html - new "Imaging Progress" nav entry.
* templates/reports.html - Serial + Model columns added.
playbook:
* shopfloor-setup/Shopfloor/lib/Send-PxeStatus.ps1 - new helper.
Dot-source this then call Send-PxeStatus -Stage X -StageIndex N
-StageTotal M from any stage script. BIOS serial via CIM, MAC via
Get-NetAdapter, pctype + machinenumber from C:\Enrollment.
Failures are swallowed to a local log so a network blip doesn't
block imaging.
* shopfloor-setup/Run-ShopfloorSetup.ps1 - dot-sources helper +
posts at three coarse milestones (start, PPKG enrollment,
handoff to Monitor-IntuneProgress).
* shopfloor-setup/gea-shopfloor-keyence/09-Setup-Keyence.ps1 -
posts at session start + after Install-FromManifest with
succeeded/failed status derived from $rc. Other 09-Setup-*.ps1
scripts can follow the same pattern.
ID is BIOS serial (stable across WinPE -> Windows transition and
across reboots, unlike hostname which is random pre-PPKG). Operator
already knows the serial of the bay they imaged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,12 @@
|
||||
Image Import
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'imaging_dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('imaging_dashboard') }}">
|
||||
Imaging Progress
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-section-divider"></div>
|
||||
|
||||
126
webapp/templates/imaging.html
Normal file
126
webapp/templates/imaging.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Imaging Progress - PXE Server Manager{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<meta http-equiv="refresh" content="5">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h2 class="mb-0">Imaging Progress</h2>
|
||||
<small class="text-muted">Auto-refresh 5s. POST updates from imaging clients arrive at <code>/imaging/status</code>.</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary fs-6">{{ sessions|length }} session{{ 's' if sessions|length != 1 }}</span>
|
||||
</div>
|
||||
|
||||
{% if not sessions %}
|
||||
<div class="card">
|
||||
<div class="card-body text-center text-muted py-5">
|
||||
<p class="mb-1">No imaging sessions yet.</p>
|
||||
<p class="small mb-0">A PC being imaged will post status here.
|
||||
Until then, nothing to show.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for s in sessions %}
|
||||
{% set stage_idx = s.stage_index | int(0) %}
|
||||
{% set stage_total = s.stage_total | int(0) %}
|
||||
{% set pct = 100 if s.status == 'succeeded' else ((stage_idx / stage_total * 100) | round(0, 'floor')) if stage_total > 0 else 0 %}
|
||||
{% set is_failed = s.status == 'failed' %}
|
||||
{% set is_done = s.status == 'succeeded' %}
|
||||
{% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %}
|
||||
<div class="card border-{{ border }} mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ s.serial or '(no serial)' }}</strong>
|
||||
{% if s.hostname_target %}<code class="ms-2 small text-muted">{{ s.hostname_target }}</code>{% endif %}
|
||||
{% if s.pctype %}<span class="badge bg-info text-dark ms-2">{{ s.pctype }}</span>{% endif %}
|
||||
{% if s.machinenumber %}<span class="badge bg-secondary ms-1">#{{ s.machinenumber }}</span>{% endif %}
|
||||
</div>
|
||||
<span class="badge bg-{{ border }}">{{ s.status or 'in_progress' }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3 small">
|
||||
<div class="col-md-3">
|
||||
<div class="text-muted">Current stage</div>
|
||||
<div><strong>{{ s.current_stage or '-' }}</strong></div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="text-muted">Stage</div>
|
||||
<div>{{ stage_idx }} / {{ stage_total or '?' }}</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="text-muted">Started</div>
|
||||
<div><code>{{ s.started_at or '-' }}</code></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-muted">Last update</div>
|
||||
<div><code>{{ s.last_updated or '-' }}</code></div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="text-muted">MAC</div>
|
||||
<div><code class="small">{{ s.mac or '-' }}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress mb-2" style="height: 1.4rem;">
|
||||
<div class="progress-bar bg-{{ border }} {% if not is_done and not is_failed %}progress-bar-striped progress-bar-animated{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ pct }}%;"
|
||||
aria-valuenow="{{ pct }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">{{ pct }}%</div>
|
||||
</div>
|
||||
|
||||
{% if s.error %}
|
||||
<div class="alert alert-danger small py-2 mb-2">
|
||||
<strong>Error:</strong> {{ s.error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if s.log_tail %}
|
||||
<details>
|
||||
<summary class="text-muted small">Log tail ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }})</summary>
|
||||
<pre class="bg-light p-2 mt-2 small mb-0" style="max-height: 12rem; overflow-y: auto;">{% for line in s.log_tail %}{{ line }}
|
||||
{% endfor %}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3 text-end">
|
||||
<form method="post" action="{{ url_for('imaging_delete_session', serial=s.serial) }}" style="display: inline;">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="return confirm('Clear session {{ s.serial }}?');">
|
||||
Clear
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body small text-muted">
|
||||
<strong>How to push status from an imaging client:</strong>
|
||||
<pre class="mb-0 mt-2">POST http://10.9.100.1:9009/imaging/status
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"serial": "4HBLF33",
|
||||
"mac": "e4:54:e8:dc:b1:f0",
|
||||
"hostname_target": "EDNMG3D4",
|
||||
"pctype": "gea-shopfloor-keyence",
|
||||
"machinenumber": "9999",
|
||||
"current_stage": "Run-ShopfloorSetup: 09-Setup-Keyence",
|
||||
"stage_index": 7,
|
||||
"stage_total": 9,
|
||||
"status": "in_progress",
|
||||
"log_lines": ["last few log lines from the stage"]
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -17,6 +17,8 @@
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Serial</th>
|
||||
<th>Model</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th>Date</th>
|
||||
@@ -27,6 +29,8 @@
|
||||
{% for r in reports %}
|
||||
<tr>
|
||||
<td><code>{{ r.filename }}</code></td>
|
||||
<td>{% if r.serial %}<code>{{ r.serial }}</code>{% else %}<span class="text-muted small">-</span>{% endif %}</td>
|
||||
<td class="small">{{ r.model or '-' }}</td>
|
||||
<td><span class="badge bg-info text-dark">{{ r.type }}</span></td>
|
||||
<td>
|
||||
{% if r.size > 1048576 %}
|
||||
|
||||
Reference in New Issue
Block a user