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>
227 lines
7.1 KiB
HTML
227 lines
7.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>{% block title %}PXE Server Manager{% endblock %}</title>
|
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
|
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--sidebar-width: 280px;
|
|
}
|
|
body {
|
|
min-height: 100vh;
|
|
}
|
|
.sidebar {
|
|
width: var(--sidebar-width);
|
|
min-height: 100vh;
|
|
background-color: #1a1d21;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 100;
|
|
overflow-y: auto;
|
|
}
|
|
.sidebar .nav-link {
|
|
color: #adb5bd;
|
|
padding: 0.6rem 1.25rem;
|
|
font-size: 0.9rem;
|
|
border-radius: 0;
|
|
}
|
|
.sidebar .nav-link:hover,
|
|
.sidebar .nav-link.active {
|
|
color: #fff;
|
|
background-color: rgba(255,255,255,0.08);
|
|
}
|
|
.sidebar-heading {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: #6c757d;
|
|
padding: 1rem 1.25rem 0.4rem;
|
|
font-weight: 600;
|
|
}
|
|
.sidebar .brand {
|
|
padding: 1.2rem 1.25rem;
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
}
|
|
.main-content {
|
|
margin-left: var(--sidebar-width);
|
|
padding: 2rem;
|
|
}
|
|
.status-dot {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
}
|
|
.status-dot.active {
|
|
background-color: #198754;
|
|
}
|
|
.status-dot.inactive {
|
|
background-color: #dc3545;
|
|
}
|
|
.status-dot.unknown {
|
|
background-color: #ffc107;
|
|
}
|
|
.card {
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
.card-header {
|
|
font-weight: 600;
|
|
}
|
|
.table th {
|
|
font-weight: 600;
|
|
}
|
|
.btn-row-action {
|
|
padding: 0.2rem 0.5rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
.drag-handle {
|
|
cursor: grab;
|
|
color: #6c757d;
|
|
}
|
|
.drag-handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
.nav-section-divider {
|
|
border-top: 1px solid rgba(255,255,255,0.06);
|
|
margin-top: 0.5rem;
|
|
}
|
|
</style>
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Sidebar -->
|
|
<nav class="sidebar d-flex flex-column">
|
|
<div class="brand">
|
|
<img src="{{ url_for('static', filename='ge-aerospace-logo.svg') }}" alt="GE Aerospace" style="height: 28px; filter: brightness(0) invert(1);">
|
|
PXE Manager
|
|
</div>
|
|
<ul class="nav flex-column mt-2">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
|
|
href="{{ url_for('dashboard') }}">
|
|
Dashboard
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'images_import' %}active{% endif %}"
|
|
href="{{ url_for('images_import') }}">
|
|
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>
|
|
<div class="sidebar-heading">Tools</div>
|
|
<ul class="nav flex-column">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
|
|
href="{{ url_for('startnet_editor') }}">
|
|
startnet.cmd
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
|
href="{{ url_for('clonezilla_backups') }}">
|
|
Clonezilla Backups
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'blancco_reports' %}active{% endif %}"
|
|
href="{{ url_for('blancco_reports') }}">
|
|
Blancco Reports
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
|
|
href="{{ url_for('enrollment') }}">
|
|
Enrollment
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'audit_log' %}active{% endif %}"
|
|
href="{{ url_for('audit_log') }}">
|
|
Audit Log
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="nav-section-divider"></div>
|
|
<div class="sidebar-heading">GE Aerospace Images</div>
|
|
<ul class="nav flex-column">
|
|
{% for it in all_image_types if it.startswith('gea-') %}
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
|
href="{{ url_for('unattend_editor', image_type=it) }}">
|
|
{{ all_friendly_names[it] }}
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
|
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
|
|
Configuration
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<div class="nav-section-divider"></div>
|
|
<div class="sidebar-heading">GE Legacy Images</div>
|
|
<ul class="nav flex-column">
|
|
{% for it in all_image_types if it.startswith('ge-') and not it.startswith('gea-') %}
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
|
href="{{ url_for('unattend_editor', image_type=it) }}">
|
|
{{ all_friendly_names[it] }}
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
|
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
|
|
Configuration
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</nav>
|
|
|
|
<!-- Main content -->
|
|
<div class="main-content">
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
|
|
<script src="{{ url_for('static', filename='bootstrap.bundle.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
|
{% block extra_scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|