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,13 +1,14 @@
# #
# 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 # Reads user_selections.json to upload only the selected OS, matching
# PXE server's image-upload share using robocopy with authentication. # packages, and config files. Drivers are EXCLUDED by default.
# #
# Usage: # 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 -CachePath "D:\MCL\Cache" (custom cache location)
# .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP) # .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP)
# #
# After upload, use the PXE webapp (http://10.9.100.1:9009) to import # After upload, use the PXE webapp (http://10.9.100.1:9009) to import
# the uploaded content into the desired image type. # the uploaded content into the desired image type.
@@ -18,11 +19,23 @@ param(
[string]$Server = "10.9.100.1", [string]$Server = "10.9.100.1",
[string]$User = "pxe-upload", [string]$User = "pxe-upload",
[string]$Pass = "pxe", [string]$Pass = "pxe",
[switch]$IncludeDell10 [switch]$IncludeDrivers
) )
$Share = "\\$Server\image-upload" $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 ""
Write-Host "========================================" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan
Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan
@@ -32,11 +45,6 @@ Write-Host ""
# --- Validate source paths --- # --- Validate source paths ---
$DeployPath = Join-Path $CachePath "Deploy" $DeployPath = Join-Path $CachePath "Deploy"
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools" $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)) { if (-not (Test-Path $ToolsPath -PathType Container)) {
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools" $ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
} }
@@ -44,24 +52,104 @@ $SourcesZip = Join-Path $CachePath "Boot\Sources.zip"
if (-not (Test-Path $DeployPath -PathType Container)) { if (-not (Test-Path $DeployPath -PathType Container)) {
Write-Host "ERROR: Deploy directory not found at $DeployPath" -ForegroundColor Red 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 exit 1
} }
Write-Host " Cache Path: $CachePath" # --- Parse user_selections.json ---
Write-Host " Deploy: $DeployPath" -ForegroundColor $(if (Test-Path $DeployPath) { "Green" } else { "Red" }) $SelectionsFile = Join-Path $ToolsPath "user_selections.json"
Write-Host " Tools: $ToolsPath" -ForegroundColor $(if (Test-Path $ToolsPath) { "Green" } else { "Yellow" }) if (-not (Test-Path $SelectionsFile)) {
Write-Host " Sources.zip: $SourcesZip" -ForegroundColor $(if (Test-Path $SourcesZip) { "Green" } else { "Yellow" }) Write-Host "ERROR: user_selections.json not found at $SelectionsFile" -ForegroundColor Red
Write-Host " Server: $Server" Write-Host " Run Media Creator Lite first to create a configuration." -ForegroundColor Yellow
if (-not $IncludeDell10) { exit 1
Write-Host " Excluding: Dell_10 drivers (use -IncludeDell10 to include)" -ForegroundColor Gray
} }
$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 "" Write-Host ""
# --- Connect to SMB share --- # --- Connect to SMB share ---
Write-Host "Connecting to $Share ..." -ForegroundColor Gray Write-Host "Connecting to $Share ..." -ForegroundColor Gray
# Remove any stale connection
net use $Share /delete 2>$null | Out-Null net use $Share /delete 2>$null | Out-Null
$netResult = net use $Share /user:$User $Pass 2>&1 $netResult = net use $Share /user:$User $Pass 2>&1
@@ -79,47 +167,135 @@ Write-Host "Connected." -ForegroundColor Green
Write-Host "" Write-Host ""
$failed = $false $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 "========================================" -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 Write-Host "========================================" -ForegroundColor Cyan
$robocopyArgs = @($DeployPath, "$Share\Deploy", "/E", "/R:3", "/W:5", "/NP", "/ETA") robocopy $DeployPath "$Share\Deploy" /E /XD "Operating Systems" "Out-of-box Drivers" "Packages" /R:3 /W:5 /NP /ETA
if (-not $IncludeDell10) {
$robocopyArgs += @("/XD", "Dell_10")
}
& robocopy @robocopyArgs
if ($LASTEXITCODE -ge 8) { 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 $failed = $true
} }
# --- Step 2: Copy Tools/ --- # --- Step: Operating System ---
Write-Host "" if ($matchedOs.Count -gt 0) {
Write-Host "========================================" -ForegroundColor Cyan $stepNum++
Write-Host "[2/3] Copying Tools/ ..." -ForegroundColor Cyan Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan Write-Host "========================================" -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) { 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 robocopy $ToolsPath "$Share\Tools" /E /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) { if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true $failed = $true
} }
} else {
Write-Host "SKIPPED: Tools directory not found at $ToolsPath" -ForegroundColor Yellow
} }
# --- Step 3: Extract and copy Sources/ --- # --- Step: Sources ---
Write-Host "" $TempSources = $null
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[3/3] Extracting and copying Sources/ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if (Test-Path $SourcesZip) { if (Test-Path $SourcesZip) {
$stepNum++
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[$stepNum/$totalSteps] Extracting and copying Sources\ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$TempExtract = Join-Path $env:TEMP "SourcesExtract" $TempExtract = Join-Path $env:TEMP "SourcesExtract"
Write-Host " Extracting Sources.zip..."
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force
@@ -135,10 +311,49 @@ if (Test-Path $SourcesZip) {
$failed = $true $failed = $true
} }
# Clean up temp extraction
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue 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 { } else {
Write-Host "SKIPPED: Sources.zip not found at $SourcesZip" -ForegroundColor Yellow Write-Host " Fixed $fixCount file(s)." -ForegroundColor Yellow
} }
# --- Disconnect --- # --- 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> <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> <Description>Bypass OOBE network requirement</Description>
</RunSynchronousCommand> </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> </RunSynchronous>
</component> </component>
</settings> </settings>
@@ -151,11 +161,16 @@
</SynchronousCommand> </SynchronousCommand>
<SynchronousCommand wcm:action="add"> <SynchronousCommand wcm:action="add">
<Order>5</Order> <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> <CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
<Description>Run GCCH Enrollment</Description> <Description>Run GCCH Enrollment</Description>
</SynchronousCommand> </SynchronousCommand>
<SynchronousCommand wcm:action="add"> <SynchronousCommand wcm:action="add">
<Order>6</Order> <Order>7</Order>
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine> <CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
<Description>Run shopfloor PC type setup</Description> <Description>Run shopfloor PC type setup</Description>
</SynchronousCommand> </SynchronousCommand>

