Commit Graph

74 Commits

Author SHA1 Message Date
cproudlock
cc1e76f3ab Webapp redesign phase 4: Updates overview and rollback review
- GET /updates/: one card per updater (Dell, iPXE, Blancco, BPRT) with
  its state, a Check now / Open action and details in accordions; the
  shared download cap is a small popover.
- Dell applies list: one line per apply titled by its first change
  ('Latitude 5350 pack A11 to A12', model looked up in the maps) and
  'and n more'; the chip says how many written files are unchanged since
  the apply. The body lists every change, skipped item, error and file.
- dell_updates.file_status(): per file an apply wrote, unchanged /
  changed / missing by content hash, the same test rollback uses.
  describe(), history_view() and rollback_preview() build on it.
- GET /updates/dell/rollback/<id>: review a rollback before doing it,
  with the files that would be restored and a diff of each. The POST
  route is unchanged; both forms can return to the overview.
2026-10-14 13:54:41 -04:00
cproudlock
731d7b044d Webapp redesign phase 3: Drivers tab
- One line per entry: position, model tokens, pack, OS, size and status
  chips; family, friendly name, osId, folder, zip state, Dell hint and
  the verbatim lint findings are in the row body.
- Status chips compare the staged map with the saved one: New, Edited,
  Moved from #n, Removing, the worst current finding, and 'Clears on
  save' / 'Clears n' when the staged changes resolve this entry's
  findings. The toolbar and a sticky save bar show lint now and after.
- Removing an entry is staged with Undo remove instead of deleting the
  row; Undo edit puts an edited entry back.
- Reordering moved into the row menu (Move up / down / to position).
  The Move dialog shows the order after the move and re-lints the staged
  map with the move applied, listing any new errors before staging it.
- An edit that changes the tokens but keeps the old friendly name is
  flagged in the row and in the review step.
- Filters: Win11/Win10 with counts, 'Findings or unsaved', 'Zip
  problems' (missing, trap and case); a status key explains the chips.
- _image_header.html: new header and underline tabs for every image tab;
  the shared review modal and diff use the design tokens.
2026-10-14 13:51:04 -04:00
cproudlock
0b86255711 Webapp redesign phase 2: Overview and imaging floor
Imaging floor (/imaging, /imaging/tiles):
- One line per PC: status chip, serial with machine number, hostname
  and IP, stage text with a segmented stage track, one action plus a
  '...' menu. Everything else (description, error, timestamps, Intune
  ID + QR, Set category / ARTS links, LAPS QR, log tail) is in the row's
  expandable body.
- Order: failed, needs action (stage 7), in progress, done; log-only
  bays fold into one collapsed group. imaging_status.floor_state() and
  floor_order() compute this.
- Status filter buttons with live counts next to the text filter. SSE
  refresh, expanded-row memory and LAPS handling are unchanged.
- Stage text is never truncated; BIOS updates and erasing are bold.

Overview (/):
- Services popover, technician to-do line, the six most urgent floor
  rows (?limit=6 partial, refreshed every 15 s), and image cards with
  lint warnings, map errors and missing zips as separate chips. New,
  clone, delete and unattend stay on the cards' menus.

Copy buttons are now handled once in static/app.js, with a fallback for
plain-http pages where navigator.clipboard is unavailable.
2026-10-14 13:44:07 -04:00
cproudlock
35cad62733 Webapp redesign phase 1: shell, tokens and shared components
- static/pxe-ui.css: the redesign's colour tokens (light and dark),
  shell, chips, stage track, expandable rows, menus and buttons. The
  legacy tokens (--bg-card, --border, --text-light, ...) alias onto the
  new ones so pages not yet redesigned blend in.
- base.html: new sidebar (Operate, Images, Updates, Tools) with the GE
  Aerospace logo on the brand blue, to-do count, theme toggle with
  aria-pressed, and a menu button on narrow screens. The to-do banner is
  now a one-line strip. Skip link and <main> landmark added.
- _ui.html: Jinja macros for chip, track, the actions menu and the page
  header.
2026-10-14 13:38:05 -04:00
cproudlock
dfd57bef17 Webapp: fix six live bugs found in the page inventory
- Report view: the hero summarised only the first erasure, so a drive whose
  Purge failed and Clear fallback succeeded showed Failed while the list said
  Successful. It now uses the per-drive outcome (report_result) and lists the
  standards that wiped each drive.
- To-do and ServiceNow: reports with no usable document id could never be
  ticked or cleared (400). They are now tracked by file name (file:<name>).
- Imaging: the LAPS password field is masked, with a Show/Hide toggle.
- Imaging: succeeded sessions not updated for ACTIVE_WINDOW_HOURS are hidden
  from the dashboard (failed and in-progress always stay), with Show all.
