Files
pxe-server/webapp/templates/enrollment.html
cproudlock dfd57bef17 Webapp: fix six live bugs found in the page inventory
- Report view: the hero summarised only the first erasure, so a drive whose
  Purge failed and Clear fallback succeeded showed Failed while the list said
  Successful. It now uses the per-drive outcome (report_result) and lists the
  standards that wiped each drive.
- To-do and ServiceNow: reports with no usable document id could never be
  ticked or cleared (400). They are now tracked by file name (file:<name>).
- Imaging: the LAPS password field is masked, with a Show/Hide toggle.
- Imaging: succeeded sessions not updated for ACTIVE_WINDOW_HOURS are hidden
  from the dashboard (failed and in-progress always stay), with Show all.
- Enrollment: the active SFLD package can no longer be deleted, which left
  ppkg.conf pointing at a missing file; its row is marked Active.
- Import: confirm before importing, saying upload-folder sources are moved
  and same-name files in the image are overwritten.
2026-10-14 13:29:00 -04:00

263 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}Enrollment Packages - PXE Server Manager{% endblock %}
{% block content %}
<div class="page-header">
<h1><i class="bi bi-pc-display"></i> Enrollment Packages</h1>
<div class="header-actions">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
<i class="bi bi-upload"></i> Upload Package
</button>
</div>
</div>
{% if active_ppkg and active_ppkg.SOURCE_PPKG %}
<div class="alert alert-info">
<strong>Active PPKG:</strong> <span class="mono">{{ active_ppkg.SOURCE_PPKG }}</span>
&nbsp;<span class="badge badge-info">{{ active_ppkg.PPKG_VER }}</span>
{% if active_ppkg.PPKG_EXP %}<span class="badge badge-secondary">exp {{ active_ppkg.PPKG_EXP }}</span>{% endif %}
<span class="form-text">- startnet.cmd boots with this (from <span class="mono">ppkg.conf</span>).</span>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<span><i class="bi bi-box-seam"></i> Shop Floor (SFLD) Provisioning Packages</span>
<span class="badge badge-secondary">{{ packages|length }}</span>
</div>
<div class="card-body p-0">
{% if packages %}
<div class="table-container">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Filename</th>
<th>Size</th>
<th>Last Modified</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for p in packages %}
{% set is_active = active_ppkg and active_ppkg.SOURCE_PPKG and p.filename|lower == active_ppkg.SOURCE_PPKG|lower %}
<tr>
<td><span class="mono"><i class="bi bi-file-earmark-zip"></i> {{ p.filename }}</span>{% if is_active %} <span class="badge badge-info">Active</span>{% endif %}</td>
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
<td>{{ p.modified | timestamp_fmt }}</td>
<td class="actions text-end text-nowrap">
<a href="{{ url_for('enrollment_download', filename=p.filename) }}"
class="btn btn-sm btn-outline-primary btn-row-action" title="Download">
<i class="bi bi-download"></i> Download
</a>
{% if is_active %}
<button type="button" class="btn btn-sm btn-outline-secondary btn-row-action" disabled>
<i class="bi bi-lock"></i> Active
</button>
{% else %}
<button type="button" class="btn btn-sm btn-outline-danger btn-row-action"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-filename="{{ p.filename }}" title="Delete">
<i class="bi bi-trash"></i> Delete
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-inbox" style="font-size: 2.5rem; opacity: 0.5;"></i>
<p class="mt-2 mb-0">No enrollment packages found. Upload a <span class="mono">.ppkg</span> file to get started.</p>
</div>
{% endif %}
</div>
</div>
<div class="pxe-card">
<h3 class="pxe-card-title"><i class="bi bi-info-circle"></i> About Enrollment Packages</h3>
<p>
GCCH enrollment <span class="mono">.ppkg</span> provisioning packages are copied to
<span class="mono">C:\Enrollment\</span> on the target machine after imaging.
At OOBE, connect to a network with internet, press <strong>Windows key 5 times</strong>,
then browse to <span class="mono">C:\Enrollment\</span> and select the package. No USB stick needed.
</p>
<p class="mb-0">
<strong>Naming convention:</strong> Use <span class="mono">with-office.ppkg</span> and
<span class="mono">without-office.ppkg</span> to match the WinPE enrollment menu options.
</p>
</div>
<div class="card" id="mcl">
<div class="card-header">
<span><i class="bi bi-box-seam"></i> Standard / Engineer (MCL) Provisioning Packages</span>
<span class="badge badge-secondary">{{ mcl_packages|length }}</span>
</div>
<div class="card-body">
{% set mcl_users = mcl_links | selectattr('state', 'equalto', 'ok') | map(attribute='image') | list %}
<p class="text-muted">
Packages live in <span class="mono">_shared/Applications/BPRT</span>, which {{ mcl_users | join(', ') if mcl_users else 'no image' }} link{{ 's' if mcl_users|length == 1 }} to.
The bulk-enroll scripts pick the package by its first three name fields (tenant, environment, purpose) and fail
enrollment if two match, so an upload <strong>replaces</strong> the package with the same fields; the old one moves
to the archive below and can be restored. Every PC built from those images copies this folder while it images, so
upload, archive or restore only when no build is in progress.
</p>
<p class="text-muted">
<i class="bi bi-arrow-left-right"></i> A whole new vendor drop (packages, Office zips and scripts from MediaCreator Lite)
goes through <a href="/updates/bprt/">BPRT Sync</a> instead: it uploads only the changed files from the work PC, checks
them and swaps the folder in one step, keeping the site's own scripts.
</p>
{% for l in mcl_links if l.state in ('copy', 'elsewhere') %}
<div class="alert alert-warning py-2 mb-2">
<strong>{{ l.image }}</strong>
{% if l.state == 'copy' %}has its own copy of BPRT (<span class="mono">{{ l.path }}</span> is a real folder, not a link to
<span class="mono">_shared/Applications/BPRT</span>; an image import does this),
{% else %}links BPRT to <span class="mono">{{ l.target or '?' }}</span> instead of <span class="mono">_shared/Applications/BPRT</span>,
{% endif %}so packages changed here do not reach its PCs. Move that folder aside and link it back to the shared one.
</div>
{% endfor %}
<form action="{{ url_for('mcl_ppkg_upload') }}" method="post" enctype="multipart/form-data" class="d-flex flex-wrap gap-2 align-items-end mb-3">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="form-field mb-0" style="min-width: min(22rem, 100%);">
<label for="mclFile" class="form-label">Package (.ppkg)</label>
<input type="file" class="form-control" id="mclFile" name="ppkg_file" accept=".ppkg" required>
<div class="form-text">Vendor name, unchanged, e.g. <span class="mono">GCCH_Prod_MCL-PERS_Exp_XXXXXXXX_v2.19.ppkg</span></div>
</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-upload"></i> Upload and replace</button>
</form>
{% if mcl_packages %}
<div class="table-container">
<table class="table table-hover mb-0">
<thead>
<tr><th>Tenant / purpose</th><th>Filename</th><th>Version</th><th>Size</th><th>Last Modified</th><th class="text-end"></th></tr>
</thead>
<tbody>
{% for p in mcl_packages %}
<tr>
<td class="mono">{{ p.prefix or '-' }}
{% if p.conflict %}<span class="badge badge-danger" title="This prefix also matches {{ p.conflict_with | join(', ') }}; enrollment for it will fail">Duplicate</span>{% endif %}
{% if not p.prefix %}<span class="badge badge-warning" title="Name does not match the vendor pattern; the enroll scripts ignore it">Ignored</span>
{% elif not p.placeholder %}<span class="badge badge-warning" title="No Exp_XXXXXXXX placeholder in the name: this looks like a package already used on a PC, and the enroll script cannot inject a new token into it">Used</span>{% endif %}
</td>
<td class="mono">{{ p.filename }}</td>
<td><span class="badge badge-info">{{ p.version or '?' }}</span></td>
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
<td>{{ p.modified | timestamp_fmt }}</td>
<td class="text-end">
<form action="{{ url_for('mcl_ppkg_archive') }}" method="post" style="display:inline;"
onsubmit="return confirm('Move this package to the archive? Enrollment for its tenant and purpose fails until another package is uploaded or restored.');">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="filename" value="{{ p.filename }}">
<button type="submit" class="btn btn-sm btn-outline-danger btn-row-action" title="Move to the archive"><i class="bi bi-archive"></i></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted mb-0">No MCL packages found in BPRT.</p>
{% endif %}
{% if mcl_archive %}
<h6 class="mt-4">Archive</h6>
<div class="table-container">
<table class="table table-sm mb-0">
<thead><tr><th>Archived</th><th>Filename</th><th>Version</th><th>Size</th><th class="text-end"></th></tr></thead>
<tbody>
{% for p in mcl_archive %}
<tr>
<td class="mono">{{ p.batch }}</td>
<td class="mono">{{ p.filename }}</td>
<td>{{ p.version or '?' }}</td>
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
<td class="text-end">
<form action="{{ url_for('mcl_ppkg_restore') }}" method="post" style="display:inline;"
onsubmit="return confirm('Restore this package? The current package for the same tenant and purpose moves to the archive.');">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="batch" value="{{ p.batch }}">
<input type="hidden" name="filename" value="{{ p.filename }}">
<button type="submit" class="btn btn-sm btn-outline-primary btn-row-action"><i class="bi bi-arrow-counterclockwise"></i> Restore</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</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="form-field">
<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">
A standard <span class="mono">GCCH_Prod_SFLD_v&lt;ver&gt;.ppkg</span> upload is set as the active PPKG automatically
(written to <span class="mono">ppkg.conf</span> on the share; startnet.cmd reads it at boot - no boot.wim edit).
</div>
</div>
<div class="form-field">
<label for="ppkgExpiry" class="form-label">Expiry (optional)</label>
<input type="text" class="form-control mono" id="ppkgExpiry" name="expiry"
placeholder="YYYYMMDD" pattern="\d{8}">
<div class="form-text">Blank keeps the current expiry.</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"><i class="bi bi-upload"></i> 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" class="mono"></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"><i class="bi bi-trash"></i> 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 = "{{ url_for('enrollment_delete', filename='__F__') }}".replace('__F__', encodeURIComponent(filename));
});
</script>
{% endblock %}