Shopfloor PC type system, webapp enhancements, slim Blancco GRUB

- Shopfloor PC type menu (CMM, WaxAndTrace, Keyence, Genspect, Display, Standard)
- Baseline scripts: OpenText CSF, Start Menu shortcuts, network/WinRM, power/display
- Standard type: eDNC + MarkZebra with 64-bit path mirroring
- CMM type: Hexagon CLM Tools, PC-DMIS 2016/2019 R2
- Display sub-type: Lobby vs Dashboard
- Webapp: enrollment management, image config editor, UI refresh
- Upload-Image.ps1: robocopy MCL cache to PXE server
- Download-Drivers.ps1: Dell driver download pipeline
- Slim Blancco GRUB EFI (10MB -> 660KB) for old hardware compat
- Shopfloor display imaging guide docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-03-26 11:25:07 -04:00
parent 6d0e6ee284
commit 76165495ff
49 changed files with 4304 additions and 147 deletions

230
Download-Drivers.ps1 Normal file
View File

@@ -0,0 +1,230 @@
#
# Download-Drivers.ps1 — Download selected hardware drivers from GE CDN
#
# Reads user_selections.json and HardwareDriver.json from the MCL cache
# to download only the driver packs for your selected hardware models.
# Bypasses Media Creator Lite's unreliable download mechanism.
#
# Downloads go into the MCL cache structure so Upload-Image.ps1 can
# upload them with -IncludeDrivers.
#
# Usage:
# .\Download-Drivers.ps1 (download all selected models)
# .\Download-Drivers.ps1 -ListOnly (show what would be downloaded)
# .\Download-Drivers.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
# .\Download-Drivers.ps1 -Force (re-download even if already cached)
#
# Requires internet access. Run on the workstation, not the PXE server.
#
param(
[string]$CachePath = "C:\ProgramData\GEAerospace\MediaCreator\Cache",
[switch]$ListOnly,
[switch]$Force
)
function Format-Size {
param([long]$Bytes)
if ($Bytes -ge 1GB) { return "{0:N1} GB" -f ($Bytes / 1GB) }
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
return "{0:N0} KB" -f ($Bytes / 1KB)
}
function Resolve-DestDir {
param([string]$Dir)
return ($Dir -replace '^\*destinationdir\*\\?', '')
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " PXE Driver Downloader" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# --- Validate paths ---
$DeployPath = Join-Path $CachePath "Deploy"
$ControlPath = Join-Path $DeployPath "Control"
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
if (-not (Test-Path $ToolsPath -PathType Container)) {
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
}
if (-not (Test-Path $ControlPath -PathType Container)) {
Write-Host "ERROR: Deploy\Control not found at $ControlPath" -ForegroundColor Red
Write-Host " Run Media Creator Lite first to cache the base content." -ForegroundColor Yellow
exit 1
}
# --- Parse user_selections.json ---
$SelectionsFile = Join-Path $ToolsPath "user_selections.json"
if (-not (Test-Path $SelectionsFile)) {
Write-Host "ERROR: user_selections.json not found at $SelectionsFile" -ForegroundColor Red
exit 1
}
$selections = (Get-Content $SelectionsFile -Raw | ConvertFrom-Json)[0]
$selectedOsId = $selections.OperatingSystemSelection
$selectedModelIds = @($selections.HardwareModelSelection | ForEach-Object { $_.Id } | Select-Object -Unique)
# --- Parse HardwareDriver.json ---
$driverJsonFile = Join-Path $ControlPath "HardwareDriver.json"
if (-not (Test-Path $driverJsonFile)) {
Write-Host "ERROR: HardwareDriver.json not found in $ControlPath" -ForegroundColor Red
exit 1
}
$driverJson = Get-Content $driverJsonFile -Raw | ConvertFrom-Json
# --- Match drivers to selections ---
$matchedDrivers = @($driverJson | Where-Object {
$selectedModelIds -contains $_.family -and $_.aOsIds -contains $selectedOsId
})
# Deduplicate by DestinationDir (some models share a driver pack)
$uniqueDrivers = [ordered]@{}
foreach ($drv in $matchedDrivers) {
$rel = Resolve-DestDir $drv.DestinationDir
if (-not $uniqueDrivers.Contains($rel)) {
$uniqueDrivers[$rel] = $drv
}
}
$totalSize = [long]0
$uniqueDrivers.Values | ForEach-Object { $totalSize += $_.size }
# --- Display plan ---
Write-Host " Cache: $CachePath"
Write-Host " OS ID: $selectedOsId"
Write-Host " Models: $($selectedModelIds.Count) selected"
Write-Host " Drivers: $($uniqueDrivers.Count) unique pack(s) ($(Format-Size $totalSize))" -ForegroundColor Cyan
Write-Host ""
if ($uniqueDrivers.Count -eq 0) {
Write-Host "No drivers match your selections." -ForegroundColor Yellow
exit 0
}
# Show each driver pack
$idx = 0
foreach ($rel in $uniqueDrivers.Keys) {
$idx++
$drv = $uniqueDrivers[$rel]
$localDir = Join-Path $CachePath $rel
$cached = Test-Path $localDir -PathType Container
$status = if ($cached -and -not $Force) { "[CACHED]" } else { "[DOWNLOAD]" }
$color = if ($cached -and -not $Force) { "Green" } else { "Yellow" }
Write-Host (" {0,3}. {1,-12} {2} ({3})" -f $idx, $status, $drv.modelsfriendlyname, (Format-Size $drv.size)) -ForegroundColor $color
Write-Host " $($drv.FileName)" -ForegroundColor Gray
}
Write-Host ""
if ($ListOnly) {
Write-Host " (list only — run without -ListOnly to download)" -ForegroundColor Gray
exit 0
}
# --- Download and extract ---
$downloadDir = Join-Path $env:TEMP "PXE-DriverDownloads"
if (-not (Test-Path $downloadDir)) { New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null }
$completed = 0
$skipped = 0
$errors = 0
foreach ($rel in $uniqueDrivers.Keys) {
$drv = $uniqueDrivers[$rel]
$localDir = Join-Path $CachePath $rel
$zipFile = Join-Path $downloadDir $drv.FileName
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[$($completed + $skipped + $errors + 1)/$($uniqueDrivers.Count)] $($drv.modelsfriendlyname)" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Skip if already cached (unless -Force)
if ((Test-Path $localDir -PathType Container) -and -not $Force) {
Write-Host " Already cached at $rel" -ForegroundColor Green
$skipped++
Write-Host ""
continue
}
# Download
Write-Host " Downloading $(Format-Size $drv.size) ..." -ForegroundColor Gray
Write-Host " URL: $($drv.url)" -ForegroundColor DarkGray
try {
# Use curl.exe for progress display on large files
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
& curl.exe -L -o $zipFile $drv.url --progress-bar --fail
if ($LASTEXITCODE -ne 0) { throw "curl failed with exit code $LASTEXITCODE" }
} else {
# Fallback: WebClient (streams to disk, no buffering)
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($drv.url, $zipFile)
}
} catch {
Write-Host " ERROR: Download failed - $_" -ForegroundColor Red
$errors++
Write-Host ""
continue
}
# Verify SHA256 hash
Write-Host " Verifying SHA256 hash ..." -ForegroundColor Gray
$actualHash = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash
if ($actualHash -ne $drv.hash) {
Write-Host " ERROR: Hash mismatch!" -ForegroundColor Red
Write-Host " Expected: $($drv.hash)" -ForegroundColor Red
Write-Host " Got: $actualHash" -ForegroundColor Red
Remove-Item -Path $zipFile -Force -ErrorAction SilentlyContinue
$errors++
Write-Host ""
continue
}
Write-Host " Hash OK." -ForegroundColor Green
# Extract to cache destination
Write-Host " Extracting to $rel ..." -ForegroundColor Gray
if (Test-Path $localDir) { Remove-Item -Recurse -Force $localDir -ErrorAction SilentlyContinue }
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
try {
Expand-Archive -Path $zipFile -DestinationPath $localDir -Force
} catch {
Write-Host " ERROR: Extraction failed - $_" -ForegroundColor Red
$errors++
Write-Host ""
continue
}
# Clean up zip
Remove-Item -Path $zipFile -Force -ErrorAction SilentlyContinue
Write-Host " Done." -ForegroundColor Green
$completed++
Write-Host ""
}
# --- Summary ---
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Download Summary" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Downloaded: $completed" -ForegroundColor Green
if ($skipped -gt 0) { Write-Host " Skipped: $skipped (already cached)" -ForegroundColor Gray }
if ($errors -gt 0) { Write-Host " Failed: $errors" -ForegroundColor Red }
Write-Host ""
if ($completed -gt 0 -or $skipped -gt 0) {
Write-Host "Driver packs are in the MCL cache at:" -ForegroundColor Cyan
Write-Host " $DeployPath\Out-of-box Drivers\" -ForegroundColor White
Write-Host ""
Write-Host "To upload to the PXE server:" -ForegroundColor Cyan
Write-Host " .\Upload-Image.ps1 -IncludeDrivers" -ForegroundColor White
Write-Host ""
}
# Clean up temp dir if empty
if ((Get-ChildItem $downloadDir -Force -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0) {
Remove-Item $downloadDir -Force -ErrorAction SilentlyContinue
}

View File

@@ -1,11 +1,12 @@
#
# Upload-Image.ps1 — Copy Media Creator Lite cached image to the PXE server
# Upload-Image.ps1 — Copy MCL cached image to the PXE server
#
# Copies Deploy/, Tools/, and Sources (from Boot/Sources.zip) to the
# PXE server's image-upload share using robocopy with authentication.
# Reads user_selections.json to upload only the selected OS, matching
# packages, and config files. Drivers are EXCLUDED by default.
#
# Usage:
# .\Upload-Image.ps1 (uses default MCL cache path)
# .\Upload-Image.ps1 (selected OS + packages, no drivers)
# .\Upload-Image.ps1 -IncludeDrivers (also upload selected hardware drivers)
# .\Upload-Image.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
# .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP)
#
@@ -18,11 +19,23 @@ param(
[string]$Server = "10.9.100.1",
[string]$User = "pxe-upload",
[string]$Pass = "pxe",
[switch]$IncludeDell10
[switch]$IncludeDrivers
)
$Share = "\\$Server\image-upload"
function Format-Size {
param([long]$Bytes)
if ($Bytes -ge 1GB) { return "{0:N1} GB" -f ($Bytes / 1GB) }
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
return "{0:N0} KB" -f ($Bytes / 1KB)
}
function Resolve-DestDir {
param([string]$Dir)
return ($Dir -replace '^\*destinationdir\*\\?', '')
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan
@@ -32,11 +45,6 @@ Write-Host ""
# --- Validate source paths ---
$DeployPath = Join-Path $CachePath "Deploy"
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
# Tools is a sibling of Cache in the MCL directory structure
if (-not (Test-Path $ToolsPath -PathType Container)) {
# Fallback: try Tools inside CachePath parent's parent
$ToolsPath = Join-Path (Split-Path (Split-Path $CachePath -Parent) -Parent) "Tools"
}
if (-not (Test-Path $ToolsPath -PathType Container)) {
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
}
@@ -44,24 +52,104 @@ $SourcesZip = Join-Path $CachePath "Boot\Sources.zip"
if (-not (Test-Path $DeployPath -PathType Container)) {
Write-Host "ERROR: Deploy directory not found at $DeployPath" -ForegroundColor Red
Write-Host " Provide the correct cache path: .\Upload-Image.ps1 -CachePath ""D:\Path\To\Cache""" -ForegroundColor Yellow
Write-Host " .\Upload-Image.ps1 -CachePath ""D:\Path\To\Cache""" -ForegroundColor Yellow
exit 1
}
Write-Host " Cache Path: $CachePath"
Write-Host " Deploy: $DeployPath" -ForegroundColor $(if (Test-Path $DeployPath) { "Green" } else { "Red" })
Write-Host " Tools: $ToolsPath" -ForegroundColor $(if (Test-Path $ToolsPath) { "Green" } else { "Yellow" })
Write-Host " Sources.zip: $SourcesZip" -ForegroundColor $(if (Test-Path $SourcesZip) { "Green" } else { "Yellow" })
Write-Host " Server: $Server"
if (-not $IncludeDell10) {
Write-Host " Excluding: Dell_10 drivers (use -IncludeDell10 to include)" -ForegroundColor Gray
# --- Parse user_selections.json ---
$SelectionsFile = Join-Path $ToolsPath "user_selections.json"
if (-not (Test-Path $SelectionsFile)) {
Write-Host "ERROR: user_selections.json not found at $SelectionsFile" -ForegroundColor Red
Write-Host " Run Media Creator Lite first to create a configuration." -ForegroundColor Yellow
exit 1
}
$selections = (Get-Content $SelectionsFile -Raw | ConvertFrom-Json)[0]
$selectedOsId = $selections.OperatingSystemSelection
$selectedModelIds = @($selections.HardwareModelSelection | ForEach-Object { $_.Id } | Select-Object -Unique)
# --- Parse control JSONs ---
$ControlPath = Join-Path $DeployPath "Control"
$osJsonFile = Join-Path $ControlPath "OperatingSystem.json"
$driverJsonFile = Join-Path $ControlPath "HardwareDriver.json"
$pkgJsonFile = Join-Path $ControlPath "packages.json"
if (-not (Test-Path $osJsonFile)) {
Write-Host "ERROR: OperatingSystem.json not found in $ControlPath" -ForegroundColor Red
exit 1
}
$osJson = Get-Content $osJsonFile -Raw | ConvertFrom-Json
$driverJson = if (Test-Path $driverJsonFile) { Get-Content $driverJsonFile -Raw | ConvertFrom-Json } else { @() }
$pkgJson = if (Test-Path $pkgJsonFile) { Get-Content $pkgJsonFile -Raw | ConvertFrom-Json } else { @() }
# --- Resolve selections to paths ---
# OS: match OperatingSystemSelection ID to OperatingSystem.json entries
$matchedOs = @($osJson | Where-Object { $_.operatingSystemVersion.id -eq [int]$selectedOsId })
$osDirs = @()
$osTotalSize = [long]0
foreach ($os in $matchedOs) {
$rel = Resolve-DestDir $os.operatingSystemVersion.wim.DestinationDir
$osDirs += $rel
$osTotalSize += $os.operatingSystemVersion.wim.size
}
# Packages: enabled + matching OS ID
$matchedPkgs = @($pkgJson | Where-Object { $_.aOsIds -contains $selectedOsId -and $_.enabled -eq 1 })
$pkgTotalSize = [long]0
foreach ($pkg in $matchedPkgs) { $pkgTotalSize += $pkg.size }
# Drivers: match selected model IDs (family) + OS ID, deduplicate by path
$allMatchingDrivers = @($driverJson | Where-Object {
$selectedModelIds -contains $_.family -and $_.aOsIds -contains $selectedOsId
})
$allDriverDirSet = [ordered]@{}
foreach ($drv in $allMatchingDrivers) {
$rel = Resolve-DestDir $drv.DestinationDir
if (-not $allDriverDirSet.Contains($rel)) { $allDriverDirSet[$rel] = $drv.size }
}
$allDriverCount = $allDriverDirSet.Count
$allDriverTotalSize = [long]0
$allDriverDirSet.Values | ForEach-Object { $allDriverTotalSize += $_ }
$driverDirs = @()
$driverTotalSize = [long]0
if ($IncludeDrivers) {
$driverDirs = @($allDriverDirSet.Keys)
$driverTotalSize = $allDriverTotalSize
}
# --- Display upload plan ---
Write-Host " Cache: $CachePath"
Write-Host " Server: $Server"
Write-Host ""
Write-Host " Upload Plan (from user_selections.json):" -ForegroundColor Cyan
Write-Host " ------------------------------------------"
if ($matchedOs.Count -gt 0) {
$osName = $matchedOs[0].operatingSystemVersion.marketingName
Write-Host " OS: $osName ($(Format-Size $osTotalSize))" -ForegroundColor Green
} else {
Write-Host " OS: No match for selection ID $selectedOsId" -ForegroundColor Red
}
Write-Host " Packages: $($matchedPkgs.Count) update(s) ($(Format-Size $pkgTotalSize))" -ForegroundColor Green
if ($IncludeDrivers) {
Write-Host " Drivers: $($driverDirs.Count) model(s) ($(Format-Size $driverTotalSize))" -ForegroundColor Green
} else {
Write-Host " Drivers: SKIPPED -- $allDriverCount available, use -IncludeDrivers" -ForegroundColor Yellow
}
Write-Host " Control: Always included" -ForegroundColor Gray
Write-Host " Tools: $(if (Test-Path $ToolsPath) { 'Yes' } else { 'Not found' })" -ForegroundColor $(if (Test-Path $ToolsPath) { "Gray" } else { "Yellow" })
Write-Host " Sources: $(if (Test-Path $SourcesZip) { 'Yes (from Boot\Sources.zip)' } else { 'Not found' })" -ForegroundColor $(if (Test-Path $SourcesZip) { "Gray" } else { "Yellow" })
Write-Host ""
# --- Connect to SMB share ---
Write-Host "Connecting to $Share ..." -ForegroundColor Gray
# Remove any stale connection
net use $Share /delete 2>$null | Out-Null
$netResult = net use $Share /user:$User $Pass 2>&1
@@ -79,47 +167,135 @@ Write-Host "Connected." -ForegroundColor Green
Write-Host ""
$failed = $false
$stepNum = 0
$totalSteps = 1 # Deploy base always
if ($matchedOs.Count -gt 0) { $totalSteps++ }
if ($matchedPkgs.Count -gt 0) { $totalSteps++ }
if ($IncludeDrivers -and $driverDirs.Count -gt 0) { $totalSteps++ }
if (Test-Path $ToolsPath -PathType Container) { $totalSteps++ }
if (Test-Path $SourcesZip) { $totalSteps++ }
# --- Step 1: Copy Deploy/ ---
# --- Step: Deploy base (Control, Applications, config -- skip big dirs) ---
$stepNum++
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[1/3] Copying Deploy/ ..." -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Copying Deploy\ base (Control, Applications, config) ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$robocopyArgs = @($DeployPath, "$Share\Deploy", "/E", "/R:3", "/W:5", "/NP", "/ETA")
if (-not $IncludeDell10) {
$robocopyArgs += @("/XD", "Dell_10")
}
& robocopy @robocopyArgs
robocopy $DeployPath "$Share\Deploy" /E /XD "Operating Systems" "Out-of-box Drivers" "Packages" /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Deploy copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
Write-Host "ERROR: Deploy base copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
# --- Step 2: Copy Tools/ ---
# --- Step: Operating System ---
if ($matchedOs.Count -gt 0) {
$stepNum++
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[2/3] Copying Tools/ ..." -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Copying Operating System ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
foreach ($osDir in $osDirs) {
$src = Join-Path $CachePath $osDir
$dst = Join-Path $Share $osDir
if (Test-Path $src -PathType Container) {
Write-Host " $osDir" -ForegroundColor Gray
robocopy $src $dst /E /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: OS copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
} else {
Write-Host " SKIPPED (not cached): $osDir" -ForegroundColor Yellow
}
}
}
# --- Step: Packages ---
if ($matchedPkgs.Count -gt 0) {
$stepNum++
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Copying Packages ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Group packages by destination directory for efficient robocopy
$pkgGroups = [ordered]@{}
foreach ($pkg in $matchedPkgs) {
$rel = Resolve-DestDir $pkg.destinationDir
if (-not $pkgGroups.Contains($rel)) { $pkgGroups[$rel] = @() }
$pkgGroups[$rel] += $pkg.fileName
}
foreach ($dir in $pkgGroups.Keys) {
$src = Join-Path $CachePath $dir
$dst = Join-Path $Share $dir
$files = $pkgGroups[$dir]
if (Test-Path $src -PathType Container) {
foreach ($f in $files) { Write-Host " $f" -ForegroundColor Gray }
$robocopyArgs = @($src, $dst) + $files + @("/R:3", "/W:5", "/NP", "/ETA")
& robocopy @robocopyArgs
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Package copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
} else {
Write-Host " SKIPPED (not cached): $dir" -ForegroundColor Yellow
}
}
}
# --- Step: Drivers (only with -IncludeDrivers) ---
if ($IncludeDrivers -and $driverDirs.Count -gt 0) {
$stepNum++
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Copying Drivers ($($driverDirs.Count) model(s)) ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$drvCopied = 0
foreach ($drvDir in $driverDirs) {
$drvCopied++
$src = Join-Path $CachePath $drvDir
$dst = Join-Path $Share $drvDir
if (Test-Path $src -PathType Container) {
Write-Host " [$drvCopied/$($driverDirs.Count)] $drvDir" -ForegroundColor Gray
robocopy $src $dst /E /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Driver copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
} else {
Write-Host " SKIPPED (not cached): $drvDir" -ForegroundColor Yellow
}
}
}
# --- Step: Tools ---
if (Test-Path $ToolsPath -PathType Container) {
$stepNum++
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Copying Tools\ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
robocopy $ToolsPath "$Share\Tools" /E /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
} else {
Write-Host "SKIPPED: Tools directory not found at $ToolsPath" -ForegroundColor Yellow
}
# --- Step 3: Extract and copy Sources/ ---
# --- Step: Sources ---
$TempSources = $null
if (Test-Path $SourcesZip) {
$stepNum++
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[3/3] Extracting and copying Sources/ ..." -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Extracting and copying Sources\ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if (Test-Path $SourcesZip) {
$TempExtract = Join-Path $env:TEMP "SourcesExtract"
Write-Host " Extracting Sources.zip..."
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force
@@ -135,10 +311,49 @@ if (Test-Path $SourcesZip) {
$failed = $true
}
# Clean up temp extraction
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
}
# --- Verify small files (SMB write-cache workaround) ---
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Verifying small files ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$fixCount = 0
$verifyPairs = @(
@{ Local = (Join-Path $DeployPath "Control"); Remote = "$Share\Deploy\Control" }
)
if (Test-Path $ToolsPath -PathType Container) {
$verifyPairs += @{ Local = $ToolsPath; Remote = "$Share\Tools" }
}
foreach ($pair in $verifyPairs) {
$localDir = $pair.Local
$remoteDir = $pair.Remote
if (-not (Test-Path $localDir -PathType Container)) { continue }
Get-ChildItem -Path $localDir -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 0 -and $_.Length -lt 1MB } |
ForEach-Object {
$rel = $_.FullName.Substring($localDir.Length)
$dstFile = Join-Path $remoteDir $rel
if (Test-Path $dstFile) {
$dstSize = (Get-Item $dstFile).Length
if ($dstSize -ne $_.Length) {
Write-Host " Fixing: $rel ($dstSize -> $($_.Length) bytes)" -ForegroundColor Yellow
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
[System.IO.File]::WriteAllBytes($dstFile, $bytes)
$fixCount++
}
}
}
}
if ($fixCount -eq 0) {
Write-Host " All files verified OK." -ForegroundColor Green
} else {
Write-Host "SKIPPED: Sources.zip not found at $SourcesZip" -ForegroundColor Yellow
Write-Host " Fixed $fixCount file(s)." -ForegroundColor Yellow
}
# --- Disconnect ---

File diff suppressed because one or more lines are too long

View File

@@ -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.

View File

@@ -87,6 +87,16 @@
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
<Description>Bypass OOBE network requirement</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>14</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipMachineOOBE /t REG_DWORD /d 1 /f</Path>
<Description>Skip machine OOBE phase</Description>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>15</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipUserOOBE /t REG_DWORD /d 1 /f</Path>
<Description>Skip user OOBE phase</Description>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
@@ -151,11 +161,16 @@
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>5</Order>
<CommandLine>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.'"</CommandLine>
<Description>Disable wired adapters and wait for WiFi internet</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>6</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
<Description>Run GCCH Enrollment</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>6</Order>
<Order>7</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
<Description>Run shopfloor PC type setup</Description>
</SynchronousCommand>

View File

@@ -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

View File

@@ -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

View File

@@ -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 ==="

View File

@@ -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')"

View File

@@ -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 ==="

View File

@@ -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 ==="

View File

@@ -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 ==="

View File

@@ -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

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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."

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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=<List Of Windows>
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

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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 ==="

View File

@@ -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 ==="

View File

@@ -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

View File

@@ -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%\"

177
sync_hardware_models.py Normal file
View File

@@ -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()

View File

@@ -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,9 +415,10 @@ 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)
# 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)
@@ -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/<image_type>/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/<image_type>/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/<filename>")
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/<filename>", 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)
# ---------------------------------------------------------------------------

View File

@@ -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 =
'<td class="order-num">' + (idx + 1) + '</td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_name_' + idx + '" value="" placeholder="Username"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_password_' + idx + '" value="" placeholder="Password"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_group_' + idx + '" value="Administrators"></td>' +
'<td><input type="text" class="form-control form-control-sm" name="account_display_' + idx + '" value="" placeholder="Display Name"></td>' +
'<td>' +
'<input type="hidden" name="account_plaintext_' + idx + '" value="true">' +
'<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">Remove</button>' +
'</td>';
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 =
'<td class="order-num">' + idx + '</td>' +
'<td><input type="text" class="form-control form-control-sm" data-field="Model" value="" placeholder="Model name"></td>' +
'<td><input type="text" class="form-control form-control-sm" data-field="Id" value="" placeholder="Driver Family ID"></td>' +
'<td><span class="badge bg-secondary badge-disk">New</span></td>' +
'<td><button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button></td>';
tbody.appendChild(tr);
var empty = document.getElementById('hwModelsEmpty');
if (empty) empty.style.display = 'none';
tr.querySelector('input').focus();
});
}
});