- Enrollment: the active SFLD package can no longer be deleted, which left
  ppkg.conf pointing at a missing file; its row is marked Active.
- Import: confirm before importing, saying upload-folder sources are moved
  and same-name files in the image are overwritten.
2026-10-14 13:29:00 -04:00
cproudlock
24b4d9df66 Dell updates: say when the results predate the last apply
After an apply the tables still showed the check from before it, so a pack
just applied looked like it still needed updating until the next check.
2026-10-13 17:38:14 -04:00
cproudlock
6187219378 BPRT sync: point the page and docstrings at static/Sync-BPRT.ps1
Follow-up to 92f1e62, which moved the file but missed these references.
2026-10-13 17:12:51 -04:00
cproudlock
92f1e62966 BPRT sync: ship Sync-BPRT.ps1 with the webapp so the page can serve it
The download link 404'd on the server: deploys copy only webapp/, and the
script lived in scripts/windows/. One copy now, in webapp/static/.
2026-10-13 17:12:41 -04:00
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
cproudlock
b691a82530 Hardware coverage: flag models the server has no drivers or BIOS for
WinPE now reports each machine's WMI model, manufacturer, Dell system
SKU and BIOS version to /hardware/seen (winpe-status-push.ps1, best
effort, wmic only). The webapp records every model seen and checks it
against what the server has:
  - each image's Deploy/Control/HardwareDriver.json, matched exactly as
    PESetup's GetDriverByModel does (as reimplemented in
    lint-driver-catalogue.py), plus whether the pack's zip is on disk;
  - the WinPE BIOS manifest (substring match like check-bios.cmd).

/hardware lists every model with per-image driver status and BIOS
status. A model missing a Win11 pack or a BIOS entry becomes item 5 on
the Technician To-Do (and the banner count) until the pack/BIOS is added
or it is dismissed. Virtual machines are ignored.

Also deploys the repo winpe-status-push.ps1 over the live copy, which
predated the -CurrentStage/-StageIndex parameters startnet.cmd passes
for the BIOS-flash stage, so that status post was failing.
2026-10-09 09:23:52 -04:00
cproudlock
e43ab2a652 webapp: 12-hour times (e.g. 2026-10-05 1:54 PM EDT)
CSV exports keep ISO 8601 24-hour timestamps.
2026-10-05 14:16:58 -04:00
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
cproudlock
121577f090 webapp: wrap destruction notes instead of widening the table
The theme sets white-space: nowrap on every table cell, so the
ServiceNow tick list's destruction note (and the to-do failure column)
ran as one line and pushed the Done buttons off to the right. A
td.wrap utility lets prose cells wrap within 16-28rem.
2026-10-05 14:04:40 -04:00
cproudlock
6fcdc8ddb5 webapp: keep a tile's Log tail open across refreshes
Each SSE/poll refresh replaces the tiles' HTML, so the nested Log tail
<details> snapped shut on every update while a wipe was running. Its
open state is now remembered per tile in localStorage like the tile
itself, and its scroll position is kept: following the bottom keeps
following new lines, scrolled up stays put.
2026-10-05 14:02:51 -04:00
cproudlock
020f043d8b webapp: technician to-do for the manual CMMC steps; count fallbacks as wiped
Technician To-Do (sidebar link with a count, and a banner on every page
while anything is open) lists the playbook steps the server cannot do:
  1. wipes still to tick on the Onsite Bin RITM (ENT-045/159)
  2. monthly evidence packs to upload to Box (ENT-056/158), Mark uploaded
  3. drives that failed to wipe, to destroy (COD recorded)
  4. reports Blancco could not upload to the cloud (Resolved)
Marks are stored in the Blancco state file; the counts are cached for 60s
and refreshed on every mark, ServiceNow tick and cloud-status post.

Results are now judged per drive. Blancco keeps every attempt in the
report, so a NIST Purge that failed followed by a successful Clear
fallback on the same drive appeared as two entries, and the Reports page
rolled the whole report up to Failed. 20 such wipes since May showed as
Failed; they were wiped (the work instruction counts the fallback as a
success). The CSV marks those rows "Fallback: ... then erased with ...".
One genuine failure remains: HNMD1V3, Seagate Z500R4KG, 2026-08-11.
2026-10-05 13:50:26 -04:00
cproudlock
44a7eeae95 blancco: CMMC evidence pack, ServiceNow tick list, cloud-upload status
Automates the Blancco parts of the onsite-support CMMC playbook:

- ENT-056/158 evidence pack: /reports/evidence?month=YYYY-MM returns
  ENT-056_<Site>_<YYYY-MM>.zip (summary.csv, README, each report's XML
  and PDF). pxe-blancco-evidence.timer saves last month's pack to the
  reports share's evidence/ folder on the 1st (Persistent).
