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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
<!-- Network Upload Import -->
<div class="card mb-3">
<div class="card-header">
<i class="bi bi-cloud-upload me-2"></i> Import from Network Upload
Import from Network Upload
</div>
<div class="card-body">
{% if upload_sources %}
@@ -44,7 +44,6 @@
</div>
<div class="alert alert-info d-flex align-items-start" role="alert">
<i class="bi bi-info-circle-fill me-2 mt-1"></i>
<div>
<strong>Shared Drivers:</strong> Out-of-box Drivers are automatically pooled
into a shared directory and symlinked for each image type to save disk space.
@@ -52,7 +51,6 @@
</div>
<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
<div>
<strong>Warning:</strong> Existing files in the target Deploy directory with the
same names will be overwritten. This operation may take several minutes for large
@@ -61,19 +59,18 @@
</div>
<button type="submit" class="btn btn-primary" id="uploadImportBtn">
<i class="bi bi-download me-1"></i> Start Import
Start Import
</button>
</form>
{% else %}
<div class="text-center py-4">
<i class="bi bi-cloud-slash display-4 text-muted"></i>
<h5 class="mt-3 text-muted">No Upload Content Found</h5>
<p class="text-muted mb-0">
Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy
the Deploy directory contents there.
</p>
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh
Refresh
</button>
</div>
{% endif %}
@@ -83,7 +80,7 @@
<!-- USB Import -->
<div class="card">
<div class="card-header">
<i class="bi bi-usb-drive me-2"></i> Import from USB Drive
Import from USB Drive
</div>
<div class="card-body">
{% if usb_mounts %}
@@ -116,7 +113,6 @@
</div>
<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
<div>
<strong>Warning:</strong> Existing files in the target Deploy directory with the
same names will be overwritten. This operation may take several minutes for large
@@ -125,19 +121,18 @@
</div>
<button type="submit" class="btn btn-primary" id="importBtn">
<i class="bi bi-download me-1"></i> Start Import
Start Import
</button>
</form>
{% else %}
<div class="text-center py-4">
<i class="bi bi-usb-plug display-4 text-muted"></i>
<h5 class="mt-3 text-muted">No USB Drives Detected</h5>
<p class="text-muted mb-0">
No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br>
Mount a USB drive and refresh this page.
</p>
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh
Refresh
</button>
</div>
{% endif %}
@@ -148,7 +143,7 @@
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle me-2"></i> Current Image Status
Current Image Status
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">

View File

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

View File

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

View File

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