View File

@@ -3,13 +3,13 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-journal-text me-2"></i>Audit Log</h2>
<h2 class="mb-0">Audit Log</h2>
<span class="badge bg-secondary fs-6">{{ entries|length }} entries</span>
</div>
<div class="card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-clock-history me-2"></i> Activity History
Activity History
</div>
<div class="card-body p-0">
{% if entries %}
@@ -52,7 +52,6 @@
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-journal-text" style="font-size: 3rem;"></i>
<p class="mt-2">No audit log entries yet.</p>
<p class="small">Actions like image imports, unattend edits, and backup operations will be logged here.</p>
</div>

View File

@@ -3,15 +3,15 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-archive me-2"></i>Clonezilla Backups</h2>
<h2 class="mb-0">Clonezilla Backups</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
<i class="bi bi-upload me-1"></i> Upload Backup
Upload Backup
</button>
</div>
<div class="card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-hdd-stack me-2"></i> Machine Backups
Machine Backups
<span class="badge bg-secondary ms-2">{{ backups|length }}</span>
</div>
<div class="card-body p-0">
@@ -36,12 +36,12 @@
<td class="text-end text-nowrap">
<a href="{{ url_for('clonezilla_download', filename=b.filename) }}"
class="btn btn-sm btn-outline-primary" title="Download">
<i class="bi bi-download"></i>
Download
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-filename="{{ b.filename }}" data-machine="{{ b.machine }}" title="Delete">
<i class="bi bi-trash"></i>
Delete
</button>
</td>
</tr>
@@ -50,7 +50,6 @@
</table>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-archive" style="font-size: 3rem;"></i>
<p class="mt-2">No backups found. Upload a Clonezilla backup .zip to get started.</p>
</div>
{% endif %}
@@ -59,7 +58,7 @@
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Backup Naming Convention</h6>
<h6 class="card-title">Backup Naming Convention</h6>
<p class="card-text mb-0">
Name backup files with the machine number (e.g., <code>6501.zip</code>).
The Samba share <code>\\pxe-server\clonezilla</code> is also available on the network for direct Clonezilla save/restore operations.
@@ -74,7 +73,7 @@
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
<h5 class="modal-title">Upload Backup</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
@@ -89,7 +88,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary"><i class="bi bi-upload me-1"></i> Upload</button>
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div>
@@ -103,7 +102,7 @@
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
@@ -112,7 +111,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger"><i class="bi bi-trash me-1"></i> Delete</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>

