- 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.
263 lines
13 KiB
HTML
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>
|
|
<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<ver>.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 %}
|