View File

@@ -37,11 +37,12 @@
- gea-standard - gea-standard
- gea-engineer - gea-engineer
- gea-shopfloor - gea-shopfloor
- gea-shopfloor-mce
- ge-standard - ge-standard
- ge-engineer - ge-engineer
- ge-shopfloor-lockdown - ge-shopfloor-lockdown
- ge-shopfloor-mce - ge-shopfloor-mce
shopfloor_types:
- gea-shopfloor
deploy_subdirs: deploy_subdirs:
- Applications - Applications
- Control - Control
@@ -298,6 +299,36 @@
state: directory state: directory
mode: '0777' 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" - name: "Create image upload staging directory"
file: file:
path: /home/pxe/image-upload path: /home/pxe/image-upload
@@ -348,6 +379,15 @@
force user = root force user = root
comment = Blancco Drive Eraser reports 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] [image-upload]
path = /home/pxe/image-upload path = /home/pxe/image-upload
browseable = yes browseable = yes
@@ -357,6 +397,9 @@
force user = pxe force user = pxe
force group = pxe force group = pxe
comment = PXE image upload staging area comment = PXE image upload staging area
oplocks = no
level2 oplocks = no
strict sync = yes
- name: "Create Samba users (pxe-upload and blancco)" - name: "Create Samba users (pxe-upload and blancco)"
shell: | shell: |
@@ -392,6 +435,15 @@
force: no force: no
loop: "{{ image_types }}" 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" - name: "Daily cron to create/refresh Media.tag for all images"
copy: copy:
content: | content: |
@@ -635,6 +687,7 @@
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
Environment=WEB_ROOT={{ web_root }} Environment=WEB_ROOT={{ web_root }}
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
Environment=ENROLLMENT_SHARE=/srv/samba/enrollment
Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
ExecStart=/usr/bin/python3 app.py ExecStart=/usr/bin/python3 app.py
Restart=always 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 # Cancel any pending reboot so it doesn't interrupt setup
shutdown -a 2>$null 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" $enrollDir = "C:\Enrollment"
$typeFile = Join-Path $enrollDir "pc-type.txt" $typeFile = Join-Path $enrollDir "pc-type.txt"
$setupDir = Join-Path $enrollDir "shopfloor-setup" $setupDir = Join-Path $enrollDir "shopfloor-setup"
@@ -56,5 +69,17 @@ if ($pcType -ne "Shopfloor") {
} }
Write-Host "Shopfloor setup complete for $pcType." 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..." Write-Host "Rebooting in 10 seconds..."
shutdown /r /t 10 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 3. Keyence
echo 4. Genspect echo 4. Genspect
echo 5. Display echo 5. Display
echo 6. Shopfloor (General) echo 6. Standard
echo. echo.
set PCTYPE= set PCTYPE=
set /p pctype_choice=Enter your choice (1-6): 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%"=="3" set PCTYPE=Keyence
if "%pctype_choice%"=="4" set PCTYPE=Genspect if "%pctype_choice%"=="4" set PCTYPE=Genspect
if "%pctype_choice%"=="5" set PCTYPE=Display 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 if "%PCTYPE%"=="" goto pctype_menu
REM --- Display sub-type selection --- 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" copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1"
REM --- Always copy Shopfloor baseline scripts --- REM --- Always copy Shopfloor baseline scripts ---
mkdir W:\Enrollment\shopfloor-setup 2>NUL 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" ( if exist "Y:\shopfloor-setup\Shopfloor" (
mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\" xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\"
echo Copied Shopfloor baseline setup files. echo Copied Shopfloor baseline setup files.
) )
REM --- Copy type-specific scripts on top of baseline --- REM --- Copy type-specific scripts on top of baseline ---
if "%PCTYPE%"=="Shopfloor" goto pctype_done
if exist "Y:\shopfloor-setup\%PCTYPE%" ( if exist "Y:\shopfloor-setup\%PCTYPE%" (
mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL
xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\" 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 #!/usr/bin/env python3
"""Flask web application for managing a GE Aerospace PXE server.""" """Flask web application for managing a GE Aerospace PXE server."""
import json
import logging import logging
import os import os
import secrets import secrets
@@ -51,14 +52,15 @@ def audit(action, detail=""):
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps") SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla") CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
BLANCCO_REPORTS = os.environ.get("BLANCCO_REPORTS", "/srv/samba/blancco-reports") 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") UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload")
SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared") SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared")
# Subdirs inside Deploy/ shared across ALL image types # Subdirs inside Deploy/ shared across ALL image types
SHARED_DEPLOY_GLOBAL = ["Out-of-box Drivers"] SHARED_DEPLOY_GLOBAL = ["Out-of-box Drivers"]
# Subdirs inside Deploy/ shared within the same image family (by prefix) # Subdirs inside Deploy/ shared within the same image family (by prefix)
SHARED_DEPLOY_SCOPED = { SHARED_DEPLOY_SCOPED = {
"gea-": ["Operating Systems"], "gea-": ["Operating Systems", "Packages"],
"ge-": ["Operating Systems"], "ge-": ["Operating Systems", "Packages"],
} }
# Sibling dirs at image root shared within the same image family # Sibling dirs at image root shared within the same image family
SHARED_ROOT_DIRS = { SHARED_ROOT_DIRS = {
@@ -149,6 +151,164 @@ def unattend_path(image_type):
return os.path.join(deploy_path(image_type), "FlatUnattendW10.xml") 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): def image_status(image_type):
"""Return a dict describing the state of an image type.""" """Return a dict describing the state of an image type."""
dp = deploy_path(image_type) dp = deploy_path(image_type)
@@ -255,10 +415,11 @@ def _import_deploy(src_deploy, dst_deploy, target="", move=False):
_replace_with_symlink(dst_item, shared_dest) _replace_with_symlink(dst_item, shared_dest)
continue continue
# Normal transfer # Normal transfer — merge to preserve existing custom content
if os.path.exists(dst_item): if os.path.isdir(dst_item):
shutil.rmtree(dst_item) _merge_tree(src_item, dst_item, move=move)
_transfer_tree(src_item, dst_item) else:
_transfer_tree(src_item, dst_item)
def _replace_with_symlink(link_path, target_path): def _replace_with_symlink(link_path, target_path):
@@ -355,6 +516,21 @@ def parse_unattend(xml_path):
"SkipMachineOOBE": "true", "SkipMachineOOBE": "true",
}, },
"firstlogon_commands": [], "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": "", "raw_xml": "",
} }
@@ -418,6 +594,19 @@ def parse_unattend(xml_path):
namespaces=ns, namespaces=ns,
): ):
comp_name = comp.get("name", "") 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: if "OOBE" in comp_name or "Shell-Setup" in comp_name:
oobe_el = comp.find(qn("OOBE")) oobe_el = comp.find(qn("OOBE"))
if oobe_el is not None: if oobe_el is not None:
@@ -426,6 +615,57 @@ def parse_unattend(xml_path):
if local in data["oobe"] and child.text: if local in data["oobe"] and child.text:
data["oobe"][local] = child.text.strip() 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 # FirstLogonCommands
flc = comp.find(qn("FirstLogonCommands")) flc = comp.find(qn("FirstLogonCommands"))
if flc is not None: 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 "", "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 return data
@@ -515,6 +760,28 @@ def build_unattend_xml(form_data):
# --- oobeSystem --- # --- oobeSystem ---
oobe_settings = _settings_pass(root, "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 = etree.SubElement(oobe_settings, qn("component"))
oobe_comp.set("name", "Microsoft-Windows-Shell-Setup") oobe_comp.set("name", "Microsoft-Windows-Shell-Setup")
oobe_comp.set("processorArchitecture", "amd64") oobe_comp.set("processorArchitecture", "amd64")
@@ -540,6 +807,46 @@ def build_unattend_xml(form_data):
child = etree.SubElement(oobe_el, qn(key)) child = etree.SubElement(oobe_el, qn(key))
child.text = str(val) 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 # FirstLogonCommands
fl_cmds = form_data.get("firstlogon_commands", []) fl_cmds = form_data.get("firstlogon_commands", [])
if fl_cmds: if fl_cmds:
@@ -556,6 +863,12 @@ def build_unattend_xml(form_data):
desc_el = etree.SubElement(sc, qn("Description")) desc_el = etree.SubElement(sc, qn("Description"))
desc_el.text = cmd.get("description", "").strip() 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( xml_bytes = etree.tostring(
root, root,
pretty_print=True, pretty_print=True,
@@ -616,6 +929,40 @@ def _extract_form_data(form):
"description": fl_descs[i] if i < len(fl_descs) else "", "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 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 # Routes — Clonezilla Backups
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -922,6 +1337,78 @@ def blancco_delete_report(filename):
return redirect(url_for("blancco_reports")) 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) # 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 // OOBE toggle switches — keep hidden input in sync
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -298,5 +363,107 @@ document.addEventListener('DOMContentLoaded', function () {
toggleEmpty('driverPathsTable', 'driverPathsEmpty'); toggleEmpty('driverPathsTable', 'driverPathsEmpty');
toggleEmpty('specCmdTable', 'specCmdEmpty'); toggleEmpty('specCmdTable', 'specCmdEmpty');
toggleEmpty('flCmdTable', 'flCmdEmpty'); 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 %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <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> <span class="badge bg-secondary fs-6">{{ entries|length }} entries</span>
</div> </div>
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <div class="card-header d-flex align-items-center">
<i class="bi bi-clock-history me-2"></i> Activity History Activity History
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
{% if entries %} {% if entries %}
@@ -52,7 +52,6 @@
</div> </div>
{% else %} {% else %}
<div class="text-center text-muted py-5"> <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="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> <p class="small">Actions like image imports, unattend edits, and backup operations will be logged here.</p>
</div> </div>

View File

@@ -3,15 +3,15 @@
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <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"> <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> </button>
</div> </div>
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <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> <span class="badge bg-secondary ms-2">{{ backups|length }}</span>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
@@ -36,12 +36,12 @@
<td class="text-end text-nowrap"> <td class="text-end text-nowrap">
<a href="{{ url_for('clonezilla_download', filename=b.filename) }}" <a href="{{ url_for('clonezilla_download', filename=b.filename) }}"
class="btn btn-sm btn-outline-primary" title="Download"> class="btn btn-sm btn-outline-primary" title="Download">
<i class="bi bi-download"></i> Download
</a> </a>
<button type="button" class="btn btn-sm btn-outline-danger" <button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-filename="{{ b.filename }}" data-machine="{{ b.machine }}" title="Delete"> data-filename="{{ b.filename }}" data-machine="{{ b.machine }}" title="Delete">
<i class="bi bi-trash"></i> Delete
</button> </button>
</td> </td>
</tr> </tr>
@@ -50,7 +50,6 @@
</table> </table>
{% else %} {% else %}
<div class="text-center text-muted py-5"> <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> <p class="mt-2">No backups found. Upload a Clonezilla backup .zip to get started.</p>
</div> </div>
{% endif %} {% endif %}
@@ -59,7 +58,7 @@
<div class="card mt-3"> <div class="card mt-3">
<div class="card-body"> <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"> <p class="card-text mb-0">
Name backup files with the machine number (e.g., <code>6501.zip</code>). 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. 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"> <form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -89,7 +88,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <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> </div>
</form> </form>
</div> </div>
@@ -103,7 +102,7 @@
<form id="deleteForm" method="post"> <form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -112,7 +111,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <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> </div>
</form> </form>
</div> </div>

View File

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

View File

@@ -5,14 +5,14 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Dashboard</h2> <h2 class="mb-0">Dashboard</h2>
<button class="btn btn-outline-secondary btn-sm" onclick="location.reload()"> <button class="btn btn-outline-secondary btn-sm" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh Refresh
</button> </button>
</div> </div>
<!-- Services --> <!-- Services -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header d-flex align-items-center"> <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>
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
@@ -27,7 +27,6 @@
{% for svc in services %} {% for svc in services %}
<tr> <tr>
<td> <td>
<i class="bi bi-server me-1 text-muted"></i>
<strong>{{ svc.name }}</strong> <strong>{{ svc.name }}</strong>
</td> </td>
<td> <td>
@@ -45,7 +44,7 @@
<!-- Images --> <!-- Images -->
<div class="card"> <div class="card">
<div class="card-header d-flex align-items-center"> <div class="card-header d-flex align-items-center">
<i class="bi bi-disc me-2"></i> Deployment Images Deployment Images
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
@@ -67,23 +66,27 @@
</td> </td>
<td> <td>
{% if img.has_content %} {% 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 %} {% else %}
<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Empty</span> <span class="badge bg-secondary">Empty</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if img.has_unattend %} {% 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 %} {% 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 %} {% endif %}
</td> </td>
<td><code class="small">{{ img.deploy_path }}</code></td> <td><code class="small">{{ img.deploy_path }}</code></td>
<td class="text-end"> <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) }}" <a href="{{ url_for('unattend_editor', image_type=img.image_type) }}"
class="btn btn-sm btn-outline-primary"> class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil-square"></i> Edit Unattend Edit Unattend
</a> </a>
</td> </td>
</tr> </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 --> <!-- Network Upload Import -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<i class="bi bi-cloud-upload me-2"></i> Import from Network Upload Import from Network Upload
</div> </div>
<div class="card-body"> <div class="card-body">
{% if upload_sources %} {% if upload_sources %}
@@ -44,7 +44,6 @@
</div> </div>
<div class="alert alert-info d-flex align-items-start" role="alert"> <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> <div>
<strong>Shared Drivers:</strong> Out-of-box Drivers are automatically pooled <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. into a shared directory and symlinked for each image type to save disk space.
@@ -52,7 +51,6 @@
</div> </div>
<div class="alert alert-warning d-flex align-items-start" role="alert"> <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> <div>
<strong>Warning:</strong> Existing files in the target Deploy directory with the <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 same names will be overwritten. This operation may take several minutes for large
@@ -61,19 +59,18 @@
</div> </div>
<button type="submit" class="btn btn-primary" id="uploadImportBtn"> <button type="submit" class="btn btn-primary" id="uploadImportBtn">
<i class="bi bi-download me-1"></i> Start Import Start Import
</button> </button>
</form> </form>
{% else %} {% else %}
<div class="text-center py-4"> <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> <h5 class="mt-3 text-muted">No Upload Content Found</h5>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy
the Deploy directory contents there. the Deploy directory contents there.
</p> </p>
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()"> <button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh Refresh
</button> </button>
</div> </div>
{% endif %} {% endif %}
@@ -83,7 +80,7 @@
<!-- USB Import --> <!-- USB Import -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<i class="bi bi-usb-drive me-2"></i> Import from USB Drive Import from USB Drive
</div> </div>
<div class="card-body"> <div class="card-body">
{% if usb_mounts %} {% if usb_mounts %}
@@ -116,7 +113,6 @@
</div> </div>
<div class="alert alert-warning d-flex align-items-start" role="alert"> <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> <div>
<strong>Warning:</strong> Existing files in the target Deploy directory with the <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 same names will be overwritten. This operation may take several minutes for large
@@ -125,19 +121,18 @@
</div> </div>
<button type="submit" class="btn btn-primary" id="importBtn"> <button type="submit" class="btn btn-primary" id="importBtn">
<i class="bi bi-download me-1"></i> Start Import Start Import
</button> </button>
</form> </form>
{% else %} {% else %}
<div class="text-center py-4"> <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> <h5 class="mt-3 text-muted">No USB Drives Detected</h5>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br> No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br>
Mount a USB drive and refresh this page. Mount a USB drive and refresh this page.
</p> </p>
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()"> <button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh Refresh
</button> </button>
</div> </div>
{% endif %} {% endif %}
@@ -148,7 +143,7 @@
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<i class="bi bi-info-circle me-2"></i> Current Image Status Current Image Status
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-sm mb-0"> <table class="table table-sm mb-0">