View File

@@ -7,7 +7,6 @@
<title>{% block title %}PXE Server Manager{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='bootstrap-icons.min.css') }}" rel="stylesheet">
<style>
:root {
--sidebar-width: 280px;
@@ -36,10 +35,6 @@
color: #fff;
background-color: rgba(255,255,255,0.08);
}
.sidebar .nav-link .bi {
margin-right: 0.5rem;
font-size: 1rem;
}
.sidebar-heading {
font-size: 0.7rem;
text-transform: uppercase;
@@ -59,10 +54,6 @@
align-items: center;
gap: 0.3rem;
}
.sidebar .brand .bi {
font-size: 1.3rem;
color: #0d6efd;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
@@ -121,13 +112,13 @@
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
href="{{ url_for('dashboard') }}">
<i class="bi bi-speedometer2"></i> Dashboard
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'images_import' %}active{% endif %}"
href="{{ url_for('images_import') }}">
<i class="bi bi-download"></i> Image Import
Image Import
</a>
</li>
</ul>
@@ -138,25 +129,31 @@
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
href="{{ url_for('startnet_editor') }}">
<i class="bi bi-terminal"></i> startnet.cmd
startnet.cmd
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
href="{{ url_for('clonezilla_backups') }}">
<i class="bi bi-archive"></i> Clonezilla Backups
Clonezilla Backups
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'blancco_reports' %}active{% endif %}"
href="{{ url_for('blancco_reports') }}">
<i class="bi bi-shield-check"></i> Blancco Reports
Blancco Reports
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
href="{{ url_for('enrollment') }}">
Enrollment
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'audit_log' %}active{% endif %}"
href="{{ url_for('audit_log') }}">
<i class="bi bi-journal-text"></i> Audit Log
Audit Log
</a>
</li>
</ul>
@@ -168,7 +165,13 @@
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
href="{{ url_for('unattend_editor', image_type=it) }}">
<i class="bi bi-file-earmark-code"></i> {{ all_friendly_names[it] }}
{{ all_friendly_names[it] }}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
Configuration
</a>
</li>
{% endfor %}
@@ -181,7 +184,13 @@
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
href="{{ url_for('unattend_editor', image_type=it) }}">
<i class="bi bi-file-earmark-code"></i> {{ all_friendly_names[it] }}
{{ all_friendly_names[it] }}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
Configuration
</a>
</li>
{% endfor %}

