Files
pxe-server/webapp/templates/_image_header.html
cproudlock 397063f987 Webapp: Updates pages, image page tabs with review-before-save, BPRT sync
Updates group:
- Dell drivers and BIOS: check Dell's catalogs against all three driver maps
  and models.txt, download with a shared speed cap, repack, preview, apply
  with backups and rollback.
- iPXE boot files: check GitHub for the signed Secure Boot set, verify the
  sha256 and each file's signer, install atomically, roll back. Includes the
  by-hand procedure.
- Blancco: upload a release, stage it, test-boot it from the menu, activate,
  roll back.
- BPRT sync: validate a batch uploaded from the MediaCreator Lite cache
  (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site
  overlay files kept.

Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and
Packages tabs. Every save shows a diff first, keeps the file's format, makes a
backup and can be undone. Driver lint runs inline.

Enrollment: upload standard/engineer MCL packages, one per tenant/purpose,
with archive and restore. Hardware coverage counts engineer as Win11.
2026-10-13 16:58:55 -04:00

151 lines
7.9 KiB
HTML

{#
_image_header.html - shared header + tabs for every GE Aerospace image page.
CONTRACT (image-shell; used by the Overview, Drivers, Unattend and OS and
Packages tabs):
Use it as the first thing inside {% block content %}:
{% set header_actions %}
<button type="button" class="btn btn-success btn-sm" id="...">Save</button>
{% endset %}
{% set header_subtitle = 'Deploy/FlatUnattendW10.xml' %}
{% include "_image_header.html" %}
Variables it reads from the template context:
image_type required. Image key, e.g. "gea-standard".
active_tab optional. One of "overview", "drivers", "unattend", "os".
When missing it is worked out from request.endpoint
(image_tab_for_endpoint), so a route need not pass it.
header_actions optional. HTML for the right side of the title row
(Save / Review / Undo buttons). Build it with a
{% set header_actions %}...{% endset %} block.
header_subtitle optional. Plain text appended to the meta line
(file path, last save time, ...).
Globals it uses (context processor in app.py):
image_tabs services/image_pages.IMAGE_TABS: id, label, icon,
endpoint for each tab, in display order.
image_tab_url(t, tab) URL of a tab; use it for links and redirects
(in Python: services.image_pages.tab_url).
image_header_info(t) friendly_name, os_label, driver_count,
package_count, has_unattend.
Tab endpoints (change a row of IMAGE_TABS if a page moves):
overview images.overview GET /images/<t> views/images.py
drivers images_drivers.page GET /images/<t>/drivers views/images_drivers.py
unattend unattend_editor GET /images/<t>/unattend app.py
os images.os_packages GET /images/<t>/os views/images.py
Old URLs: /images/<t>/config redirects to the Overview (or ?tab=<id>).
The sidebar marks an image active whenever active_tab is set (or the
endpoint maps to a tab) and image_type matches, so any page that includes
this partial highlights the right sidebar entry.
It also loads static/image-pages.js and the shared review modal
(#imageReviewModal). For a "Review changes" step call
PxeImage.reviewAndSave({previewUrl, saveUrl, body, title})
where previewUrl answers {"files": [control_files.public(plan), ...]}
and saveUrl takes the same body plus "expect": {name: sha_before} and
answers {"ok": true} (it flashes; the page reloads) or {"error": "..."}.
PxeImage.renderDiff(el, files) draws the same diff anywhere.
#}
{% set _tab = active_tab if active_tab is defined and active_tab else image_tab_for_endpoint(request.endpoint) %}
{% set _hdr = image_header_info(image_type) %}
<style>
.image-head { margin-bottom: 1.25rem; }
.image-head-top {
display: flex; flex-wrap: wrap; gap: 0.75rem 1rem;
justify-content: space-between; align-items: flex-end;
margin-bottom: 0.75rem;
}
.image-head-title { flex: 1 1 20rem; min-width: 0; }
.image-head h1 { font-size: 20px; font-weight: 500; line-height: 1.3; margin: 0.15rem 0 0.2rem; }
.image-head-meta { font-size: 13px; color: var(--text-light); display: flex; flex-wrap: wrap; gap: 0.25rem 0.6rem; align-items: center; }
.image-head-meta .sep { opacity: 0.5; }
.image-head .header-actions { flex-wrap: wrap; align-items: center; }
.image-tabs.nav-tabs { flex-wrap: wrap; }
.image-tabs .nav-link { font-size: 12px; padding: 0.65rem 1rem; display: flex; align-items: center; gap: 0.4rem; }
.image-tabs .nav-link .bi { font-size: 14px; }
.image-tabs .tab-count {
font-size: 10px; font-weight: 600; letter-spacing: 0; line-height: 1;
padding: 2px 7px; border-radius: 999px;
background: var(--bg); border: 1px solid var(--border); color: var(--text-light);
}
.image-tabs .nav-link.active .tab-count { color: var(--primary); border-color: var(--primary); }
/* Unified diff (review step of every save) */
.diff-file { border: 1px solid var(--border); border-radius: 0.25rem; margin-bottom: 1rem; overflow: hidden; }
.diff-file-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 13px; }
.diff-view {
margin: 0; max-height: 55vh; overflow: auto; font-size: 12px; line-height: 1.45;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; background: var(--bg-card-solid);
}
.diff-view span { display: block; padding: 0 0.75rem; white-space: pre-wrap; word-break: break-all; }
.diff-view .d-add { background: rgba(10, 214, 79, 0.16); }
.diff-view .d-del { background: rgba(220, 53, 69, 0.14); }
.diff-view .d-hunk { color: var(--primary); background: rgba(65, 129, 255, 0.08); }
.diff-view .d-meta { color: var(--text-light); }
[data-theme="dark"] .diff-view .d-add { background: rgba(10, 214, 79, 0.18); }
[data-theme="dark"] .diff-view .d-del { background: rgba(245, 54, 92, 0.2); }
</style>
<div class="image-head">
<div class="image-head-top">
<div class="image-head-title">
<div class="eyebrow">GE Aerospace image</div>
<h1>{{ _hdr.friendly_name }}</h1>
<div class="image-head-meta">
<span class="mono">{{ image_type }}</span>
{% if _hdr.os_label %}<span class="sep">|</span><span><i class="bi bi-windows"></i> {{ _hdr.os_label }}</span>{% endif %}
{% if header_subtitle is defined and header_subtitle %}<span class="sep">|</span><span class="mono">{{ header_subtitle }}</span>{% endif %}
</div>
</div>
{% if header_actions is defined and header_actions %}
<div class="header-actions">{{ header_actions }}</div>
{% endif %}
</div>
<ul class="nav nav-tabs image-tabs" role="tablist">
{% for t in image_tabs %}
<li class="nav-item">
<a class="nav-link {% if t.id == _tab %}active{% endif %}"
{% if t.id == _tab %}aria-current="page"{% endif %}
href="{{ image_tab_url(image_type, t.id) }}">
<i class="bi {{ t.icon }}"></i> {{ t.label }}
{% if t.id == 'drivers' %}<span class="tab-count" title="Entries in HardwareDriver.json">{{ _hdr.driver_count }}</span>
{% elif t.id == 'os' and _hdr.package_count %}<span class="tab-count" title="Entries in packages.json">{{ _hdr.package_count }}</span>
{% elif t.id == 'unattend' and not _hdr.has_unattend %}<span class="tab-count" title="FlatUnattendW10.xml is missing">missing</span>{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
{# Shared review step: filled by PxeImage.reviewAndSave (static/image-pages.js) #}
<div class="modal fade" id="imageReviewModal" tabindex="-1" aria-labelledby="imageReviewTitle">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="imageReviewTitle">Review changes</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted small mb-3" id="imageReviewIntro">
This is exactly what will be written. Each changed file is backed up next to itself
(<span class="mono">.bak-&lt;date&gt;-webui</span>) first, and the save can be undone from this page.
</p>
<div id="imageReviewBody"></div>
<div class="alert alert-danger d-none mb-0" id="imageReviewError"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" id="imageReviewConfirm" disabled>
<i class="bi bi-check-lg"></i> Save
</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='image-pages.js') }}"></script>