Files
pxe-server/webapp/templates/_image_header.html
cproudlock b9a4d0230d Engineer image: upload the Engineering Layer from the web app
New Engineering Layer tab on the engineer image page. A site uploads
Engineering_Install_v<ver>.exe (keeps prereqs) or a .zip of the whole
extra_engineering payload. The upload is streamed to disk, checked (MZ
header, no unsafe zip paths) and swapped in; the replaced payload is
archived (two kept) with a Restore button. Install-EngineeringLayer.ps1
stays repo-owned and now installs the newest Engineering_Install_v*.exe.

KB: Part 4 becomes Site Content (enrollment package, Engineering Layer,
Blancco site settings, Shop Floor apps) plus a new Adding Shop Floor
Apps section for preinstall.json/postppkg.json. WJ examples removed.
2026-10-16 20:48:25 -04:00

135 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: 20px; }
.image-head-top { display: flex; flex-wrap: wrap; gap: 12px 16px; justify-content: space-between; align-items: flex-end; margin-bottom: 14px; }
.image-head-title { flex: 1 1 20rem; min-width: 0; }
.image-head h1 { font-size: 28px; font-weight: 600; line-height: 1.25; margin: 2px 0 4px; letter-spacing: 0; }
.image-head .eyebrow-t { font-size: 13px; color: var(--muted); }
.image-head-meta { font-size: 12px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 2px 8px; align-items: center; }
.image-head-meta .sep { opacity: 0.5; }
.image-head .header-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.image-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line-2); overflow-x: auto; scrollbar-width: none; margin: 0; padding: 0; list-style: none; }
.image-tabs a { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 42px; padding: 0 14px; font-size: 14px;
color: var(--text-2); text-decoration: none; white-space: nowrap; border-bottom: 3px solid transparent; margin-bottom: -1px; }
.image-tabs a:hover { color: var(--text); }
.image-tabs a[aria-current] { color: var(--text); font-weight: 600; border-bottom-color: var(--text); }
.image-tabs .tab-count { font-family: var(--font-mono); font-size: 12px; color: var(--muted); font-weight: 400; }
/* Unified diff (review step of every save) */
.diff-file { border: 1px solid var(--line-2); border-radius: 10px; margin-bottom: 1rem; overflow: hidden; }
.diff-file-head { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--line-2);
background: var(--surface-2); font-weight: 600; font-size: 13px; }
.diff-view { margin: 0; max-height: 55vh; overflow: auto; font-size: 12px; line-height: 1.45; font-family: var(--font-mono); background: var(--surface); }
.diff-view span { display: block; padding: 0 12px; white-space: pre-wrap; word-break: break-all; }
.diff-view .d-add { background: var(--add-bg); color: var(--add-text); }
.diff-view .d-del { background: var(--del-bg); color: var(--del-text); }
.diff-view .d-hunk { color: var(--accent); background: var(--surface-2); }
.diff-view .d-meta { color: var(--muted); }
</style>
<div class="image-head">
<div class="image-head-top">
<div class="image-head-title">
<div class="eyebrow-t">Image</div>
<h1>{{ _hdr.friendly_name | replace('GE Aerospace ', '') }}</h1>
<div class="image-head-meta">
<span class="mono">{{ image_type }}</span>
{% if _hdr.os_label %}<span class="sep">|</span><span>{{ _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>
<nav aria-label="Image sections"><ul class="image-tabs">
{% for t in image_tabs if not t.images or image_type in t.images %}
<li><a href="{{ image_tab_url(image_type, t.id) }}"{% if t.id == _tab %} aria-current="page"{% endif %}>
{{ 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="chip c-warn" style="font-size: 11px; padding: 1px 6px">missing</span>{% endif %}
</a></li>
{% endfor %}
</ul></nav>
</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="ui-btn" data-bs-dismiss="modal">Back to editing</button>
<button type="button" class="ui-btn pri" id="imageReviewConfirm" disabled>Save and back up</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='image-pages.js') }}"></script>