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

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 %}