This guide covers the complete process for imaging a Dell Pro Micro MicroPC as a Shopfloor Dashboard Display using the PXE boot server.
+
+
Prerequisites:
+
+
Dell Pro Micro MicroPC (e.g. QCM1250)
+
USB mouse and keyboard
+
Ethernet cable connected to PXE switch
+
PXE server running and reachable on the network
+
+
+
+
+
Step 1: BIOS Configuration
+
+
+ Step 1
+
Enter BIOS Setup
+
+
+
Plug the MicroPC into the PXE switch along with a mouse and keyboard.
+
Power on the MicroPC and tap F12 to reach the One-Time-Boot menu.
+
Select BIOS Setup.
+
Enable Advanced Setup.
+
+
+
+
+ Step 1a
+
Verify Boot Configuration
+
+
Select Boot Configuration and verify the following settings:
+
+
+
Enable Secure Boot is set to ON
+
Enable Microsoft UEFI CA is set to Enabled
+
+
+
+
+ Step 1b
+
Verify Storage
+
+
Select Storage and verify:
+
+
+
AHCI/NVMe is selected
+
+
+
+
+ Step 1c
+
Verify Connection
+
+
Select Connection and verify:
+
+
+
Integrated NIC is set to Enabled with PXE
+
+
+
+
+ Step 1d
+
Apply and Exit
+
+
+
Click Apply Changes.
+
Click OK.
+
Click Exit and immediately begin tapping F12 again.
+
+
+
+
+
+
+
Step 2: PXE Boot
+
+
+ Step 2
+
Boot from Network
+
+
+
Once you're back in the One-Time-Boot menu, select the IPV4 option.
+
In the PXE Boot Menu, select Windows PE (Image Deployment) (auto-selected after 30 seconds).
+
Hit any key to verify that Secure Boot is enabled, or wait 5 seconds to automatically continue.
+
+
+
+
+
+
+
Step 3: Image Selection
+
+
+ Step 3
+
Select Image and Configuration
+
+
Make the following selections at each menu:
+
+
+
WinPE Setup Menu: Select 3. GEA Shopfloor
+
GCCH Enrollment Profile: Select 1. No Office
+
Shopfloor PC Type: Select 5. Display
+
Display Type: Select 2. Dashboard
+
+
+
+ Note:
+ The enrollment profile and PC type selections determine what software and configuration will be applied after imaging completes.
+
+
+
+
+
+
+
Step 4: Imaging
+
+
+ Step 4
+
Start Imaging
+
+
+
Once GE Image Setup launches, click Start.
+
From this point the process is mostly automated.
+
Note the Serial Number displayed on screen.
+
Provide the serial number to Patrick and let him know it's a Shopfloor Display.
+
When imaging completes, the PC will reboot into Windows.
+
Once Windows begins loading, unplug the ethernet cable from the PXE switch and plug it into an internet-connected port.
+
+
+
+ Important: Switch network cable after reboot!
+ The first-logon setup requires internet connectivity to complete GCCH enrollment. The PC will wait at a "Waiting for internet connectivity..." prompt until a connection is available. Unplug from the PXE switch and plug into the corporate network.
+
+
+
+ Do not power off or disconnect the MicroPC during imaging.
+ The imaging process will take several minutes. The PC will reboot automatically when complete.
+
+
+
+
+
+ Imaging Complete
+ Once the process finishes and the PC reboots, the Shopfloor Dashboard display configuration will be applied automatically during the first logon sequence.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/shopfloor-display-imaging-guide.md b/docs/shopfloor-display-imaging-guide.md
new file mode 100644
index 0000000..b7ac2a8
--- /dev/null
+++ b/docs/shopfloor-display-imaging-guide.md
@@ -0,0 +1,40 @@
+# Shopfloor Display MicroPC Imaging Guide
+
+## Prerequisites
+- MicroPC connected to PXE switch
+- USB mouse and keyboard connected
+- PXE server running and reachable
+
+## Step 1: BIOS Configuration
+
+1. Power on the MicroPC and **tap F12** to reach the One-Time-Boot menu.
+2. Select **BIOS Setup**.
+3. Enable **Advanced Setup**.
+4. Select **Boot Configuration**:
+ - Verify **Enable Secure Boot** is **ON**
+ - Verify **Enable Microsoft UEFI CA** is set to **Enabled**
+5. Select **Storage**:
+ - Verify **AHCI/NVMe** is selected
+6. Select **Connection**:
+ - Verify **Integrated NIC** is set to **Enabled with PXE**
+7. Click **Apply Changes**, then click **OK**.
+8. Click **Exit** and immediately begin **tapping F12** again.
+
+## Step 2: PXE Boot
+
+1. Once you're back in the One-Time-Boot menu, select the **IPV4** option.
+2. In the PXE Boot Menu, select **Windows PE (Image Deployment)** (auto-selected after 30 seconds).
+3. Hit any key to verify Secure Boot is enabled, or wait 5 seconds to automatically continue.
+
+## Step 3: Image Selection
+
+1. For the WinPE Setup Menu, select **3. GEA Shopfloor**.
+2. For GCCH Enrollment Profile, select **1. No Office**.
+3. For Shopfloor PC Type, select **5. Display**.
+4. For Display Type, select **2. Dashboard**.
+
+## Step 4: Imaging
+
+1. Once GE Image Setup launches, click **Start**.
+2. From this point the process is mostly automated.
+3. Note the **Serial Number** from the screen and let Patrick know it's a Shopfloor Display.
diff --git a/playbook/FlatUnattendW10-shopfloor.xml b/playbook/FlatUnattendW10-shopfloor.xml
index df26d82..4de6e7a 100644
--- a/playbook/FlatUnattendW10-shopfloor.xml
+++ b/playbook/FlatUnattendW10-shopfloor.xml
@@ -87,6 +87,16 @@
reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /fBypass OOBE network requirement
+
+ 14
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipMachineOOBE /t REG_DWORD /d 1 /f
+ Skip machine OOBE phase
+
+
+ 15
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipUserOOBE /t REG_DWORD /d 1 /f
+ Skip user OOBE phase
+
@@ -151,11 +161,16 @@
5
+ powershell.exe -ExecutionPolicy Bypass -Command "Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Disable-NetAdapter -Confirm:$false; while (-not (Test-Connection -ComputerName login.microsoftonline.us -Count 1 -Quiet -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }; Write-Host 'Internet confirmed over WiFi.'"
+ Disable wired adapters and wait for WiFi internet
+
+
+ 6powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"Run GCCH Enrollment
-
- 6
+
+ 7powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"Run shopfloor PC type setup
diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml
index ab5b5c3..89001d6 100644
--- a/playbook/pxe_server_setup.yml
+++ b/playbook/pxe_server_setup.yml
@@ -37,11 +37,12 @@
- gea-standard
- gea-engineer
- gea-shopfloor
- - gea-shopfloor-mce
- ge-standard
- ge-engineer
- ge-shopfloor-lockdown
- ge-shopfloor-mce
+ shopfloor_types:
+ - gea-shopfloor
deploy_subdirs:
- Applications
- Control
@@ -298,6 +299,36 @@
state: directory
mode: '0777'
+ - name: "Create enrollment packages directory"
+ file:
+ path: /srv/samba/enrollment
+ state: directory
+ mode: '0777'
+
+ - name: "Deploy shopfloor setup scripts to enrollment share"
+ copy:
+ src: "{{ usb_mount }}/shopfloor-setup/"
+ dest: /srv/samba/enrollment/shopfloor-setup/
+ mode: '0755'
+ directory_mode: '0755'
+ ignore_errors: yes
+
+ - name: "Create BIOS update directory on enrollment share"
+ file:
+ path: /srv/samba/enrollment/BIOS
+ state: directory
+ mode: '0755'
+
+ - name: "Deploy BIOS check script and manifest"
+ copy:
+ src: "{{ usb_mount }}/shopfloor-setup/BIOS/{{ item }}"
+ dest: /srv/samba/enrollment/BIOS/{{ item }}
+ mode: '0644'
+ loop:
+ - check-bios.cmd
+ - models.txt
+ ignore_errors: yes
+
- name: "Create image upload staging directory"
file:
path: /home/pxe/image-upload
@@ -348,6 +379,15 @@
force user = root
comment = Blancco Drive Eraser reports
+ [enrollment]
+ path = /srv/samba/enrollment
+ browseable = yes
+ read only = no
+ guest ok = no
+ valid users = pxe-upload
+ force user = root
+ comment = GCCH bulk enrollment packages
+
[image-upload]
path = /home/pxe/image-upload
browseable = yes
@@ -357,6 +397,9 @@
force user = pxe
force group = pxe
comment = PXE image upload staging area
+ oplocks = no
+ level2 oplocks = no
+ strict sync = yes
- name: "Create Samba users (pxe-upload and blancco)"
shell: |
@@ -392,6 +435,15 @@
force: no
loop: "{{ image_types }}"
+ - name: "Deploy shopfloor unattend.xml template"
+ copy:
+ src: "{{ usb_mount }}/FlatUnattendW10-shopfloor.xml"
+ dest: "{{ samba_share }}/{{ item }}/Deploy/FlatUnattendW10.xml"
+ mode: '0644'
+ force: no
+ loop: "{{ shopfloor_types }}"
+ ignore_errors: yes
+
- name: "Daily cron to create/refresh Media.tag for all images"
copy:
content: |
@@ -635,6 +687,7 @@
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
Environment=WEB_ROOT={{ web_root }}
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
+ Environment=ENROLLMENT_SHARE=/srv/samba/enrollment
Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
ExecStart=/usr/bin/python3 app.py
Restart=always
diff --git a/playbook/shopfloor-setup/BIOS/check-bios.cmd b/playbook/shopfloor-setup/BIOS/check-bios.cmd
new file mode 100644
index 0000000..d745d68
--- /dev/null
+++ b/playbook/shopfloor-setup/BIOS/check-bios.cmd
@@ -0,0 +1,158 @@
+@echo off
+REM check-bios.cmd - Check and apply Dell BIOS update from WinPE x64
+REM Called from startnet.cmd before imaging menu
+REM Requires: Flash64W.exe (Dell 64-Bit BIOS Flash Utility) in same directory
+REM
+REM Exit behavior:
+REM - BIOS update applied -> reboots automatically (flashes during POST)
+REM - Already up to date -> returns to startnet.cmd
+REM - No match / no files -> returns to startnet.cmd
+
+set BIOSDIR=%~dp0
+set FLASH=%BIOSDIR%Flash64W.exe
+set MANIFEST=%BIOSDIR%models.txt
+
+if not exist "%FLASH%" (
+ echo Flash64W.exe not found, skipping BIOS check.
+ exit /b 0
+)
+
+if not exist "%MANIFEST%" (
+ echo models.txt not found, skipping BIOS check.
+ exit /b 0
+)
+
+REM --- Get system model from WMI ---
+set SYSMODEL=
+for /f "skip=1 tokens=*" %%M in ('wmic csproduct get name 2^>NUL') do (
+ if not defined SYSMODEL set "SYSMODEL=%%M"
+)
+REM Trim trailing whitespace
+for /f "tokens=*" %%a in ("%SYSMODEL%") do set "SYSMODEL=%%a"
+
+if "%SYSMODEL%"=="" (
+ echo Could not detect system model, skipping BIOS check.
+ exit /b 0
+)
+
+echo Model: %SYSMODEL%
+
+REM --- Get current BIOS version ---
+set BIOSVER=
+for /f "skip=1 tokens=*" %%V in ('wmic bios get smbiosbiosversion 2^>NUL') do (
+ if not defined BIOSVER set "BIOSVER=%%V"
+)
+for /f "tokens=*" %%a in ("%BIOSVER%") do set "BIOSVER=%%a"
+echo Current BIOS: %BIOSVER%
+
+REM --- Read manifest and find matching BIOS file ---
+REM Format: ModelSubstring|BIOSFile|Version
+set BIOSFILE=
+set TARGETVER=
+for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
+ echo "%SYSMODEL%" | find /I "%%A" >NUL
+ if not errorlevel 1 (
+ set "BIOSFILE=%%B"
+ set "TARGETVER=%%C"
+ goto :found_bios
+ )
+)
+
+echo No BIOS update available for this model.
+exit /b 0
+
+:found_bios
+if not exist "%BIOSDIR%%BIOSFILE%" (
+ echo WARNING: %BIOSFILE% not found in BIOS folder.
+ exit /b 0
+)
+
+REM --- Skip if already at target version ---
+echo.%BIOSVER%| find /I "%TARGETVER%" >NUL
+if not errorlevel 1 goto :already_current
+
+REM --- Compare versions to prevent downgrade ---
+REM Split current and target into major.minor.patch and compare numerically
+call :compare_versions "%BIOSVER%" "%TARGETVER%"
+if "%VERCMP%"=="newer" goto :already_newer
+goto :do_flash
+
+:already_current
+echo BIOS is already up to date - %TARGETVER%
+exit /b 0
+
+:already_newer
+echo Current BIOS %BIOSVER% is newer than target %TARGETVER% - skipping.
+exit /b 0
+
+:do_flash
+
+echo Update: %BIOSVER% -^> %TARGETVER%
+echo Applying BIOS update (this may take a few minutes, do not power off)...
+
+REM --- Run Flash64W.exe from BIOS directory to avoid UNC path issues ---
+REM Exit codes: 0=success, 2=reboot needed, 3=already current, 6=reboot needed
+pushd "%BIOSDIR%"
+Flash64W.exe /b="%BIOSFILE%" /s /f /l=X:\bios-update.log
+set FLASHRC=%ERRORLEVEL%
+popd
+echo Flash complete (exit code %FLASHRC%).
+
+if "%FLASHRC%"=="3" (
+ echo BIOS is already up to date.
+ exit /b 0
+)
+
+if "%FLASHRC%"=="0" (
+ echo BIOS update complete.
+ exit /b 0
+)
+
+if "%FLASHRC%"=="2" goto :staged
+if "%FLASHRC%"=="6" goto :staged
+
+echo WARNING: Flash64W.exe returned unexpected code %FLASHRC%.
+echo Check X:\bios-update.log for details.
+exit /b 0
+
+:staged
+echo.
+echo ========================================
+echo BIOS update staged successfully.
+echo It will flash during POST after the
+echo post-imaging reboot.
+echo ========================================
+echo.
+exit /b 0
+
+REM ============================================================
+REM compare_versions - Compare two dotted version strings
+REM Usage: call :compare_versions "current" "target"
+REM Sets VERCMP=newer if current > target, older if current < target, equal if same
+REM ============================================================
+:compare_versions
+set "VERCMP=equal"
+set "_CV=%~1"
+set "_TV=%~2"
+
+REM Parse current version parts
+for /f "tokens=1,2,3 delims=." %%a in ("%_CV%") do (
+ set /a "C1=%%a" 2>NUL
+ set /a "C2=%%b" 2>NUL
+ set /a "C3=%%c" 2>NUL
+)
+REM Parse target version parts
+for /f "tokens=1,2,3 delims=." %%a in ("%_TV%") do (
+ set /a "T1=%%a" 2>NUL
+ set /a "T2=%%b" 2>NUL
+ set /a "T3=%%c" 2>NUL
+)
+
+if %C1% GTR %T1% ( set "VERCMP=newer" & goto :eof )
+if %C1% LSS %T1% ( set "VERCMP=older" & goto :eof )
+if %C2% GTR %T2% ( set "VERCMP=newer" & goto :eof )
+if %C2% LSS %T2% ( set "VERCMP=older" & goto :eof )
+if %C3% GTR %T3% ( set "VERCMP=newer" & goto :eof )
+if %C3% LSS %T3% ( set "VERCMP=older" & goto :eof )
+set "VERCMP=equal"
+goto :eof
diff --git a/playbook/shopfloor-setup/CMM/01-Setup-CMM.ps1 b/playbook/shopfloor-setup/CMM/01-Setup-CMM.ps1
new file mode 100644
index 0000000..87d8820
--- /dev/null
+++ b/playbook/shopfloor-setup/CMM/01-Setup-CMM.ps1
@@ -0,0 +1,45 @@
+# 01-Setup-CMM.ps1 — CMM-specific setup (runs after Shopfloor baseline)
+# Installs Hexagon CLM Tools, PC-DMIS 2016, and PC-DMIS 2019 R2
+
+Write-Host "=== CMM Setup ==="
+
+$hexDir = "C:\Enrollment\shopfloor-setup\CMM\hexagon"
+
+if (-not (Test-Path $hexDir)) {
+ Write-Warning "Hexagon folder not found at $hexDir — skipping CMM installs."
+ exit 0
+}
+
+# --- Find installers ---
+$clm = Get-ChildItem -Path $hexDir -Filter "CLM_*.exe" | Select-Object -First 1
+$pcdmis16 = Get-ChildItem -Path $hexDir -Filter "Pcdmis2016*x64.exe" | Select-Object -First 1
+$pcdmis19 = Get-ChildItem -Path $hexDir -Filter "Pcdmis2019*x64.exe" | Select-Object -First 1
+
+# --- 1. Install CLM Tools (license manager — must be first) ---
+if ($clm) {
+ Write-Host "Installing CLM Tools: $($clm.Name)..."
+ $p = Start-Process -FilePath $clm.FullName -ArgumentList "-q -norestart" -Wait -PassThru
+ Write-Host " CLM Tools exit code: $($p.ExitCode)"
+} else {
+ Write-Warning "CLM Tools installer not found in $hexDir (expected CLM_*.exe)"
+}
+
+# --- 2. Install PC-DMIS 2016 ---
+if ($pcdmis16) {
+ Write-Host "Installing PC-DMIS 2016: $($pcdmis16.Name)..."
+ $p = Start-Process -FilePath $pcdmis16.FullName -ArgumentList "-q INSTALLPDFCONVERTER=0 INSTALLOFFLINEHELP=0 HEIP=0 -norestart" -Wait -PassThru
+ Write-Host " PC-DMIS 2016 exit code: $($p.ExitCode)"
+} else {
+ Write-Warning "PC-DMIS 2016 installer not found in $hexDir (expected Pcdmis2016*x64.exe)"
+}
+
+# --- 3. Install PC-DMIS 2019 R2 ---
+if ($pcdmis19) {
+ Write-Host "Installing PC-DMIS 2019 R2: $($pcdmis19.Name)..."
+ $p = Start-Process -FilePath $pcdmis19.FullName -ArgumentList "-q INSTALLPDFCONVERTER=0 INSTALLOFFLINEHELP=0 HEIP=0 -norestart" -Wait -PassThru
+ Write-Host " PC-DMIS 2019 exit code: $($p.ExitCode)"
+} else {
+ Write-Warning "PC-DMIS 2019 installer not found in $hexDir (expected Pcdmis2019*x64.exe)"
+}
+
+Write-Host "=== CMM Setup Complete ==="
diff --git a/playbook/shopfloor-setup/Check-Policies.bat b/playbook/shopfloor-setup/Check-Policies.bat
new file mode 100644
index 0000000..592150f
--- /dev/null
+++ b/playbook/shopfloor-setup/Check-Policies.bat
@@ -0,0 +1,13 @@
+@echo off
+title MDM Policy Check
+powershell.exe -ExecutionPolicy Bypass -Command ^
+ "$path = 'HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device\ADMX_Power';" ^
+ "Write-Host '';" ^
+ "if (Test-Path $path) {" ^
+ " Write-Host ' READY FOR LOCKDOWN ' -ForegroundColor White -BackgroundColor DarkGreen;" ^
+ "} else {" ^
+ " Write-Host ' NOT READY FOR LOCKDOWN ' -ForegroundColor White -BackgroundColor Red;" ^
+ "};" ^
+ "Write-Host '';" ^
+ "Write-Host 'Press any key to exit...';" ^
+ "$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
diff --git a/playbook/shopfloor-setup/Display/01-Setup-Display.ps1 b/playbook/shopfloor-setup/Display/01-Setup-Display.ps1
new file mode 100644
index 0000000..bcd393d
--- /dev/null
+++ b/playbook/shopfloor-setup/Display/01-Setup-Display.ps1
@@ -0,0 +1,45 @@
+# 01-Setup-Display.ps1 — Display-specific setup (runs after Shopfloor baseline)
+# Reads display-type.txt to install either LobbyDisplay or Dashboard kiosk app.
+
+$enrollDir = "C:\Enrollment"
+$typeFile = Join-Path $enrollDir "display-type.txt"
+$setupDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+
+if (-not (Test-Path $typeFile)) {
+ Write-Warning "No display-type.txt found - skipping display setup."
+ return
+}
+
+$displayType = (Get-Content $typeFile -First 1).Trim()
+Write-Host "=== Display Setup: $displayType ==="
+
+switch ($displayType) {
+ "Lobby" {
+ $installer = Join-Path $setupDir "GEAerospaceLobbyDisplaySetup.exe"
+ $appName = "Lobby Display"
+ }
+ "Dashboard" {
+ $installer = Join-Path $setupDir "GEAerospaceDashboardSetup.exe"
+ $appName = "Dashboard"
+ }
+ default {
+ Write-Warning "Unknown display type: $displayType"
+ return
+ }
+}
+
+if (-not (Test-Path $installer)) {
+ Write-Warning "$appName installer not found at $installer - skipping."
+ return
+}
+
+Write-Host "Installing $appName..."
+$proc = Start-Process -FilePath $installer -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', "/LOG=C:\Enrollment\$appName-install.log" -Wait -PassThru
+
+if ($proc.ExitCode -eq 0) {
+ Write-Host "$appName installed successfully."
+} else {
+ Write-Warning "$appName exited with code $($proc.ExitCode). Check C:\Enrollment\$appName-install.log"
+}
+
+Write-Host "=== Display Setup Complete ==="
diff --git a/playbook/shopfloor-setup/Genspect/01-Setup-Genspect.ps1 b/playbook/shopfloor-setup/Genspect/01-Setup-Genspect.ps1
new file mode 100644
index 0000000..d5a7010
--- /dev/null
+++ b/playbook/shopfloor-setup/Genspect/01-Setup-Genspect.ps1
@@ -0,0 +1,14 @@
+# 01-Setup-Genspect.ps1 — Genspect-specific setup (runs after Shopfloor baseline)
+
+Write-Host "=== Genspect Setup ==="
+
+# --- Add Genspect credentials ---
+# cmdkey /generic:genspect-server /user:domain\genspectuser /pass:password
+
+# --- Install Genspect applications ---
+# Start-Process msiexec.exe -ArgumentList '/i "C:\Enrollment\shopfloor-setup\Genspect\GenspectApp.msi" /qn' -Wait
+
+# --- Genspect configuration ---
+# Set-ItemProperty -Path "HKLM:\SOFTWARE\CompanyName" -Name "PCType" -Value "Genspect"
+
+Write-Host "=== Genspect Setup Complete ==="
diff --git a/playbook/shopfloor-setup/Keyence/01-Setup-Keyence.ps1 b/playbook/shopfloor-setup/Keyence/01-Setup-Keyence.ps1
new file mode 100644
index 0000000..1377245
--- /dev/null
+++ b/playbook/shopfloor-setup/Keyence/01-Setup-Keyence.ps1
@@ -0,0 +1,14 @@
+# 01-Setup-Keyence.ps1 — Keyence-specific setup (runs after Shopfloor baseline)
+
+Write-Host "=== Keyence Setup ==="
+
+# --- Add Keyence credentials ---
+# cmdkey /generic:keyence-server /user:domain\keyenceuser /pass:password
+
+# --- Install Keyence applications ---
+# Start-Process msiexec.exe -ArgumentList '/i "C:\Enrollment\shopfloor-setup\Keyence\KeyenceApp.msi" /qn' -Wait
+
+# --- Keyence configuration ---
+# Set-ItemProperty -Path "HKLM:\SOFTWARE\CompanyName" -Name "PCType" -Value "Keyence"
+
+Write-Host "=== Keyence Setup Complete ==="
diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1
index 151a624..7574579 100644
--- a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1
+++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1
@@ -4,6 +4,19 @@
# Cancel any pending reboot so it doesn't interrupt setup
shutdown -a 2>$null
+# Prompt user to unplug from PXE switch before re-enabling wired adapters
+Write-Host ""
+Write-Host "========================================" -ForegroundColor Yellow
+Write-Host " UNPLUG the ethernet cable from the" -ForegroundColor Yellow
+Write-Host " PXE imaging switch NOW." -ForegroundColor Yellow
+Write-Host "========================================" -ForegroundColor Yellow
+Write-Host ""
+Write-Host "Press any key to continue..." -ForegroundColor Yellow
+$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+
+# Re-enable wired adapters
+Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Enable-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue
+
$enrollDir = "C:\Enrollment"
$typeFile = Join-Path $enrollDir "pc-type.txt"
$setupDir = Join-Path $enrollDir "shopfloor-setup"
@@ -56,5 +69,17 @@ if ($pcType -ne "Shopfloor") {
}
Write-Host "Shopfloor setup complete for $pcType."
+
+# Copy backup lockdown script to SupportUser desktop
+$lockdownScript = Join-Path $setupDir "backup_lockdown.bat"
+if (Test-Path $lockdownScript) {
+ Copy-Item -Path $lockdownScript -Destination "C:\Users\SupportUser\Desktop\backup_lockdown.bat" -Force
+ Write-Host "backup_lockdown.bat copied to desktop."
+}
+
+# Set auto-logon to expire after 2 more logins
+reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 2 /f | Out-Null
+Write-Host "Auto-logon set to 2 remaining logins."
+
Write-Host "Rebooting in 10 seconds..."
shutdown /r /t 10
diff --git a/playbook/shopfloor-setup/Shopfloor/02-OpenTextCSF.ps1 b/playbook/shopfloor-setup/Shopfloor/02-OpenTextCSF.ps1
new file mode 100644
index 0000000..daf5bb6
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/02-OpenTextCSF.ps1
@@ -0,0 +1,51 @@
+# 02-OpenTextCSF.ps1 — Deploy OpenText HostExplorer CSF profiles (baseline)
+# Copies connection profiles, keymaps, menus, and macros to ProgramData.
+
+$setupDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$csfSource = Join-Path $setupDir "csf"
+$destRoot = "C:\ProgramData\Hummingbird\Connectivity\15.00\Shared"
+
+if (-not (Test-Path $csfSource)) {
+ Write-Warning "CSF source folder not found at $csfSource - skipping."
+ return
+}
+
+Write-Host "Deploying OpenText CSF profiles to $destRoot ..."
+
+# Map of source subdirectories to destination subdirectories
+$folders = @(
+ @{ Src = "Profile"; Dest = "Profile" }
+ @{ Src = "Accessories\EB"; Dest = "Accessories\EB" }
+ @{ Src = "HostExplorer\Keymap"; Dest = "HostExplorer\Keymap" }
+ @{ Src = "HostExplorer\Menu"; Dest = "HostExplorer\Menu" }
+)
+
+foreach ($folder in $folders) {
+ $src = Join-Path $csfSource $folder.Src
+ $dest = Join-Path $destRoot $folder.Dest
+
+ if (-not (Test-Path $src)) {
+ Write-Host " Skipping $($folder.Src) (not present in csf source)"
+ continue
+ }
+
+ if (-not (Test-Path $dest)) {
+ New-Item -Path $dest -ItemType Directory -Force | Out-Null
+ Write-Host " Created $dest"
+ }
+
+ $files = Get-ChildItem -Path $src -File
+ foreach ($file in $files) {
+ Copy-Item -Path $file.FullName -Destination $dest -Force
+ Write-Host " Copied $($file.Name) -> $dest"
+ }
+}
+
+# Copy pre-made .lnk shortcuts to Public Desktop
+$lnkFiles = Get-ChildItem -Path $csfSource -Filter "*.lnk" -File
+foreach ($lnk in $lnkFiles) {
+ Copy-Item -Path $lnk.FullName -Destination "C:\Users\Public\Desktop" -Force
+ Write-Host " Copied $($lnk.Name) -> Public Desktop"
+}
+
+Write-Host "OpenText CSF deployment complete."
diff --git a/playbook/shopfloor-setup/Shopfloor/03-StartMenu.ps1 b/playbook/shopfloor-setup/Shopfloor/03-StartMenu.ps1
new file mode 100644
index 0000000..5ad476f
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/03-StartMenu.ps1
@@ -0,0 +1,46 @@
+# 03-StartMenu.ps1 — Create Start Menu shortcuts for all users (baseline)
+# Shortcuts in ProgramData\Microsoft\Windows\Start Menu\Programs\ persist for all accounts.
+
+$startMenu = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"
+$shell = New-Object -ComObject WScript.Shell
+
+# --- Defect Tracker ---
+$lnk = $shell.CreateShortcut("$startMenu\Defect Tracker.lnk")
+$lnk.TargetPath = "S:\DT\Defect_Tracker\Defect_Tracker.application"
+$lnk.Save()
+Write-Host "Created Start Menu shortcut: Defect Tracker"
+
+# --- Plant Applications (Edge) ---
+$lnk = $shell.CreateShortcut("$startMenu\Plant Applications.lnk")
+$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+$lnk.Arguments = "https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications"
+$lnk.Save()
+Write-Host "Created Start Menu shortcut: Plant Applications"
+
+# --- ShopDB ---
+$lnk = $shell.CreateShortcut("$startMenu\ShopDB.lnk")
+$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+$lnk.Arguments = "http://tsgwp00524.logon.ds.ge.com"
+$lnk.Save()
+Write-Host "Created Start Menu shortcut: ShopDB"
+
+# --- Shopfloor Dashboard ---
+$lnk = $shell.CreateShortcut("$startMenu\Shopfloor Dashboard.lnk")
+$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+$lnk.Arguments = "https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/"
+$lnk.Save()
+Write-Host "Created Start Menu shortcut: Shopfloor Dashboard"
+
+# --- ShopDB (GEA) ---
+$lnk = $shell.CreateShortcut("$startMenu\ShopDB (GEA).lnk")
+$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+$lnk.Arguments = "https://tsgwp00525.wjs.geaerospace.net/shopdb/"
+$lnk.Save()
+Write-Host "Created Start Menu shortcut: ShopDB (GEA)"
+
+# --- Add more shortcuts below ---
+# $lnk = $shell.CreateShortcut("$startMenu\AppName.lnk")
+# $lnk.TargetPath = "C:\Path\To\App.exe"
+# $lnk.Save()
+
+Write-Host "Start Menu shortcuts complete."
diff --git a/playbook/shopfloor-setup/Shopfloor/04-NetworkAndWinRM.ps1 b/playbook/shopfloor-setup/Shopfloor/04-NetworkAndWinRM.ps1
new file mode 100644
index 0000000..3f8de95
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/04-NetworkAndWinRM.ps1
@@ -0,0 +1,9 @@
+# 04-NetworkAndWinRM.ps1 — Set network profiles to Private and enable WinRM (baseline)
+
+# --- Set all network profiles to Private ---
+Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
+Write-Host "All network profiles set to Private."
+
+# --- Enable and configure WinRM ---
+Enable-PSRemoting -Force -SkipNetworkProfileCheck
+Write-Host "WinRM enabled."
diff --git a/playbook/shopfloor-setup/Shopfloor/05-PowerAndDisplay.ps1 b/playbook/shopfloor-setup/Shopfloor/05-PowerAndDisplay.ps1
new file mode 100644
index 0000000..0829b7f
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/05-PowerAndDisplay.ps1
@@ -0,0 +1,24 @@
+# 05-PowerAndDisplay.ps1 — Shopfloor power plan and display settings (baseline)
+
+# --- Set display timeout to Never (0 = never) on AC and DC ---
+powercfg /change monitor-timeout-ac 0
+powercfg /change monitor-timeout-dc 0
+powercfg /change standby-timeout-ac 0
+powercfg /change standby-timeout-dc 0
+Write-Host "Power: display and standby set to Never."
+
+# --- Set High Performance power plan ---
+$highPerf = powercfg /list | Select-String "High performance"
+if ($highPerf -match "([a-f0-9-]{36})") {
+ powercfg /setactive $Matches[1]
+ Write-Host "Power plan set to High Performance."
+} else {
+ Write-Host "High Performance plan not found, using current plan."
+}
+
+# --- Disable lock screen timeout (screen saver) ---
+Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name ScreenSaveActive -Value "0" -Force
+Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name ScreenSaveTimeOut -Value "0" -Force
+Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "SCRNSAVE.EXE" -Value "" -Force
+Write-Host "Screen saver set to None and disabled."
+
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/Office.ebs b/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/Office.ebs
new file mode 100755
index 0000000..fc7a516
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/Office.ebs
@@ -0,0 +1,51 @@
+'----------------------------------------------------------------------
+' This macro was created by the macro recorder.
+' Macro File: Office.ebs
+' Date: Wed May 18 09:55:44 2016
+' Recorded for profile: WJ_Office
+'----------------------------------------------------------------------
+
+Sub Main
+ Dim HostExplorer as Object
+ Dim MyHost as Object
+ Dim Rc as Integer
+
+ Dim iPSUpdateTimeout
+ Dim iWaitForStringTimeout
+
+ On Error goto GenericErrorHandler
+
+ Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
+ Set MyHost = HostExplorer.HostFromProfile("WJ_Office") ' Set object for the desired session
+ If MyHost is Nothing Then Goto NoSession
+
+ iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
+ iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
+
+ Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
+ If Rc = 0 Then Goto OnWaitForStringTimeout
+ Rc = MyHost.Keys("shop_pc^M")
+ Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
+ If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
+ Rc = MyHost.WaitForString( "Do you have a barcode reader?", -1, iWaitForStringTimeout, TRUE )
+ If Rc = 0 Then Goto OnWaitForStringTimeout
+ Exit Sub
+
+'-------------------- Runtime Error Handlers --------------------
+GenericErrorHandler:
+ Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
+ Exit Sub
+
+NoSession:
+ Msgbox "Profile ""WJ_Office"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
+ Exit Sub
+
+OnWaitPSUpdatedTimeout:
+ Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
+ Exit Sub
+
+OnWaitForStringTimeout:
+ Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
+ Exit Sub
+
+End Sub
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/mmcs.ebs b/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/mmcs.ebs
new file mode 100755
index 0000000..e6cb9a6
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/mmcs.ebs
@@ -0,0 +1,35 @@
+'----------------------------------------------------------------------
+' This macro was created by the macro recorder.
+' Macro File: mmcs.ebs
+' Date: Tue Jun 04 08:56:51 2013
+' Recorded for profile: mmcs
+'----------------------------------------------------------------------
+
+Sub Main
+ Dim HostExplorer as Object
+ Dim MyHost as Object
+ Dim iIdleTime
+
+ Dim iPSUpdateTime
+
+ On Error goto ErrorCheck
+
+ Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
+ Set MyHost = HostExplorer.HostFromProfile("mmcs") ' Set object for the desired session
+
+ iPSUpdateTime = 60 ' PS Update wait time set to 60 seconds
+ iIdleTime = 2000 ' Idle time set to 2000 milliseconds
+
+ MyHost.WaitPSUpdated(iPSUpdateTime)
+ Rc = MyHost.WaitForString("Username:", -1, 9999, TRUE)
+ MyHost.Keys("mmcswj^M")
+ MyHost.WaitPSUpdated(iPSUpdateTime)
+ Rc = MyHost.WaitForString("| Badge : |", -1, 9999, TRUE)
+ Exit Sub
+
+ErrorCheck:
+ if (Err = 440) Then
+ Msgbox "The specified session is not running.", 16, "Hummingbird Macro Error"
+ End If
+ Exit Sub
+End Sub
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/shopfloor.ebs b/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/shopfloor.ebs
new file mode 100755
index 0000000..796b971
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/shopfloor.ebs
@@ -0,0 +1,56 @@
+'----------------------------------------------------------------------
+' This macro was created by the macro recorder.
+' Macro File: shopfloor.ebs
+' Date: Wed May 18 09:57:00 2016
+' Recorded for profile: WJ Shopfloor
+'----------------------------------------------------------------------
+
+Sub Main
+ Dim HostExplorer as Object
+ Dim MyHost as Object
+ Dim Rc as Integer
+
+ Dim iPSUpdateTimeout
+ Dim iWaitForStringTimeout
+
+ On Error goto GenericErrorHandler
+
+ Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
+ Set MyHost = HostExplorer.HostFromProfile("WJ Shopfloor") ' Set object for the desired session
+ If MyHost is Nothing Then Goto NoSession
+
+ iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
+ iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
+
+ Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
+ If Rc = 0 Then Goto OnWaitForStringTimeout
+ Rc = MyHost.Keys("shop_xmi^M")
+ Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
+ If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
+ Rc = MyHost.WaitForString( "Password:", -1, iWaitForStringTimeout, TRUE )
+ If Rc = 0 Then Goto OnWaitForStringTimeout
+ Rc = MyHost.Keys("dnc123^M")
+ Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
+ If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
+ Rc = MyHost.WaitForString( "| EXIT | |", -1, iWaitForStringTimeout, TRUE )
+ If Rc = 0 Then Goto OnWaitForStringTimeout
+ Exit Sub
+
+'-------------------- Runtime Error Handlers --------------------
+GenericErrorHandler:
+ Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
+ Exit Sub
+
+NoSession:
+ Msgbox "Profile ""WJ Shopfloor"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
+ Exit Sub
+
+OnWaitPSUpdatedTimeout:
+ Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
+ Exit Sub
+
+OnWaitForStringTimeout:
+ Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
+ Exit Sub
+
+End Sub
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Default.kmv b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Default.kmv
new file mode 100755
index 0000000..11b0978
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Default.kmv
@@ -0,0 +1,9 @@
+[KEYMAP]
+Normal Entries=1
+Normal0=Backspace,Delete
+Shift Entries=0
+Ctrl Entries=0
+ShiftCtrl Entries=0
+Alt Entries=0
+ShiftAlt Entries=0
+AltCtrl Entries=0
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Office.kmv b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Office.kmv
new file mode 100755
index 0000000..3dbfc48
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Office.kmv
@@ -0,0 +1,13 @@
+[KEYMAP]
+Normal Entries=5
+Normal0=F4,Pf4
+Normal1=Subtract,Num-Pad-Minus
+Normal2=Multiply,Num-Pad-*
+Normal3=Divide,Num-Pad-/
+Normal4=Backspace,Delete
+Shift Entries=0
+Ctrl Entries=0
+ShiftCtrl Entries=0
+Alt Entries=0
+ShiftAlt Entries=0
+AltCtrl Entries=0
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default Shop.hmv b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default Shop.hmv
new file mode 100755
index 0000000..9a5d572
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default Shop.hmv
@@ -0,0 +1,23 @@
+[Version]
+AssemblyVersion=1
+[Console]
+Title=HostExplorer Menu Editor
+IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
+IconID=IDI_CSS_CONSOLE
+[Attributes]
+SupportAssemblyEditing=1
+SupportSeparator=1
+ParentLevels=100
+[Parent Node]
+name=Menu
+[SubParent Node]
+name=Submenu
+[Child Node]
+name=Menu Option
+[Messages]
+Create New Parent=Create New Menu
+Create New SubParent=Create New Submenu
+Insert New Parent=Insert New Menu
+Insert New SubParent=Insert New Submenu
+[Root Group]
+Name=Menu
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default VT.hmv b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default VT.hmv
new file mode 100755
index 0000000..9c43eca
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default VT.hmv
@@ -0,0 +1,474 @@
+[version]
+AssemblyVersion=1
+
+[nls]
+filename=menunls$LANGUAGE_EXTENSION$.hma
+
+[Console]
+Title=HostExplorer Menu Editor
+IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
+IconID=IDI_CSS_CONSOLE
+
+[Attributes]
+SupportAssemblyEditing=1
+SupportEnablingDisablingItems=0
+SupportSeparator=1
+
+[Parent Node]
+Nameid=Parent
+
+[SubParent Node]
+Nameid=SubParent
+
+[Child Node]
+Nameid=Child
+
+[Messages]
+Nameid1=Create New Parent
+Nameid2=Create New SubParent
+Nameid3=Insert New Parent
+Nameid4=Insert New SubParent
+
+[root group]
+Name=Menu
+item1=File
+item2=Edit
+item3=Transfer
+item4=Fonts
+item5=options
+item6=tools
+item7=view
+item8=window
+item9=help
+
+[Separator]
+name=
+id=0
+
+[File]
+Nameid=File
+id=4071
+Parent=1
+item1=new session
+item2=duplicate session
+item3=open session
+item4=open session in same window
+item5=save session profile
+item6=close session
+item7=separator
+item8=open layout
+item9=save layout
+item10=separator
+item11=connect
+item12=disconnect
+item13=separator
+item14=print screen
+item15=print multiple screens
+item16=save screen
+item17=send screen
+item18=separator
+item19=screen capture
+item20=separator
+item21=Recent Sessions Submenu
+item22=separator
+item23=exit all
+
+[New Session]
+nameid=New Session
+id=Dlg-New-Session
+
+[Duplicate Session]
+nameid=Duplicate Session
+id=Duplicate-Session
+
+[Open Session]
+nameid=Open Session
+id=Dlg-Open-Session
+
+[Open Session In Same Window]
+nameid=Open Session In Same Window
+id=Dlg-Open-Session-In-Same-Window
+
+[Save Session Profile]
+nameid=Save Session Profile
+id=Dlg-Save-Profile
+
+[Close Session]
+nameid=Close Session
+id=Dlg-Close-Session
+
+[open layout]
+nameid=Open Layout
+id=Dlg-Open-Layout
+
+[save layout]
+nameid=Save Layout
+id=Dlg-Save-Layout
+
+[connect]
+nameid=connect
+id=Connect
+
+[disconnect]
+nameid=disconnect
+id=Disconnect
+
+[print screen]
+nameid=print screen
+id=Dlg-Print-Screen
+
+[print multiple screens]
+nameid=print multiple screens
+id=Dlg-Print-Multiple-Screens
+
+
+[save screen]
+nameid=Save Screen
+id=Dlg-Save-Screen
+
+[send screen]
+nameid=Send Screen
+id=Send-Screen
+
+[screen capture]
+nameid=Screen Capture
+id=Toggle-Capture
+
+[Recent Sessions Submenu]
+nameid=Recent Sessions
+id=4187
+Parent=1
+item1=recent sessions
+
+[recent sessions]
+nameid=Recent Sessions
+id=Recent-Sessions
+
+[Exit all]
+nameid=exit all
+id=Dlg-Exit
+
+
+[Edit]
+Nameid=Edit
+id=4072
+Parent=1
+item1=copy
+item2=copy append
+item3=paste
+item4=separator
+item5=select all
+item6=separator
+item7=edit find
+item8=separator
+item9=clear display
+item10=clear all
+item11=soft terminal reset
+
+[undo]
+nameid=undo
+id=Edit-Undo
+
+[redo]
+nameid=redo
+id=Edit-Redo
+
+[cut]
+nameid=cut
+id=Edit-Cut
+
+[Copy]
+nameid=copy
+id=Edit-Copy
+
+[Copy append]
+nameid=Copy Append
+id=Edit-Copy-Append
+
+[Paste]
+nameid=Paste
+id=Edit-Paste
+
+[Paste continue]
+nameid=Paste Continue
+id=Edit-Paste-Continue
+
+[Select All]
+nameid=Select All
+id=Edit-SelectAll
+
+[Edit Options]
+nameid=Options
+id=Dlg-Options
+
+[edit find]
+nameid=Find
+id=Dlg-Find
+
+[clear display]
+nameid=Clear Display
+id=Clear-Display
+
+[clear all]
+nameid=Clear All
+id=Clear-Buffer
+
+[soft terminal reset]
+nameid=Soft Terminal Reset
+id=Power-On-Reset
+
+
+[Transfer]
+Nameid=Transfer
+id=4073
+Parent=1
+item1=send
+item2=receive
+
+[send]
+nameid=Send File to Host
+id=Dlg-Upload
+
+[receive]
+nameid=Receive File from Host
+id=Dlg-Download
+
+
+[Fonts]
+Nameid=Fonts
+id=4074
+Parent=1
+item1=next larger
+item2=next smaller
+item3=choose font
+item4=maximize font
+
+[next larger]
+nameid=next larger font
+id=Font-Larger
+
+[next smaller]
+nameid=Next smaller font
+id=Font-Smaller
+
+[choose font]
+nameid=font
+id=Dlg-Font-Select
+
+[maximize font]
+nameid=Maximize font
+id=Maximize-Font
+
+
+[options]
+nameid=Options
+id=4075
+Parent=1
+item1=global options
+item2=api settings
+item3=separator
+item4=keyboard mapping
+item5=quick keys
+item6=separator
+item7=session properties
+
+[global options]
+nameid=Global Options
+id=Dlg-Global
+
+[api settings]
+nameid=API Settings
+id=Dlg-API-Settings
+
+[keyboard mapping]
+nameid=Keyboard Mapping
+id=Dlg-Keyboard-Mapper
+
+[quick keys]
+nameid=Quick Keys
+id=Dlg-Quick-Key-Editor
+
+[session properties]
+nameid=session Properties
+id=Dlg-Edit-Session-Profile
+
+
+[tools]
+nameid=Tools
+id=4076
+Parent=1
+item1=Macro Submenu
+item2=Quick Script Submenu
+item3=separator
+item4=Customizetoolbar
+item5=Customizemenu
+item6=Customizesessionproperties
+
+[Macro Submenu]
+nameid=Macro
+id=4177
+Parent=1
+item1=edit macro
+item2=run macro
+item3=separator
+item4=start recording
+item5=pause recording
+item6=resume recording
+item7=stop recording
+item8=cancel recording
+
+[Edit macro]
+nameid=Macro Edit
+id=Dlg-Edit-Macro
+
+[run macro]
+nameid=run
+id=Dlg-Run-Macro
+
+[start recording]
+nameid=Start Recording
+id=Record-Macro
+
+[pause recording]
+nameid=Pause Recording
+id=Toggle-Recording-Pause
+
+[resume recording]
+nameid=Resume Recording
+id=Resume-Recording-Macro
+
+[stop recording]
+nameid=Stop Recording
+id=End-Recording
+
+[cancel recording]
+nameid=Cancel Recording
+id=Cancel-Macro-Recording
+
+[Quick Script Submenu]
+nameid=Quick Script
+id=4091
+Parent=1
+item1=edit qs
+item2=run qs
+item3=stop qs
+item4=separator
+item5=start recording qs
+item6=pause recording qs
+item7=resume recording qs
+item8=stop recording qs
+item9=cancel recording qs
+
+[Customizetoolbar]
+nameid=Customize Toolbar
+id=Dlg-Toolbars
+
+[Customizemenu]
+nameid=Customize Menu
+id=Dlg-Customize-Menus
+
+[CustomizeSessionProperties]
+nameid=Customize Session Properties
+id=Dlg-Customize-Session-Properties
+
+[Edit qs]
+nameid=Edit
+id=Dlg-QuickScript-Editor
+
+[run qs]
+nameid=Run
+id=Dlg-Run-QuickScript
+
+[stop qs]
+nameid=Stop
+id=Stop-QuickScript
+
+[start recording qs]
+nameid=Start Recording
+id=Record-QuickScript
+
+[pause recording qs]
+nameid=Pause Recording
+id=Pause-Recording-QuickScript
+
+[resume recording qs]
+nameid=Resume Recording
+id=Resume-Recording-QuickScript
+
+[stop recording qs]
+nameid=Stop Recording
+id=Stop-Recording-QuickScript
+
+[cancel recording qs]
+nameid=Cancel Recording
+id=Cancel-Recording-QuickScript
+
+
+[view]
+nameid=view
+id=4178
+Parent=1
+item1=hotspots
+item2=row and column indicator
+item3=cross hair cursor
+item4=full screen
+
+[hotspots]
+nameid=Hotspots
+id=Toggle-View-Hotspots
+
+[Row and Column Indicator]
+nameid=Row and Column Indicator
+id=Toggle-Row-And-Column-Indicator
+
+[cross hair cursor]
+nameid=Cross-Hair Cursor
+id=Toggle-CrossHair-Cursor
+
+[full screen]
+nameid=Full Screen
+id=Toggle-Full-Screen
+
+
+[window]
+nameid=Window
+id=4179
+Parent=1
+item1=cascade
+item2=next session
+item3=previous session
+item4=separator
+item5=session window list
+
+[cascade]
+nameid=Cascade
+id=Cascade-Session-Windows
+
+[next session]
+nameid=next session
+id=Next-Session
+
+[previous session]
+nameid=previous session
+id=Prev-Session
+
+[session window list]
+name=
+id=4142
+
+
+[help]
+nameid=Help
+id=4180
+Parent=1
+item1=contents
+item2=separator
+item3=about
+
+[contents]
+nameid=Contents
+id=Help-Index
+
+[about]
+nameid=about
+id=Help-About
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/IBM_qks.lnk b/playbook/shopfloor-setup/Shopfloor/csf/IBM_qks.lnk
new file mode 100755
index 0000000..973732c
Binary files /dev/null and b/playbook/shopfloor-setup/Shopfloor/csf/IBM_qks.lnk differ
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Profile/IBM_qks.hep b/playbook/shopfloor-setup/Shopfloor/csf/Profile/IBM_qks.hep
new file mode 100755
index 0000000..68fc23a
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Profile/IBM_qks.hep
@@ -0,0 +1,106 @@
+[PROFILE]
+Session Properties=Default 3270
+Menu=Default 3270
+Auto Run Macro Delay Time=1
+Gateway Name=ibmqks.ae.ge.com
+Session Keep Alive Interval=600
+PC Keyboard Type=5
+IND$FILE Parms=0,0,1,1,0,0,0,257,253,2048,200,430,430,0,1252,"IND$FILE","","","",0
+SSL/TLS Current User Certificate=1
+Window Title=%s - %p (%h)
+WinInfo Default Restored=0 0 0 0 70 405 822 595 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
+WinInfo Default Maximized=0 0 0 0 -8 -8 1936 1048 0 0 0=26 12 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
+Print Header2=Page: %# Document Name: %F
+QuickKey=Default
+Never Run Profile=Off
+Profile File Version=200
+Space Allocation Parms=0,0,0,0
+Kerberos Use Hummingbird Kerberos=On
+Cryptographic Mode=1
+IP Port=992
+Security Option=2
+SSL/TLS User Cipher Suites=DEFAULT
+[HEBar-59440]
+sizeFloatCX=505
+sizeFloatCY=28
+[Toolbars Layout-Summary]
+Bars=3
+ScreenCX=1920
+ScreenCY=1080
+[Session Options]
+Last Selected Page=139
+[Toolbars]
+ToolBarVersion=6
+bar0=Default 3270 ToolBar.tb3
+[Toolbars Layout-Bar0]
+BarID=59393
+[Toolbars Layout-Bar1]
+BarID=59419
+Bars=3
+Bar#0=0
+Bar#1=59440
+Bar#2=0
+[Toolbars Layout-Bar2]
+BarID=59440
+XPos=-2
+YPos=-2
+Docking=1
+MRUDockID=59419
+MRUDockLeftPos=-2
+MRUDockTopPos=-2
+MRUDockRightPos=512
+MRUDockBottomPos=26
+MRUFloatStyle=8196
+MRUFloatXPos=-1
+MRUFloatYPos=1025
+[Hotspots]
+ShowTips=1
+[ShortcutReplacers]
+Size=0
+[Site Info]
+EnableCache=Yes
+RefreshCacheOnConnect=0
+AlwaysUploadUsing=0
+AlwaysDownloadUsing=0
+ProfileVersion=5
+SecurityType=0
+Port=21
+SSL Implicit Port=990
+Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+SFTPProxyAddress=localhost
+SFTPProxyPort=0
+SFTPWriteSize=256
+GssApiServiceName=host
+GssApiProtLevel=P
+GssApiIssueCCC=No
+GssApiPBSZ=16000
+GssApiKerbClient=0
+GssApiUseHMS2MIT=No
+PassiveMode=2
+AllowServer2ServerTransfer=0
+KeepAlive=No
+AutoDetectServerType=Yes
+KeepAliveTime=0
+FirewallPort=21
+FirewallType=0
+NetworkTimeout=120
+ConvertGMTTime=0
+ResolveLinks=1
+RootToInitialDir=0
+ChangeDirChangesRoot=0
+DropVmsVersion=0
+LocalHourOffset=0
+LocalMinuteOffset=0
+TemporaryProfile=0
+SSLVersion=3
+SSLReuse=0
+SSLUserCertificateMode=0
+SSLCloseOnNegotiateionFail=0
+SSLAcceptServerSelfSignedCertificates=Yes
+SSLAcceptServerUnverifiedCertificates=Yes
+SSLDataChannelProtected=Yes
+SSLCryptographicMode=1
+AllowResume=No
+EPSV4=0
+[Stats]
+NumVisits=0
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ Shopfloor.hep b/playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ Shopfloor.hep
new file mode 100755
index 0000000..d380778
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ Shopfloor.hep
@@ -0,0 +1,122 @@
+[PROFILE]
+Session Terminal Type=2
+Session Properties=Default VT
+Menu=Default VT
+Auto Run Macro Delay Time=1
+Gateway Name=wjfms3.apps.wlm.geaerospace.net
+Session Keep Alive Interval=600
+PC Keyboard Type=5
+Tabstops='8 16 24 32 40 48 56 64 72 '
+VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
+Default VT Recv Path=$MYDOCUMENTS$\
+SSL/TLS Version=3
+SSL/TLS Current User Certificate=1
+Window Title=%s - %p (%h)
+WinInfo Default Restored=0 0 0 0 176 21 892 619 0 0 0=-17 0 0 0 400 0 0 0 0 3 2 2 0 Courier New| 0 0
+WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
+Print Header2=Page: %# Document Name: %F
+QuickKey=Super Key F6
+Never Run Profile=Off
+dwVTFlags1=29369344
+VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
+Connect Timeout=60
+Disable Secure Connection Warning=Yes
+Profile File Version=200
+Cryptographic Mode=1
+Display RowCol Indicator=Off
+VT Scrollback Save Attribs=Off
+VT True Display Mode=1
+Status Line=1
+Kerberos Use Hummingbird Kerberos=On
+Keyboard=Office
+Auto Start Quick-Key=shopfloor.ebs
+On Disconnect=0
+Allow sleep while connected=0
+Toolbar Scheme Name=Default VT
+Save Profile on Window Close=Off
+Toolbar Scheme Modified=Default VT:30026745--1815298816
+Save Toolbar Info on Exit=Off
+Save Mode=0
+[Session Options]
+Last Selected Page=4000
+[HEBar-59440]
+[Toolbars Layout-Summary]
+Bars=3
+ScreenCX=1600
+ScreenCY=1200
+[Toolbars]
+ToolBarVersion=5
+bar0=Default VT ToolBar.tbv
+[Toolbars Layout-Bar0]
+BarID=59393
+Visible=0
+[Toolbars Layout-Bar1]
+BarID=59419
+Bars=3
+Bar#0=0
+Bar#1=59440
+Bar#2=0
+[Toolbars Layout-Bar2]
+BarID=59440
+XPos=-2
+YPos=-2
+Docking=1
+MRUDockID=59419
+MRUDockLeftPos=-2
+MRUDockTopPos=-2
+MRUDockRightPos=541
+MRUDockBottomPos=26
+MRUFloatStyle=8196
+MRUFloatXPos=-2147483648
+MRUFloatYPos=0
+[Hotspots]
+ShowTips=1
+[ShortcutReplacers]
+Size=0
+[Site Info]
+EnableCache=Yes
+RefreshCacheOnConnect=0
+AlwaysUploadUsing=0
+AlwaysDownloadUsing=0
+ProfileVersion=5
+SecurityType=0
+Port=21
+SSL Implicit Port=990
+Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+SFTPProxyAddress=localhost
+SFTPProxyPort=0
+SFTPWriteSize=256
+GssApiServiceName=host
+GssApiProtLevel=P
+GssApiIssueCCC=No
+GssApiPBSZ=16000
+GssApiKerbClient=0
+GssApiUseHMS2MIT=No
+PassiveMode=2
+AllowServer2ServerTransfer=0
+KeepAlive=No
+AutoDetectServerType=Yes
+KeepAliveTime=0
+FirewallPort=21
+FirewallType=0
+NetworkTimeout=120
+ConvertGMTTime=0
+ResolveLinks=1
+RootToInitialDir=0
+ChangeDirChangesRoot=0
+DropVmsVersion=0
+LocalHourOffset=0
+LocalMinuteOffset=0
+TemporaryProfile=0
+SSLVersion=3
+SSLReuse=0
+SSLUserCertificateMode=0
+SSLCloseOnNegotiateionFail=0
+SSLAcceptServerSelfSignedCertificates=Yes
+SSLAcceptServerUnverifiedCertificates=Yes
+SSLDataChannelProtected=Yes
+SSLCryptographicMode=1
+AllowResume=No
+EPSV4=0
+[Stats]
+NumVisits=0
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ_Office.hep b/playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ_Office.hep
new file mode 100755
index 0000000..79f4546
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ_Office.hep
@@ -0,0 +1,122 @@
+[PROFILE]
+Session Terminal Type=2
+Session Properties=Default VT
+Menu=Default VT
+Auto Run Macro Delay Time=1
+Gateway Name=wjfms3.apps.wlm.geaerospace.net
+Session Keep Alive Interval=600
+PC Keyboard Type=5
+Tabstops='8 16 24 32 40 48 56 64 72 '
+VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
+Default VT Recv Path=$MYDOCUMENTS$\
+SSL/TLS Version=3
+SSL/TLS Current User Certificate=1
+Window Title=%s - %p (%h)
+WinInfo Default Restored=0 0 0 0 22 22 892 619 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
+WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
+Print Header2=Page: %# Document Name: %F
+QuickKey=Super Key F6
+Never Run Profile=Off
+dwVTFlags1=29369344
+VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
+Connect Timeout=60
+Disable Secure Connection Warning=Yes
+Profile File Version=200
+Cryptographic Mode=1
+Display RowCol Indicator=Off
+VT Scrollback Save Attribs=Off
+VT True Display Mode=1
+Status Line=1
+Kerberos Use Hummingbird Kerberos=On
+Keyboard=Office
+Auto Start Quick-Key=Office.ebs
+On Disconnect=0
+Allow sleep while connected=0
+Toolbar Scheme Name=Default VT
+Save Profile on Window Close=Off
+Toolbar Scheme Modified=Default VT:30026745--1815298816
+Save Toolbar Info on Exit=Off
+Save Mode=0
+[Session Options]
+Last Selected Page=4000
+[HEBar-59440]
+[Toolbars Layout-Summary]
+Bars=3
+ScreenCX=1600
+ScreenCY=1200
+[Toolbars]
+ToolBarVersion=5
+bar0=Default VT ToolBar.tbv
+[Toolbars Layout-Bar0]
+BarID=59393
+Visible=0
+[Toolbars Layout-Bar1]
+BarID=59419
+Bars=3
+Bar#0=0
+Bar#1=59440
+Bar#2=0
+[Toolbars Layout-Bar2]
+BarID=59440
+XPos=-2
+YPos=-2
+Docking=1
+MRUDockID=59419
+MRUDockLeftPos=-2
+MRUDockTopPos=-2
+MRUDockRightPos=541
+MRUDockBottomPos=26
+MRUFloatStyle=8196
+MRUFloatXPos=-2147483648
+MRUFloatYPos=0
+[Hotspots]
+ShowTips=1
+[ShortcutReplacers]
+Size=0
+[Site Info]
+EnableCache=Yes
+RefreshCacheOnConnect=0
+AlwaysUploadUsing=0
+AlwaysDownloadUsing=0
+ProfileVersion=5
+SecurityType=0
+Port=21
+SSL Implicit Port=990
+Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+SFTPProxyAddress=localhost
+SFTPProxyPort=0
+SFTPWriteSize=256
+GssApiServiceName=host
+GssApiProtLevel=P
+GssApiIssueCCC=No
+GssApiPBSZ=16000
+GssApiKerbClient=0
+GssApiUseHMS2MIT=No
+PassiveMode=2
+AllowServer2ServerTransfer=0
+KeepAlive=No
+AutoDetectServerType=Yes
+KeepAliveTime=0
+FirewallPort=21
+FirewallType=0
+NetworkTimeout=120
+ConvertGMTTime=0
+ResolveLinks=1
+RootToInitialDir=0
+ChangeDirChangesRoot=0
+DropVmsVersion=0
+LocalHourOffset=0
+LocalMinuteOffset=0
+TemporaryProfile=0
+SSLVersion=3
+SSLReuse=0
+SSLUserCertificateMode=0
+SSLCloseOnNegotiateionFail=0
+SSLAcceptServerSelfSignedCertificates=Yes
+SSLAcceptServerUnverifiedCertificates=Yes
+SSLDataChannelProtected=Yes
+SSLCryptographicMode=1
+AllowResume=No
+EPSV4=0
+[Stats]
+NumVisits=0
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/Profile/mmcs.hep b/playbook/shopfloor-setup/Shopfloor/csf/Profile/mmcs.hep
new file mode 100755
index 0000000..d569cb6
--- /dev/null
+++ b/playbook/shopfloor-setup/Shopfloor/csf/Profile/mmcs.hep
@@ -0,0 +1,128 @@
+[PROFILE]
+Session Terminal Type=2
+Never Run Profile=Off
+Session Properties=Default VT
+Menu=Default VT
+Auto Run Macro Delay Time=1
+Gateway Name=wjfms3.apps.wlm.geaerospace.net
+Session Keep Alive Interval=600
+dwVTFlags1=29369344
+PC Keyboard Type=5
+Keyboard=ATM-VT420
+Tabstops='8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 136 144 152 160 168 176 184 192 200 208 216 224 232 240 248 256 264 272 280 288 296 '
+Default VT Recv Path=$MYDOCUMENTS$\
+SSL/TLS Version=3
+SSL/TLS Current User Certificate=1
+Window Title=%s - %p (%h)
+WinInfo Default Restored=0 0 0 0 10 10 757 491 0 0 0=16 9 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
+WinInfo Default Maximized=0 0 0 0 -8 -8 1382 744 0 0 0=26 13 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
+Window State=2
+Use Specific PRTSCR Printer=On
+PrtScr DevMode=1025,1024,156,1497,33565,1,256,2794,1240,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
+PrtScr Driver=winspool
+PrtScr Device=ZDesigner TLP 3842
+PrtScr Output=LPT1:
+Print Header2=Page: %# Document Name: %F
+Use Specific VT Printer=On
+VT DevMode=1025,1024,156,1497,323347,1,256,190,914,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
+VT Driver=winspool
+VT Device=ZDesigner TLP 3842
+VT Output=USB001
+Printer Target=1
+VT Printer Bypass Windows Print Driver=Off
+VT Printer Use Default Font=On
+VT Printer Margins Left=320
+VT Printer Margins Right=0
+VT Printer Margins Bottom=0
+VT Printer Margins Top=0
+QuickKey=ID
+Auto Start Quick-Key=mmcs.ebs
+VT Auto Wrap=On
+Profile File Version=200
+Kerberos Use Hummingbird Kerberos=On
+Cryptographic Mode=1
+Save Mode=0
+[HEBar-59440]
+sizeFloatCX=534
+sizeFloatCY=28
+[Toolbars Layout-Summary]
+Bars=3
+ScreenCX=1366
+ScreenCY=768
+[Session Options]
+Last Selected Page=4000
+[Toolbars]
+ToolBarVersion=5
+bar0=Default VT ToolBar.tbv
+[Toolbars Layout-Bar0]
+BarID=59393
+[Toolbars Layout-Bar1]
+BarID=59419
+Bars=3
+Bar#0=0
+Bar#1=59440
+Bar#2=0
+[Toolbars Layout-Bar2]
+BarID=59440
+XPos=-2
+YPos=-2
+Docking=1
+MRUDockID=59419
+MRUDockLeftPos=-2
+MRUDockTopPos=-2
+MRUDockRightPos=541
+MRUDockBottomPos=26
+MRUFloatStyle=8196
+MRUFloatXPos=-1
+MRUFloatYPos=0
+[Hotspots]
+ShowTips=1
+[ShortcutReplacers]
+Size=0
+[Site Info]
+EnableCache=Yes
+RefreshCacheOnConnect=0
+AlwaysUploadUsing=0
+AlwaysDownloadUsing=0
+ProfileVersion=5
+SecurityType=0
+Port=21
+SSL Implicit Port=990
+Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+SFTPProxyAddress=localhost
+SFTPProxyPort=0
+SFTPWriteSize=256
+GssApiServiceName=host
+GssApiProtLevel=P
+GssApiIssueCCC=No
+GssApiPBSZ=16000
+GssApiKerbClient=0
+GssApiUseHMS2MIT=No
+PassiveMode=2
+AllowServer2ServerTransfer=0
+KeepAlive=No
+AutoDetectServerType=Yes
+KeepAliveTime=0
+FirewallPort=21
+FirewallType=0
+NetworkTimeout=120
+ConvertGMTTime=0
+ResolveLinks=1
+RootToInitialDir=0
+ChangeDirChangesRoot=0
+DropVmsVersion=0
+LocalHourOffset=0
+LocalMinuteOffset=0
+TemporaryProfile=0
+SSLVersion=1
+SSLReuse=0
+SSLUserCertificateMode=0
+SSLCloseOnNegotiateionFail=0
+SSLAcceptServerSelfSignedCertificates=Yes
+SSLAcceptServerUnverifiedCertificates=Yes
+SSLDataChannelProtected=Yes
+SSLCryptographicMode=1
+AllowResume=No
+EPSV4=0
+[Stats]
+NumVisits=0
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/WJ Shopfloor.lnk b/playbook/shopfloor-setup/Shopfloor/csf/WJ Shopfloor.lnk
new file mode 100755
index 0000000..1469565
Binary files /dev/null and b/playbook/shopfloor-setup/Shopfloor/csf/WJ Shopfloor.lnk differ
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/WJ_Office.lnk b/playbook/shopfloor-setup/Shopfloor/csf/WJ_Office.lnk
new file mode 100755
index 0000000..c5103ea
Binary files /dev/null and b/playbook/shopfloor-setup/Shopfloor/csf/WJ_Office.lnk differ
diff --git a/playbook/shopfloor-setup/Shopfloor/csf/mmcs.lnk b/playbook/shopfloor-setup/Shopfloor/csf/mmcs.lnk
new file mode 100755
index 0000000..02046e8
Binary files /dev/null and b/playbook/shopfloor-setup/Shopfloor/csf/mmcs.lnk differ
diff --git a/playbook/shopfloor-setup/Standard/01-eDNC.ps1 b/playbook/shopfloor-setup/Standard/01-eDNC.ps1
new file mode 100644
index 0000000..58a94f6
--- /dev/null
+++ b/playbook/shopfloor-setup/Standard/01-eDNC.ps1
@@ -0,0 +1,84 @@
+# 01-eDNC.ps1 — Install eDNC and MarkZebra, deploy custom eMxInfo.txt (Standard)
+
+Write-Host "=== eDNC / MarkZebra Setup ==="
+
+$edncDir = "C:\Enrollment\shopfloor-setup\Standard\eDNC"
+
+if (-not (Test-Path $edncDir)) {
+ Write-Warning "eDNC folder not found at $edncDir — skipping."
+ exit 0
+}
+
+# --- Find installers ---
+$edncMsi = Get-ChildItem -Path $edncDir -Filter "eDNC-*.msi" | Select-Object -First 1
+$markMsi = Get-ChildItem -Path $edncDir -Filter "MarkZebra.msi" | Select-Object -First 1
+$emxInfo = Join-Path $edncDir "eMxInfo.txt"
+
+# --- 1. Install eDNC ---
+if ($edncMsi) {
+ Write-Host "Installing eDNC: $($edncMsi.Name)..."
+ $p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$($edncMsi.FullName)`" /qn /norestart LAUNCHNTLARS=false" -Wait -PassThru
+ Write-Host " eDNC exit code: $($p.ExitCode)"
+} else {
+ Write-Warning "eDNC installer not found in $edncDir (expected eDNC-*.msi)"
+}
+
+# --- 2. Install MarkZebra ---
+if ($markMsi) {
+ Write-Host "Installing MarkZebra: $($markMsi.Name)..."
+ $p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$($markMsi.FullName)`" /qn /norestart LAUNCHNTLARS=false" -Wait -PassThru
+ Write-Host " MarkZebra exit code: $($p.ExitCode)"
+} else {
+ Write-Warning "MarkZebra installer not found in $edncDir (expected MarkZebra.msi)"
+}
+
+# --- 3. Mirror x86 installs to 64-bit Program Files (app uses hardcoded paths) ---
+# MarkZebra.exe references \Mark\, mxTransactionDll.dll references \Dnc\Server Files\
+$copies = @(
+ @{ Src = "C:\Program Files (x86)\Mark"; Dst = "C:\Program Files\Mark" },
+ @{ Src = "C:\Program Files (x86)\Dnc"; Dst = "C:\Program Files\Dnc" }
+)
+foreach ($c in $copies) {
+ if (Test-Path $c.Src) {
+ if (-not (Test-Path $c.Dst)) {
+ New-Item -Path $c.Dst -ItemType Directory -Force | Out-Null
+ }
+ Copy-Item -Path "$($c.Src)\*" -Destination $c.Dst -Recurse -Force
+ Write-Host " Copied $($c.Src) -> $($c.Dst)"
+ }
+}
+
+# --- 4. Set DNC site and MarkZebra config ---
+$regBase = "HKLM\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC"
+reg add "$regBase\General" /v Site /t REG_SZ /d WestJefferson /f | Out-Null
+Write-Host " DNC site set to WestJefferson."
+
+reg add "$regBase\Mark" /v "Port Id" /t REG_SZ /d COM1 /f | Out-Null
+reg add "$regBase\Mark" /v "Baud" /t REG_SZ /d 9600 /f | Out-Null
+reg add "$regBase\Mark" /v "Parity" /t REG_SZ /d None /f | Out-Null
+reg add "$regBase\Mark" /v "Data Bits" /t REG_SZ /d 8 /f | Out-Null
+reg add "$regBase\Mark" /v "Stop Bits" /t REG_SZ /d 1 /f | Out-Null
+reg add "$regBase\Mark" /v "Message Type" /t REG_SZ /d V /f | Out-Null
+reg add "$regBase\Mark" /v "Debug" /t REG_SZ /d ON /f | Out-Null
+reg add "$regBase\Mark" /v "MarkerType" /t REG_SZ /d Mark2D /f | Out-Null
+reg add "$regBase\Mark" /v "DncPatterns" /t REG_SZ /d NO /f | Out-Null
+Set-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\Mark" -Name "CageCode" -Value "" -Force
+Write-Host " MarkZebra registry configured."
+
+# --- 5. Deploy custom eMxInfo.txt to both Program Files paths ---
+if (Test-Path $emxInfo) {
+ $dest86 = "C:\Program Files (x86)\DNC\Server Files"
+ $dest64 = "C:\Program Files\DNC\Server Files"
+
+ foreach ($dest in @($dest86, $dest64)) {
+ if (-not (Test-Path $dest)) {
+ New-Item -Path $dest -ItemType Directory -Force | Out-Null
+ }
+ Copy-Item -Path $emxInfo -Destination (Join-Path $dest "eMxInfo.txt") -Force
+ Write-Host " eMxInfo.txt -> $dest"
+ }
+} else {
+ Write-Warning "eMxInfo.txt not found at $emxInfo"
+}
+
+Write-Host "=== eDNC / MarkZebra Setup Complete ==="
diff --git a/playbook/shopfloor-setup/WaxAndTrace/01-Setup-WaxAndTrace.ps1 b/playbook/shopfloor-setup/WaxAndTrace/01-Setup-WaxAndTrace.ps1
new file mode 100644
index 0000000..3851da3
--- /dev/null
+++ b/playbook/shopfloor-setup/WaxAndTrace/01-Setup-WaxAndTrace.ps1
@@ -0,0 +1,14 @@
+# 01-Setup-WaxAndTrace.ps1 — Wax and Trace-specific setup (runs after Shopfloor baseline)
+
+Write-Host "=== Wax and Trace Setup ==="
+
+# --- Add Wax and Trace credentials ---
+# cmdkey /generic:wax-server /user:domain\waxuser /pass:password
+
+# --- Install Wax and Trace applications ---
+# Start-Process msiexec.exe -ArgumentList '/i "C:\Enrollment\shopfloor-setup\WaxAndTrace\WaxApp.msi" /qn' -Wait
+
+# --- Wax and Trace configuration ---
+# Set-ItemProperty -Path "HKLM:\SOFTWARE\CompanyName" -Name "PCType" -Value "WaxAndTrace"
+
+Write-Host "=== Wax and Trace Setup Complete ==="
diff --git a/playbook/shopfloor-setup/backup_lockdown.bat b/playbook/shopfloor-setup/backup_lockdown.bat
new file mode 100644
index 0000000..3d1781a
--- /dev/null
+++ b/playbook/shopfloor-setup/backup_lockdown.bat
@@ -0,0 +1,54 @@
+@echo off
+title Shopfloor Backup Lockdown
+
+:: Self-elevate to administrator
+net session >nul 2>&1
+if %errorlevel% neq 0 (
+ echo Requesting administrator privileges...
+ powershell -Command "Start-Process '%~f0' -Verb RunAs"
+ exit /b
+)
+
+echo.
+echo ========================================
+echo Shopfloor Backup Lockdown
+echo ========================================
+echo.
+
+:: Run SFLD autologon script first
+echo Running SFLD autologon script...
+"C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -File "C:\Program Files\Sysinternals\sfld_autologon.ps1"
+
+echo.
+echo Waiting 10 seconds...
+ping -n 11 127.0.0.1 >nul
+
+:: Discover the EnterpriseMgmt enrollment GUID
+for /f "delims=" %%G in ('powershell -NoProfile -Command "$t = Get-ScheduledTask | Where-Object { $_.TaskPath -match '\\Microsoft\\EnterpriseMgmt\\' -and $_.TaskName -match 'Schedule #1' }; if ($t) { $t.TaskPath -replace '.*EnterpriseMgmt\\([^\\]+)\\.*','$1' | Select-Object -First 1 } else { '' }"') do set GUID=%%G
+
+if not defined GUID (
+ echo ERROR: No EnterpriseMgmt enrollment GUID found.
+ echo The device may not be enrolled in MDM yet.
+ pause
+ exit /b 1
+)
+
+echo Enrollment GUID: %GUID%
+echo.
+
+echo Running EnterpriseMgmt Schedule #1...
+schtasks /run /tn "\Microsoft\EnterpriseMgmt\%GUID%\Schedule #1 created by enrollment client"
+echo Waiting 30 seconds...
+ping -n 31 127.0.0.1 >nul
+
+echo Running EnterpriseMgmt Schedule #2...
+schtasks /run /tn "\Microsoft\EnterpriseMgmt\%GUID%\Schedule #2 created by enrollment client"
+echo Waiting 90 seconds...
+ping -n 91 127.0.0.1 >nul
+
+echo Running EnterpriseMgmt Schedule #3...
+schtasks /run /tn "\Microsoft\EnterpriseMgmt\%GUID%\Schedule #3 created by enrollment client"
+
+echo.
+echo Lockdown complete.
+pause
diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd
index 0116090..3439fc2 100644
--- a/playbook/startnet.cmd
+++ b/playbook/startnet.cmd
@@ -86,7 +86,7 @@ echo 2. Wax and Trace
echo 3. Keyence
echo 4. Genspect
echo 5. Display
-echo 6. Shopfloor (General)
+echo 6. Standard
echo.
set PCTYPE=
set /p pctype_choice=Enter your choice (1-6):
@@ -95,7 +95,7 @@ if "%pctype_choice%"=="2" set PCTYPE=WaxAndTrace
if "%pctype_choice%"=="3" set PCTYPE=Keyence
if "%pctype_choice%"=="4" set PCTYPE=Genspect
if "%pctype_choice%"=="5" set PCTYPE=Display
-if "%pctype_choice%"=="6" set PCTYPE=Shopfloor
+if "%pctype_choice%"=="6" set PCTYPE=Standard
if "%PCTYPE%"=="" goto pctype_menu
REM --- Display sub-type selection ---
@@ -245,13 +245,13 @@ if not "%DISPLAYTYPE%"=="" echo %DISPLAYTYPE%> W:\Enrollment\display-type.txt
copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1"
REM --- Always copy Shopfloor baseline scripts ---
mkdir W:\Enrollment\shopfloor-setup 2>NUL
+copy /Y "Y:\shopfloor-setup\backup_lockdown.bat" "W:\Enrollment\shopfloor-setup\backup_lockdown.bat"
if exist "Y:\shopfloor-setup\Shopfloor" (
mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\"
echo Copied Shopfloor baseline setup files.
)
REM --- Copy type-specific scripts on top of baseline ---
-if "%PCTYPE%"=="Shopfloor" goto pctype_done
if exist "Y:\shopfloor-setup\%PCTYPE%" (
mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\"
diff --git a/sync_hardware_models.py b/sync_hardware_models.py
new file mode 100644
index 0000000..1470a20
--- /dev/null
+++ b/sync_hardware_models.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+"""Sync HardwareDriver.json and user_selections.json across all PXE image types.
+
+Reads all HardwareDriver.json files, builds a unified driver catalog,
+then updates each image to include all known hardware models.
+Run after adding new driver packs to the shared Out-of-box Drivers directory.
+"""
+import json
+import os
+import sys
+from pathlib import Path
+from collections import OrderedDict
+
+WINPEAPPS = Path("/srv/samba/winpeapps")
+SHARED_DRIVERS = WINPEAPPS / "_shared" / "Out-of-box Drivers"
+
+
+def normalize_entry(entry):
+ """Normalize a HardwareDriver.json entry to a consistent format."""
+ norm = {}
+ norm["manufacturer"] = entry.get("manufacturer", "Dell")
+ norm["product"] = entry.get("product") or entry.get("manufacturerfriendlyname", "Dell")
+ norm["family"] = entry.get("family", "")
+ norm["modelswminame"] = entry.get("modelswminame") or entry.get("models", "")
+ norm["modelsfriendlyname"] = entry.get("modelsfriendlyname", "")
+ norm["fileName"] = entry.get("fileName") or entry.get("FileName", "")
+ norm["destinationDir"] = entry.get("destinationDir") or entry.get("DestinationDir", "")
+ norm["url"] = entry.get("url", "")
+ norm["hash"] = entry.get("hash", "")
+ norm["size"] = entry.get("size", 0)
+ norm["modifiedDate"] = entry.get("modifiedDate", "0001-01-01T00:00:00")
+ norm["osId"] = entry.get("osId", "")
+ norm["imagedisk"] = entry.get("imagedisk", 0)
+ return norm
+
+
+def merge_os_ids(a, b):
+ """Merge two osId strings (e.g., '18' + '20,21' -> '18,20,21')."""
+ ids = set()
+ for oid in [a, b]:
+ for part in str(oid).split(","):
+ part = part.strip()
+ if part:
+ ids.add(part)
+ return ",".join(sorted(ids, key=lambda x: int(x) if x.isdigit() else 0))
+
+
+def check_driver_exists(entry):
+ """Check if the driver zip actually exists in the shared directory."""
+ dest = entry["destinationDir"]
+ dest = dest.replace("*destinationdir*", "")
+ dest = dest.lstrip("\\")
+ dest = dest.replace("\\", "/")
+ # Strip leading path components that are already in SHARED_DRIVERS
+ for prefix in ["Deploy/Out-of-box Drivers/", "Out-of-box Drivers/"]:
+ if dest.startswith(prefix):
+ dest = dest[len(prefix):]
+ break
+ dest = dest.lstrip("/")
+ zip_path = SHARED_DRIVERS / dest / entry["fileName"]
+ return zip_path.exists()
+
+
+def main():
+ print("=== PXE Hardware Model Sync ===")
+ print()
+
+ # Step 1: Build unified catalog from all images
+ print("Reading driver catalogs...")
+ catalog = OrderedDict()
+
+ image_dirs = sorted(
+ [d for d in WINPEAPPS.iterdir() if d.is_dir() and not d.name.startswith("_")]
+ )
+
+ for img_dir in image_dirs:
+ hw_file = img_dir / "Deploy" / "Control" / "HardwareDriver.json"
+ if not hw_file.exists():
+ continue
+ with open(hw_file) as f:
+ entries = json.load(f)
+ print(" Read {} entries from {}".format(len(entries), img_dir.name))
+ for entry in entries:
+ norm = normalize_entry(entry)
+ key = (norm["family"], norm["fileName"])
+ if key in catalog:
+ catalog[key]["osId"] = merge_os_ids(
+ catalog[key]["osId"], norm["osId"]
+ )
+ # Prefer longer/more complete model names
+ if len(norm["modelswminame"]) > len(catalog[key]["modelswminame"]):
+ catalog[key]["modelswminame"] = norm["modelswminame"]
+ if len(norm["modelsfriendlyname"]) > len(
+ catalog[key]["modelsfriendlyname"]
+ ):
+ catalog[key]["modelsfriendlyname"] = norm["modelsfriendlyname"]
+ else:
+ catalog[key] = norm
+
+ unified = list(catalog.values())
+ print()
+ print("Unified catalog: {} unique driver entries".format(len(unified)))
+
+ # Step 2: Check which drivers actually exist on disk
+ missing = []
+ found = 0
+ for entry in unified:
+ if check_driver_exists(entry):
+ found += 1
+ else:
+ missing.append(
+ " {}: {}".format(entry["family"], entry["fileName"])
+ )
+
+ print(" {} drivers found on disk".format(found))
+ if missing:
+ print(" WARNING: {} driver zips NOT found on disk:".format(len(missing)))
+ for m in missing[:15]:
+ print(m)
+ if len(missing) > 15:
+ print(" ... and {} more".format(len(missing) - 15))
+ print(" (Entries still included - PESetup may download them)")
+
+ # Step 3: Build unified model selection from all driver entries
+ models = []
+ seen = set()
+ for entry in unified:
+ friendly_names = [
+ n.strip()
+ for n in entry["modelsfriendlyname"].split(",")
+ if n.strip()
+ ]
+ family = entry["family"]
+ for name in friendly_names:
+ key = (name, family)
+ if key not in seen:
+ seen.add(key)
+ models.append({"Model": name, "Id": family})
+ models.sort(key=lambda x: x["Model"])
+ print()
+ print("Unified model selection: {} models".format(len(models)))
+
+ # Step 4: Update each image
+ print()
+ print("Updating images...")
+ for img_dir in image_dirs:
+ hw_file = img_dir / "Deploy" / "Control" / "HardwareDriver.json"
+ us_file = img_dir / "Tools" / "user_selections.json"
+ if not hw_file.exists() or not us_file.exists():
+ continue
+
+ # Write unified HardwareDriver.json
+ with open(hw_file, "w") as f:
+ json.dump(unified, f, indent=2)
+ f.write("\n")
+
+ # Update user_selections.json (preserve OperatingSystemSelection etc.)
+ with open(us_file) as f:
+ user_sel = json.load(f)
+ old_count = len(user_sel[0].get("HardwareModelSelection", []))
+ user_sel[0]["HardwareModelSelection"] = models
+ with open(us_file, "w") as f:
+ json.dump(user_sel, f, indent=2)
+ f.write("\n")
+
+ print(
+ " {}: {} -> {} models, {} driver entries".format(
+ img_dir.name, old_count, len(models), len(unified)
+ )
+ )
+
+ print()
+ print("Done!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/webapp/app.py b/webapp/app.py
index 6ba685d..8eafa78 100644
--- a/webapp/app.py
+++ b/webapp/app.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Flask web application for managing a GE Aerospace PXE server."""
+import json
import logging
import os
import secrets
@@ -51,14 +52,15 @@ def audit(action, detail=""):
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
BLANCCO_REPORTS = os.environ.get("BLANCCO_REPORTS", "/srv/samba/blancco-reports")
+ENROLLMENT_SHARE = os.environ.get("ENROLLMENT_SHARE", "/srv/samba/enrollment")
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload")
SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared")
# Subdirs inside Deploy/ shared across ALL image types
SHARED_DEPLOY_GLOBAL = ["Out-of-box Drivers"]
# Subdirs inside Deploy/ shared within the same image family (by prefix)
SHARED_DEPLOY_SCOPED = {
- "gea-": ["Operating Systems"],
- "ge-": ["Operating Systems"],
+ "gea-": ["Operating Systems", "Packages"],
+ "ge-": ["Operating Systems", "Packages"],
}
# Sibling dirs at image root shared within the same image family
SHARED_ROOT_DIRS = {
@@ -149,6 +151,164 @@ def unattend_path(image_type):
return os.path.join(deploy_path(image_type), "FlatUnattendW10.xml")
+def control_path(image_type):
+ """Return the Deploy/Control directory for an image type."""
+ return os.path.join(deploy_path(image_type), "Control")
+
+
+def tools_path(image_type):
+ """Return the Tools directory for an image type."""
+ return os.path.join(SAMBA_SHARE, image_type, "Tools")
+
+
+def _load_json(filepath):
+ """Parse a JSON file and return its contents, or [] on failure."""
+ try:
+ with open(filepath, "r", encoding="utf-8-sig") as fh:
+ return json.load(fh)
+ except (OSError, json.JSONDecodeError):
+ return []
+
+
+def _save_json(filepath, data):
+ """Write data as pretty-printed JSON."""
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ with open(filepath, "w", encoding="utf-8") as fh:
+ json.dump(data, fh, indent=2, ensure_ascii=False)
+ fh.write("\n")
+
+
+def _resolve_destination(dest_dir, image_type):
+ """Convert a Windows *destinationdir* path to a Linux filesystem path.
+
+ Replaces the ``*destinationdir*`` placeholder and backslashes, then
+ prepends ``SAMBA_SHARE/image_type/`` and resolves symlinks.
+ """
+ if not dest_dir:
+ return ""
+ # Replace the placeholder (case-insensitive)
+ path = dest_dir
+ lower = path.lower()
+ idx = lower.find("*destinationdir*")
+ if idx != -1:
+ path = path[idx + len("*destinationdir*"):]
+ # Backslash → forward slash, strip leading slash
+ path = path.replace("\\", "/").lstrip("/")
+ full = os.path.join(SAMBA_SHARE, image_type, path)
+ # Resolve symlinks so shared dirs are found
+ try:
+ full = os.path.realpath(full)
+ except OSError:
+ pass
+ return full
+
+
+def _load_image_config(image_type):
+ """Load all JSON configs for an image and check on-disk presence."""
+ ctrl = control_path(image_type)
+ tools = tools_path(image_type)
+
+ # --- Drivers (merge HardwareDriver.json + hw_drivers.json) ---
+ hw_driver_file = os.path.join(ctrl, "HardwareDriver.json")
+ hw_drivers_extra = os.path.join(ctrl, "hw_drivers.json")
+ drivers_raw = _load_json(hw_driver_file)
+ extra_raw = _load_json(hw_drivers_extra)
+
+ # Merge: dedup by FileName (case-insensitive)
+ seen_files = set()
+ drivers = []
+ for d in drivers_raw + extra_raw:
+ fname = (d.get("FileName") or d.get("fileName") or "").lower()
+ if fname and fname in seen_files:
+ continue
+ if fname:
+ seen_files.add(fname)
+ drivers.append(d)
+
+ # Check disk presence for each driver
+ for d in drivers:
+ fname = d.get("FileName") or d.get("fileName") or ""
+ dest = d.get("DestinationDir") or d.get("destinationDir") or ""
+ resolved = _resolve_destination(dest, image_type)
+ if resolved and fname:
+ d["_on_disk"] = os.path.isfile(os.path.join(resolved, fname))
+ else:
+ d["_on_disk"] = False
+
+ # --- Operating Systems ---
+ os_file = os.path.join(ctrl, "OperatingSystem.json")
+ operating_systems = _load_json(os_file)
+ for entry in operating_systems:
+ osv = entry.get("operatingSystemVersion", {})
+ wim = osv.get("wim", {})
+ dest = wim.get("DestinationDir") or wim.get("destinationDir") or ""
+ resolved = _resolve_destination(dest, image_type)
+ if resolved:
+ entry["_on_disk"] = os.path.isfile(os.path.join(resolved, "install.wim"))
+ else:
+ entry["_on_disk"] = False
+
+ # --- Packages ---
+ pkg_file = os.path.join(ctrl, "packages.json")
+ packages = _load_json(pkg_file)
+ for p in packages:
+ fname = p.get("fileName") or p.get("FileName") or ""
+ dest = p.get("destinationDir") or p.get("DestinationDir") or ""
+ resolved = _resolve_destination(dest, image_type)
+ if resolved and fname:
+ p["_on_disk"] = os.path.isfile(os.path.join(resolved, fname))
+ else:
+ p["_on_disk"] = False
+
+ # --- Hardware Models (user_selections.json) ---
+ us_file = os.path.join(tools, "user_selections.json")
+ us_raw = _load_json(us_file)
+ us_data = us_raw[0] if us_raw and isinstance(us_raw, list) else {}
+ hardware_models = us_data.get("HardwareModelSelection", [])
+ os_selection = str(us_data.get("OperatingSystemSelection", ""))
+
+ # Build a lookup: family → driver entry (for disk-presence on models)
+ family_lookup = {}
+ for d in drivers:
+ family = d.get("family", "")
+ if family:
+ family_lookup[family] = d
+
+ for hm in hardware_models:
+ family_id = hm.get("Id", "")
+ matched = family_lookup.get(family_id)
+ hm["_on_disk"] = matched["_on_disk"] if matched else False
+
+ # --- Orphan drivers: zip files on disk not referenced in any JSON ---
+ orphan_drivers = []
+ oob_dir = os.path.join(deploy_path(image_type), "Out-of-box Drivers")
+ # Resolve symlinks
+ try:
+ oob_dir = os.path.realpath(oob_dir)
+ except OSError:
+ pass
+ registered_files = set()
+ for d in drivers:
+ fname = d.get("FileName") or d.get("fileName") or ""
+ if fname:
+ registered_files.add(fname.lower())
+ if os.path.isdir(oob_dir):
+ for dirpath, _dirnames, filenames in os.walk(oob_dir):
+ for fn in filenames:
+ if fn.lower().endswith(".zip") and fn.lower() not in registered_files:
+ rel = os.path.relpath(os.path.join(dirpath, fn), oob_dir)
+ orphan_drivers.append({"fileName": fn, "relPath": rel})
+
+ return {
+ "hardware_models": hardware_models,
+ "drivers": drivers,
+ "operating_systems": operating_systems,
+ "packages": packages,
+ "orphan_drivers": orphan_drivers,
+ "os_selection": os_selection,
+ }
+
+
def image_status(image_type):
"""Return a dict describing the state of an image type."""
dp = deploy_path(image_type)
@@ -255,10 +415,11 @@ def _import_deploy(src_deploy, dst_deploy, target="", move=False):
_replace_with_symlink(dst_item, shared_dest)
continue
- # Normal transfer
- if os.path.exists(dst_item):
- shutil.rmtree(dst_item)
- _transfer_tree(src_item, dst_item)
+ # Normal transfer — merge to preserve existing custom content
+ if os.path.isdir(dst_item):
+ _merge_tree(src_item, dst_item, move=move)
+ else:
+ _transfer_tree(src_item, dst_item)
def _replace_with_symlink(link_path, target_path):
@@ -355,6 +516,21 @@ def parse_unattend(xml_path):
"SkipMachineOOBE": "true",
},
"firstlogon_commands": [],
+ "user_accounts": [],
+ "autologon": {
+ "enabled": "",
+ "username": "",
+ "password": "",
+ "plain_text": "true",
+ "logon_count": "",
+ },
+ "intl": {
+ "input_locale": "",
+ "system_locale": "",
+ "ui_language": "",
+ "user_locale": "",
+ },
+ "oobe_timezone": "",
"raw_xml": "",
}
@@ -418,6 +594,19 @@ def parse_unattend(xml_path):
namespaces=ns,
):
comp_name = comp.get("name", "")
+
+ # International-Core component
+ if "International-Core" in comp_name:
+ for tag, key in [
+ ("InputLocale", "input_locale"),
+ ("SystemLocale", "system_locale"),
+ ("UILanguage", "ui_language"),
+ ("UserLocale", "user_locale"),
+ ]:
+ el = comp.find(qn(tag))
+ if el is not None and el.text:
+ data["intl"][key] = el.text.strip()
+
if "OOBE" in comp_name or "Shell-Setup" in comp_name:
oobe_el = comp.find(qn("OOBE"))
if oobe_el is not None:
@@ -426,6 +615,57 @@ def parse_unattend(xml_path):
if local in data["oobe"] and child.text:
data["oobe"][local] = child.text.strip()
+ # UserAccounts / LocalAccounts
+ ua = comp.find(qn("UserAccounts"))
+ if ua is not None:
+ la_container = ua.find(qn("LocalAccounts"))
+ if la_container is not None:
+ for acct in la_container.findall(qn("LocalAccount")):
+ name_el = acct.find(qn("Name"))
+ group_el = acct.find(qn("Group"))
+ display_el = acct.find(qn("DisplayName"))
+ pw_el = acct.find(qn("Password"))
+ pw_val = ""
+ pw_plain = "true"
+ if pw_el is not None:
+ v = pw_el.find(qn("Value"))
+ p = pw_el.find(qn("PlainText"))
+ if v is not None and v.text:
+ pw_val = v.text.strip()
+ if p is not None and p.text:
+ pw_plain = p.text.strip()
+ data["user_accounts"].append({
+ "name": name_el.text.strip() if name_el is not None and name_el.text else "",
+ "password": pw_val,
+ "plain_text": pw_plain,
+ "group": group_el.text.strip() if group_el is not None and group_el.text else "Administrators",
+ "display_name": display_el.text.strip() if display_el is not None and display_el.text else "",
+ })
+
+ # AutoLogon
+ al = comp.find(qn("AutoLogon"))
+ if al is not None:
+ enabled_el = al.find(qn("Enabled"))
+ user_el = al.find(qn("Username"))
+ count_el = al.find(qn("LogonCount"))
+ pw_el = al.find(qn("Password"))
+ pw_val = ""
+ pw_plain = "true"
+ if pw_el is not None:
+ v = pw_el.find(qn("Value"))
+ p = pw_el.find(qn("PlainText"))
+ if v is not None and v.text:
+ pw_val = v.text.strip()
+ if p is not None and p.text:
+ pw_plain = p.text.strip()
+ data["autologon"] = {
+ "enabled": enabled_el.text.strip() if enabled_el is not None and enabled_el.text else "",
+ "username": user_el.text.strip() if user_el is not None and user_el.text else "",
+ "password": pw_val,
+ "plain_text": pw_plain,
+ "logon_count": count_el.text.strip() if count_el is not None and count_el.text else "",
+ }
+
# FirstLogonCommands
flc = comp.find(qn("FirstLogonCommands"))
if flc is not None:
@@ -439,6 +679,11 @@ def parse_unattend(xml_path):
"description": desc_el.text.strip() if desc_el is not None and desc_el.text else "",
})
+ # TimeZone (oobeSystem pass)
+ tz_el = comp.find(qn("TimeZone"))
+ if tz_el is not None and tz_el.text:
+ data["oobe_timezone"] = tz_el.text.strip()
+
return data
@@ -515,6 +760,28 @@ def build_unattend_xml(form_data):
# --- oobeSystem ---
oobe_settings = _settings_pass(root, "oobeSystem")
+
+ # International-Core component (before Shell-Setup)
+ intl = form_data.get("intl", {})
+ if any(v.strip() for v in intl.values() if v):
+ intl_comp = etree.SubElement(oobe_settings, qn("component"))
+ intl_comp.set("name", "Microsoft-Windows-International-Core")
+ intl_comp.set("processorArchitecture", "amd64")
+ intl_comp.set("publicKeyToken", "31bf3856ad364e35")
+ intl_comp.set("language", "neutral")
+ intl_comp.set("versionScope", "nonSxS")
+ for tag, key in [
+ ("InputLocale", "input_locale"),
+ ("SystemLocale", "system_locale"),
+ ("UILanguage", "ui_language"),
+ ("UserLocale", "user_locale"),
+ ]:
+ val = intl.get(key, "").strip()
+ if val:
+ el = etree.SubElement(intl_comp, qn(tag))
+ el.text = val
+
+ # Shell-Setup component
oobe_comp = etree.SubElement(oobe_settings, qn("component"))
oobe_comp.set("name", "Microsoft-Windows-Shell-Setup")
oobe_comp.set("processorArchitecture", "amd64")
@@ -540,6 +807,46 @@ def build_unattend_xml(form_data):
child = etree.SubElement(oobe_el, qn(key))
child.text = str(val)
+ # UserAccounts / LocalAccounts
+ accounts = form_data.get("user_accounts", [])
+ if accounts:
+ ua = etree.SubElement(oobe_comp, qn("UserAccounts"))
+ la_container = etree.SubElement(ua, qn("LocalAccounts"))
+ for acct in accounts:
+ if not acct.get("name", "").strip():
+ continue
+ la = etree.SubElement(la_container, qn("LocalAccount"))
+ la.set(qwcm("action"), "add")
+ pw = etree.SubElement(la, qn("Password"))
+ pw_val = etree.SubElement(pw, qn("Value"))
+ pw_val.text = acct.get("password", "")
+ pw_plain = etree.SubElement(pw, qn("PlainText"))
+ pw_plain.text = acct.get("plain_text", "true")
+ name_el = etree.SubElement(la, qn("Name"))
+ name_el.text = acct["name"].strip()
+ group_el = etree.SubElement(la, qn("Group"))
+ group_el.text = acct.get("group", "Administrators").strip()
+ display_el = etree.SubElement(la, qn("DisplayName"))
+ display_el.text = acct.get("display_name", acct["name"]).strip()
+
+ # AutoLogon
+ autologon = form_data.get("autologon", {})
+ if autologon.get("username", "").strip():
+ al = etree.SubElement(oobe_comp, qn("AutoLogon"))
+ al_pw = etree.SubElement(al, qn("Password"))
+ al_pw_val = etree.SubElement(al_pw, qn("Value"))
+ al_pw_val.text = autologon.get("password", "")
+ al_pw_plain = etree.SubElement(al_pw, qn("PlainText"))
+ al_pw_plain.text = autologon.get("plain_text", "true")
+ al_enabled = etree.SubElement(al, qn("Enabled"))
+ al_enabled.text = autologon.get("enabled", "true")
+ al_user = etree.SubElement(al, qn("Username"))
+ al_user.text = autologon["username"].strip()
+ logon_count = autologon.get("logon_count", "").strip()
+ if logon_count:
+ al_count = etree.SubElement(al, qn("LogonCount"))
+ al_count.text = logon_count
+
# FirstLogonCommands
fl_cmds = form_data.get("firstlogon_commands", [])
if fl_cmds:
@@ -556,6 +863,12 @@ def build_unattend_xml(form_data):
desc_el = etree.SubElement(sc, qn("Description"))
desc_el.text = cmd.get("description", "").strip()
+ # TimeZone (oobeSystem pass)
+ oobe_tz = form_data.get("oobe_timezone", "").strip()
+ if oobe_tz:
+ tz_el = etree.SubElement(oobe_comp, qn("TimeZone"))
+ tz_el.text = oobe_tz
+
xml_bytes = etree.tostring(
root,
pretty_print=True,
@@ -616,6 +929,40 @@ def _extract_form_data(form):
"description": fl_descs[i] if i < len(fl_descs) else "",
})
+ # User accounts
+ accounts = []
+ i = 0
+ while form.get(f"account_name_{i}"):
+ accounts.append({
+ "name": form.get(f"account_name_{i}", ""),
+ "password": form.get(f"account_password_{i}", ""),
+ "plain_text": form.get(f"account_plaintext_{i}", "true"),
+ "group": form.get(f"account_group_{i}", "Administrators"),
+ "display_name": form.get(f"account_display_{i}", ""),
+ })
+ i += 1
+ data["user_accounts"] = accounts
+
+ # AutoLogon
+ data["autologon"] = {
+ "enabled": form.get("autologon_enabled", ""),
+ "username": form.get("autologon_username", ""),
+ "password": form.get("autologon_password", ""),
+ "plain_text": form.get("autologon_plaintext", "true"),
+ "logon_count": form.get("autologon_logoncount", ""),
+ }
+
+ # International settings
+ data["intl"] = {
+ "input_locale": form.get("intl_input_locale", ""),
+ "system_locale": form.get("intl_system_locale", ""),
+ "ui_language": form.get("intl_ui_language", ""),
+ "user_locale": form.get("intl_user_locale", ""),
+ }
+
+ # OOBE TimeZone
+ data["oobe_timezone"] = form.get("oobe_timezone", "")
+
return data
@@ -799,6 +1146,74 @@ def unattend_editor(image_type):
)
+@app.route("/images//config")
+def image_config(image_type):
+ if image_type not in IMAGE_TYPES:
+ flash("Unknown image type.", "danger")
+ return redirect(url_for("dashboard"))
+
+ config = _load_image_config(image_type)
+ return render_template(
+ "image_config.html",
+ image_type=image_type,
+ friendly_name=FRIENDLY_NAMES.get(image_type, image_type),
+ config=config,
+ )
+
+
+@app.route("/images//config/save", methods=["POST"])
+def image_config_save(image_type):
+ if image_type not in IMAGE_TYPES:
+ flash("Unknown image type.", "danger")
+ return redirect(url_for("dashboard"))
+
+ section = request.form.get("section", "")
+ payload = request.form.get("payload", "[]")
+ try:
+ data = json.loads(payload)
+ except json.JSONDecodeError:
+ flash("Invalid JSON payload.", "danger")
+ return redirect(url_for("image_config", image_type=image_type))
+
+ ctrl = control_path(image_type)
+ tools = tools_path(image_type)
+
+ try:
+ if section == "hardware_models":
+ us_file = os.path.join(tools, "user_selections.json")
+ us_raw = _load_json(us_file)
+ us_data = us_raw[0] if us_raw and isinstance(us_raw, list) else {}
+ us_data["HardwareModelSelection"] = data
+ _save_json(us_file, [us_data])
+ audit("CONFIG_SAVE", f"{image_type}/hardware_models")
+
+ elif section == "drivers":
+ # Strip internal fields before saving
+ clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
+ _save_json(os.path.join(ctrl, "HardwareDriver.json"), clean)
+ audit("CONFIG_SAVE", f"{image_type}/drivers")
+
+ elif section == "operating_systems":
+ clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
+ _save_json(os.path.join(ctrl, "OperatingSystem.json"), clean)
+ audit("CONFIG_SAVE", f"{image_type}/operating_systems")
+
+ elif section == "packages":
+ clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
+ _save_json(os.path.join(ctrl, "packages.json"), clean)
+ audit("CONFIG_SAVE", f"{image_type}/packages")
+
+ else:
+ flash(f"Unknown section: {section}", "danger")
+ return redirect(url_for("image_config", image_type=image_type))
+
+ flash(f"Saved {section.replace('_', ' ')} successfully.", "success")
+ except Exception as exc:
+ flash(f"Failed to save {section}: {exc}", "danger")
+
+ return redirect(url_for("image_config", image_type=image_type))
+
+
# ---------------------------------------------------------------------------
# Routes — Clonezilla Backups
# ---------------------------------------------------------------------------
@@ -922,6 +1337,78 @@ def blancco_delete_report(filename):
return redirect(url_for("blancco_reports"))
+# ---------------------------------------------------------------------------
+# Routes — Enrollment Packages
+# ---------------------------------------------------------------------------
+
+@app.route("/enrollment")
+def enrollment():
+ packages = []
+ if os.path.isdir(ENROLLMENT_SHARE):
+ for f in sorted(os.listdir(ENROLLMENT_SHARE)):
+ fpath = os.path.join(ENROLLMENT_SHARE, f)
+ if os.path.isfile(fpath) and f.lower().endswith(".ppkg"):
+ stat = os.stat(fpath)
+ packages.append({
+ "filename": f,
+ "size": stat.st_size,
+ "modified": stat.st_mtime,
+ })
+ return render_template(
+ "enrollment.html",
+ packages=packages,
+ image_types=IMAGE_TYPES,
+ friendly_names=FRIENDLY_NAMES,
+ )
+
+
+@app.route("/enrollment/upload", methods=["POST"])
+def enrollment_upload():
+ if "ppkg_file" not in request.files:
+ flash("No file selected.", "danger")
+ return redirect(url_for("enrollment"))
+
+ f = request.files["ppkg_file"]
+ if not f.filename:
+ flash("No file selected.", "danger")
+ return redirect(url_for("enrollment"))
+
+ filename = secure_filename(f.filename)
+ if not filename.lower().endswith(".ppkg"):
+ flash("Only .ppkg files are accepted.", "danger")
+ return redirect(url_for("enrollment"))
+
+ os.makedirs(ENROLLMENT_SHARE, exist_ok=True)
+ dest = os.path.join(ENROLLMENT_SHARE, filename)
+ f.save(dest)
+ audit("ENROLLMENT_UPLOAD", filename)
+ flash(f"Uploaded {filename} successfully.", "success")
+ return redirect(url_for("enrollment"))
+
+
+@app.route("/enrollment/download/")
+def enrollment_download(filename):
+ filename = secure_filename(filename)
+ fpath = os.path.join(ENROLLMENT_SHARE, filename)
+ if not os.path.isfile(fpath):
+ flash(f"Package not found: {filename}", "danger")
+ return redirect(url_for("enrollment"))
+ return send_file(fpath, as_attachment=True)
+
+
+@app.route("/enrollment/delete/", methods=["POST"])
+def enrollment_delete(filename):
+ filename = secure_filename(filename)
+ fpath = os.path.join(ENROLLMENT_SHARE, filename)
+ if os.path.isfile(fpath):
+ os.remove(fpath)
+ audit("ENROLLMENT_DELETE", filename)
+ flash(f"Deleted {filename}.", "success")
+ else:
+ flash(f"Package not found: {filename}", "danger")
+ return redirect(url_for("enrollment"))
+
+
# ---------------------------------------------------------------------------
# Routes — startnet.cmd Editor (WIM)
# ---------------------------------------------------------------------------
diff --git a/webapp/static/app.js b/webapp/static/app.js
index 115ab91..9db19a5 100644
--- a/webapp/static/app.js
+++ b/webapp/static/app.js
@@ -150,6 +150,71 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
+ // -----------------------------------------------------------------------
+ // Add User Account
+ // -----------------------------------------------------------------------
+ var addUserAccountBtn = document.getElementById('addUserAccount');
+ if (addUserAccountBtn) {
+ addUserAccountBtn.addEventListener('click', function () {
+ var tbody = document.querySelector('#userAccountsTable tbody');
+ var idx = tbody.querySelectorAll('tr').length;
+ var tr = document.createElement('tr');
+ tr.innerHTML =
+ '
' + (idx + 1) + '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
';
+ tbody.appendChild(tr);
+ var emptyEl = document.getElementById('userAccountsEmpty');
+ if (emptyEl) emptyEl.style.display = 'none';
+ tr.querySelector('input[name^="account_name_"]').focus();
+ });
+ }
+
+ // -----------------------------------------------------------------------
+ // Remove User Account row + renumber indices (delegated)
+ // -----------------------------------------------------------------------
+ document.addEventListener('click', function (e) {
+ var btn = e.target.closest('.remove-account-row');
+ if (!btn) return;
+ var row = btn.closest('tr');
+ var tbody = row.parentElement;
+ row.remove();
+ // Renumber order and field name indices
+ var rows = tbody.querySelectorAll('tr');
+ rows.forEach(function (r, i) {
+ var orderCell = r.querySelector('.order-num');
+ if (orderCell) orderCell.textContent = i + 1;
+ // Rename inputs to match new index
+ r.querySelectorAll('input').forEach(function (inp) {
+ var name = inp.getAttribute('name');
+ if (name) {
+ inp.setAttribute('name', name.replace(/_\d+$/, '_' + i));
+ }
+ });
+ });
+ var emptyEl = document.getElementById('userAccountsEmpty');
+ if (emptyEl) emptyEl.style.display = rows.length > 0 ? 'none' : '';
+ });
+
+ // -----------------------------------------------------------------------
+ // AutoLogon Enabled toggle — keep hidden input in sync
+ // -----------------------------------------------------------------------
+ var autologonToggle = document.getElementById('autologonEnabledToggle');
+ if (autologonToggle) {
+ autologonToggle.addEventListener('change', function () {
+ var hidden = document.getElementById('autologon_enabled_val');
+ if (hidden) {
+ hidden.value = this.checked ? 'true' : 'false';
+ }
+ });
+ }
+
// -----------------------------------------------------------------------
// OOBE toggle switches — keep hidden input in sync
// -----------------------------------------------------------------------
@@ -298,5 +363,107 @@ document.addEventListener('DOMContentLoaded', function () {
toggleEmpty('driverPathsTable', 'driverPathsEmpty');
toggleEmpty('specCmdTable', 'specCmdEmpty');
toggleEmpty('flCmdTable', 'flCmdEmpty');
+ toggleEmpty('userAccountsTable', 'userAccountsEmpty');
+
+ // =======================================================================
+ // Image Configuration Editor handlers
+ // =======================================================================
+
+ // -----------------------------------------------------------------------
+ // Generic: collect table rows into JSON and submit form
+ // -----------------------------------------------------------------------
+ function collectAndSubmit(tableId, hiddenId, formId, extractor) {
+ var tbody = document.querySelector('#' + tableId + ' tbody');
+ if (!tbody) return;
+ var rows = tbody.querySelectorAll('tr');
+ var data = [];
+ rows.forEach(function (row) {
+ var item = extractor(row);
+ if (item) data.push(item);
+ });
+ document.getElementById(hiddenId).value = JSON.stringify(data);
+ document.getElementById(formId).submit();
+ }
+
+ // Helper: extract JSON from data-json attr, stripping internal _fields
+ function extractDataJson(row) {
+ var raw = row.getAttribute('data-json');
+ if (!raw) return null;
+ try {
+ var obj = JSON.parse(raw);
+ Object.keys(obj).forEach(function (k) {
+ if (k.charAt(0) === '_') delete obj[k];
+ });
+ return obj;
+ } catch (e) { return null; }
+ }
+
+ // -----------------------------------------------------------------------
+ // Save: Hardware Models
+ // -----------------------------------------------------------------------
+ var saveHwModelsBtn = document.getElementById('saveHwModels');
+ if (saveHwModelsBtn) {
+ saveHwModelsBtn.addEventListener('click', function () {
+ collectAndSubmit('hwModelsTable', 'hwModelsData', 'hwModelsForm', function (row) {
+ var m = row.querySelector('[data-field="Model"]');
+ var f = row.querySelector('[data-field="Id"]');
+ if (!m || !f) return null;
+ return { Model: m.value, Id: f.value };
+ });
+ });
+ }
+
+ // -----------------------------------------------------------------------
+ // Save: Drivers
+ // -----------------------------------------------------------------------
+ var saveDriversBtn = document.getElementById('saveDrivers');
+ if (saveDriversBtn) {
+ saveDriversBtn.addEventListener('click', function () {
+ collectAndSubmit('driversTable', 'driversData', 'driversForm', extractDataJson);
+ });
+ }
+
+ // -----------------------------------------------------------------------
+ // Save: Operating Systems
+ // -----------------------------------------------------------------------
+ var saveOsBtn = document.getElementById('saveOs');
+ if (saveOsBtn) {
+ saveOsBtn.addEventListener('click', function () {
+ collectAndSubmit('osTable', 'osData', 'osForm', extractDataJson);
+ });
+ }
+
+ // -----------------------------------------------------------------------
+ // Save: Packages
+ // -----------------------------------------------------------------------
+ var savePackagesBtn = document.getElementById('savePackages');
+ if (savePackagesBtn) {
+ savePackagesBtn.addEventListener('click', function () {
+ collectAndSubmit('packagesTable', 'packagesData', 'packagesForm', extractDataJson);
+ });
+ }
+
+ // -----------------------------------------------------------------------
+ // Add Hardware Model row
+ // -----------------------------------------------------------------------
+ var addHwModelBtn = document.getElementById('addHwModel');
+ if (addHwModelBtn) {
+ addHwModelBtn.addEventListener('click', function () {
+ var tbody = document.querySelector('#hwModelsTable tbody');
+ var idx = tbody.querySelectorAll('tr').length + 1;
+ var tr = document.createElement('tr');
+ tr.innerHTML =
+ '
No backups found. Upload a Clonezilla backup .zip to get started.
{% endif %}
@@ -59,7 +58,7 @@
-
Backup Naming Convention
+
Backup Naming Convention
Name backup files with the machine number (e.g., 6501.zip).
The Samba share \\pxe-server\clonezilla is also available on the network for direct Clonezilla save/restore operations.
@@ -74,7 +73,7 @@
- Import from Network Upload
+ Import from Network Upload
{% if upload_sources %}
@@ -44,7 +44,6 @@
-
Shared Drivers: Out-of-box Drivers are automatically pooled
into a shared directory and symlinked for each image type to save disk space.
@@ -52,7 +51,6 @@
-
Warning: Existing files in the target Deploy directory with the
same names will be overwritten. This operation may take several minutes for large
@@ -61,19 +59,18 @@
{% else %}
-
No Upload Content Found
Map \\10.9.100.1\image-upload on your Windows PC and copy
the Deploy directory contents there.
{% endif %}
@@ -83,7 +80,7 @@
- Import from USB Drive
+ Import from USB Drive
{% if usb_mounts %}
@@ -116,7 +113,6 @@
-
Warning: Existing files in the target Deploy directory with the
same names will be overwritten. This operation may take several minutes for large
@@ -125,19 +121,18 @@
{% else %}
-
No USB Drives Detected
No mounted USB drives were found under /mnt/ or /media/.
Mount a USB drive and refresh this page.