View File

@@ -5,14 +5,14 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Dashboard</h2>
<button class="btn btn-outline-secondary btn-sm" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh
Refresh
</button>
</div>
<!-- Services -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center">
<i class="bi bi-gear-wide-connected me-2"></i> PXE Services
PXE Services
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
@@ -27,7 +27,6 @@
{% for svc in services %}
<tr>
<td>
<i class="bi bi-server me-1 text-muted"></i>
<strong>{{ svc.name }}</strong>
</td>
<td>
@@ -45,7 +44,7 @@
<!-- Images -->
<div class="card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-disc me-2"></i> Deployment Images
Deployment Images
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
@@ -67,23 +66,27 @@
</td>
<td>
{% if img.has_content %}
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Present</span>
<span class="badge bg-success">Present</span>
{% else %}
<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Empty</span>
<span class="badge bg-secondary">Empty</span>
{% endif %}
</td>
<td>
{% if img.has_unattend %}
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Exists</span>
<span class="badge bg-success">Exists</span>
{% else %}
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Missing</span>
<span class="badge bg-warning text-dark">Missing</span>
{% endif %}
</td>
<td><code class="small">{{ img.deploy_path }}</code></td>
<td class="text-end">
<a href="{{ url_for('image_config', image_type=img.image_type) }}"
class="btn btn-sm btn-outline-secondary me-1">
Config
</a>
<a href="{{ url_for('unattend_editor', image_type=img.image_type) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil-square"></i> Edit Unattend
Edit Unattend
</a>
</td>
</tr>

