Webapp redesign phase 6: the remaining pages
Restyled to the same row pattern (one line, details in an expandable body, one action plus a menu, tokens for light and dark): - hardware coverage, audit log (family chips, destructive actions in red, client-side filters, first 300 rows), Clonezilla backups, enrollment packages (active package marked and undeletable, MCL flags with a visible legend) - image Overview, OS and packages, Unattend (sticky save bar), Import - Dell, iPXE, Blancco and BPRT update pages; Dell history links to the rollback review instead of rolling back on one click - boot menu editor (tabs, rows with a menu, lint chips that jump to the line, tokens instead of hard-coded dark colours) and the imaging session detail page Fixes found on the way: - BPRT batch Activate never asked to confirm: a hidden input named 'confirm' shadowed window.confirm in the inline handler, so the form submitted without a prompt. It now calls window.confirm. - Backup upload refused when a backup with that name exists, unless 'Replace the existing backup' is ticked (it overwrote silently). - app.js: em dashes in comments replaced (plain ASCII).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}BPRT Sync - PXE Server Manager{% endblock %}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% block title %}BPRT sync - PXE Manager{% endblock %}
|
||||
|
||||
{% macro size(n) -%}
|
||||
{%- if n is none -%}-{%- elif n >= 1000000000 -%}{{ '%.2f' | format(n / 1000000000) }} GB{%- elif n >= 1000000 -%}{{ '%.0f' | format(n / 1000000) }} MB{%- elif n >= 1000 -%}{{ '%.0f' | format(n / 1000) }} KB{%- else -%}{{ n }} B{%- endif -%}
|
||||
@@ -10,484 +11,593 @@
|
||||
{%- for part in n.split('_') %}{{ part }}{% if not loop.last %}_<wbr>{% endif %}{% endfor -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro val_badge(v, lg=false) -%}
|
||||
{% set cls = 'badge badge-lg' if lg else 'badge' %}
|
||||
{% if not v %}<span class="{{ cls }} badge-secondary">Not validated</span>
|
||||
{% elif v.stale %}<span class="{{ cls }} badge-warning" title="{{ v.stale }}">Changed since validation</span>
|
||||
{% elif v.state == 'valid' %}<span class="{{ cls }} badge-success">Valid</span>
|
||||
{% else %}<span class="{{ cls }} badge-danger">{{ v.errors | length }} error{{ 's' if v.errors | length != 1 }}</span>{% endif %}
|
||||
{% macro val_chip(v) -%}
|
||||
{% if not v %}{{ ui.chip('Not validated', 'off') }}
|
||||
{% elif v.stale %}{{ ui.chip('Changed since validation', 'warn') }}
|
||||
{% elif v.state == 'valid' %}{{ ui.chip('Valid', 'ok') }}
|
||||
{% else %}{{ ui.chip(v.errors | length ~ ' error' ~ ('s' if v.errors | length != 1), 'err') }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro job_panel(job) -%}
|
||||
{% if job.running %}
|
||||
<div class="section-card" id="bprtJob" data-running="1">
|
||||
<div class="section-title"><span class="spinner-border spinner-border-sm"></span>
|
||||
{{ {'manifest': 'Hashing the live BPRT', 'validate': 'Validating', 'activate': 'Activating', 'unzip': 'Unpacking the zip'}.get(job.kind, job.kind) }}
|
||||
{% if job.arg %}<span class="mono small">{{ job.arg }}</span>{% endif %}</div>
|
||||
<div class="small mb-1"><span id="bprtJobStep">{{ job.step or 'Starting' }}</span> <span id="bprtJobCurrent" class="mono text-muted">{{ job.current or '' }}</span></div>
|
||||
<div class="progress" style="height: 0.5rem;"><div id="bprtJobBar" class="progress-bar" style="width: {{ job.percent or 0 }}%;"></div></div>
|
||||
<div class="small text-muted mt-1" id="bprtJobPct">{% if job.percent is not none %}{{ job.percent }}%{% endif %}</div>
|
||||
<div class="panel strip" id="bprtJob" data-running="1" role="status">
|
||||
<span class="pulse" aria-hidden="true"></span>
|
||||
<span><strong>{{ {'manifest': 'Hashing the live BPRT', 'validate': 'Validating', 'activate': 'Activating', 'unzip': 'Unpacking the zip'}.get(job.kind, job.kind) }}</strong>
|
||||
{% if job.arg %}<span class="mono">{{ job.arg }}</span>{% endif %}</span>
|
||||
<span id="bprtJobStep">{{ job.step or 'Starting' }}</span>
|
||||
<span id="bprtJobCurrent" class="mono sub" style="min-width: 0; overflow-wrap: anywhere">{{ job.current or '' }}</span>
|
||||
<div class="bar" aria-hidden="true"><div id="bprtJobBar" style="width: {{ job.percent or 0 }}%"></div></div>
|
||||
<span class="sub" id="bprtJobPct">{% if job.percent is not none %}{{ job.percent }}%{% endif %}</span>
|
||||
</div>
|
||||
{% elif job.state in ('error', 'interrupted') %}
|
||||
<div class="alert alert-danger">
|
||||
<div class="errbox">
|
||||
<strong>The last BPRT {{ job.kind }} job {{ 'failed' if job.state == 'error' else 'stopped before it finished' }}</strong>
|
||||
{% if job.arg %}(<span class="mono">{{ job.arg }}</span>){% endif %}{% if job.finished %}, {{ job.finished | local_dt }}{% endif %}{% if job.error %}: {{ job.error }}{% endif %}
|
||||
{% if job.kind == 'activate' %}<div class="small mt-1">The live BPRT is only replaced by the last step (two renames), so it is either the old folder or the new one, never a half-built one.</div>{% endif %}
|
||||
{% if job.kind == 'activate' %}<div style="margin-top: 4px">The live BPRT is only replaced by the last step (two renames), so it is either the old folder or the new one, never a half-built one.</div>{% endif %}
|
||||
</div>
|
||||
{% elif job.state == 'done' and job.result %}
|
||||
<div class="small text-muted mb-3"><i class="bi bi-check2-circle"></i> Last job: {{ job.kind }}{% if job.arg %} <span class="mono">{{ job.arg }}</span>{% endif %}, {{ job.result }}{% if job.finished %} ({{ job.finished | local_dt }}){% endif %}.</div>
|
||||
<div class="sub">Last job: {{ job.kind }}{% if job.arg %} <span class="mono">{{ job.arg }}</span>{% endif %}, {{ job.result }}{% if job.finished %} ({{ job.finished | local_dt }}){% endif %}.</div>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro imaging_alert(imaging) -%}
|
||||
{% if imaging %}
|
||||
<div class="alert alert-warning">
|
||||
<strong><i class="bi bi-exclamation-triangle"></i> {{ imaging | length }} PC{{ 's are' if imaging | length != 1 else ' is' }} imaging now.</strong>
|
||||
<div class="notice"><span>
|
||||
<strong>{{ imaging | length }} PC{{ 's are' if imaging | length != 1 else ' is' }} imaging now.</strong>
|
||||
PESetup copies BPRT to each PC while it images; a PC copying during the swap can end up with a mix of old and new files.
|
||||
Activate or roll back when these have finished:
|
||||
{% for s in imaging[:8] %}<span class="mono">{{ s.hostname or s.id }}</span>{% if s.stage %} ({{ s.stage }}){% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if imaging | length > 8 %} and {{ imaging | length - 8 }} more{% endif %}.
|
||||
<a href="{{ url_for('imaging_dashboard') }}">Imaging progress</a>
|
||||
<a href="{{ url_for('imaging_dashboard') }}">Imaging progress</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.bprt td.wrap { white-space: normal; word-break: break-word; min-width: 8rem; }
|
||||
.bprt .file-list { columns: 2 22rem; column-gap: 2rem; margin: 0.25rem 0 0; padding-left: 1.1rem; }
|
||||
.bprt .file-list li { break-inside: avoid; }
|
||||
.bprt details > summary { cursor: pointer; }
|
||||
.bprt .fname { word-break: normal; overflow-wrap: normal; }
|
||||
.bprt .msg-list { margin: 0; padding-left: 1.1rem; }
|
||||
.bprt .msg-list li { margin-bottom: 0.25rem; }
|
||||
.bprt pre.cmd { white-space: pre-wrap; word-break: break-all; margin: 0.25rem 0 0.75rem; }
|
||||
.sec-h { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 8px 16px; padding: 18px 24px 12px; }
|
||||
.sec-h .sub { margin-top: 2px; }
|
||||
summary.sec-h { flex-wrap: nowrap; align-items: center; }
|
||||
.state-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 6px; }
|
||||
.strip { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 12px 20px; font-size: 13px; }
|
||||
.pulse { width: 10px; height: 10px; border-radius: 5px; background: var(--run); flex: none; animation: pulse 1.4s ease-in-out infinite; }
|
||||
@keyframes pulse { 50% { opacity: .35; } }
|
||||
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }
|
||||
.bar { flex: 1 1 160px; height: 6px; border-radius: 3px; background: var(--track-empty); overflow: hidden; min-width: 60px; max-width: 360px; }
|
||||
.bar > div { height: 100%; width: 0; background: var(--run); }
|
||||
.bar > div.bg-danger { background: var(--fail); }
|
||||
.pad { padding: 0 24px 16px; display: flex; flex-direction: column; gap: 10px; font-size: 13px; }
|
||||
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); gap: 12px 20px; margin: 0; }
|
||||
.facts dt { font-size: 12px; color: var(--muted); font-weight: 400; }
|
||||
.facts dd { margin: 2px 0 0; overflow-wrap: anywhere; font-size: 14px; }
|
||||
.sub-h { padding: 12px 24px 6px; border-top: 1px solid var(--line); font-size: 13px; font-weight: 600; color: var(--text-2); }
|
||||
.pgrid { display: grid; grid-template-columns: 170px minmax(0, 1fr) 90px 70px; align-items: center; gap: 0 16px; padding: 9px 24px; border-top: 1px solid var(--line); font-size: 13px; }
|
||||
.ogrid { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 0 16px; padding: 9px 24px; border-top: 1px solid var(--line); font-size: 13px; }
|
||||
.ogrid .chips { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.bgrid { display: grid; grid-template-columns: minmax(0, 1fr) 150px 130px 170px 14px; align-items: center; gap: 0 16px;
|
||||
padding: 12px 190px 12px 24px; font-size: 14px; min-height: 24px; }
|
||||
.xgrid { display: grid; grid-template-columns: 200px minmax(0, 1fr) 180px 14px; align-items: center; gap: 0 16px;
|
||||
padding: 12px 190px 12px 24px; font-size: 14px; min-height: 24px; }
|
||||
.hgrid { display: grid; grid-template-columns: 170px minmax(0, 1fr) 14px; align-items: center; gap: 0 16px; padding: 12px 24px; font-size: 14px; }
|
||||
.zgrid { display: grid; grid-template-columns: minmax(0, 1fr) 90px; align-items: center; gap: 0 16px; padding: 12px 190px 12px 24px; font-size: 14px; min-height: 36px; }
|
||||
.rbody { padding: 2px 24px 16px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; line-height: 1.55; }
|
||||
.rbody .mono { overflow-wrap: anywhere; }
|
||||
.msg-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.file-list { columns: 2 22rem; column-gap: 2rem; margin: 4px 0 0; padding-left: 18px; font-size: 12px; }
|
||||
.file-list li { break-inside: avoid; overflow-wrap: anywhere; }
|
||||
.fname { word-break: normal; overflow-wrap: anywhere; }
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); gap: 12px; }
|
||||
.stat { padding: 14px 18px; display: flex; flex-direction: column; gap: 2px; }
|
||||
.stat .n { font-size: 24px; font-weight: 600; }
|
||||
.ppk { display: grid; grid-template-columns: 170px minmax(0, 1fr) auto minmax(0, 1fr); gap: 4px 12px; align-items: baseline; padding: 9px 24px; border-top: 1px solid var(--line); font-size: 13px; }
|
||||
.ppk .old { color: var(--del-text); } .ppk .new { color: var(--add-text); }
|
||||
pre.cmd { white-space: pre-wrap; word-break: break-all; margin: 8px 0 4px; padding: 10px 12px; border-radius: 8px; background: var(--surface-2); border: 1px solid var(--line); font-size: 12px; color: var(--text); }
|
||||
.steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 10px; font-size: 13px; line-height: 1.6; }
|
||||
.upl { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-end; padding: 0 24px 16px; }
|
||||
.upl .fld { display: flex; flex-direction: column; gap: 4px; flex: 1 1 280px; min-width: 0; }
|
||||
.upl label { font-size: 13px; font-weight: 600; }
|
||||
input[type=file].field { padding: 5px 8px; height: auto; min-height: 36px; width: 100%; }
|
||||
.prog { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 0 24px 16px; font-size: 13px; }
|
||||
.foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; }
|
||||
.foot form { margin: 0; }
|
||||
@container (max-width: 900px) {
|
||||
.bgrid { grid-template-columns: minmax(0, 1fr) 150px 14px; }
|
||||
.bgrid .c-from, .bgrid .c-size { display: none; }
|
||||
.xgrid { grid-template-columns: 170px minmax(0, 1fr) 160px 14px; }
|
||||
}
|
||||
@container (max-width: 560px) {
|
||||
.bgrid .c-from, .bgrid .c-size { display: block; }
|
||||
.bgrid, .xgrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 12px 16px; gap: 4px 10px; }
|
||||
.bgrid .c-from, .bgrid .c-size, .xgrid .c-pk { grid-column: 1 / -1; }
|
||||
.bgrid .c-st, .xgrid .c-st { grid-column: 2; grid-row: 1; }
|
||||
.bgrid .chev, .xgrid .chev { grid-column: 3; grid-row: 1; }
|
||||
.hgrid { grid-template-columns: minmax(0, 1fr) 14px; padding: 12px 16px; gap: 2px 10px; }
|
||||
.hgrid .when { grid-column: 1; }
|
||||
.zgrid { padding: 12px 16px; }
|
||||
.pgrid { grid-template-columns: minmax(0, 1fr) auto; padding: 9px 16px; gap: 2px 10px; }
|
||||
.pgrid .c-file { grid-column: 1 / -1; grid-row: 2; }
|
||||
.pgrid .c-size { display: none; }
|
||||
.ppk { grid-template-columns: minmax(0, 1fr); padding: 9px 16px; }
|
||||
.ppk .to { display: none; }
|
||||
.ogrid { padding: 9px 16px; }
|
||||
.ui-row > .act { position: static; padding: 0 16px 10px; display: flex; justify-content: flex-end; }
|
||||
.rbody { padding: 2px 16px 14px; }
|
||||
.sec-h, .pad, .upl, .prog, .sub-h { padding-left: 16px; padding-right: 16px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bprt">
|
||||
{% if detail %}
|
||||
{# ======================= one incoming batch ============================== #}
|
||||
{% set b = detail %}
|
||||
<div class="page-header">
|
||||
<h1><i class="bi bi-inboxes"></i> Incoming batch <span class="mono">{{ b.name }}</span></h1>
|
||||
<div class="header-actions">
|
||||
{{ val_badge(res, lg=true) }}
|
||||
<a href="{{ url_for('bprt_sync.page') }}" class="pxe-btn pxe-btn-ghost"><i class="bi bi-arrow-left"></i> BPRT Sync</a>
|
||||
<header class="ui-head">
|
||||
<div>
|
||||
<div class="eyebrow-t"><a href="{{ url_for('bprt_sync.page') }}">BPRT sync</a></div>
|
||||
<h1>Incoming batch <span class="mono" style="font-size: 22px; overflow-wrap: anywhere">{{ b.name }}</span></h1>
|
||||
<div class="state-line">{{ val_chip(res) }}
|
||||
{% if res %}<span class="sub">Validated {{ res.validated | local_dt }}</span>{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions"><a href="{{ url_for('bprt_sync.page') }}" class="ui-btn">Back to BPRT sync</a></div>
|
||||
</header>
|
||||
|
||||
<p class="text-muted">
|
||||
{% if b.sync %}From <strong>{{ b.sync.computer or 'unknown PC' }}</strong>{% if b.sync.user %} ({{ b.sync.user }}){% endif %}{% if b.sync.created %}, {{ b.sync.created | local_dt }}{% endif %}.
|
||||
{% if b.sync.source %}Source <span class="mono">{{ b.sync.source }}</span>.{% endif %}
|
||||
{{ b.sync.listed }} vendor files listed, {{ b.files }} uploaded ({{ size(b.bytes) }}).{% endif %}
|
||||
Folder <span class="mono">{{ incoming }}/{{ b.name }}</span>.
|
||||
</p>
|
||||
{% if b.sync_error %}<div class="alert alert-danger">{{ b.sync_error }}</div>{% endif %}
|
||||
<div class="ui-stack">
|
||||
<section class="panel" aria-labelledby="src-h" style="padding: 16px 24px; display: flex; flex-direction: column; gap: 10px">
|
||||
<h2 id="src-h" class="vh">Where this batch came from</h2>
|
||||
<dl class="facts">
|
||||
{% if b.sync %}
|
||||
<div><dt>From</dt><dd>{{ b.sync.computer or 'unknown PC' }}{% if b.sync.user %} ({{ b.sync.user }}){% endif %}</dd></div>
|
||||
{% if b.sync.created %}<div><dt>Made</dt><dd>{{ b.sync.created | local_dt }}</dd></div>{% endif %}
|
||||
<div><dt>Files</dt><dd>{{ b.sync.listed }} listed, {{ b.files }} uploaded ({{ size(b.bytes) }})</dd></div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
<div class="sub" style="overflow-wrap: anywhere">Folder <span class="mono">{{ incoming }}/{{ b.name }}</span>{% if b.sync and b.sync.source %}. Source on the PC <span class="mono">{{ b.sync.source }}</span>{% endif %}.</div>
|
||||
{% if b.sync_error %}<div class="errbox">{{ b.sync_error }}</div>{% endif %}
|
||||
</section>
|
||||
|
||||
{{ job_panel(job) }}
|
||||
|
||||
{% if not res %}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-clipboard-check"></i> Validate</div>
|
||||
<p>Validation hashes every uploaded file against <span class="mono">sync.json</span>, checks that every other listed file is
|
||||
<section class="panel" aria-labelledby="val-h" style="padding: 18px 24px; display: flex; flex-direction: column; gap: 12px">
|
||||
<h2 id="val-h" class="sec">Validate</h2>
|
||||
<p class="sub" style="margin: 0; max-width: 820px">Validation hashes every uploaded file against <span class="mono">sync.json</span>, checks that every other listed file is
|
||||
unchanged in the live BPRT, applies the vendor's package rules and works out what activating the batch would change.
|
||||
Nothing on the share changes.</p>
|
||||
<form method="post" action="{{ url_for('bprt_sync.validate', name=b.name) }}">
|
||||
<div class="foot"><form method="post" action="{{ url_for('bprt_sync.validate', name=b.name) }}" style="margin: 0">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary" {% if job.running %}disabled{% endif %}><i class="bi bi-clipboard-check"></i> Validate</button>
|
||||
<button type="submit" class="ui-btn pri" {% if job.running %}disabled{% endif %}>Validate</button>
|
||||
</form>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete', name=b.name) }}" style="margin: 0"
|
||||
onsubmit="return confirm('Delete incoming batch {{ b.name }} and its uploaded files? The live BPRT is not touched.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="ui-btn danger" {% if job.running and job.arg == b.name %}disabled{% endif %}>Delete this batch</button>
|
||||
</form></div>
|
||||
</section>
|
||||
{% else %}
|
||||
|
||||
{% if res.errors %}
|
||||
<div class="section-card">
|
||||
<div class="section-title text-danger"><i class="bi bi-x-octagon"></i> Errors <span class="badge badge-danger">{{ res.errors | length }}</span></div>
|
||||
<p class="small text-muted">This batch cannot be activated. Fix the cause on the work PC and run Sync-BPRT.ps1 again (it makes a new batch), then delete this one.</p>
|
||||
<ul class="msg-list">{% for e in res.errors %}<li>{{ e }}</li>{% endfor %}</ul>
|
||||
</div>
|
||||
<section class="panel" aria-labelledby="err-h" style="padding: 18px 24px; display: flex; flex-direction: column; gap: 10px">
|
||||
<h2 id="err-h" class="sec" style="color: var(--fail-text)">{{ res.errors | length }} error{{ 's' if res.errors | length != 1 }}</h2>
|
||||
<p class="sub" style="margin: 0">This batch cannot be activated. Fix the cause on the work PC and run Sync-BPRT.ps1 again (it makes a new batch), then delete this one.</p>
|
||||
<div class="errbox"><ul class="msg-list">{% for e in res.errors %}<li>{{ e }}</li>{% endfor %}</ul></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if res.stale %}<div class="alert alert-warning">{{ res.stale }}: validate again before activating.</div>{% endif %}
|
||||
{% if res.stale %}<div class="notice">{{ res.stale }}: validate again before activating.</div>{% endif %}
|
||||
{% if res.warnings %}
|
||||
<div class="section-card">
|
||||
<div class="section-title text-warning"><i class="bi bi-exclamation-triangle"></i> Warnings <span class="badge badge-warning">{{ res.warnings | length }}</span></div>
|
||||
<ul class="msg-list">{% for w in res.warnings %}<li>{{ w }}</li>{% endfor %}</ul>
|
||||
</div>
|
||||
<div class="notice"><div><strong>{{ res.warnings | length }} warning{{ 's' if res.warnings | length != 1 }}</strong>
|
||||
<ul class="msg-list" style="margin-top: 4px">{% for w in res.warnings %}<li>{{ w }}</li>{% endfor %}</ul></div></div>
|
||||
{% endif %}
|
||||
|
||||
{% if res.counts %}
|
||||
<div class="dashboard-grid mb-4">
|
||||
<div class="stat-card {{ 'info' if res.counts.new else '' }}"><div class="label"><i class="bi bi-plus-circle"></i> New files</div><div class="value">{{ res.counts.new }}</div></div>
|
||||
<div class="stat-card {{ 'warning' if res.counts.changed else '' }}"><div class="label"><i class="bi bi-arrow-repeat"></i> Changed</div><div class="value">{{ res.counts.changed }}</div></div>
|
||||
<div class="stat-card {{ 'danger' if res.counts.removed else '' }}"><div class="label"><i class="bi bi-dash-circle"></i> Removed</div><div class="value">{{ res.counts.removed }}</div></div>
|
||||
<div class="stat-card success"><div class="label"><i class="bi bi-check2"></i> Unchanged</div><div class="value">{{ res.counts.unchanged }}</div></div>
|
||||
<div class="stats">
|
||||
<div class="panel stat"><span class="sub">New files</span><span class="n">{{ res.counts.new }}</span></div>
|
||||
<div class="panel stat"><span class="sub">Changed</span><span class="n">{{ res.counts.changed }}</span></div>
|
||||
<div class="panel stat"><span class="sub">Removed</span><span class="n" {% if res.counts.removed %}style="color: var(--fail-text)"{% endif %}>{{ res.counts.removed }}</span></div>
|
||||
<div class="panel stat"><span class="sub">Unchanged</span><span class="n">{{ res.counts.unchanged }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-box-seam"></i> MCL packages</div>
|
||||
{% if res.ppkg %}
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Tenant / purpose</th><th>Live now</th><th></th><th>After activation</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in res.ppkg %}
|
||||
<tr>
|
||||
<td class="mono">{{ p.prefix }}</td>
|
||||
<td class="wrap">{% for o in p.old %}<div><span class="badge badge-secondary">{{ o.version }}</span> <span class="mono small fname">{{ fname(o.name) }}</span></div>{% else %}<span class="text-muted">none</span>{% endfor %}</td>
|
||||
<td class="text-muted">to</td>
|
||||
<td class="wrap">{% for n in p.new %}<div><span class="badge badge-info">{{ n.version }}</span> <span class="mono small fname">{{ fname(n.name) }}</span></div>{% else %}<span class="text-danger">none (removed)</span>{% endfor %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="panel ui-list" aria-labelledby="pk-h">
|
||||
<div class="sec-h"><div><h2 id="pk-h" class="sec">MCL packages</h2><div class="sub">Live now, then after activation.</div></div></div>
|
||||
{% for p in res.ppkg %}
|
||||
<div class="ppk">
|
||||
<span class="mono">{{ p.prefix }}</span>
|
||||
<span>{% for o in p.old %}<div>{{ ui.chip(o.version, 'neutral') }} <span class="mono sub fname old">{{ fname(o.name) }}</span></div>{% else %}<span class="muted">none</span>{% endfor %}</span>
|
||||
<span class="sub to">to</span>
|
||||
<span>{% for n in p.new %}<div>{{ ui.chip(n.version, 'w11') }} <span class="mono sub fname new">{{ fname(n.name) }}</span></div>{% else %}<span style="color: var(--fail-text)">none (removed)</span>{% endfor %}</span>
|
||||
</div>
|
||||
{% else %}<p class="text-muted mb-0">No package changes.</p>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="hidden-bar">No package changes.</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{% if res.zips or res.bom_fix %}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-file-earmark-zip"></i> Large files and fixes</div>
|
||||
{% if res.zips %}
|
||||
<div class="table-container mb-2">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Zip</th><th>Live size</th><th>New size</th></tr></thead>
|
||||
<tbody>{% for z in res.zips %}<tr><td class="mono wrap">{{ z.path }}</td><td>{{ size(z.old_size) if z.old_size is not none else 'new' }}</td><td>{{ size(z.new_size) }}</td></tr>{% endfor %}</tbody>
|
||||
</table>
|
||||
<section class="panel ui-list" aria-labelledby="zp-h">
|
||||
<div class="sec-h"><div><h2 id="zp-h" class="sec">Large files and fixes</h2></div></div>
|
||||
{% for z in res.zips %}
|
||||
<div class="pgrid" style="grid-template-columns: minmax(0, 1fr) 90px 90px">
|
||||
<span class="mono fname">{{ z.path }}</span><span class="sub">{{ ('live ' ~ size(z.old_size)) if z.old_size is not none else 'new file' }}</span><span>now {{ size(z.new_size) }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if res.bom_fix %}
|
||||
<div class="pad" style="padding-top: 12px; border-top: 1px solid var(--line)">
|
||||
<span>Activation adds a UTF-8 byte order mark to {{ res.bom_fix | length }} script{{ 's' if res.bom_fix | length != 1 }}
|
||||
that hold{{ 's' if res.bom_fix | length == 1 }} non-ASCII text without one (Windows PowerShell 5.1 would read them as ANSI):</span>
|
||||
<ul class="file-list mono">{% for f in res.bom_fix %}<li>{{ f }}</li>{% endfor %}</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if res.bom_fix %}
|
||||
<p class="small mb-1">Activation adds a UTF-8 byte order mark to {{ res.bom_fix | length }} script{{ 's' if res.bom_fix | length != 1 }}
|
||||
that hold{{ 's' if res.bom_fix | length == 1 }} non-ASCII text without one (Windows PowerShell 5.1 would read them as ANSI):</p>
|
||||
<ul class="file-list small mono">{% for f in res.bom_fix %}<li>{{ f }}</li>{% endfor %}</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-list-ul"></i> Files</div>
|
||||
<section class="panel ui-list" aria-labelledby="fl-h">
|
||||
<div class="sec-h"><div><h2 id="fl-h" class="sec">Files</h2></div></div>
|
||||
{% for key, label in [('new', 'New'), ('changed', 'Changed'), ('removed', 'Removed (not in the vendor folder any more; kept in the archive)'), ('unchanged', 'Unchanged')] %}
|
||||
{% set rows = res.diff[key] %}
|
||||
{% if rows %}
|
||||
<details class="mb-2" {% if key != 'unchanged' and rows | length <= 40 %}open{% endif %}>
|
||||
<summary><strong>{{ label }}</strong> <span class="badge badge-secondary">{{ rows | length }}</span></summary>
|
||||
<ul class="file-list small mono">{% for f in rows %}<li>{{ f }}{% if f in renamed %} <span class="text-muted">(kept as {{ renamed[f] }})</span>{% endif %}</li>{% endfor %}</ul>
|
||||
<details style="border-top: 1px solid var(--line)" {% if key != 'unchanged' and rows | length <= 40 %}open{% endif %}>
|
||||
<summary class="disc" style="padding: 0 24px; min-height: 44px; color: var(--text)"><span class="chev" aria-hidden="true">▾</span>{{ label }} {{ ui.chip(rows | length, 'neutral') }}</summary>
|
||||
<ul class="file-list mono" style="margin: 0 24px 14px">{% for f in rows %}<li>{{ f }}{% if f in renamed %} <span class="muted">(kept as {{ renamed[f] }})</span>{% endif %}</li>{% endfor %}</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<p class="small text-muted mb-0">Site files carried over from the live BPRT as they are:
|
||||
{% for o in overlay %}<span class="mono">{{ o }}</span>{% if not loop.last %}, {% endif %}{% endfor %}.</p>
|
||||
</div>
|
||||
<div class="hidden-bar"><span>Site files carried over from the live BPRT as they are:
|
||||
{% for o in overlay %}<span class="mono">{{ o }}</span>{% if not loop.last %}, {% endif %}{% endfor %}.</span></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-rocket-takeoff"></i> Activate</div>
|
||||
<section class="panel" aria-labelledby="act-h" style="padding: 18px 24px; display: flex; flex-direction: column; gap: 12px">
|
||||
<h2 id="act-h" class="sec">Activate</h2>
|
||||
{{ imaging_alert(imaging) }}
|
||||
<p class="small text-muted">
|
||||
Builds <span class="mono">BPRT.new</span> beside the live folder (hard links: unchanged files take no space), puts the site
|
||||
files back on top, checks it once more, then swaps it in with two renames: the live folder becomes
|
||||
<span class="mono">BPRT-archive/full-<time></span> (roll back to it at any time) and <span class="mono">BPRT.new</span> becomes
|
||||
<span class="mono">BPRT</span>. Every image links to that path, so they all switch at once.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<details>
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>What activating does</summary>
|
||||
<p class="sub" style="margin: 4px 0 0; max-width: 820px; line-height: 1.6">
|
||||
Builds <span class="mono">BPRT.new</span> beside the live folder (hard links: unchanged files take no space), puts the site
|
||||
files back on top, checks it once more, then swaps it in with two renames: the live folder becomes
|
||||
<span class="mono">BPRT-archive/full-<time></span> (roll back to it at any time) and <span class="mono">BPRT.new</span> becomes
|
||||
<span class="mono">BPRT</span>. Every image links to that path, so they all switch at once.</p>
|
||||
</details>
|
||||
<div class="foot">
|
||||
<form method="post" action="{{ url_for('bprt_sync.activate', name=b.name) }}"
|
||||
onsubmit="return confirm('Replace the live BPRT with batch {{ b.name }}?{% if imaging %} {{ imaging | length }} PC(s) are imaging right now.{% endif %} The current folder is archived and can be rolled back.');">
|
||||
onsubmit="return window.confirm('Replace the live BPRT with batch {{ b.name }}?{% if imaging %} {{ imaging | length }} PC(s) are imaging right now.{% endif %} The current folder is archived and can be rolled back.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="confirm" value="{{ b.name }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-success" {% if res.state != 'valid' or res.stale or job.running %}disabled{% endif %}><i class="bi bi-check2-circle"></i> Activate</button>
|
||||
<button type="submit" class="ui-btn pri" {% if res.state != 'valid' or res.stale or job.running %}disabled{% endif %}>Activate</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('bprt_sync.validate', name=b.name) }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost" {% if job.running %}disabled{% endif %}><i class="bi bi-clipboard-check"></i> Validate again</button>
|
||||
<button type="submit" class="ui-btn" {% if job.running %}disabled{% endif %}>Validate again</button>
|
||||
</form>
|
||||
{% if res.state != 'valid' %}<span class="sub">Activate needs a valid batch.</span>{% elif res.stale %}<span class="sub">Validate again first: something changed.</span>{% endif %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete', name=b.name) }}" style="margin-left: auto"
|
||||
onsubmit="return confirm('Delete incoming batch {{ b.name }} and its uploaded files? The live BPRT is not touched.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="ui-btn danger" {% if job.running and job.arg == b.name %}disabled{% endif %}>Delete this batch</button>
|
||||
</form>
|
||||
<span class="small text-muted align-self-center">Validated {{ res.validated | local_dt }}.</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete', name=b.name) }}" class="mb-4"
|
||||
onsubmit="return confirm('Delete incoming batch {{ b.name }} and its uploaded files? The live BPRT is not touched.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost" {% if job.running and job.arg == b.name %}disabled{% endif %}><i class="bi bi-trash"></i> Delete this batch</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{# ======================= overview ======================================= #}
|
||||
<div class="page-header">
|
||||
<h1><i class="bi bi-arrow-left-right"></i> BPRT Sync</h1>
|
||||
<div class="header-actions">
|
||||
{% if live.missing_required %}<span class="badge badge-danger badge-lg">{{ live.missing_required | length }} required file{{ 's' if live.missing_required | length != 1 }} missing</span>
|
||||
{% elif live.exists %}<span class="badge badge-success badge-lg">{{ packages | selectattr('prefix') | list | length }} MCL packages live</span>{% endif %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.rebuild') }}" style="display:inline;">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost" {% if job.running %}disabled{% endif %}
|
||||
title="Hash any live file the cache does not know; Sync-BPRT.ps1 starts this itself when it needs it"><i class="bi bi-fingerprint"></i> Rehash live folder</button>
|
||||
</form>
|
||||
{% set n_pk = packages | selectattr('prefix') | list | length %}
|
||||
<header class="ui-head">
|
||||
<div>
|
||||
<div class="eyebrow-t">Updates</div>
|
||||
<h1>BPRT sync</h1>
|
||||
<div class="state-line">
|
||||
{% if not live.exists %}{{ ui.chip('No live BPRT folder', 'fail') }}
|
||||
{% elif live.missing_required %}{{ ui.chip(live.missing_required | length ~ ' required file' ~ ('s' if live.missing_required | length != 1) ~ ' missing', 'fail') }}
|
||||
{% else %}{{ ui.chip(n_pk ~ ' MCL packages live', 'ok') }}{% endif %}
|
||||
{% set waiting = batches | rejectattr('uploading') | list | length %}
|
||||
<span class="sub">{% if waiting %}{{ waiting }} incoming batch{{ 'es' if waiting != 1 }} waiting{% else %}No batch waiting{% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted">
|
||||
<span class="mono">_shared/Applications/BPRT</span> is the vendor's bulk-enroll folder (MCL packages, Office zips, orchestrator
|
||||
scripts) that every image links to and PESetup copies to every PC. The vendor keeps it current on the GE work PC with
|
||||
MediaCreator Lite; <strong>Sync-BPRT.ps1</strong> on that PC compares it with this server's copy and uploads only what changed,
|
||||
over SSH, into <span class="mono">BPRT-incoming</span>. Here a batch is validated, its changes shown, and activated with the
|
||||
old folder archived. The site's own files ({% for o in overlay %}<span class="mono">{{ o }}</span>{% if not loop.last %}, {% endif %}{% endfor %})
|
||||
are never touched by a sync.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<form method="post" action="{{ url_for('bprt_sync.rebuild') }}" style="margin: 0">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="ui-btn" {% if job.running %}disabled{% endif %}
|
||||
title="Hash any live file the cache does not know; Sync-BPRT.ps1 starts this itself when it needs it">Rehash live folder</button>
|
||||
</form>
|
||||
{% if script_available %}<a class="ui-btn pri" href="{{ url_for('bprt_sync.script') }}">Download Sync-BPRT.ps1</a>{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ui-stack">
|
||||
{% if live.missing_required %}
|
||||
<div class="notice bad"><span><strong>Missing files the bulk-enroll flow calls by path:</strong>
|
||||
{% for r in live.missing_required %}<span class="mono">{{ r }}</span>{% if not loop.last %}, {% endif %}{% endfor %}. Enrollment breaks on every PC until they are back.</span></div>
|
||||
{% endif %}
|
||||
{{ imaging_alert(imaging) }}
|
||||
{{ job_panel(job) }}
|
||||
|
||||
<details>
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>What BPRT sync does</summary>
|
||||
<p class="sub" style="margin: 4px 0 0; max-width: 900px; line-height: 1.6">
|
||||
<span class="mono">_shared/Applications/BPRT</span> is the vendor's bulk-enroll folder (MCL packages, Office zips, orchestrator
|
||||
scripts) that every image links to and PESetup copies to every PC. The vendor keeps it current on the GE work PC with
|
||||
MediaCreator Lite; Sync-BPRT.ps1 on that PC compares it with this server's copy and uploads only what changed,
|
||||
over SSH, into <span class="mono">BPRT-incoming</span>. Here a batch is validated, its changes shown, and activated with the
|
||||
old folder archived. The site's own files ({% for o in overlay %}<span class="mono">{{ o }}</span>{% if not loop.last %}, {% endif %}{% endfor %})
|
||||
are never touched by a sync.</p>
|
||||
</details>
|
||||
|
||||
{# ---- Live folder --------------------------------------------------------- #}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-folder-check"></i> Live BPRT</div>
|
||||
<section class="panel ui-list" aria-labelledby="live-h">
|
||||
<div class="sec-h"><div><h2 id="live-h" class="sec">Live BPRT</h2></div>
|
||||
{% if live.exists %}<span>{% if live.missing_required %}{{ ui.chip('Required files missing', 'fail') }}{% else %}{{ ui.chip('Required files present', 'ok') }}{% endif %}</span>{% endif %}</div>
|
||||
{% if not live.exists %}
|
||||
<div class="alert alert-danger mb-0"><span class="mono">{{ live.path }}</span> does not exist.</div>
|
||||
<div class="pad"><div class="notice bad"><span><span class="mono">{{ live.path }}</span> does not exist.</span></div></div>
|
||||
{% else %}
|
||||
<p class="small text-muted"><span class="mono">{{ live.path }}</span>: {{ live.count }} files, {{ size(live.bytes) }}{% if live.newest %}, newest file {{ live.newest | timestamp_fmt }}{% endif %}.</p>
|
||||
{% if live.is_link %}<div class="alert alert-danger"><span class="mono">{{ live.path }}</span> is a symlink; activation needs it to be a real folder and refuses to run.</div>{% endif %}
|
||||
{% for l in links if l.state in ('copy', 'elsewhere', 'none') %}
|
||||
<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; an image import does this), so a sync here does not reach its PCs.
|
||||
{% elif l.state == 'elsewhere' %}links BPRT to <span class="mono">{{ l.target or '?' }}</span>, not to the shared folder, so a sync here does not reach its PCs.
|
||||
{% else %}has no <span class="mono">Deploy/Applications/BPRT</span>.{% endif %}
|
||||
<div class="pad">
|
||||
<dl class="facts">
|
||||
<div><dt>Files</dt><dd>{{ live.count }}</dd></div>
|
||||
<div><dt>Size</dt><dd>{{ size(live.bytes) }}</dd></div>
|
||||
{% if live.newest %}<div><dt>Newest file</dt><dd class="mono">{{ live.newest | timestamp_fmt }}</dd></div>{% endif %}
|
||||
</dl>
|
||||
<div class="sub" style="overflow-wrap: anywhere">Folder <span class="mono">{{ live.path }}</span></div>
|
||||
{% if live.is_link %}<div class="notice bad"><span><span class="mono">{{ live.path }}</span> is a symlink; activation needs it to be a real folder and refuses to run.</span></div>{% endif %}
|
||||
{% for l in links if l.state in ('copy', 'elsewhere', 'none') %}
|
||||
<div class="notice"><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; an image import does this), so a sync here does not reach its PCs.
|
||||
{% elif l.state == 'elsewhere' %}links BPRT to <span class="mono">{{ l.target or '?' }}</span>, not to the shared folder, so a sync here does not reach its PCs.
|
||||
{% else %}has no <span class="mono">Deploy/Applications/BPRT</span>.{% endif %}</span></div>
|
||||
{% endfor %}
|
||||
{% if live.bom_needed %}
|
||||
<div class="notice"><span>{{ live.bom_needed | length }} script{{ 's' if live.bom_needed | length != 1 }} hold{{ 's' if live.bom_needed | length == 1 }} non-ASCII text without a UTF-8 byte order mark, which Windows PowerShell 5.1 misreads:
|
||||
{% for r in live.bom_needed %}<span class="mono">{{ r }}</span>{% if not loop.last %}, {% endif %}{% endfor %}. The next activation fixes the copies it brings in.</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sub-h">MCL packages</div>
|
||||
{% set clashes = packages | selectattr('conflict') | list %}
|
||||
{% if clashes %}
|
||||
<div class="pad" style="padding-top: 8px">{% for p in clashes %}<div class="notice bad"><span><span class="mono">{{ p.filename }}</span> also matches {{ p.conflict_with | join(', ') }}: one prefix finding two packages breaks enrollment for that tenant and purpose.</span></div>{% endfor %}</div>
|
||||
{% endif %}
|
||||
{% for p in packages %}
|
||||
<div class="pgrid">
|
||||
<span class="mono">{{ p.prefix or '-' }}{% if p.conflict %} {{ ui.chip('Duplicate', 'err') }}{% endif %}</span>
|
||||
<span class="tip c-file" data-tip="{{ p.filename }}"><span class="trunc mono sub">{{ p.filename }}</span></span>
|
||||
<span>{{ ui.chip(p.version or '?', 'w11') }}</span>
|
||||
<span class="sub c-size">{{ size(p.size) }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="hidden-bar">No packages.</div>
|
||||
{% endfor %}
|
||||
<div class="hidden-bar"><span>A single package can also be replaced on <a href="{{ url_for('enrollment') }}#mcl">Enrollment packages</a>.</span></div>
|
||||
|
||||
<div class="sub-h">Site files (kept on every sync)</div>
|
||||
{% for o in live.overlay %}
|
||||
<div class="ogrid">
|
||||
<span class="mono" style="overflow-wrap: anywhere">{{ o.name }}</span>
|
||||
<span class="chips">
|
||||
{% if o.ok %}{{ ui.chip('Present', 'ok') }}
|
||||
{% elif o.present %}{{ ui.chip('Vendor copy, not our shim', 'err') }}
|
||||
{% else %}{{ ui.chip('Missing', 'fail' if o.name == 'Register-BulkEnrollTask-Standard.ps1' or o.name in renamed else 'off') }}{% endif %}
|
||||
{% if o.vendor_copy is defined %}{% if o.vendor_copy %}{{ ui.chip('vendor dialog kept', 'neutral') }}{% else %}{{ ui.chip('no vendor dialog: ' ~ renamed[o.name] ~ ' missing', 'warn') }}{% endif %}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if live.missing_required %}
|
||||
<div class="alert alert-danger py-2">Missing files the bulk-enroll flow calls by path: {% for r in live.missing_required %}<span class="mono">{{ r }}</span>{% if not loop.last %}, {% endif %}{% endfor %}.</div>
|
||||
<div class="hidden-bar"><span>
|
||||
<span class="mono">Select-PCConfig.ps1</span> is our shim (repo <span class="mono">playbook/shopfloor-setup/BPRT</span>); the vendor's
|
||||
dialog is kept as <span class="mono">Select-PCConfig-vendor.ps1</span>. The standard and engineer unattends run
|
||||
<span class="mono">Register-BulkEnrollTask-Standard.ps1</span> (specialize Order 39).</span></div>
|
||||
{% endif %}
|
||||
{% if live.bom_needed %}
|
||||
<div class="alert alert-warning py-2">{{ live.bom_needed | length }} script{{ 's' if live.bom_needed | length != 1 }} hold{{ 's' if live.bom_needed | length == 1 }} non-ASCII text without a UTF-8 byte order mark, which Windows PowerShell 5.1 misreads:
|
||||
{% for r in live.bom_needed %}<span class="mono">{{ r }}</span>{% if not loop.last %}, {% endif %}{% endfor %}. The next activation fixes the copies it brings in.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-7">
|
||||
<h6>MCL packages</h6>
|
||||
{% if packages %}
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Tenant / purpose</th><th>File</th><th>Version</th><th>Size</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in packages %}
|
||||
<tr>
|
||||
<td class="mono text-nowrap">{{ p.prefix or '-' }}{% if p.conflict %} <span class="badge badge-danger" title="Also matches {{ p.conflict_with | join(', ') }}">Duplicate</span>{% endif %}</td>
|
||||
<td class="mono small fname">{{ fname(p.filename) }}</td>
|
||||
<td><span class="badge badge-info">{{ p.version or '?' }}</span></td>
|
||||
<td class="text-nowrap">{{ size(p.size) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}<p class="text-muted">No packages.</p>{% endif %}
|
||||
<div class="small text-muted mt-1">A single package can also be replaced on <a href="{{ url_for('enrollment') }}#mcl">Enrollment Packages</a>.</div>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<h6>Site files (kept on every sync)</h6>
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
{% for o in live.overlay %}
|
||||
<tr>
|
||||
<td class="mono small wrap">{{ o.name }}</td>
|
||||
<td class="text-nowrap">
|
||||
{% if o.ok %}<span class="badge badge-success">Present</span>
|
||||
{% elif o.present %}<span class="badge badge-danger" title="This is the vendor's dialog, not our shim">Vendor copy</span>
|
||||
{% else %}<span class="badge badge-{{ 'danger' if o.name == 'Register-BulkEnrollTask-Standard.ps1' or o.name in renamed else 'secondary' }}">Missing</span>{% endif %}
|
||||
{% if o.vendor_copy is defined %}{% if o.vendor_copy %}<span class="badge badge-secondary" title="{{ renamed[o.name] }}">vendor dialog kept</span>{% else %}<span class="badge badge-warning" title="{{ renamed[o.name] }} is missing: the shim cannot fall back to the vendor dialog">no vendor dialog</span>{% endif %}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="small text-muted mt-1">
|
||||
<span class="mono">Select-PCConfig.ps1</span> is our shim (repo <span class="mono">playbook/shopfloor-setup/BPRT</span>); the vendor's
|
||||
dialog is kept as <span class="mono">Select-PCConfig-vendor.ps1</span>. The standard and engineer unattends run
|
||||
<span class="mono">Register-BulkEnrollTask-Standard.ps1</span> (specialize Order 39).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# ---- Incoming batches ---------------------------------------------------- #}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-inboxes"></i> Incoming batches <span class="badge badge-secondary">{{ batches | length }}</span></div>
|
||||
{% if batches %}
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Batch</th><th>From</th><th>Uploaded</th><th>Status</th><th class="text-end"></th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in batches %}
|
||||
<tr>
|
||||
<td class="mono small wrap">{% if b.uploading %}{{ b.name }}{% else %}<a href="{{ url_for('bprt_sync.batch', name=b.name) }}">{{ b.name }}</a>{% endif %}</td>
|
||||
<td class="small wrap">{% if b.sync %}{{ b.sync.computer or '-' }}{% if b.sync.created %}<div class="text-muted">{{ b.sync.created | local_dt }}</div>{% endif %}{% else %}-{% endif %}</td>
|
||||
<td class="small text-nowrap">{{ b.files }} file{{ 's' if b.files != 1 }}, {{ size(b.bytes) }}</td>
|
||||
<td class="small">
|
||||
{% if b.uploading %}<span class="badge badge-info" title="Sync-BPRT.ps1 renames the folder when the upload is complete">Uploading</span>
|
||||
{% elif b.sync_error %}<span class="badge badge-danger" title="{{ b.sync_error }}">Bad sync.json</span>
|
||||
{% elif not b.sync %}<span class="badge badge-danger" title="sync.json is uploaded last; this batch is incomplete">No sync.json</span>
|
||||
{% else %}{{ val_badge(b.validation) }}{% endif %}
|
||||
</td>
|
||||
<td class="text-end text-nowrap">
|
||||
{% if not b.uploading and b.sync %}
|
||||
<a class="pxe-btn pxe-btn-ghost pxe-btn-sm" href="{{ url_for('bprt_sync.batch', name=b.name) }}"><i class="bi bi-eye"></i> {{ 'Review' if b.validation else 'Open' }}</a>
|
||||
{% if not b.validation %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.validate', name=b.name) }}" style="display:inline;">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary pxe-btn-sm" {% if job.running %}disabled{% endif %}><i class="bi bi-clipboard-check"></i> Validate</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete', name=b.name) }}" style="display:inline;"
|
||||
onsubmit="return confirm('Delete incoming batch {{ b.name }}?{% if b.uploading %} If Sync-BPRT.ps1 is still uploading it, its upload fails.{% endif %} The live BPRT is not touched.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-row-action" title="Delete batch"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="panel ui-list" aria-labelledby="in-h">
|
||||
<div class="sec-h"><div><h2 id="in-h" class="sec">Incoming batches</h2>
|
||||
<div class="sub">Validate a batch, read its changes, then activate it from its page.</div></div>
|
||||
<span class="sub">{{ batches | length }}</span></div>
|
||||
{% for b in batches %}
|
||||
<div class="ui-row">
|
||||
<details {% if b.sync_error or (b.validation and b.validation.errors) %}open{% endif %}>
|
||||
<summary class="bgrid">
|
||||
<span class="tip" data-tip="{{ b.name }}"><span class="trunc mono">{{ b.name }}</span></span>
|
||||
{% set frm = ((b.sync.computer or '-') ~ (', ' ~ (b.sync.created | local_dt) if b.sync.created else '')) if b.sync else '-' %}
|
||||
<span class="tip c-from" data-tip="{{ frm }}"><span class="trunc sub">{{ frm }}</span></span>
|
||||
<span class="sub c-size">{{ b.files }} file{{ 's' if b.files != 1 }}, {{ size(b.bytes) }}</span>
|
||||
<span class="c-st">
|
||||
{% if b.uploading %}{{ ui.chip('Uploading', 'run') }}
|
||||
{% elif b.sync_error %}{{ ui.chip('Bad sync.json', 'err') }}
|
||||
{% elif not b.sync %}{{ ui.chip('No sync.json', 'err') }}
|
||||
{% else %}{{ val_chip(b.validation) }}{% endif %}
|
||||
</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="rbody">
|
||||
{% if b.uploading %}<div class="sub">Sync-BPRT.ps1 renames the folder when the upload is complete.</div>{% endif %}
|
||||
{% if b.sync_error %}<div class="errbox">{{ b.sync_error }}</div>
|
||||
{% elif not b.uploading and not b.sync %}<div class="errbox">sync.json is uploaded last; this batch is incomplete.</div>{% endif %}
|
||||
{% set v = b.validation %}
|
||||
{% if v %}
|
||||
{% if v.errors %}<div class="errbox"><ul class="msg-list">{% for e in v.errors %}<li>{{ e }}</li>{% endfor %}</ul></div>{% endif %}
|
||||
{% if v.stale %}<div class="notice">{{ v.stale }}</div>{% endif %}
|
||||
{% if v.warnings %}<div class="notice"><ul class="msg-list">{% for w in v.warnings %}<li>{{ w }}</li>{% endfor %}</ul></div>{% endif %}
|
||||
{% if v.counts %}<div>{{ v.counts.new }} new, {{ v.counts.changed }} changed, {{ v.counts.removed }} removed, {{ v.counts.unchanged }} unchanged.</div>{% endif %}
|
||||
{% for p in v.ppkg or [] %}<div class="sub mono">{{ p.prefix }}: {{ p.old | map(attribute='version') | join(', ') or 'none' }} to {{ p.new | map(attribute='version') | join(', ') or 'none' }}</div>{% endfor %}
|
||||
<div class="sub">Validated {{ v.validated | local_dt }}.</div>
|
||||
{% elif b.sync %}<div class="sub">Not validated yet. Validate hashes the upload and works out what activating would change; nothing on the share changes.</div>{% endif %}
|
||||
{% if b.sync %}<div class="sub">From {{ b.sync.computer or 'unknown PC' }}{% if b.sync.created %}, {{ b.sync.created | local_dt }}{% endif %}. {{ b.sync.listed }} vendor files listed{% if b.sync.user %}, by {{ b.sync.user }}{% endif %}{% if b.sync.source %}, from <span class="mono">{{ b.sync.source }}</span>{% endif %}.</div>{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="act">
|
||||
{% if not b.uploading and b.sync %}
|
||||
{% if not b.validation %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.validate', name=b.name) }}" style="margin: 0">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="ui-btn pri" {% if job.running %}disabled{% endif %}>Validate</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="ui-btn{{ ' pri' if b.validation.state == 'valid' and not b.validation.stale }}" href="{{ url_for('bprt_sync.batch', name=b.name) }}">Review</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% call ui.more('batch ' ~ b.name) %}
|
||||
{% if not b.uploading and b.sync %}<a href="{{ url_for('bprt_sync.batch', name=b.name) }}">Open batch page</a><div class="sep"></div>{% endif %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete', name=b.name) }}"
|
||||
onsubmit="return confirm('Delete incoming batch {{ b.name }}?{% if b.uploading %} If Sync-BPRT.ps1 is still uploading it, its upload fails.{% endif %} The live BPRT is not touched.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="del">Delete batch</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Nothing waiting. Run Sync-BPRT.ps1 on the work PC (below), or upload a zip.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hidden-bar">Nothing waiting. Run Sync-BPRT.ps1 on the work PC (below), or upload a zip.</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{# ---- How to sync --------------------------------------------------------- #}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-terminal"></i> Sync from the work PC</div>
|
||||
<ol class="mb-2">
|
||||
<li>Let MediaCreator Lite finish updating its cache on the GE work PC.</li>
|
||||
<li>Get <span class="mono">Sync-BPRT.ps1</span>
|
||||
{% if script_available %}(<a href="{{ url_for('bprt_sync.script') }}">download it from here</a>)
|
||||
{% else %}from the pxe repo, <span class="mono">webapp/static/Sync-BPRT.ps1</span>{% endif %},
|
||||
and run it in Windows PowerShell. Look first with <span class="mono">-DryRun</span> (nothing is uploaded):
|
||||
<pre class="cmd mono small">powershell -ExecutionPolicy Bypass -File .\Sync-BPRT.ps1 -DryRun
|
||||
<section class="panel" aria-labelledby="how-h">
|
||||
<details>
|
||||
<summary class="sec-h">
|
||||
<div><h2 id="how-h" class="sec" style="font-size: 15px">Sync from the work PC</h2>
|
||||
<div class="sub">Run Sync-BPRT.ps1 on the GE work PC; it uploads only what changed.</div></div>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div style="padding: 0 24px 20px; display: flex; flex-direction: column; gap: 12px">
|
||||
<ol class="steps">
|
||||
<li>Let MediaCreator Lite finish updating its cache on the GE work PC.</li>
|
||||
<li>Get <span class="mono">Sync-BPRT.ps1</span>
|
||||
{% if script_available %}(<a href="{{ url_for('bprt_sync.script') }}">download it from here</a>)
|
||||
{% else %}from the pxe repo, <span class="mono">webapp/static/Sync-BPRT.ps1</span>{% endif %},
|
||||
and run it in Windows PowerShell. Look first with <span class="mono">-DryRun</span> (nothing is uploaded):
|
||||
<pre class="cmd mono">powershell -ExecutionPolicy Bypass -File .\Sync-BPRT.ps1 -DryRun
|
||||
powershell -ExecutionPolicy Bypass -File .\Sync-BPRT.ps1</pre>
|
||||
It reads this server's manifest, hashes the local folder (<span class="mono">C:\ProgramData\GEAerospace\MediaCenter\Cache\Deploy\Applications\BPRT</span>
|
||||
unless <span class="mono">-Source</span> says otherwise), parse-checks the vendor scripts, and uploads the new and changed files with one
|
||||
<span class="mono">sftp</span> session: type the <span class="mono">pxe</span> password once.</li>
|
||||
<li>Come back here, open the new batch, <strong>Validate</strong>, read the changes, <strong>Activate</strong>.</li>
|
||||
</ol>
|
||||
<p class="small text-muted mb-0">
|
||||
No SSH from that PC? Run it with <span class="mono">-ZipOut C:\Temp\bprt.zip</span> and upload the zip below. A zip of the whole
|
||||
vendor folder works too, but is taken as the complete new folder: anything not in it is removed from BPRT (and kept in the archive).
|
||||
Uploads land in <span class="mono">{{ incoming }}</span>.
|
||||
</p>
|
||||
</div>
|
||||
It reads this server's manifest, hashes the local folder (<span class="mono" style="overflow-wrap: anywhere">C:\ProgramData\GEAerospace\MediaCenter\Cache\Deploy\Applications\BPRT</span>
|
||||
unless <span class="mono">-Source</span> says otherwise), parse-checks the vendor scripts, and uploads the new and changed files with one
|
||||
<span class="mono">sftp</span> session: type the <span class="mono">pxe</span> password once.</li>
|
||||
<li>Come back here, open the new batch, Validate, read the changes, Activate.</li>
|
||||
</ol>
|
||||
<p class="sub" style="margin: 0; overflow-wrap: anywhere">
|
||||
No SSH from that PC? Run it with <span class="mono">-ZipOut C:\Temp\bprt.zip</span> and upload the zip below. A zip of the whole
|
||||
vendor folder works too, but is taken as the complete new folder: anything not in it is removed from BPRT (and kept in the archive).
|
||||
Uploads land in <span class="mono">{{ incoming }}</span>.</p>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
{# ---- Zip upload ---------------------------------------------------------- #}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-file-earmark-zip"></i> Upload a zip instead</div>
|
||||
<form id="bprtUpload" method="post" action="{{ url_for('bprt_sync.upload') }}" enctype="multipart/form-data"
|
||||
class="d-flex flex-wrap gap-2 align-items-end">
|
||||
<section class="panel ui-list" aria-labelledby="zip-h">
|
||||
<div class="sec-h"><div><h2 id="zip-h" class="sec">Upload a zip instead</h2>
|
||||
<div class="sub">For a work PC without SSH: the output of Sync-BPRT.ps1 -ZipOut, or a zip of the whole vendor folder.</div></div></div>
|
||||
<form id="bprtUpload" method="post" action="{{ url_for('bprt_sync.upload') }}" enctype="multipart/form-data" class="upl">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-field mb-0" style="min-width: min(22rem, 100%);">
|
||||
<label for="bprtZip" class="form-label">Zip (.zip)</label>
|
||||
<input type="file" class="form-control" id="bprtZip" name="zip_file" accept=".zip" required>
|
||||
<div class="fld">
|
||||
<label for="bprtZip">Zip (.zip)</label>
|
||||
<input type="file" class="field" id="bprtZip" name="zip_file" accept=".zip" required>
|
||||
</div>
|
||||
<button type="submit" class="pxe-btn pxe-btn-primary" {% if job.running %}disabled{% endif %}><i class="bi bi-upload"></i> Upload and validate</button>
|
||||
<button type="submit" class="ui-btn pri" {% if job.running %}disabled{% endif %}>Upload and validate</button>
|
||||
</form>
|
||||
<div id="bprtUploadProgress" class="mt-2" style="display: none;">
|
||||
<div class="progress" style="height: 0.5rem;"><div id="bprtUploadBar" class="progress-bar" style="width: 0%;"></div></div>
|
||||
<div id="bprtUploadText" class="small text-muted mt-1"></div>
|
||||
<div id="bprtUploadProgress" class="prog" style="display: none;" role="status">
|
||||
<div class="bar"><div id="bprtUploadBar" style="width: 0%;"></div></div>
|
||||
<span id="bprtUploadText"></span>
|
||||
</div>
|
||||
{% if zips %}
|
||||
<div class="table-container mt-3">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>Uploaded zip, not unpacked</th><th>Size</th><th class="text-end"></th></tr></thead>
|
||||
<tbody>
|
||||
{% for z in zips %}
|
||||
<tr>
|
||||
<td class="mono small wrap">{{ z.name }}</td><td class="text-nowrap">{{ size(z.size) }}</td>
|
||||
<td class="text-end text-nowrap">
|
||||
<form method="post" action="{{ url_for('bprt_sync.unzip', name=z.name) }}" style="display:inline;">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost pxe-btn-sm" {% if job.running %}disabled{% endif %}>Unpack</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete_zip', name=z.name) }}" style="display:inline;" onsubmit="return confirm('Delete {{ z.name }}?');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-row-action" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for z in zips %}
|
||||
<div class="ui-row">
|
||||
<div class="zgrid">
|
||||
<span class="tip" data-tip="{{ z.name }}"><span class="trunc mono">{{ z.name }}</span></span>
|
||||
<span class="sub">{{ size(z.size) }}</span>
|
||||
</div>
|
||||
<div class="act" style="top: 6px">
|
||||
<form method="post" action="{{ url_for('bprt_sync.unzip', name=z.name) }}" style="margin: 0">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="ui-btn" {% if job.running %}disabled{% endif %}>Unpack</button>
|
||||
</form>
|
||||
{% call ui.more(z.name) %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete_zip', name=z.name) }}" onsubmit="return confirm('Delete {{ z.name }}?');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="del">Delete zip</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if zips %}<div class="hidden-bar">Uploaded zips, not unpacked yet.</div>{% endif %}
|
||||
</section>
|
||||
|
||||
{# ---- Archives ------------------------------------------------------------ #}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-archive"></i> Previous BPRT folders <span class="badge badge-secondary">{{ archives | length }}</span></div>
|
||||
{% if archives %}
|
||||
<p class="small text-muted">Each activation and rollback keeps the folder it replaced in <span class="mono">BPRT-archive</span>. Unchanged files are
|
||||
hard links shared with the live folder, so an archive only costs the files that differ ("own size").</p>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Archive</th><th>MCL packages</th><th>Files</th><th>Own size</th><th class="text-end"></th></tr></thead>
|
||||
<tbody>
|
||||
{% for a in archives %}
|
||||
<tr>
|
||||
<td class="mono small text-nowrap">{{ a.name }}<div class="text-muted">{{ a.modified | timestamp_fmt }}</div></td>
|
||||
<td class="small wrap">{% for p in a.packages %}<div class="mono fname">{{ fname(p) }}</div>{% else %}<span class="text-danger">none</span>{% endfor %}
|
||||
{% if a.missing_required %}<div class="text-danger">missing {{ a.missing_required | join(', ') }}</div>{% endif %}</td>
|
||||
<td class="small text-nowrap">{{ a.count }}, {{ size(a.bytes) }}</td>
|
||||
<td class="small text-nowrap">{{ size(a.own_bytes) }}</td>
|
||||
<td class="text-end text-nowrap">
|
||||
<form method="post" action="{{ url_for('bprt_sync.rollback') }}" style="display:inline;"
|
||||
onsubmit="return confirm('Make {{ a.name }} the live BPRT again?{% if imaging %} {{ imaging | length }} PC(s) are imaging right now.{% endif %} The current folder is archived first.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="archive" value="{{ a.name }}">
|
||||
<button type="submit" class="pxe-btn pxe-btn-ghost pxe-btn-sm" {% if job.running %}disabled{% endif %}><i class="bi bi-arrow-counterclockwise"></i> Roll back</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete_archive') }}" style="display:inline;"
|
||||
onsubmit="return confirm('Delete archive {{ a.name }}? This frees {{ size(a.own_bytes) }}; it cannot be rolled back to afterwards.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="archive" value="{{ a.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger btn-row-action" title="Delete archive" {% if job.running %}disabled{% endif %}><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">None yet. The first activation archives the current folder here.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# ---- Archives (folded) ---------------------------------------------------- #}
|
||||
<section class="panel ui-list" aria-labelledby="ar-h">
|
||||
<details>
|
||||
<summary class="sec-h">
|
||||
<div><h2 id="ar-h" class="sec" style="font-size: 15px">Previous BPRT folders</h2>
|
||||
<div class="sub">Each activation and rollback keeps the folder it replaced in <span class="mono">BPRT-archive</span>.</div></div>
|
||||
<span style="display: flex; align-items: center; gap: 10px">{{ ui.chip(archives | length ~ ' archive' ~ ('s' if archives | length != 1), 'neutral') }}<span class="chev" aria-hidden="true">▾</span></span>
|
||||
</summary>
|
||||
{% for a in archives %}
|
||||
<div class="ui-row">
|
||||
<details>
|
||||
<summary class="xgrid">
|
||||
<span class="mono">{{ a.name }}</span>
|
||||
<span class="sub c-pk">{{ a.packages | length }} MCL package{{ 's' if a.packages | length != 1 }}, {{ a.count }} files, own size {{ size(a.own_bytes) }}</span>
|
||||
<span class="c-st">{% if a.missing_required %}{{ ui.chip('Required files missing', 'err') }}{% elif not a.packages %}{{ ui.chip('No packages', 'warn') }}{% else %}<span class="sub mono" style="white-space: nowrap">{{ a.modified | timestamp_fmt }}</span>{% endif %}</span>
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="rbody">
|
||||
{% if a.missing_required %}<div class="notice bad">Missing {{ a.missing_required | join(', ') }}</div>{% endif %}
|
||||
<div class="sub">Saved {{ a.modified | timestamp_fmt }}. {{ a.count }} files, {{ size(a.bytes) }}; {{ size(a.own_bytes) }} not shared with the live folder.</div>
|
||||
{% for p in a.packages %}<div class="mono fname">{{ fname(p) }}</div>{% else %}<div style="color: var(--fail-text)">No MCL packages.</div>{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="act">
|
||||
<form method="post" action="{{ url_for('bprt_sync.rollback') }}" style="margin: 0"
|
||||
onsubmit="return confirm('Make {{ a.name }} the live BPRT again?{% if imaging %} {{ imaging | length }} PC(s) are imaging right now.{% endif %} The current folder is archived first.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="archive" value="{{ a.name }}">
|
||||
<button type="submit" class="ui-btn" {% if job.running %}disabled{% endif %}>Roll back</button>
|
||||
</form>
|
||||
{% call ui.more('archive ' ~ a.name) %}
|
||||
<form method="post" action="{{ url_for('bprt_sync.delete_archive') }}"
|
||||
onsubmit="return confirm('Delete archive {{ a.name }}? This frees {{ size(a.own_bytes) }}; it cannot be rolled back to afterwards.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="archive" value="{{ a.name }}">
|
||||
<button type="submit" class="del" {% if job.running %}disabled{% endif %}>Delete archive</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="hidden-bar">None yet. The first activation archives the current folder here.</div>
|
||||
{% endfor %}
|
||||
{% if archives %}<div class="hidden-bar">Unchanged files are hard links shared with the live folder, so an archive only costs the files that differ ("own size").</div>{% endif %}
|
||||
</details>
|
||||
</section>
|
||||
|
||||
{# ---- History (folded) ----------------------------------------------------- #}
|
||||
{% if history %}
|
||||
<div class="section-card">
|
||||
<div class="section-title"><i class="bi bi-clock-history"></i> History</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>When</th><th>What</th><th>Previous folder</th></tr></thead>
|
||||
<tbody>
|
||||
{% for h in history %}
|
||||
<tr>
|
||||
<td class="small text-nowrap">{{ h.at | local_dt }}</td>
|
||||
<td class="small wrap">
|
||||
{% if h.kind == 'rollback' %}Rolled back to <span class="mono">{{ h.restored }}</span>
|
||||
{% else %}Activated <span class="mono">{{ h.batch }}</span>{% if h.sync and h.sync.computer %} from {{ h.sync.computer }}{% endif %}:
|
||||
{{ h.counts.new or 0 }} new, {{ h.counts.changed or 0 }} changed, {{ h.counts.removed or 0 }} removed
|
||||
{% for p in h.ppkg %}<div class="mono">{{ p.prefix }}: {{ p.old | map(attribute='version') | join(', ') or 'none' }} to {{ p.new | map(attribute='version') | join(', ') or 'none' }}</div>{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="mono small">{{ h.archive }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<section class="panel ui-list" aria-labelledby="hi-h">
|
||||
<details>
|
||||
<summary class="sec-h">
|
||||
<div><h2 id="hi-h" class="sec" style="font-size: 15px">History</h2><div class="sub">Activations and rollbacks, newest first.</div></div>
|
||||
<span style="display: flex; align-items: center; gap: 10px">{{ ui.chip(history | length, 'neutral') }}<span class="chev" aria-hidden="true">▾</span></span>
|
||||
</summary>
|
||||
{% for h in history %}
|
||||
<div class="ui-row">
|
||||
<details>
|
||||
<summary class="hgrid">
|
||||
<span class="sub when" style="white-space: nowrap">{{ h.at | local_dt }}</span>
|
||||
{% if h.kind == 'rollback' %}
|
||||
<span class="tip" data-tip="Rolled back to {{ h.restored }}"><span class="trunc">Rolled back to <span class="mono">{{ h.restored }}</span></span></span>
|
||||
{% else %}
|
||||
<span class="tip" data-tip="Activated {{ h.batch }}"><span class="trunc">Activated <span class="mono">{{ h.batch }}</span>: {{ h.counts.new or 0 }} new, {{ h.counts.changed or 0 }} changed, {{ h.counts.removed or 0 }} removed</span></span>
|
||||
{% endif %}
|
||||
<span class="chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="rbody">
|
||||
{% if h.kind != 'rollback' %}
|
||||
<div>Activated <span class="mono">{{ h.batch }}</span>{% if h.sync and h.sync.computer %} from {{ h.sync.computer }}{% endif %}:
|
||||
{{ h.counts.new or 0 }} new, {{ h.counts.changed or 0 }} changed, {{ h.counts.removed or 0 }} removed.</div>
|
||||
{% for p in h.ppkg %}<div class="mono">{{ p.prefix }}: {{ p.old | map(attribute='version') | join(', ') or 'none' }} to {{ p.new | map(attribute='version') | join(', ') or 'none' }}</div>{% endfor %}
|
||||
{% endif %}
|
||||
<div class="sub">Previous folder kept as <span class="mono">{{ h.archive }}</span>.</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
|
||||
Reference in New Issue
Block a user