- ENT-045/159 ServiceNow tick list: /reports/servicenow lists recent
  reports as "Wiped" or "Blancco Wipe failed" with a pre-written
  destruction note, copy buttons and Done (CSRF-protected).
- Cloud upload: blancco-progress.py posts each report's upload result
  ("Sending report document id" -> "Report sent to MC") to
  /reports/cloud-status (CSRF-exempt like /imaging/status); the Reports
  page and the CSVs show it, so a wipe missing from BMP is visible.
- CSV gains failure_reason (exception messages, failed steps, error
  counts), business_location and cloud_upload.
- Preferences: business_location "West Jefferson" (config.BLANCCO_SITE)
  and pdf+xml reports; the report copier now copies the PDF too, named
  after its XML. .part and hidden files are not listed as reports.

State (cloud confirmations, ServiceNow ticks) is a JSON file at
/var/lib/pxe-webapp/blancco-state.json, off the report share.
2026-10-05 13:43:16 -04:00
cproudlock
eff956ae6c webapp: Blancco variant of the /imaging tiles
Sessions with source/pctype "blancco" (posted by blancco-init.sh and
blancco-progress.py) now render with their own stage labels
(Downloading, Starting, Licensing, Erasing, Erasure finished, Report
sent, Wipe complete; Erasure FAILED on failure), a WIPE badge with the
drive percentage instead of "no DeviceId", the current step beside the
stage, and an Erasure reports link. The drive percentage is folded into
the bar across stage 4. The LAPS QR card is hidden for Blancco tiles.

