Everything West Jefferson specific moved out to a separate repo (pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers, CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the WJ preinstall list, and the WJ shopfloor PC types (Collections, NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace, Display, PartMarker) with GE-Enforce, machine numbers and the desktop layout. The part-marker bench tools moved to their own repo. Dead scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models, mirror-from-gold, pull-bios, startnet-template.cmd. Shopfloor is one generic type, gea-shopfloor-standard, with today's timing: - PPKG at first logon (unattend Order 10, unchanged). - Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup. - A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once after the post-PPKG reboot. It also clears AutoLogonCount after the ShopFloor handover (the Users-level monitor can be denied that HKLM write) and removes the Intune Sync task when done. - Both lists ship empty. A site adds entries and installers. - WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal carry only the generic payload. The webapp's shopfloor sub-menu editor is gone, and the imaging tiles name the new stages. - The playbook deploys the empty lists only if missing. It now deploys the PowerShell 7 MSI the shopfloor unattend installs, which it never did before. Blancco site settings move into the webapp (Updates > Blancco releases): business location, licensing Wi-Fi SSID and password. They are rendered into the served preferences.xml and applied to every staged release, and are seeded from site-vars (site_name, blancco_wifi_ssid, blancco_wifi_password) only while none exist. The repo XML carries no site values; the plaintext Wi-Fi password is gone from it. New site_name and site_timezone settings: report and evidence-pack site name, server clock and webapp time zone. No more West Jefferson defaults in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the server address filled in at download instead of WJ's GE address. USB on Windows: - scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus payload) with diskpart and reads every file back by SHA-256. - Tested with Windows PowerShell 5.1 in the win11 VM, and the layout installs and first-boots in the pxetest VM. - scripts/stage-usb-share.sh stages the build folder on a share. - site-content/ is optional for both builders. KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment, Windows only, with a site-vars.yml guide. Checks: webapp tests pass (5 suites); the unattend lint and playbook syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names or PC types in playbook/, webapp/, scripts/ or autoinstall/.
228 lines
16 KiB
HTML
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.'),
|
|
3: ('Installing site apps', 'The site app list (pre-install). Empty unless the site added apps.'),
|
|
4: ('Site apps done', 'Pre-install list 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">▾</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">▾</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">▾</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 %}
|