Blocker - /reports/export returned 500: REPORT_EXPORT_RANGES and _months_ago were deleted with the old reports route. Restored. Major - A session without stage_index/stage_total took down /, /imaging and /imaging/tiles (int filter on Undefined). The templates default both; saving a LAPS password for a session that no longer exists is refused (it created a bare session). - /reports/todo parsed every report four times (23 s at 700 reports). blancco_report.parse() now caches by path, mtime and size: 0.09 s. - Stored XSS through the session serial in the Clear confirm (the browser decodes ' before running an inline handler). The message moves to data-confirm, and update_session() only accepts serials of letters, digits, '.', '_' and '-' (400 from /imaging/status otherwise). - Reports without a document id and with odd characters in the file name got a key the mark routes rejected, so they could never be cleared. report_key() now always produces a valid key. Minor - failure_note() handles a list-valued erasure target (500 on the report and to-do pages). - _recently_marked() tolerates naive, 'Z' and non-dict state entries. - Key regexes use fullmatch (a trailing newline was accepted). - Unreadable reports are no longer counted as to tick / to destroy; the reports page already flags them as Unreadable. - Rollback review recounts restorable files after reading the backups. - Enrollment: the startnet fallback package (no or empty ppkg.conf) and a quoted SOURCE_PPKG are protected too. - Backup upload overwrite check is case-insensitive (SMB share) and replaces the existing file rather than adding a case twin. - Overview polling keeps the last good rows on an error and pauses while the tab is hidden. A shown or typed LAPS password survives the floor's refresh. - hardware_dismiss only redirects to same-site paths; tiles ?limit is clamped; the Updates overview survives an unreadable Dell scan or iPXE state; describe() uses aversion(); a client cannot label its session 'inferred'.
365 lines
20 KiB
HTML
365 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}Enrollment packages - PXE Manager{% endblock %}
|
|
|
|
{# Enrollment packages, in sections: the active SFLD PPKG (ppkg.conf), the
|
|
SFLD packages (the active one is marked and cannot be deleted), the MCL
|
|
packages in _shared/Applications/BPRT with their duplicate/ignored/used
|
|
flags, the MCL archive, and which images link to the shared BPRT. #}
|
|
|
|
{% set link_chip = {
|
|
'ok': ('Linked', 'ok'),
|
|
'copy': ('Own copy', 'err'),
|
|
'elsewhere': ('Links elsewhere', 'err'),
|
|
'none': ('No BPRT', 'off'),
|
|
} %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.phead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 18px 24px 12px; }
|
|
.phead .sp { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
.ptext { padding: 0 24px 14px; font-size: 13px; color: var(--text-2); display: flex; flex-direction: column; gap: 8px; max-width: 90ch; }
|
|
.ptext p { margin: 0; }
|
|
.notice > span, .ptext { overflow-wrap: anywhere; min-width: 0; }
|
|
.colhead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; min-height: 0; }
|
|
.when { font-size: 13px; color: var(--muted); white-space: nowrap; }
|
|
.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
.empty { padding: 22px 24px; color: var(--muted); border-top: 1px solid var(--line); font-size: 13px; }
|
|
|
|
/* Active PPKG */
|
|
.active { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 18px 24px; }
|
|
.active .file { font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
|
|
|
|
/* SFLD rows */
|
|
.sgrid { display: grid; grid-template-columns: minmax(0, 1fr) 90px 190px; align-items: center; gap: 0 14px;
|
|
padding: 11px 160px 11px 24px; font-size: 14px; min-height: 36px; }
|
|
.s-name { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
.s-name .tip { flex: 0 1 auto; }
|
|
|
|
/* MCL rows */
|
|
.mgrid { display: grid; grid-template-columns: 210px minmax(0, 1fr) 70px 80px 14px; align-items: center; gap: 0 14px;
|
|
padding: 11px 72px 11px 24px; font-size: 14px; min-height: 24px; }
|
|
.m-pre { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
.m-pre .tip { flex: 0 1 auto; }
|
|
.flags { display: flex; gap: 6px; flex-wrap: nowrap; }
|
|
.mbody { padding: 2px 72px 14px 24px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; }
|
|
.mfacts { display: flex; flex-wrap: wrap; gap: 4px 20px; color: var(--muted); font-size: 12px; }
|
|
.mfacts .mono { overflow-wrap: anywhere; color: var(--text-2); }
|
|
.legend { display: flex; flex-direction: column; gap: 6px; padding: 0 24px 14px; font-size: 13px; }
|
|
.legend div { display: flex; gap: 8px; align-items: baseline; }
|
|
.upform { display: flex; flex-wrap: wrap; gap: 10px 12px; align-items: flex-end; padding: 0 24px 18px; }
|
|
.upform .form-field { min-width: min(22rem, 100%); flex: 0 1 28rem; margin: 0; }
|
|
|
|
/* Archive rows */
|
|
.agrid { display: grid; grid-template-columns: 150px minmax(0, 1fr) 70px 80px; align-items: center; gap: 0 14px;
|
|
padding: 11px 130px 11px 24px; font-size: 14px; min-height: 36px; }
|
|
|
|
/* Image links */
|
|
.lgrid { display: grid; grid-template-columns: 170px 130px minmax(0, 1fr); align-items: center; gap: 4px 14px; padding: 11px 24px; font-size: 14px; }
|
|
.lgrid .mono { overflow-wrap: anywhere; font-size: 13px; }
|
|
|
|
@container (max-width: 760px) {
|
|
.colhead { display: none; }
|
|
.sgrid { grid-template-columns: minmax(0, 1fr) auto; padding: 12px 16px 4px; gap: 2px 12px; }
|
|
.sgrid .when { grid-column: 1 / -1; }
|
|
.mgrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 12px 64px 12px 16px; gap: 4px 10px; }
|
|
.mgrid .m-file { grid-column: 1 / -1; grid-row: 2; }
|
|
.mgrid .m-size { display: none; }
|
|
.mbody { padding: 2px 16px 12px; }
|
|
.agrid { grid-template-columns: minmax(0, 1fr) auto; padding: 12px 16px 4px; gap: 2px 12px; }
|
|
.agrid .a-file { grid-column: 1 / -1; }
|
|
.agrid .a-size { display: none; }
|
|
.ui-row > .act.wide { position: static; display: flex; justify-content: flex-end; padding: 0 16px 10px; }
|
|
.lgrid { grid-template-columns: minmax(0, 1fr) auto; padding: 11px 16px; }
|
|
.lgrid .mono, .lgrid .lnote { grid-column: 1 / -1; }
|
|
.phead, .active { padding-left: 16px; padding-right: 16px; }
|
|
.ptext, .legend, .upform { padding-left: 16px; padding-right: 16px; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% call ui.head('Enrollment packages', 'Tools', 'Shop floor (SFLD) and Standard / Engineer (MCL) provisioning packages.') %}
|
|
<button type="button" class="ui-btn pri" data-bs-toggle="modal" data-bs-target="#uploadModal">Upload SFLD package</button>
|
|
{% endcall %}
|
|
|
|
<div class="ui-stack">
|
|
|
|
{# ---------- Active PPKG ---------- #}
|
|
<section class="panel" aria-labelledby="act-h">
|
|
<div class="phead" style="padding-bottom: 0"><h2 id="act-h" class="sec">Active SFLD package</h2></div>
|
|
{% if active_ppkg and active_ppkg.SOURCE_PPKG %}
|
|
<div class="active">
|
|
<span class="file mono">{{ active_ppkg.SOURCE_PPKG }}</span>
|
|
{% if active_ppkg.PPKG_VER %}{{ ui.chip(active_ppkg.PPKG_VER, 'run') }}{% endif %}
|
|
{% if active_ppkg.PPKG_EXP %}{{ ui.chip('Expires ' ~ active_ppkg.PPKG_EXP, 'neutral') }}{% endif %}
|
|
<span class="sub" style="flex-basis: 100%">startnet.cmd boots with this (from <span class="mono">ppkg.conf</span> on the enrollment share).</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="active"><div class="notice" style="flex: 1"><span aria-hidden="true" style="font-weight: 700">!</span><span>No active package is set in <span class="mono">ppkg.conf</span>. Upload a standard <span class="mono">GCCH_Prod_SFLD_v<ver>.ppkg</span> to set one.</span></div></div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{# ---------- SFLD packages ---------- #}
|
|
<section class="panel ui-list" aria-labelledby="sfld-h">
|
|
<div class="phead">
|
|
<h2 id="sfld-h" class="sec">Shop floor (SFLD) packages</h2>
|
|
<span class="sub">{{ packages | length }}</span>
|
|
</div>
|
|
{% if packages %}
|
|
<div class="sgrid colhead" aria-hidden="true"><span>File</span><span class="num">Size</span><span class="when">Modified</span></div>
|
|
{% for p in packages %}
|
|
{% set is_active = p.filename|lower == effective_ppkg|lower %}
|
|
<div class="ui-row">
|
|
<div class="sgrid">
|
|
<span class="s-name">
|
|
<span class="tip" data-tip="{{ p.filename }}"><span class="trunc mono">{{ p.filename }}</span></span>
|
|
{% if is_active %}{{ ui.chip('Active', 'ok') }}{% endif %}
|
|
</span>
|
|
<span class="num">{{ "%.1f"|format(p.size / 1048576) }} MB</span>
|
|
<span class="when mono">{{ p.modified | timestamp_fmt }}</span>
|
|
</div>
|
|
<div class="act wide">
|
|
<a class="ui-btn" href="{{ url_for('enrollment_download', filename=p.filename) }}">Download</a>
|
|
{% call ui.more(p.filename) %}
|
|
<button type="button" class="copy-btn" data-copy-text="{{ p.filename }}">Copy file name</button>
|
|
<div class="sep"></div>
|
|
{% if is_active %}
|
|
<button type="button" disabled>Active: cannot delete</button>
|
|
{% else %}
|
|
<button type="button" class="del" data-bs-toggle="modal" data-bs-target="#deleteModal"
|
|
data-filename="{{ p.filename }}">Delete package...</button>
|
|
{% endif %}
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty">No enrollment packages found. Upload a <span class="mono">.ppkg</span> file to get started.</div>
|
|
{% endif %}
|
|
<details class="hidden-bar" style="display: block">
|
|
<summary class="disc" style="min-height: 28px">About SFLD packages <span class="chev" aria-hidden="true">▾</span></summary>
|
|
<div class="ptext" style="padding: 6px 0 4px">
|
|
<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><strong>Naming:</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>
|
|
</details>
|
|
</section>
|
|
|
|
{# ---------- MCL packages ---------- #}
|
|
{% set mcl_users = mcl_links | selectattr('state', 'equalto', 'ok') | map(attribute='image') | list %}
|
|
{% set any_dup = mcl_packages | selectattr('conflict') | list | length %}
|
|
{% set any_ign = mcl_packages | rejectattr('prefix') | list | length %}
|
|
{% set any_used = mcl_packages | selectattr('prefix') | rejectattr('placeholder') | list | length %}
|
|
<section class="panel ui-list" id="mcl" aria-labelledby="mcl-h">
|
|
<div class="phead">
|
|
<h2 id="mcl-h" class="sec">Standard / Engineer (MCL) packages</h2>
|
|
<span class="sub">{{ mcl_packages | length }}</span>
|
|
</div>
|
|
<div class="ptext">
|
|
<p>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>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>
|
|
</div>
|
|
{% for l in mcl_links if l.state in ('copy', 'elsewhere') %}
|
|
<div style="padding: 0 24px 10px">
|
|
<div class="notice"><span aria-hidden="true" style="font-weight: 700">!</span><span>
|
|
<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.
|
|
</span></div>
|
|
</div>
|
|
{% endfor %}
|
|
<form action="{{ url_for('mcl_ppkg_upload') }}" method="post" enctype="multipart/form-data" class="upform">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="form-field">
|
|
<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="ui-btn pri" style="margin-bottom: 22px">Upload and replace</button>
|
|
</form>
|
|
|
|
{% if any_dup or any_ign or any_used %}
|
|
<div class="legend" aria-label="What the flags mean">
|
|
{% if any_dup %}<div>{{ ui.chip('Duplicate', 'err') }}<span>Its prefix also matches another package: enrollment for that tenant and purpose fails.</span></div>{% endif %}
|
|
{% if any_ign %}<div>{{ ui.chip('Ignored', 'warn') }}<span>The name does not match the vendor pattern, so the enroll scripts ignore it.</span></div>{% endif %}
|
|
{% if any_used %}<div>{{ ui.chip('Used', 'warn') }}<span>No <span class="mono">Exp_XXXXXXXX</span> placeholder in the name: it looks already used on a PC, and the enroll script cannot inject a new token into it.</span></div>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if mcl_packages %}
|
|
<div class="mgrid colhead" aria-hidden="true"><span>Tenant / purpose</span><span class="m-file">File</span><span>Version</span><span class="num m-size">Size</span><span></span></div>
|
|
{% for p in mcl_packages %}
|
|
<div class="ui-row">
|
|
<details>
|
|
<summary class="mgrid">
|
|
<span class="m-pre">
|
|
<span class="tip" data-tip="{{ p.prefix or 'No prefix' }}"><span class="trunc mono">{{ p.prefix or '-' }}</span></span>
|
|
<span class="flags">
|
|
{% if p.conflict %}{{ ui.chip('Duplicate', 'err') }}{% endif %}
|
|
{% if not p.prefix %}{{ ui.chip('Ignored', 'warn') }}{% elif not p.placeholder %}{{ ui.chip('Used', 'warn') }}{% endif %}
|
|
</span>
|
|
</span>
|
|
<span class="m-file tip" data-tip="{{ p.filename }}"><span class="trunc mono sub">{{ p.filename }}</span></span>
|
|
<span>{{ ui.chip(p.version or '?', 'neutral') }}</span>
|
|
<span class="num m-size">{{ "%.1f"|format(p.size / 1048576) }} MB</span>
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</summary>
|
|
<div class="mbody">
|
|
{% if p.conflict %}<div class="notice bad"><span aria-hidden="true" style="font-weight: 700">!</span><span>This prefix also matches {{ p.conflict_with | join(', ') }}; enrollment for it will fail.</span></div>{% endif %}
|
|
{% if not p.prefix %}<div class="notice"><span>Name does not match the vendor pattern; the enroll scripts ignore it.</span></div>
|
|
{% elif not p.placeholder %}<div class="notice"><span>No <span class="mono">Exp_XXXXXXXX</span> 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.</span></div>{% endif %}
|
|
<div class="mfacts"><span>File <span class="mono">{{ p.filename }}</span></span><span>{{ "%.1f"|format(p.size / 1048576) }} MB</span><span>Modified {{ p.modified | timestamp_fmt }}</span></div>
|
|
</div>
|
|
</details>
|
|
<div class="act">
|
|
{% call ui.more(p.filename) %}
|
|
<button type="button" class="copy-btn" data-copy-text="{{ p.filename }}">Copy file name</button>
|
|
<div class="sep"></div>
|
|
<form action="{{ url_for('mcl_ppkg_archive') }}" method="post"
|
|
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="del">Move to archive...</button>
|
|
</form>
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty">No MCL packages found in BPRT.</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{# ---------- MCL archive ---------- #}
|
|
{% if mcl_archive %}
|
|
<section class="panel ui-list" aria-labelledby="arc-h">
|
|
<div class="phead">
|
|
<h2 id="arc-h" class="sec">MCL archive</h2>
|
|
<span class="sub">{{ mcl_archive | length }}</span>
|
|
</div>
|
|
<div class="agrid colhead" aria-hidden="true"><span>Archived</span><span class="a-file">File</span><span>Version</span><span class="num a-size">Size</span></div>
|
|
{% for p in mcl_archive %}
|
|
<div class="ui-row">
|
|
<div class="agrid">
|
|
<span class="mono when">{{ p.batch }}</span>
|
|
<span class="a-file tip" data-tip="{{ p.filename }}"><span class="trunc mono sub">{{ p.filename }}</span></span>
|
|
<span>{{ ui.chip(p.version or '?', 'neutral') }}</span>
|
|
<span class="num a-size">{{ "%.1f"|format(p.size / 1048576) }} MB</span>
|
|
</div>
|
|
<div class="act wide">
|
|
<form action="{{ url_for('mcl_ppkg_restore') }}" method="post" style="margin: 0"
|
|
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="ui-btn">Restore</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ---------- Image links ---------- #}
|
|
{% if mcl_links %}
|
|
<section class="panel ui-list" aria-labelledby="lnk-h">
|
|
<div class="phead">
|
|
<h2 id="lnk-h" class="sec">Image links to the shared BPRT</h2>
|
|
</div>
|
|
{% for l in mcl_links %}
|
|
{% set c = link_chip.get(l.state, (l.state, 'neutral')) %}
|
|
<div class="ui-row lgrid">
|
|
<span>{{ l.image }}</span>
|
|
<span>{{ ui.chip(c[0], c[1]) }}</span>
|
|
<span class="mono">{% if l.state == 'ok' %}{{ l.path }}{% elif l.state == 'elsewhere' %}{{ l.path }} links to {{ l.target or '?' }}{% elif l.state == 'copy' %}{{ l.path }} is a real folder{% else %}<span class="sub">No Deploy/Applications/BPRT</span>{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div class="modal fade" id="uploadModal" tabindex="-1" aria-labelledby="uploadTitle">
|
|
<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">
|
|
<h3 class="modal-title" id="uploadTitle">Upload SFLD package</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></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="ui-btn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn pri">Upload</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteTitle">
|
|
<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">
|
|
<h3 class="modal-title" id="deleteTitle">Delete package</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete <strong id="deleteFilename" class="mono"></strong>?</p>
|
|
<p class="muted mb-0">This action cannot be undone.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn pri" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-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 = "{{ url_for('enrollment_delete', filename='__F__') }}".replace('__F__', encodeURIComponent(filename));
|
|
});
|
|
</script>
|
|
{% endblock %}
|