Also render the bar percentage as an integer: round() returns a float,
so every tile showed e.g. "57.0%".
2026-10-05 12:52:08 -04:00
cproudlock
96c4e5dd11 webapp: export Blancco reports for the last 1/3/6/12 months
/reports/export?range=1m|3m|6m|12m&format=csv|zip. CSV has one row per
erased drive (system and drive serials, standard, result, times, licence
consumption id). ZIP holds the original XML/PDF reports plus summary.csv.
Reports are selected by the date inside each XML, falling back to mtime
only when it is missing. Exports are audit-logged.
2026-10-05 09:32:32 -04:00
cproudlock
3880151e71 webapp: shopfloor sub-menu tab + nav rename (the edits dropped by a failed git add)
The prior 'nest shopfloor menu as a tab' commit only captured the shopfloor_menu.html deletion - the git add also named the just-removed file, which errored the add and staged nothing else. This commits the actual integration that was already deployed to preview and prod: startnet_editor GET passes shopfloor_items/shopfloor_available, the Shopfloor Sub-Menu tab pane + sf-prefixed JS in startnet_editor.html, /shopfloor-menu redirects into the tab, and the sidebar 'startnet.cmd' -> 'Boot Menu' rename.
2026-07-23 13:58:08 -04:00
cproudlock
5f97e81dec webapp: nest shopfloor menu as a tab in the Boot Menu editor; rename nav to Boot Menu
The shopfloor sub-menu editor is now a 'Shopfloor Sub-Menu' tab inside the startnet editor (it IS a sub-menu of the boot menu), not a separate page - sf-prefixed IDs avoid clashing with the top-level Boot Menu tab. /shopfloor-menu GET + save now redirect back into that tab; standalone shopfloor_menu.html removed. Sidebar 'startnet.cmd' renamed to 'Boot Menu'.
2026-07-23 13:37:55 -04:00
cproudlock
626561a1fa shopfloor menu: data-driven from menu.json (picker + webapp editor)
Replace the hardcoded GEA Shopfloor PC-type sub-menu with a data-driven one:
- menu.json on the enrollment share lists the shopfloor items {key=PCTYPE, label, hint, enabled}; key must match a shopfloor-setup/gea-shopfloor-* handler dir.
- select-shopfloor-type.ps1 renders it in WinPE and writes the chosen PCTYPE (mirrors the CMM bay picker); startnet.cmd runs it and falls back to the baked-in menu if the share/picker is unavailable.
- Webapp /shopfloor-menu editor: reorder/rename/hide/add items; the PC-type is a dropdown of existing handler dirs (can't wire a choice to a non-existent type); writes menu.json. Nav link under Tools.
Kills the duplicated-knowledge problem (menu list was hardcoded in startnet AND the handler dirs AND site-config); add a PC-type = drop in the handler dir + it appears in the menu.
2026-07-23 13:27:08 -04:00
cproudlock
f1d9bdf478 startnet editor: harden _split_lines against doubled CRLF (\r\r\n)
A CRLF file re-CRLF'd (e.g. sed adding \r to already-CRLF lines) yields \r\r\n; the old _split_lines left a stray \r that split into a blank line between every line, so parse_boot_menu/settings/lint saw a garbled file and returned nothing. Now collapse any run of CR before a newline. Also re-deployed a clean-CRLF startnet.cmd into the live boot.wim (the earlier sed-based deploys had doubled the CR).
2026-07-23 11:42:51 -04:00
cproudlock
cc391529bd startnet editor v2: settings/menu forms, syntax-highlight+lint, backup/restore+diff
Rebuilt the /startnet editor (Fable/Opus 4-stage build) into tabs:
- Settings: server IP (+ menu timeout/default when a choice construct exists) as form fields; apply rewrites only the targeted tokens.
- Boot Menu: add/remove/reorder image entries; regenerates only the menu echo/dispatch + action blocks, refuses reorders that would desync the enrollment %choice% router.
- Raw: full-text editor (still source of truth) with line-number gutter, batch syntax highlighting, a lint panel (unmatched goto/label, CRLF), and diff-vs-current.
- History: timestamped snapshots on every save, per-row diff + restore.
New wim.py helpers (framework-free): parse/apply_settings, parse/apply_boot_menu, lint_startnet, save/list/read_snapshot, unified_diff; BACKUPS_DIR=/var/lib/pxe-webapp/startnet-backups.
Fable review fixed a CRITICAL pre-existing bug: update_startnet's newline=CRLF write retranslated posted CRLF into \r\r\n, corrupting boot.wim on every raw save; now normalizes to LF first. Also fixed a false CRLF lint warning (verbatim read) and menu payload validation. All JS inline (no CDN).
2026-07-23 11:37:24 -04:00
cproudlock
0ffdcc79ed enrollment: auto-activate uploaded ppkg via ppkg.conf (no boot.wim edit)
startnet.cmd now sources SOURCE_PPKG/PPKG_VER/PPKG_EXP from \\<pxe>\enrollment\ppkg.conf (baked-in values kept as fallback if the file is missing). The webapp writes ppkg.conf on upload for standard GCCH_..._v<ver>.ppkg files: version parsed from the filename, optional expiry field (blank keeps current), so a new ppkg goes live at next boot with no startnet/boot.wim edit. Enrollment page shows the active ppkg. ppkg.conf seeded on the share (v4.16) and the ppkg.conf-aware startnet.cmd deployed into boot.wim.
2026-07-23 11:02:43 -04:00
cproudlock
0cb6b26c27 webapp import: checksum-aware sync (skip unchanged, update only changed)
Image import previously full-copied everything and rmtree'd existing target dirs on every run. Replaced the shutil copy/move/copytree/rmtree with deploy.sync_tree(), which shells out to rsync -a --checksum: files whose content already matches the target are skipped, only new or changed files are written, and existing target files not in the source are left untouched (merge, not mirror). move=True uses --remove-source-files (frees the SMB upload dir) and prunes emptied source dirs. Applies to the Deploy import, the _shared redirections, and the root-level items. Big re-imports now only rewrite what actually changed.
2026-07-23 10:45:58 -04:00
cproudlock
8fbae24b4d webapp: fix a11y contrast, heading hierarchy, prefix-safe modal actions
- Accessibility: dark ink (#00003d) on warning/success buttons, badges, and success alert - white text on #ff9500/#0ad64f failed WCAG AA (~2:1); dark-on-bright now passes.
- Heading hierarchy: sidebar brand h1 -> div.brand-title (with matching CSS selector), and standardized all page-header titles to a single <h1> per page (8 templates were h2).
- Prefix-safe modal JS: dashboard/backups/reports/enrollment delete+clone actions now build their form action from url_for(...'__K__'/'__F__').replace(...) instead of hardcoded paths, so they survive a URL-prefix mount.
2026-07-23 10:14:29 -04:00
cproudlock
18db077475 webapp: reskin to shopdb-flask design system
Adopt the shopdb-flask visual language across the PXE webapp (presentation only, Flask/Jinja logic unchanged):
- New static/pxe-theme.css: GE Aerospace palette (atmosphere-blue sidebar, sky-blue primary, avionics-green), Inter font stack, light/dark theming via data-theme + localStorage (key pxe-theme) with system-pref fallback, and card/button/table/form/badge/alert component styles layered over Bootstrap.
- base.html: shopdb-style sidebar (logo + title, nav sections, footer light/dark toggle) + theme boot script.
- All 13 content templates restyled to the new page-header + card/table/badge vocabulary; unattend_editor grouped per unattend-UX research.
- Fixed a pre-existing CRITICAL bug found during review: nested <form>s in image_config.html made Adopt submit the delete form and Delete-selected post every orphan filename regardless of checkboxes; split into standalone forms wired via the form= attribute.
Built by a Fable-orchestrated Opus workflow (17 agents). All 14 templates parse clean under Jinja2.
2026-07-23 10:05:09 -04:00
cproudlock
0c4c2a245d Remove legacy ge-* image types (gea-only fleet)
Drop ge-standard/ge-engineer/ge-shopfloor-lockdown/ge-shopfloor-mce from image_types + standard_types, both startnet boot menus (choices + labels + net use), and the webapp IMAGE_TYPES/FRIENDLY_NAMES. Fleet is gea-* only; the empty ge-* winpeapps stub dirs were removed on the live share.
2026-07-23 09:21:49 -04:00
cproudlock
69a1682a7f webapp: imaging UX overhaul + image management CRUD
Imaging dashboard
- services/imaging_log_tail.py: parses dnsmasq leases, Apache access log,
  Samba per-host log files, and dnsmasq syslog (DHCP/TFTP). Synthesizes
  inferred sessions keyed by MAC for bays that have only touched the boot
  chain but not yet pushed to /imaging/status. Active window 90 min.
- imaging_status.list_sessions() merges inferred sessions into the dashboard
  list. Real client-pushed sessions win for the same MAC.
- imaging_status: stage_history field tracks every stage transition (capped
  30); sidecar .log file per serial records every log_lines push uncapped
  (read_full_log() caps detail-page response to 1 MB).
- delete_session/delete_all_sessions clean up sidecar .log too.
- New SSE endpoint /imaging/stream emits a session-list hash every 5s.
  Client fetches /imaging/tiles (HTML partial) on hash change and swaps
  #imaging-tiles innerHTML. Polling fallback at 15s if SSE drops.
- Tile-swap preserves scroll, filter input, expanded state via localStorage,
  and any LAPS input the operator is mid-pasting (swap skipped when a
  laps-input is focused).
- imaging.html: removed 15s location.reload(). Added live-status dot in
  header (gray idle / green SSE connected / red SSE lost).
- _imaging_tiles.html: shared partial used by both /imaging full render and
  /imaging/tiles SSE refresh. Inferred bays render with yellow border +
  log-inferred badge + no progress bar (stage_index inference is coarse).
- imaging_detail.html (new): per-bay forensics page at /imaging/session/
  <serial>. Session metadata grid, stage timeline table, full sidecar log
  with truncation indicator, Copy-support-summary button. Linked from each
  client-pushed tile.
- qr-render.js exposes window.renderAllQRs() so the SSE swap can re-render
  Intune device-ID QRs in the swapped-in tiles.

Image management
- services/image_registry.py: JSON registry of image types at
  {SAMBA_SHARE}/image-registry.json. Bootstraps from baked-in
  config.IMAGE_TYPES on first run. create/clone/delete/rename_friendly
  mutate the file then call reload() which rewrites config.IMAGE_TYPES +
  config.FRIENDLY_NAMES in place. Sidebar reflects on next request.
- app.py routes: /images/new, /images/<t>/clone, /images/<t>/delete (with
  optional content-wipe checkbox), /images/<t>/rename.
- dashboard.html: + New image type button + Clone/Delete per row, all in
  Bootstrap modals with confirmation copy.
- Clone copies Deploy/ tree but preserves symlinks to shared dirs (Out-of-
  box Drivers, Operating Systems, Packages) so disk usage stays low.
- Delete with content checked unlinks symlinks (does not follow into shared
  dirs).

Driver / package upload + orphan adoption
- services/images.py: upload_driver, adopt_orphan, remove_orphans,
  upload_package. Filename sanitization blocks path traversal.
- app.py routes: /images/<t>/drivers/upload, /images/<t>/drivers/adopt,
  /images/<t>/drivers/orphans/delete, /images/<t>/packages/upload.
- image_config.html: Upload .zip button + modal on Drivers section. Orphan
  drivers card-footer rebuilt as interactive list with per-row Adopt inline
  form (family + destinationDir inputs) and bulk select+delete.
- Upload .zip on Packages section with optional destinationDir field that
  appends a packages.json entry.

Configuration
- config.py: new env vars DNSMASQ_LEASES, APACHE_ACCESS_LOG, SAMBA_LOG_DIR,
  DNSMASQ_SYSLOG for the log-tailer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:21:06 -04:00
cproudlock
b57ba0fb6f webapp: add CSRF token to imaging Clear-all form
The dashboard Clear-all button posts to /imaging/delete-all but the form
was missing the hidden _csrf_token input that the rest of the webapp's
POST forms include, so the endpoint would reject the request when CSRF
enforcement is active.
2026-05-24 07:04:20 -04:00
cproudlock
3aabd47571 imaging dashboard: add Clear all button + endpoint
New /imaging/delete_all endpoint wipes every per-bay JSON in IMAGING_DIR
via imaging_status.delete_all_sessions(). Template adds "Clear all"
outline-danger button next to the count badge, gated on sessions list
non-empty, with confirm() prompt naming the count.

Deployed via scp + systemctl restart pxe-webapp on 172.16.9.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:58:31 -04:00
cproudlock
d8c64bef2b Add conditional BIOS-update sub-stage on idx=1
winpe-status-push.ps1 now accepts -CurrentStage / -StageIndex
params so callers can override the default "WinPE: PESetup / WIM
apply" string. Backwards compatible.

startnet.cmd: after the existing initial WinPE status push,
inspect $BIOS_STATUS for the "->" marker that check-bios.cmd
writes when an update was actually applied or staged. If present,
fire a second idx=1 push with stage="WinPE: BIOS firmware update -
<status>". No-op for clean "up to date" / "no update in catalog"
runs.

imaging.html: at stage_idx=1 with "bios" in current_stage, swap
friendly label to "Updating BIOS firmware" with a do-NOT-power-off
hint. Bays without firmware updates show the default "Booting from
PXE" label as before.

boot.wim startnet.cmd updated via wimupdate so live PXE clients
pick it up at next boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:26:31 -04:00
cproudlock
3385bc87aa Monitor + imaging: per-phase sub-stages within idx=7
Monitor's Get-Snapshot already tracks Phase 1-5 (Intune Registration,
Device Configuration, Software Deployment, Credentials, Lockdown).
The webapp dashboard only saw a single idx=7 push for the entire
post-PPKG / pre-lockdown window, so the friendly label couldn't
reflect "where is this bay actually". Operator looking at the
dashboard had no idea whether to assign category or hit ARTS for
lockdown next.

Monitor now pushes additional idx=7 entries as it crosses Phase
boundaries:
 - On DeviceId capture: "Intune Device ID captured" (existing)
 - On Phase 2 done (SFLD policy delivered = category was assigned):
   "Phase 2 SFLD policy delivered (device configuration)"
 - On Phase 1-4 all complete: "Phases 1-4 complete - ready for
   lockdown (ARTS request)"
 - On lockdown done: idx=8 (existing)

imaging.html maps the stage_string substring to friendly labels:
 - default idx=7         -> "Registered - assign category"
 - 'sfld policy' / 'phase 2' -> "Phase 2 - device configuration"
 - 'credentials' / 'phase 4' -> "Phase 3 / 4 - DSC + credentials"
 - 'ready for lockdown' / 'request lockdown' -> "Ready - request
                              lockdown" (hint: click ARTS request)

Operator now knows exactly when to act vs when to wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:39:20 -04:00
cproudlock
220c5db5b9 webapp: LAPS clear actually removes the password from session JSON
Previous /imaging/<serial>/laps clear path used update_session() to
re-feed state minus laps_password. But update_session MERGES payload
into existing state - it cannot delete a key the existing state
already has. The laps_password persisted on disk across the "clear"
POST, then came back into the page on next reload.

Fix: bypass update_session for the clear case. Read the session JSON
directly, pop laps_password, write via atomic tempfile-rename. Same
write pattern update_session uses for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:33:07 -04:00
cproudlock
8debc4ddb3 imaging: LAPS input always visible, not gated on intune_device_id
Was hiding LAPS QR section until idx=7 pushed with a DeviceId.
Operator couldn't paste a password if Monitor hadn't gotten around
to capturing the DeviceId yet. The QR encoding doesn't depend on
DeviceId - it's just the password being encoded - so the section is
useful any time the bay is past the LAPS reboot.

Drop the {% if s.intune_device_id %} gate. LAPS section now appears
in every expanded tile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:30:48 -04:00
cproudlock
036090348c imaging: persist tile expanded state across page refresh
localStorage-backed set of serials at key 'imaging-expanded'. On
DOMContentLoaded, walk each .imaging-card; if its data-serial is in
the set, set card.open=true. On every <details> toggle, update the
set. Refresh no longer collapses the tile the operator was looking
at.

Per-browser state (localStorage), no server round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:29:45 -04:00
cproudlock
b9f66687ac imaging: search now also matches stage name, stage-N, and status
Extended the data-filter attribute to include:
 - friendly stage label (e.g. "awaiting intune lockdown")
 - "stage-N" token (e.g. type "stage-7" to find idx=7 bays)
 - status string (in_progress / succeeded / failed)

Use cases:
 - Find all bays waiting on lockdown: type "lockdown"
 - Find all bays at the same stage: "stage-7"
 - Find failed bays: "failed"
 - Find succeeded bays: "succeeded"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:28:08 -04:00
cproudlock
65eeead5a0 imaging: collapsible tile + ARTS link + reword stage 7
Tile is now <details>. Always-visible summary:
  - QR (96px)
  - serial / hostname / pctype / machine# / status badge
  - friendly stage label + N/M badge + pct
  - progress bar

Click to expand. Body shows:
  - friendly stage hint
  - Intune device id row with [copy] [set category] [ARTS request]
  - metadata one-liner (started / last / MAC / raw current_stage)
  - error banner (if any)
  - LAPS password QR generator
  - log tail
  - Clear button

ARTS button links to https://arts.dw.geaerospace.net/requests/type
for kicking off a new lockdown request (Intune-side step happens
externally; this is a deep-link for convenience).

Stage 7 wording: "Awaiting Intune lockdown" (was "awaiting category /
lockdown" - confusing when category was already assigned). Hint
explicitly mentions category check for cases where it isn't yet set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:27:35 -04:00
cproudlock
a9a7478d5a imaging: organize tile metadata into deterministic rows
Previously last_updated, MAC, started, Intune device id, and the
raw current_stage string were sprinkled around the card in
hard-to-track positions. Reorganized:

Row 1 (header): serial | hostname | pctype | machine# | status badge
Row 2 (stage):  friendly label + N/M badge | pct% (right)
Row 3:          full-width progress bar
Row 4:          friendly hint (optional)
Row 5:          Intune device id + copy + set-category (optional)
Row 6:          started ... last ... MAC ... raw current_stage

Each row consistent across all cards regardless of which fields
are populated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:16:34 -04:00
cproudlock
1c361e138b imaging: compact tile + search filter + stage 7 label tweak
Tile shrunk for fleet density:
 - QR: 160px -> 96px
 - Drop big h4 for serial, use fs-6 strong instead
 - DeviceId + buttons + MAC + started time consolidated into one
   small grey row instead of three separate sections
 - Progress bar 1.2rem -> 0.7rem
 - mb-4 -> mb-2 between cards
 - card-body py-2 for tighter vertical rhythm

Search:
 - Sticky search input above the card list
 - Filters live on serial, hostname, pctype, machinenumber,
   intune_device_id via lowercase substring match on a data-filter
   attribute
 - Visible-count badge updates as you type ("3/12")
 - Auto-refresh paused while query has text or while input is focused

Stage 7 label: was "assign category" only, now "awaiting category /
lockdown" to reflect that bays past category assignment are still
waiting on the Intune-driven LAPS-prompt reboot before lockdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:14:51 -04:00
cproudlock
ca647cb690 imaging: redesign tile + LAPS persist + 15s refresh
Tile redesign:
 - QR (or placeholder if not yet captured) on the left as a fixed 160px block
 - Right side: header (serial / hostname / pctype / machinenumber / status)
   then stage label as a big h4 with stage badge + % on the same row,
   then full-width progress bar, then friendly stage hint
 - Intune device id row with copy + set-category buttons consolidated
   under the progress section
 - Footer one-liner: started / last / MAC / raw current_stage (small grey)
 - LAPS QR + log tail still expandable below
 - shadow-sm for visual lift, no card-header line splitting

LAPS persist: POST password to /imaging/<serial>/laps so it survives
the dashboard refresh. Auto-renders QR on page load if the session
already has a stored password. Clear button POSTs empty string to
wipe server-side. No more 60s auto-clear - stays until cleared (or
daily server reset).

Refresh: 5s -> 15s. Reduces polling jitter + gives the eye time to
read before page flickers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:11:58 -04:00
cproudlock
5f322d1110 imaging: operator-friendly stage labels per bay card
Was showing the raw push label (e.g. "Run-ShopfloorSetup: handoff to
Monitor-IntuneProgress") which only makes sense if you know the
playbook internals. Added a stage_index -> (label, hint) lookup table:

  1  Booting from PXE                    WinPE loaded
  2  Configuring Windows                 First boot baseline scripts
  3  Installing apps                     09-Setup-<pctype>
  4  Apps installed                      preparing for enrollment
  5  Enrolling in Intune                 PPKG + AAD/Intune join
  6  Waiting on first Intune sync        post-PPKG settle (~120s)
  7  Registered - assign category        idx=7 with QR + set-category btn
  8  Imaging complete                    lockdown applied

Friendly label + one-line hint shown bold, raw stage string shown
underneath in small monospace for techs who want the playbook
breadcrumb. Stage index/total folded into a badge next to the
"Current stage" header so it doesn't need its own column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:08:05 -04:00
cproudlock
894305e906 Monitor: drop AESFMA-connected from Phase 1 done; webapp: LAPS endpoint
1. Phase 1 done gate was requiring 'AESFMA WLAN connected' in addition
   to the data-side signals (AAD + Intune + EmTask + baseline). If the
   bay never reached AESFMA (cert never landed, RADIUS unreachable),
   Phase 1 stayed IN PROGRESS forever even though Intune registration
   was actually complete. Reverting to the data-side-only definition.

2. New webapp endpoint POST /imaging/<serial>/laps stores a LAPS
   password in the session JSON so it survives the 5s dashboard
   auto-refresh. Empty body clears the field. Daily reset of the
   server (cron/restart) is the lifetime cap on stored passwords.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:53:05 -04:00
cproudlock
1b7e1bfee4 imaging: pause page auto-refresh while a LAPS QR is showing
meta http-equiv=refresh fires every 5s and reloads the entire page,
wiping the LAPS QR state mid-scan. Replaced the meta tag with a
JS-driven setTimeout(location.reload, 5000) so renderLapsQR() can
clearTimeout it. Reload resumes when the QR is cleared (manual or
60s auto). Multi-bay safety: only resumes if no other bay still has
a QR rendered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:50:24 -04:00
cproudlock
d5398bdd74 imaging: LAPS-password-to-QR generator per bay card
Per-bay <details> section with:
 - Input field for LAPS password (paste from Intune portal manually,
   since deep-link to LAPS blade needs AAD objectId we can't obtain)
 - Make QR button generates a client-side QR from the input
 - QR displayed below at 280px with 4-cell quiet zone
 - Auto-clears input + QR after 60s with live countdown
 - Manual Clear button
 - Enter key on the input also triggers QR generation

Password never POSTs to server, never logged, never persists past the
60s window. Generated using the same qrcode-generator lib already
loaded for the device-id QR. Scan with a USB barcode scanner plugged
into the bay (HID keyboard mode) -> password types into bay login
field. Faster than reading off the Intune portal letter-by-letter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:48:43 -04:00
cproudlock
cdb6655e4a imaging: drop LAPS deep-link, keep only category
LAPS retrieval blade is keyed on AAD object id, not aadDeviceId /
mdmDeviceId. We capture aadDeviceId from dsregcmd; resolving to
objectId would require a Graph API call with Device.Read.All which
we don't have at WJ. Removed the LAPS button - operator goes to
Intune portal manually for LAPS as before.

set-category button stays - aadDeviceId works for that blade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:47:46 -04:00
cproudlock
74ba3d1339 imaging: deep-link buttons for Set Category + LAPS per bay
Two buttons next to the Intune device id on each bay card:
 - "set category" -> portal.azure.us Intune device blade properties
   via aadDeviceId/{deviceId}
 - "LAPS" -> intune.microsoft.us encryptionKeys blade via
   mdmDeviceId/{deviceId}

Both use the dsregcmd DeviceId we already capture - no Graph API
lookup or objectId resolution needed. One click from the dashboard
takes the tech to the right page for category assignment or LAPS
retrieval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:20:10 -04:00
cproudlock
ce604adcda Renumber PXE LAN from 10.9.100.0/24 to 172.16.9.0/24
Single-site bay-stuck issue at WJ: GE Intune Report IP script filters
Get-NetIPAddress on StartsWith("10.") and posts everything matching
to the GE Tines webhook. Bays at WJ get the PXE LAN 10.9.100.x IP
captured and reported -> GE backend tags bays as on a non-corp 10.x
subnet -> dynamic group eligibility for SFLD policy never matches.
Other GE sites work because their PXE LANs aren't on 10.x at all.

Renumber PXE LAN to RFC1918 172.16.9.0/24 so the GE filter naturally
skips wired PXE addresses without any disable-NIC dance.

Server-side already in flight (netplan dual-bound, dnsmasq scope +
boot URL repointed, blancco preferences + grub.cfg + iPXE GetPxeScript
all sed'd to 172.16.9.1). This commit is the playbook / scripts /
docs side: 109 hits across 35 files sed'd in one shot.

After this lands + boot.wim is rebuilt + bays renumber off DHCP,
the 10.9.100.1 binding will be dropped from netplan as the final
cleanup step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:30:32 -04:00
cproudlock
0eb52c6a9e imaging: copy button HTTP fallback (execCommand)
navigator.clipboard.writeText is gated on isSecureContext - HTTPS
or localhost only. PXE dashboard is served over plain HTTP
(10.9.100.1:9009) so the API was undefined and the chain threw
before .catch fired - user saw nothing. Wrap clipboard write in
copyText() that prefers the modern API and falls back to the
classic invisible-textarea + document.execCommand('copy') path
which works on HTTP. Visual flash logic moved into flashCopied()
for reuse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:30:16 -04:00
cproudlock
6275a6a2b3 imaging: add visual feedback to device-id copy button
Click effect: button flashes green with "copied!" text and 1.15x
scale pulse, reverts after 1.2s. Failure case (clipboard API blocked
or HTTP context) shows red "failed" for 1.5s. Handler moved out of
inline onclick into a single delegated click listener at the doc
level so future copy buttons just need the .copy-btn class +
data-copy-text attribute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:29:00 -04:00