Files
pxe-server/webapp/templates/_imaging_tiles.html
cproudlock 9d75d03b2d Standard/Engineer builds are done at OOBE on the imaging floor
Everything after OOBE needs the employee to log in, which happens off the
PXE network, so the floor waited forever at step 3 of 5. Now 3 steps:
WinPE, Windows setup installing apps, ready at OOBE (succeeded).

- both unattends: specialize Order 49 posts step 3 as succeeded
  (Path 182 chars; lint clean). Only that line changed.
- Send-ImagingStage.ps1: StageTotal 3; steps above the total (the old
  first-logon steps 4 and 5 still in the unattends) are logged and not
  posted, so they cannot reopen a finished build
- winpe-status-push.ps1: total 3 for gea-standard / gea-engineer
- floor tile step 3: "Ready at OOBE"

Tested in the win11 VM against the test server: steps 2 and 3 post,
step 3 shows succeeded, step 4 is skipped. Not deployed: the unattends
go out only through scripts/deploy-unattend.sh.
2026-10-16 10:09:40 -04:00

228 lines
16 KiB
HTML

{# Imaging floor rows. Rendered standalone by /imaging/tiles for the SSE
refresh, and included by imaging.html and the Overview for first paint.
Receives `sessions` (already in floor order, each with floor_state),
and optionally `compact` / `more` from the Overview.
One line per PC; everything else is in the row's expandable body. #}
{% import '_ui.html' as ui %}
{% set stage_labels = {
1: ('Booting from PXE', 'WinPE loaded - applying Windows image to disk.'),
2: ('Configuring Windows', 'First boot. Running shopfloor setup baseline scripts.'),
3: ('Installing apps', 'Type-specific app installs (eDNC, UDC, NTLARS, etc).'),
4: ('Apps installed', 'Type-specific scripts complete. Preparing for Intune enrollment.'),
5: ('Enrolling in Intune', 'PPKG installing - device joining Azure AD + Intune. ~5-10 min, reboot to follow.'),
6: ('Waiting on first Intune sync','Post-PPKG settle (~120s). Triggering Schedule #3 sync repeatedly.'),
7: ('Registered - assign category',
'Phase 1 (Intune Registration) complete. Click "set category" in the Intune portal to drop the bay into the right config-profile group.'),
8: ('Imaging complete',
'Lockdown applied. Bay rebooted into ShopFloor session. Ready for production.')
} %}
{# gea-standard / gea-engineer builds: WinPE push, then
Send-ImagingStage.ps1 from the image's unattend; stage_total 3. Done
at OOBE: everything after needs the employee's logon, off the PXE
network. (4 and 5 are kept for builds still running the old unattend.) #}
{% set desk_stage_labels = {
1: ('Applying Windows image', 'WinPE loaded - PESetup is applying the Windows image to disk.'),
2: ('Windows setup: installing apps','First boot (specialize): certificates, WiFi, fonts, Adobe, Java and the enrollment task.'),
3: ('Ready at OOBE', 'PXE imaging is done. Hand the PC to the employee: first logon (and the engineering layer on Engineer builds) runs when they sign in.'),
4: ('First logon', 'First logon commands running (the engineering layer on Engineer builds).'),
5: ('Setup complete', 'Imaging is done; bulk enrollment continues on the PC.')
} %}
{% set desk_kind = {'gea-standard': 'Standard', 'gea-engineer': 'Engineer'} %}
{# Blancco Drive Eraser sessions (source/pctype "blancco"), posted by
blancco-init.sh and blancco-progress.py; stage_total 7. #}
{% set blancco_stage_labels = {
1: ('Downloading Blancco', 'PXE boot: loading the Blancco image into RAM.'),
2: ('Starting Blancco', 'Blancco is starting up.'),
3: ('Licensing', 'Waiting for the Blancco cloud licence (WiFi INTERNETACCESS).'),
4: ('Erasing', 'Drive erasure in progress. Do not power off.'),
5: ('Erasure finished', 'Report saved on the PC.'),
6: ('Report sent', 'Report uploaded to the Blancco cloud; a copy goes to the share.'),
7: ('Wipe complete', 'Erasure succeeded. Report is on the Blancco reports share.')
} %}
{% set inferred_stage_labels = {
'stage_0_dhcp': ('DHCP lease issued', 'Client got a DHCP lease but has not fetched the bootloader yet.'),
'stage_0_tftp_bios': ('TFTP bootloader (BIOS)','Client fetched undionly.kpxe via TFTP. Booting iPXE.'),
'stage_0_tftp_uefi': ('TFTP bootloader (UEFI)','Client fetched ipxe.efi via TFTP. Booting iPXE.'),
'stage_0_menu': ('Boot menu loaded', 'Client loaded the iPXE menu. Waiting on selection or auto-boot.'),
'stage_0_boot_pre': ('Pre-boot fetch', 'Client downloading WinPE boot files from HTTP.'),
'stage_1_wim_get': ('Downloading boot.wim', 'Client pulling WinPE image. No /imaging/status push yet.'),
'stage_2_smb_winpe': ('WinPE running (no push)','Client has opened SMB shares - WinPE is up but startnet has not pushed status.'),
} %}
{% set state_chip = {
'failed': ('Failed', 'fail'),
'action': ('Needs action', 'act'),
'running': ('In progress', 'run'),
'done': ('Done', 'ok'),
'logs': ('Seen in logs', 'off'),
} %}
{% set track_kind = {'failed': 'fail', 'action': 'act', 'running': 'run', 'done': 'ok'} %}
{% macro floor_row(s) %}
{# Sessions are sanitised on load; these guards keep a row rendering if a
non-string still slips through (a lookup or `in` on a list raises). #}
{% set pctype = s.pctype if s.pctype is string else '' %}
{% set cur_stage = s.current_stage if s.current_stage is string else '' %}
{% set is_inferred = (s.source == 'inferred') %}
{% set is_blancco = (s.source == 'blancco' or pctype == 'blancco') %}
{% set stage_idx = (s.stage_index or 0) | int(0) %}
{% set stage_total = (s.stage_total or 0) | int(0) %}
{% set state = s.floor_state if s.floor_state in state_chip else 'running' %}
{% set drive_pct = None %}
{% if is_blancco and stage_idx == 4 and cur_stage and '%]' in cur_stage %}
{% set drive_pct = cur_stage.split('[')[-1].split('%')[0] | int(0) %}
{% endif %}
{% set warn = False %}
{% if is_inferred %}
{% set friendly = inferred_stage_labels.get(cur_stage, ('Inferred from logs', cur_stage)) %}
{% elif is_blancco %}
{% if s.status == 'failed' %}
{% set friendly = ('Erasure FAILED', 'Blancco reported a failed erasure step. Check the log tail and the report.') %}
{% else %}
{% set friendly = blancco_stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
{% endif %}
{% if stage_idx == 4 and s.status != 'failed' %}{% set warn = True %}{% endif %}
{% else %}
{% set friendly = (desk_stage_labels if pctype in desk_kind else stage_labels).get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
{% if stage_idx == 1 and cur_stage and 'bios update staged' in cur_stage|lower %}
{% set friendly = ('Applying image - BIOS update staged',
'WinPE staged a firmware update for this model. It flashes during the reboot after the image is applied: do NOT power off then.') %}
{% set warn = True %}
{% elif stage_idx == 1 and cur_stage and 'bios' in cur_stage|lower %}
{% set friendly = ('Updating BIOS: do not power off',
'WinPE detected a firmware update for this model. Do NOT power off until the next reboot. Imaging continues afterward.') %}
{% set warn = True %}
{% endif %}
{% if stage_idx == 7 and cur_stage %}
{% set _cs = cur_stage|lower %}
{% if 'ready for lockdown' in _cs or 'request lockdown' in _cs %}
{% set friendly = ('Ready - request lockdown',
'Phase 1-4 all complete (Registration, Device Config, Software Deploy, Credentials). Click "ARTS request" to initiate the lockdown workflow.') %}
{% elif 'credentials' in _cs or 'phase 4' in _cs %}
{% set friendly = ('Phase 3 / 4 - DSC + credentials',
'SFLD policy delivered, DSC pulling device-config.yaml + running per-app wrappers. SFLD share creds populating.') %}
{% elif 'sfld policy' in _cs or 'phase 2' in _cs or 'device configuration' in _cs %}
{% set friendly = ('Phase 2 - device configuration',
'Category was assigned in Intune. SFLD ConfigurationProfile (Function + SasToken) has delivered. DSC kicking off next.') %}
{% endif %}
{% endif %}
{% endif %}
{% set label = friendly[0] %}
{% if drive_pct is not none and s.status != 'failed' %}{% set label = 'Erasing: ' ~ drive_pct ~ '%' %}{% endif %}
{% set card_key = s.serial or ('mac-' ~ (s.mac or 'unknown') ~ '-' ~ (s.ip or '')) %}
{% set ident = [] %}
{% if s.machinenumber %}{% set _ = ident.append('Machine ' ~ s.machinenumber) %}{% endif %}
{% if s.hostname_target and s.serial %}{% set _ = ident.append(s.hostname_target) %}{% endif %}
{% if s.ip %}{% set _ = ident.append(s.ip) %}{% endif %}
{% set title = s.serial or s.hostname_target or s.mac or s.ip or '(unknown bay)' %}
{% set kind = 'Blancco wipe' if is_blancco else desk_kind.get(pctype, pctype) %}
<div class="ui-row floor-row imaging-card" data-state="{{ state }}"
data-serial="{{ s.serial or '' }}" data-key="{{ card_key }}" data-mac="{{ s.mac or '' }}"
data-ip="{{ s.ip or '' }}" data-source="{{ s.source or 'client' }}"
data-filter="{{ (s.serial or '')|lower }} {{ (s.hostname_target or '')|lower }} {{ pctype|lower }} {{ (s.machinenumber or '')|lower }} {{ (s.intune_device_id or '')|lower }} {{ friendly[0]|lower }} stage-{{ stage_idx }} {{ (s.status or 'in_progress')|lower }} {{ (s.mac or '')|lower }} {{ (s.ip or '') }} {{ s.source or 'client' }}">
<details class="imaging-toggle">
<summary class="frow">
<span class="g-st">{{ ui.chip(state_chip[state][0], state_chip[state][1]) }}</span>
<span class="g-pc"><span class="mono serial">{{ title }}</span><span class="sub">{{ ident | join(' | ') or ('MAC ' ~ s.mac if s.mac else '') }}</span></span>
<span class="g-stage">
<span class="stageline">{% if warn %}<span class="txt warnline">{{ label }}</span>{% else %}<span class="txt tip" data-tip="{{ label }}"><span class="trunc">{{ label }}</span></span>{% endif %}{% if kind %}<span class="kind">{{ kind }}</span>{% endif %}{% if not is_inferred %}<span class="mono step">{{ stage_idx }}/{{ stage_total or '?' }}</span>{% endif %}<span class="chev" aria-hidden="true">&#9662;</span></span>
{% if not is_inferred and stage_total %}{{ ui.track(stage_total, stage_idx, track_kind.get(state, 'run'),
label=('Done, all ' ~ stage_total ~ ' stages' if state == 'done' else 'Stage ' ~ stage_idx ~ ' of ' ~ stage_total)) }}{% endif %}
</span>
</summary>
<div class="fbody">
<div class="fbody-main">
{% if friendly[1] %}<p class="muted" style="margin: 0">{{ friendly[1] }}</p>{% endif %}
{% if s.error %}<div class="errbox"><strong>Error:</strong> {{ s.error }}</div>{% endif %}
<div class="sub fmeta">
<span>Started <span class="mono">{{ s.started_at | local_dt(true) }}</span></span>
<span>Last update <span class="mono">{{ s.last_updated | local_dt(true) }}</span></span>
<span>MAC <span class="mono">{{ s.mac or '-' }}</span></span>
{% if s.current_stage %}<span>Raw stage <span class="mono">{{ s.current_stage }}</span></span>{% endif %}
</div>
{% if s.intune_device_id %}
<div class="fline"><span class="sub">Intune device</span> <span class="mono">{{ s.intune_device_id }}</span>
<button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ s.intune_device_id }}">Copy</button>
<a class="ui-btn sm" target="_blank" rel="noopener"
href="https://portal.azure.us/?feature.msaljs=false#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/properties/aadDeviceId/{{ s.intune_device_id }}">Set category</a>
<a class="ui-btn sm" target="_blank" rel="noopener" href="https://arts.dw.geaerospace.net/requests/type">ARTS request</a>
</div>
{% endif %}
{% if compact and not is_inferred and not is_blancco %}
<div class="sub">LAPS QR and the live log are on the <a href="{{ url_for('imaging_dashboard') }}">imaging floor</a>.</div>
{% elif not is_inferred and not is_blancco %}
<div class="laps-card" data-serial="{{ s.serial }}">
<div class="sub" style="margin-bottom: 6px">LAPS password QR: paste, then scan on the bay. Kept until cleared.</div>
<div class="fline">
{# Masked: the floor is on a shared screen; Show reveals it while typing. #}
<input type="password" class="field laps-input mono" style="max-width: 22rem; flex: 1 1 200px"
placeholder="Paste the LAPS password from Intune" autocomplete="off" aria-label="LAPS password for {{ title }}"
value="{{ s.laps_password or '' }}">
<button type="button" class="ui-btn laps-show-btn" aria-pressed="false">Show</button>
<button type="button" class="ui-btn pri laps-make-btn">{% if s.laps_password %}Update QR{% else %}Make QR{% endif %}</button>
<button type="button" class="ui-btn laps-clear-btn" {% if not s.laps_password %}style="display:none;"{% endif %}>Clear</button>
</div>
<div class="laps-qr-container" style="margin-top: 8px"></div>
</div>
{% endif %}
{% if s.log_tail %}
<details class="imaging-log" data-key="{{ card_key }}">
<summary class="disc"><span class="chev" aria-hidden="true">&#9662;</span>{% if is_inferred %}Evidence{% else %}Log tail{% endif %} ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }})</summary>
<pre class="mono imaging-log-pre">{% for line in s.log_tail %}{{ line }}
{% endfor %}</pre>
</details>
{% endif %}
</div>
{% if s.intune_device_id %}
<div class="fbody-qr" data-qr="{{ s.intune_device_id }}" data-qr-size="112" data-qr-ec="M"
title="Intune device ID {{ s.intune_device_id }}" role="img" aria-label="QR code of the Intune device ID"></div>
{% endif %}
</div>
</details>
<div class="act g-act">
{% if state == 'action' and s.intune_device_id %}
<a class="ui-btn pri" target="_blank" rel="noopener"
href="https://portal.azure.us/?feature.msaljs=false#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/properties/aadDeviceId/{{ s.intune_device_id }}">Set category</a>
{% elif is_blancco and state in ('done', 'failed') %}
<a class="ui-btn" href="{{ url_for('blancco_reports') }}">Report</a>
{% elif not is_inferred and s.serial %}
<a class="ui-btn" href="{{ url_for('imaging_session_detail', serial=s.serial) }}">{{ 'Full log' if state == 'failed' else 'Details' }}</a>
{% endif %}
{% if not is_inferred and s.serial %}
{% call ui.more(title) %}
<a href="{{ url_for('imaging_session_detail', serial=s.serial) }}">Details and full log</a>
{% if s.intune_device_id %}<button type="button" class="copy-btn" data-copy-text="{{ s.intune_device_id }}">Copy Intune device ID</button>
<a target="_blank" rel="noopener" href="https://arts.dw.geaerospace.net/requests/type">ARTS request</a>{% endif %}
{% if is_blancco %}<a href="{{ url_for('blancco_reports') }}">Erasure reports</a>{% endif %}
<div class="sep"></div>
<form method="post" action="{{ url_for('imaging_delete_session', serial=s.serial) }}"
data-confirm="Clear the session for {{ s.serial }}? Its status and log files are deleted."
onsubmit="return confirm(this.dataset.confirm);">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="del">Clear session</button>
</form>
{% endcall %}
{% endif %}
</div>
</div>
{% endmacro %}
{% set real = sessions | rejectattr('floor_state', 'equalto', 'logs') | list %}
{% set logs = sessions | selectattr('floor_state', 'equalto', 'logs') | list %}
{% for s in real %}{{ floor_row(s) }}{% endfor %}
{% if not compact and logs %}
<details class="floor-logs">
<summary class="disc"><span class="chev" aria-hidden="true">&#9662;</span><span><strong>{{ logs | length }}</strong> seen in DHCP/TFTP logs only, no status pushed yet</span></summary>
{% for s in logs %}{{ floor_row(s) }}{% endfor %}
</details>
{% endif %}
{% if compact and more %}
<div class="hidden-bar"><span>{{ more }} more on the floor</span><a class="ui-btn" href="{{ url_for('imaging_dashboard') }}">Open imaging floor</a></div>
{% endif %}
{% if not sessions %}
<div class="floor-empty muted">No PCs on the imaging network right now. A PC appears here once it touches DHCP, TFTP or boot.wim.</div>
{% endif %}