View File

@@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block title %}Enrollment Packages - PXE Server Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Enrollment Packages</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
Upload Package
</button>
</div>
<div class="card">
<div class="card-header d-flex align-items-center">
GCCH Provisioning Packages
<span class="badge bg-secondary ms-2">{{ packages|length }}</span>
</div>
<div class="card-body p-0">
{% if packages %}
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Filename</th>
<th>Size</th>
<th>Last Modified</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for p in packages %}
<tr>
<td><code>{{ p.filename }}</code></td>
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
<td>{{ p.modified | timestamp_fmt }}</td>
<td class="text-end text-nowrap">
<a href="{{ url_for('enrollment_download', filename=p.filename) }}"
class="btn btn-sm btn-outline-primary" title="Download">
Download
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-filename="{{ p.filename }}" title="Delete">
Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-center text-muted py-5">
<p class="mt-2">No enrollment packages found. Upload a <code>.ppkg</code> file to get started.</p>
</div>
{% endif %}
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title">About Enrollment Packages</h6>
<p class="card-text">
GCCH enrollment <code>.ppkg</code> provisioning packages are copied to
<code>C:\Enrollment\</code> on the target machine after imaging.
At OOBE, connect to a network with internet, press <strong>Windows key 5 times</strong>,
then browse to <code>C:\Enrollment\</code> and select the package. No USB stick needed.
</p>
<p class="card-text mb-0">
<strong>Naming convention:</strong> Use <code>with-office.ppkg</code> and
<code>without-office.ppkg</code> to match the WinPE enrollment menu options.
</p>
</div>
</div>
<!-- Upload Modal -->
<div class="modal fade" id="uploadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('enrollment_upload') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title">Upload Enrollment Package</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="ppkgFile" class="form-label">Provisioning Package (.ppkg)</label>
<input type="file" class="form-control" id="ppkgFile" name="ppkg_file"
accept=".ppkg" required>
<div class="form-text">
Use <code>with-office.ppkg</code> or <code>without-office.ppkg</code> to match the WinPE boot menu.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <strong id="deleteFilename"></strong>?</p>
<p class="text-muted mb-0">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.getElementById('deleteModal').addEventListener('show.bs.modal', function (event) {
var btn = event.relatedTarget;
var filename = btn.getAttribute('data-filename');
document.getElementById('deleteFilename').textContent = filename;
document.getElementById('deleteForm').action = '/enrollment/delete/' + encodeURIComponent(filename);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,331 @@
{% extends "base.html" %}
{% block title %}{{ friendly_name }} - Configuration{% endblock %}
{% block extra_head %}
<style>
.section-card { margin-bottom: 1.5rem; }
.section-card .card-header { padding: 0.6rem 1rem; font-size: 0.95rem; }
.badge-disk { font-size: 0.75rem; }
.orphan-section { background-color: #fff8e1; }
.config-table td, .config-table th { vertical-align: middle; }
.config-table .form-control-sm { min-width: 120px; }
.text-truncate-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1">{{ friendly_name }}</h2>
<small class="text-muted">
Image Configuration
&mdash; OS Selection: <strong>{{ config.os_selection or 'Not set' }}</strong>
</small>
</div>
<a href="{{ url_for('unattend_editor', image_type=image_type) }}" class="btn btn-outline-secondary btn-sm">
Edit Unattend
</a>
</div>
{# ==================== SECTION 1: Hardware Models ==================== #}
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Hardware Models
<span class="badge bg-secondary ms-1">{{ config.hardware_models|length }}</span>
</span>
<div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addHwModel">
Add
</button>
<button type="button" class="btn btn-sm btn-success ms-1" id="saveHwModels">
Save
</button>
</div>
</div>
<div class="card-body p-0">
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="hwModelsForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="section" value="hardware_models">
<input type="hidden" name="payload" id="hwModelsData" value="[]">
</form>
<table class="table table-sm table-hover mb-0 config-table" id="hwModelsTable">
<thead class="table-light">
<tr>
<th style="width:40px">#</th>
<th>Model</th>
<th>Driver Family ID</th>
<th style="width:90px">On Disk</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
{% for hm in config.hardware_models %}
<tr>
<td class="order-num">{{ loop.index }}</td>
<td><input type="text" class="form-control form-control-sm" data-field="Model" value="{{ hm.Model }}"></td>
<td><input type="text" class="form-control form-control-sm" data-field="Id" value="{{ hm.Id }}"></td>
<td>
{% if hm._on_disk %}
<span class="badge bg-success badge-disk">Yes</span>
{% else %}
<span class="badge bg-danger badge-disk">No</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not config.hardware_models %}
<div class="text-center text-muted py-3 empty-message" id="hwModelsEmpty">
No hardware models configured.
</div>
{% endif %}
</div>
</div>
{# ==================== SECTION 2: Driver Packs ==================== #}
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Driver Packs
<span class="badge bg-secondary ms-1">{{ config.drivers|length }}</span>
</span>
<div>
<button type="button" class="btn btn-sm btn-success" id="saveDrivers">
Save
</button>
</div>
</div>
<div class="card-body p-0">
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="driversForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="section" value="drivers">
<input type="hidden" name="payload" id="driversData" value="[]">
</form>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 config-table" id="driversTable">
<thead class="table-light">
<tr>
<th style="width:40px">#</th>
<th>Family</th>
<th>Models</th>
<th>File Name</th>
<th style="width:70px">OS IDs</th>
<th style="width:90px">On Disk</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
{% for drv in config.drivers %}
<tr data-json='{{ drv | tojson }}'>
<td class="order-num">{{ loop.index }}</td>
<td class="text-truncate-cell" title="{{ drv.family }}">{{ drv.family }}</td>
<td class="text-truncate-cell" title="{{ drv.models }}">{{ drv.models }}</td>
<td class="text-truncate-cell" title="{{ drv.FileName or drv.get('fileName','') }}">
<small>{{ drv.FileName or drv.get('fileName','') }}</small>
</td>
<td><small>{{ drv.osId }}</small></td>
<td>
{% if drv._on_disk %}
<span class="badge bg-success badge-disk">Yes</span>
{% else %}
<span class="badge bg-danger badge-disk">No</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not config.drivers %}
<div class="text-center text-muted py-3 empty-message" id="driversEmpty">
No driver packs configured.
</div>
{% endif %}
</div>
{# Orphan drivers sub-section #}
{% if config.orphan_drivers %}
<div class="card-footer orphan-section p-0">
<div class="px-3 py-2">
<a class="text-decoration-none" data-bs-toggle="collapse" href="#orphanDrivers" role="button">
<strong>Unregistered Drivers ({{ config.orphan_drivers|length }})</strong>
<small class="text-muted ms-1">zip files on disk not in any JSON</small>
</a>
</div>
<div class="collapse" id="orphanDrivers">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>File Name</th>
<th>Relative Path</th>
</tr>
</thead>
<tbody>
{% for orph in config.orphan_drivers %}
<tr>
<td><small>{{ orph.fileName }}</small></td>
<td><small class="text-muted">{{ orph.relPath }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
{# ==================== SECTION 3: Operating Systems ==================== #}
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Operating Systems
<span class="badge bg-secondary ms-1">{{ config.operating_systems|length }}</span>
</span>
<div>
<button type="button" class="btn btn-sm btn-success" id="saveOs">
Save
</button>
</div>
</div>
<div class="card-body p-0">
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="osForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="section" value="operating_systems">
<input type="hidden" name="payload" id="osData" value="[]">
</form>
<table class="table table-sm table-hover mb-0 config-table" id="osTable">
<thead class="table-light">
<tr>
<th style="width:40px">#</th>
<th>Product Name</th>
<th>Version</th>
<th>Build</th>
<th style="width:60px">ID</th>
<th style="width:70px">Active</th>
<th style="width:90px">WIM On Disk</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
{% for entry in config.operating_systems %}
{% set osv = entry.operatingSystemVersion %}
<tr data-json='{{ entry | tojson }}'>
<td class="order-num">{{ loop.index }}</td>
<td>{{ osv.productName }}</td>
<td>{{ osv.versionNumber }}</td>
<td>{{ osv.buildNumber }}</td>
<td>{{ osv.id }}</td>
<td>
{% if osv.isActive %}
<span class="badge bg-success badge-disk">Active</span>
{% else %}
<span class="badge bg-secondary badge-disk">Inactive</span>
{% endif %}
</td>
<td>
{% if entry._on_disk %}
<span class="badge bg-success badge-disk">Yes</span>
{% else %}
<span class="badge bg-danger badge-disk">No</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not config.operating_systems %}
<div class="text-center text-muted py-3 empty-message" id="osEmpty">
No operating systems configured.
</div>
{% endif %}
</div>
</div>
{# ==================== SECTION 4: Packages ==================== #}
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Packages
<span class="badge bg-secondary ms-1">{{ config.packages|length }}</span>
</span>
<div>
<button type="button" class="btn btn-sm btn-success" id="savePackages">
Save
</button>
</div>
</div>
<div class="card-body p-0">
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="packagesForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="section" value="packages">
<input type="hidden" name="payload" id="packagesData" value="[]">
</form>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 config-table" id="packagesTable">
<thead class="table-light">
<tr>
<th style="width:40px">#</th>
<th>Name</th>
<th>Comment</th>
<th>File</th>
<th style="width:70px">OS IDs</th>
<th style="width:80px">Enabled</th>
<th style="width:90px">On Disk</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
{% for pkg in config.packages %}
<tr data-json='{{ pkg | tojson }}'>
<td class="order-num">{{ loop.index }}</td>
<td class="text-truncate-cell" title="{{ pkg.name }}"><small>{{ pkg.name }}</small></td>
<td class="text-truncate-cell" title="{{ pkg.comment }}"><small>{{ pkg.comment }}</small></td>
<td class="text-truncate-cell" title="{{ pkg.fileName or pkg.get('FileName','') }}">
<small>{{ pkg.fileName or pkg.get('FileName','') }}</small>
</td>
<td><small>{{ pkg.osId }}</small></td>
<td>
{% if pkg.enabled %}
<span class="badge bg-success badge-disk">Yes</span>
{% else %}
<span class="badge bg-secondary badge-disk">No</span>
{% endif %}
</td>
<td>
{% if pkg._on_disk %}
<span class="badge bg-success badge-disk">Yes</span>
{% else %}
<span class="badge bg-danger badge-disk">No</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not config.packages %}
<div class="text-center text-muted py-3 empty-message" id="packagesEmpty">
No packages configured.
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -10,7 +10,7 @@
<!-- Network Upload Import -->
<div class="card mb-3">
<div class="card-header">
<i class="bi bi-cloud-upload me-2"></i> Import from Network Upload
Import from Network Upload
</div>
<div class="card-body">
{% if upload_sources %}
@@ -44,7 +44,6 @@
</div>
<div class="alert alert-info d-flex align-items-start" role="alert">
<i class="bi bi-info-circle-fill me-2 mt-1"></i>
<div>
<strong>Shared Drivers:</strong> 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 @@
</div>
<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
<div>
<strong>Warning:</strong> 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 @@
</div>
<button type="submit" class="btn btn-primary" id="uploadImportBtn">
<i class="bi bi-download me-1"></i> Start Import
Start Import
</button>
</form>
{% else %}
<div class="text-center py-4">
<i class="bi bi-cloud-slash display-4 text-muted"></i>
<h5 class="mt-3 text-muted">No Upload Content Found</h5>
<p class="text-muted mb-0">
Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy
the Deploy directory contents there.
</p>
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh
Refresh
</button>
</div>
{% endif %}
@@ -83,7 +80,7 @@
<!-- USB Import -->
<div class="card">
<div class="card-header">
<i class="bi bi-usb-drive me-2"></i> Import from USB Drive
Import from USB Drive
</div>
<div class="card-body">
{% if usb_mounts %}
@@ -116,7 +113,6 @@
</div>
<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
<div>
<strong>Warning:</strong> 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 @@
</div>
<button type="submit" class="btn btn-primary" id="importBtn">
<i class="bi bi-download me-1"></i> Start Import
Start Import
</button>
</form>
{% else %}
<div class="text-center py-4">
<i class="bi bi-usb-plug display-4 text-muted"></i>
<h5 class="mt-3 text-muted">No USB Drives Detected</h5>
<p class="text-muted mb-0">
No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br>
Mount a USB drive and refresh this page.
</p>
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh
Refresh
</button>
</div>
{% endif %}
@@ -148,7 +143,7 @@
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i> Current Image Status
Current Image Status
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">

View File

@@ -3,13 +3,13 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-file-earmark-check me-2"></i>Blancco Erasure Reports</h2>
<h2 class="mb-0">Blancco Erasure Reports</h2>
<span class="badge bg-secondary fs-6">{{ reports|length }} report{{ 's' if reports|length != 1 }}</span>
</div>
<div class="card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-shield-check me-2"></i> Drive Erasure Certificates
Drive Erasure Certificates
</div>
<div class="card-body p-0">
{% if reports %}
@@ -39,12 +39,12 @@
<td class="text-end text-nowrap">
<a href="{{ url_for('blancco_download_report', filename=r.filename) }}"
class="btn btn-sm btn-outline-primary" title="Download">
<i class="bi bi-download"></i>
Download
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-filename="{{ r.filename }}" title="Delete">
<i class="bi bi-trash"></i>
Delete
</button>
</td>
</tr>
@@ -53,7 +53,6 @@
</table>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-shield-check" style="font-size: 3rem;"></i>
<p class="mt-2">No erasure reports yet.</p>
<p class="small">Reports will appear here after Blancco Drive Eraser completes a wipe.</p>
</div>
@@ -63,7 +62,7 @@
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Report Storage</h6>
<h6 class="card-title">Report Storage</h6>
<p class="card-text mb-1">
Blancco Drive Eraser saves erasure certificates to the network share
<code>\\10.9.100.1\blancco-reports</code>.
@@ -81,7 +80,7 @@
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
@@ -90,7 +89,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger"><i class="bi bi-trash me-1"></i> Delete</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>

View File

@@ -34,12 +34,11 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0"><i class="bi bi-terminal me-2"></i>startnet.cmd Editor</h2>
<h2 class="mb-0">startnet.cmd Editor</h2>
</div>
{% if not wim_exists %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>boot.wim not found</strong> at <code>{{ wim_path }}</code>.
Run the PXE server setup playbook and import WinPE boot files first.
</div>
@@ -49,7 +48,7 @@
<div class="col-lg-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-file-earmark-code me-2"></i>Windows\System32\startnet.cmd</span>
<span>Windows\System32\startnet.cmd</span>
<span class="badge bg-secondary">boot.wim</span>
</div>
<div class="card-body p-0">
@@ -64,14 +63,14 @@
Editing the startnet.cmd inside <code>{{ wim_path }}</code>
</small>
<button type="submit" form="startnetForm" class="btn btn-primary">
<i class="bi bi-floppy me-1"></i> Save to boot.wim
Save to boot.wim
</button>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Common startnet.cmd Commands</h6>
<h6 class="card-title">Common startnet.cmd Commands</h6>
<div class="row">
<div class="col-md-6">
<code class="d-block mb-1">wpeinit</code>
@@ -93,7 +92,7 @@
<div class="col-lg-3">
<div class="card">
<div class="card-header">
<i class="bi bi-info-square me-2"></i>WIM Info
WIM Info
</div>
<div class="card-body wim-info">
<dl class="mb-0">

View File

@@ -43,13 +43,12 @@
<div>
<h2 class="mb-1">{{ friendly_name }}</h2>
<small class="text-muted">
<i class="bi bi-file-earmark-code me-1"></i>
<code>{{ image_type }}/Deploy/FlatUnattendW10.xml</code>
</small>
</div>
<div>
<button type="button" class="btn btn-success" id="saveFormBtn">
<i class="bi bi-floppy me-1"></i> Save
Save
</button>
</div>
</div>
@@ -59,13 +58,13 @@
<li class="nav-item">
<a class="nav-link active" id="form-tab" data-bs-toggle="tab"
href="#formView" role="tab">
<i class="bi bi-ui-checks-grid me-1"></i> Form Editor
Form Editor
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="raw-tab" data-bs-toggle="tab"
href="#rawView" role="tab">
<i class="bi bi-code-slash me-1"></i> Raw XML
Raw XML
</a>
</li>
</ul>
@@ -80,9 +79,9 @@
<!-- 1. Driver Paths -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-motherboard me-1"></i> Driver Paths</span>
<span>Driver Paths</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
<i class="bi bi-plus-lg"></i> Add
Add
</button>
</div>
<div class="card-body p-0">
@@ -104,7 +103,7 @@
</td>
<td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i>
Remove
</button>
</td>
</tr>
@@ -122,7 +121,7 @@
<!-- 2. Machine Settings -->
<div class="card section-card">
<div class="card-header">
<i class="bi bi-pc-display me-1"></i> Machine Settings
Machine Settings
</div>
<div class="card-body">
<div class="row g-3">
@@ -154,9 +153,9 @@
<!-- 3. Specialize Commands (RunSynchronous) -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-terminal me-1"></i> Specialize Commands (RunSynchronous)</span>
<span>Specialize Commands (RunSynchronous)</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
<i class="bi bi-plus-lg"></i> Add
Add
</button>
</div>
<div class="card-body p-0">
@@ -173,7 +172,7 @@
<tbody>
{% for cmd in data.specialize_commands %}
<tr draggable="true">
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
<td class="drag-handle">::</td>
<td class="order-num">{{ loop.index }}</td>
<td>
<input type="text" class="form-control form-control-sm"
@@ -185,13 +184,13 @@
</td>
<td class="text-nowrap">
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
<i class="bi bi-arrow-up"></i>
Up
</button>
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
<i class="bi bi-arrow-down"></i>
Down
</button>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i>
Remove
</button>
</td>
</tr>
@@ -209,7 +208,7 @@
<!-- 4. OOBE Settings -->
<div class="card section-card">
<div class="card-header">
<i class="bi bi-shield-check me-1"></i> OOBE Settings
OOBE Settings
</div>
<div class="card-body">
<div class="row g-3">
@@ -260,12 +259,160 @@
</div>
</div>
<!-- 5. First Logon Commands -->
<!-- 5. User Accounts -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-play-circle me-1"></i> First Logon Commands</span>
<span>User Accounts (Local)</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addUserAccount">
Add
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" id="userAccountsTable">
<thead class="table-light">
<tr>
<th style="width:40px">#</th>
<th>Name</th>
<th>Password</th>
<th>Group</th>
<th>Display Name</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
{% for acct in data.user_accounts %}
<tr>
<td class="order-num">{{ loop.index }}</td>
<td>
<input type="text" class="form-control form-control-sm"
name="account_name_{{ loop.index0 }}" value="{{ acct.name }}">
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="account_password_{{ loop.index0 }}" value="{{ acct.password }}">
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="account_group_{{ loop.index0 }}" value="{{ acct.group }}">
</td>
<td>
<input type="text" class="form-control form-control-sm"
name="account_display_{{ loop.index0 }}" value="{{ acct.display_name }}">
</td>
<td>
<input type="hidden" name="account_plaintext_{{ loop.index0 }}" value="{{ acct.plain_text }}">
<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not data.user_accounts %}
<div class="text-center text-muted py-3 empty-message" id="userAccountsEmpty">
No local accounts configured. Click <strong>Add</strong> to add one.
</div>
{% endif %}
</div>
</div>
<!-- 6. AutoLogon -->
<div class="card section-card">
<div class="card-header">
AutoLogon
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" id="autologonEnabledToggle"
{% if data.autologon.enabled|lower == 'true' %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="autologonEnabledToggle">Enabled</label>
<input type="hidden" name="autologon_enabled" id="autologon_enabled_val"
value="{{ data.autologon.enabled }}">
</div>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Username</label>
<input type="text" class="form-control" name="autologon_username"
value="{{ data.autologon.username }}" placeholder="e.g. SupportUser">
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Password</label>
<input type="text" class="form-control" name="autologon_password"
value="{{ data.autologon.password }}">
<input type="hidden" name="autologon_plaintext" value="{{ data.autologon.plain_text }}">
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">Logon Count</label>
<input type="text" class="form-control" name="autologon_logoncount"
value="{{ data.autologon.logon_count }}" placeholder="e.g. 2 or 999">
<div class="form-text">Number of auto-logon attempts.</div>
</div>
</div>
</div>
</div>
<!-- 7. International Settings -->
<div class="card section-card">
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#intlCollapse"
role="button" style="cursor:pointer">
International Settings
<small class="text-muted ms-2">(click to expand)</small>
</div>
<div class="collapse {% if data.intl.input_locale or data.intl.system_locale or data.intl.ui_language or data.intl.user_locale %}show{% endif %}"
id="intlCollapse">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label fw-semibold">Input Locale</label>
<input type="text" class="form-control" name="intl_input_locale"
value="{{ data.intl.input_locale }}" placeholder="e.g. en-US">
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">System Locale</label>
<input type="text" class="form-control" name="intl_system_locale"
value="{{ data.intl.system_locale }}" placeholder="e.g. en-US">
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">UI Language</label>
<input type="text" class="form-control" name="intl_ui_language"
value="{{ data.intl.ui_language }}" placeholder="e.g. en-US">
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">User Locale</label>
<input type="text" class="form-control" name="intl_user_locale"
value="{{ data.intl.user_locale }}" placeholder="e.g. en-US">
</div>
</div>
</div>
</div>
</div>
<!-- 8. OOBE Time Zone -->
<div class="card section-card">
<div class="card-header">
OOBE Time Zone
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Time Zone (oobeSystem pass)</label>
<input type="text" class="form-control" name="oobe_timezone"
value="{{ data.oobe_timezone }}" placeholder="e.g. Eastern Standard Time">
<div class="form-text">Separate from the specialize-pass time zone in Machine Settings above.</div>
</div>
</div>
</div>
</div>
<!-- 9. First Logon Commands -->
<div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>First Logon Commands</span>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
<i class="bi bi-plus-lg"></i> Add
Add
</button>
</div>
<div class="card-body p-0">
@@ -282,7 +429,7 @@
<tbody>
{% for cmd in data.firstlogon_commands %}
<tr draggable="true">
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
<td class="drag-handle">::</td>
<td class="order-num">{{ loop.index }}</td>
<td>
<input type="text" class="form-control form-control-sm"
@@ -294,13 +441,13 @@
</td>
<td class="text-nowrap">
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
<i class="bi bi-arrow-up"></i>
Up
</button>
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
<i class="bi bi-arrow-down"></i>
Down
</button>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i>
Remove
</button>
</td>
</tr>
@@ -321,9 +468,9 @@
<div class="tab-pane fade" id="rawView" role="tabpanel">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-code-slash me-1"></i> Raw XML</span>
<span>Raw XML</span>
<button type="button" class="btn btn-sm btn-success" id="saveRawBtn">
<i class="bi bi-floppy me-1"></i> Save Raw XML
Save Raw XML
</button>
</div>
<div class="card-body">