Files
pxe-server/webapp/templates/_imaging_tiles.html
cproudlock 83ba4865e8 Show every time in US Eastern (America/New_York)
Webapp: config.DISPLAY_TZ (default America/New_York) and a local_dt
filter that renders datetimes, ISO strings and Unix timestamps as e.g.
"2026-10-05 13:54 EDT", DST included. Used for the ServiceNow tick list,
technician to-do, imaging tiles and detail page; timestamp_fmt (reports,
backups, enrollment) goes through it too instead of relying on the
process TZ. CSV report_date is Eastern with its offset. Evidence-pack
months are Eastern calendar months, so a wipe late on the 31st lands in
that month rather than the next.

Blancco: init points the new root's /etc/localtime at America/New_York
so the GUI clock is local. Report timestamps keep their explicit
offset; report.utc_offset stays 0 since a fixed offset cannot follow DST.
Report file names stay UTC, as Blancco's own Save names them.
2026-10-05 14:16:14 -04:00

228 lines
14 KiB
HTML

{# Per-bay tiles. Rendered standalone by /imaging/tiles for SSE refresh, and
included by imaging.html for first paint. Receives `sessions`. #}
{% 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.')
} %}
{# 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.'),
} %}
{% for s in sessions %}
{% set is_inferred = (s.source == 'inferred') %}
{% set is_blancco = (s.source == 'blancco' or s.pctype == 'blancco') %}
{% 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 %}
{# Blancco erasing: fold the drive % ("... [75%]") into the bar between
the start of stage 4 and the start of stage 5. #}
{% set drive_pct = None %}
{% if is_blancco and stage_idx == 4 and s.current_stage and '%]' in s.current_stage %}
{% set drive_pct = s.current_stage.split('[')[-1].split('%')[0] | int(0) %}
{% if stage_total > 0 and s.status != 'succeeded' %}
{% set pct = (((stage_idx - 1) + drive_pct / 100) / stage_total * 100) | round(0, 'floor') %}
{% endif %}
{% endif %}
{% set pct = pct | int %}
{% set is_failed = s.status == 'failed' %}
{% set is_done = s.status == 'succeeded' %}
{% if is_inferred %}
{% set border = 'warning' %}
{% else %}
{% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %}
{% endif %}
{% if is_inferred %}
{% set friendly = inferred_stage_labels.get(s.current_stage, ('Inferred from logs', s.current_stage or '')) %}
{% 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 %}
{% else %}
{% set friendly = stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
{% if stage_idx == 1 and s.current_stage and 'bios' in s.current_stage|lower %}
{% set friendly = ('Updating BIOS firmware',
'WinPE detected a firmware update for this model. Do NOT power off until the next reboot. Imaging continues afterward.') %}
{% endif %}
{% if stage_idx == 7 and s.current_stage %}
{% set _cs = s.current_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 card_key = s.serial or ('mac-' ~ (s.mac or 'unknown') ~ '-' ~ (s.ip or '')) %}
<details class="card mb-2 imaging-card{% if is_inferred %} imaging-inferred{% endif %}"
style="border-left:4px solid var(--{{ border }});"
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 }} {{ (s.pctype or '')|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' }}">
<summary class="card-body py-2" style="cursor:pointer; list-style:none;">
<div class="d-flex flex-wrap gap-3 align-items-center">
{% if s.intune_device_id %}
<div data-qr="{{ s.intune_device_id }}" data-qr-size="96" data-qr-ec="M"
style="line-height:0; flex-shrink:0;"
title="Intune Device ID: {{ s.intune_device_id }}"></div>
{% else %}
<div class="d-flex align-items-center justify-content-center eyebrow"
style="width:96px; height:96px; border-radius:0.25rem; flex-shrink:0; text-align:center; padding:0.25rem; background:var(--bg); border:1px solid var(--border);">
{% if is_inferred %}log-only{% elif is_blancco %}WIPE{% if drive_pct is not none %}<br>{{ drive_pct }}%{% endif %}{% else %}no DeviceId{% endif %}
</div>
{% endif %}
<div class="flex-grow-1" style="min-width:0;">
<div class="d-flex flex-wrap align-items-center gap-2">
<strong class="fs-6">
{% if s.serial %}{{ s.serial }}
{% elif s.hostname_target %}{{ s.hostname_target }}
{% elif s.mac %}{{ s.mac }}
{% else %}{{ s.ip or '(unknown bay)' }}{% endif %}
</strong>
{% if s.hostname_target and s.serial %}<code class="mono" style="color:var(--text-light);">{{ s.hostname_target }}</code>{% endif %}
{% if s.ip %}<code class="mono" style="color:var(--text-light);">{{ s.ip }}</code>{% endif %}
{% if s.pctype %}<span class="badge badge-info">{{ s.pctype }}</span>{% endif %}
{% if s.machinenumber %}<span class="badge badge-secondary">#{{ s.machinenumber }}</span>{% endif %}
{% if is_inferred %}<span class="badge badge-warning">log-inferred</span>{% endif %}
<span class="badge badge-{{ border }} ms-auto">{{ s.status or 'in_progress' }}</span>
</div>
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="d-flex align-items-center gap-2">
<strong>{{ friendly[0] }}</strong>
{% if is_blancco and stage_idx == 4 and s.current_stage %}<span class="mono" style="color:var(--text-light); font-size:0.8rem;">{{ s.current_stage | replace('Erasing: ', '') }}</span>{% endif %}
{% if not is_inferred %}
<span class="badge badge-secondary">{{ stage_idx }}/{{ stage_total or '?' }}</span>
{% endif %}
</div>
{% if not is_inferred %}<span class="eyebrow">{{ pct }}%</span>{% endif %}
</div>
{% if not is_inferred %}
<div class="progress mt-1" style="height:0.7rem; background:var(--bg); border-radius:0.25rem;">
<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 }}%; background-color:var(--{{ border }});"
aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{% endif %}
</div>
</div>
</summary>
<div class="card-body pt-0 pb-3" style="border-top:1px solid var(--border);">
{% if friendly[1] %}<div class="mt-2" style="color:var(--text-light); font-size:0.85rem;">{{ friendly[1] }}</div>{% endif %}
{% if s.intune_device_id %}
<div class="d-flex flex-wrap align-items-center gap-2 mt-3">
<span class="eyebrow">Intune</span>
<code class="mono">{{ s.intune_device_id }}</code>
<button type="button" class="btn btn-sm btn-outline-secondary btn-row-action copy-btn"
data-copy-text="{{ s.intune_device_id }}">copy</button>
<a class="btn btn-sm btn-outline-primary btn-row-action"
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="btn btn-sm btn-outline-warning btn-row-action"
target="_blank" rel="noopener"
href="https://arts.dw.geaerospace.net/requests/type">ARTS request</a>
</div>
{% endif %}
<div class="mt-2 d-flex flex-wrap gap-3" style="color:var(--text-light); font-size:0.75rem;">
<span>started <code class="mono">{{ s.started_at | local_dt(true) }}</code></span>
<span>last <code class="mono">{{ s.last_updated | local_dt(true) }}</code></span>
<span>MAC <code class="mono">{{ s.mac or '-' }}</code></span>
{% if s.current_stage %}<span class="mono">{{ s.current_stage }}</span>{% endif %}
</div>
{% if s.error %}
<div class="alert alert-danger py-2 mb-2 mt-3" style="font-size:0.85rem;">
<strong>Error:</strong> {{ s.error }}
</div>
{% endif %}
{% if is_blancco %}
<div class="mt-3">
<a href="{{ url_for('blancco_reports') }}" class="btn btn-sm btn-outline-primary btn-row-action">Erasure reports</a>
</div>
{% endif %}
{% if not is_inferred and not is_blancco %}
<div class="mt-3 laps-card" data-serial="{{ s.serial }}">
<div class="eyebrow mb-2">LAPS password QR (paste -> scan on bay - persists until cleared)</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<input type="text"
class="form-control form-control-sm laps-input mono"
style="max-width: 22rem;"
placeholder="paste LAPS password from Intune portal here"
autocomplete="off"
value="{{ s.laps_password or '' }}">
<button type="button" class="btn btn-sm btn-primary laps-make-btn">{% if s.laps_password %}Update QR{% else %}Make QR{% endif %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary laps-clear-btn" {% if not s.laps_password %}style="display:none;"{% endif %}>Clear</button>
</div>
<div class="laps-qr-container mt-2"></div>
</div>
{% endif %}
{% if s.log_tail %}
<details class="mt-3 imaging-log" data-key="{{ card_key }}">
<summary class="eyebrow" style="cursor:pointer;">{% if is_inferred %}Evidence{% else %}Log tail{% endif %} ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }})</summary>
<pre class="mono p-2 mt-2 mb-0 imaging-log-pre" style="max-height: 12rem; overflow-y: auto; background:var(--bg); border:1px solid var(--border); border-radius:0.25rem; font-size:0.78rem;">{% for line in s.log_tail %}{{ line }}
{% endfor %}</pre>
</details>
{% endif %}
{% if not is_inferred and s.serial %}
<div class="mt-3 d-flex justify-content-between align-items-center">
<a href="{{ url_for('imaging_session_detail', serial=s.serial) }}"
class="btn btn-sm btn-outline-primary">
Details / full log
</a>
<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>
{% endif %}
</div>
</details>
{% endfor %}