View File

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

View File

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

View File

@@ -43,13 +43,12 @@
<div> <div>
<h2 class="mb-1">{{ friendly_name }}</h2> <h2 class="mb-1">{{ friendly_name }}</h2>
<small class="text-muted"> <small class="text-muted">
<i class="bi bi-file-earmark-code me-1"></i>
<code>{{ image_type }}/Deploy/FlatUnattendW10.xml</code> <code>{{ image_type }}/Deploy/FlatUnattendW10.xml</code>
</small> </small>
</div> </div>
<div> <div>
<button type="button" class="btn btn-success" id="saveFormBtn"> <button type="button" class="btn btn-success" id="saveFormBtn">
<i class="bi bi-floppy me-1"></i> Save Save
</button> </button>
</div> </div>
</div> </div>
@@ -59,13 +58,13 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" id="form-tab" data-bs-toggle="tab" <a class="nav-link active" id="form-tab" data-bs-toggle="tab"
href="#formView" role="tab"> href="#formView" role="tab">
<i class="bi bi-ui-checks-grid me-1"></i> Form Editor Form Editor
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" id="raw-tab" data-bs-toggle="tab" <a class="nav-link" id="raw-tab" data-bs-toggle="tab"
href="#rawView" role="tab"> href="#rawView" role="tab">
<i class="bi bi-code-slash me-1"></i> Raw XML Raw XML
</a> </a>
</li> </li>
</ul> </ul>
@@ -80,9 +79,9 @@
<!-- 1. Driver Paths --> <!-- 1. Driver Paths -->
<div class="card section-card"> <div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center"> <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"> <button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
<i class="bi bi-plus-lg"></i> Add Add
</button> </button>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
@@ -104,7 +103,7 @@
</td> </td>
<td> <td>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row"> <button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i> Remove
</button> </button>
</td> </td>
</tr> </tr>
@@ -122,7 +121,7 @@
<!-- 2. Machine Settings --> <!-- 2. Machine Settings -->
<div class="card section-card"> <div class="card section-card">
<div class="card-header"> <div class="card-header">
<i class="bi bi-pc-display me-1"></i> Machine Settings Machine Settings
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
@@ -154,9 +153,9 @@
<!-- 3. Specialize Commands (RunSynchronous) --> <!-- 3. Specialize Commands (RunSynchronous) -->
<div class="card section-card"> <div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center"> <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"> <button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
<i class="bi bi-plus-lg"></i> Add Add
</button> </button>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
@@ -173,7 +172,7 @@
<tbody> <tbody>
{% for cmd in data.specialize_commands %} {% for cmd in data.specialize_commands %}
<tr draggable="true"> <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 class="order-num">{{ loop.index }}</td>
<td> <td>
<input type="text" class="form-control form-control-sm" <input type="text" class="form-control form-control-sm"
@@ -185,13 +184,13 @@
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up"> <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>
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down"> <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>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row"> <button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i> Remove
</button> </button>
</td> </td>
</tr> </tr>
@@ -209,7 +208,7 @@
<!-- 4. OOBE Settings --> <!-- 4. OOBE Settings -->
<div class="card section-card"> <div class="card section-card">
<div class="card-header"> <div class="card-header">
<i class="bi bi-shield-check me-1"></i> OOBE Settings OOBE Settings
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
@@ -260,12 +259,160 @@
</div> </div>
</div> </div>
<!-- 5. First Logon Commands --> <!-- 5. User Accounts -->
<div class="card section-card"> <div class="card section-card">
<div class="card-header d-flex justify-content-between align-items-center"> <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"> <button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
<i class="bi bi-plus-lg"></i> Add Add
</button> </button>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
@@ -282,7 +429,7 @@
<tbody> <tbody>
{% for cmd in data.firstlogon_commands %} {% for cmd in data.firstlogon_commands %}
<tr draggable="true"> <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 class="order-num">{{ loop.index }}</td>
<td> <td>
<input type="text" class="form-control form-control-sm" <input type="text" class="form-control form-control-sm"
@@ -294,13 +441,13 @@
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up"> <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>
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down"> <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>
<button type="button" class="btn btn-outline-danger btn-row-action remove-row"> <button type="button" class="btn btn-outline-danger btn-row-action remove-row">
<i class="bi bi-trash"></i> Remove
</button> </button>
</td> </td>
</tr> </tr>
@@ -321,9 +468,9 @@
<div class="tab-pane fade" id="rawView" role="tabpanel"> <div class="tab-pane fade" id="rawView" role="tabpanel">
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <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"> <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> </button>
</div> </div>
<div class="card-body"> <div class="card-body">