Engineer image: upload the Engineering Layer from the web app
New Engineering Layer tab on the engineer image page. A site uploads Engineering_Install_v<ver>.exe (keeps prereqs) or a .zip of the whole extra_engineering payload. The upload is streamed to disk, checked (MZ header, no unsafe zip paths) and swapped in; the replaced payload is archived (two kept) with a Restore button. Install-EngineeringLayer.ps1 stays repo-owned and now installs the newest Engineering_Install_v*.exe. KB: Part 4 becomes Site Content (enrollment package, Engineering Layer, Blancco site settings, Shop Floor apps) plus a new Adding Shop Floor Apps section for preinstall.json/postppkg.json. WJ examples removed.
This commit is contained in:
137
webapp/templates/image_engineering_layer.html
Normal file
137
webapp/templates/image_engineering_layer.html
Normal file
@@ -0,0 +1,137 @@
|
||||
{% extends "base.html" %}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% block title %}{{ friendly_name }} - Engineering Layer{% endblock %}
|
||||
|
||||
{# Engineer image > Engineering Layer: what the engineer unattend will
|
||||
install (extra_engineering/), an upload that replaces it, and the two
|
||||
previous versions to restore. services/engineering_layer.py. #}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.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; }
|
||||
.cur { padding: 20px 24px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.cur-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
|
||||
.el-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 20px; margin: 4px 0 0; }
|
||||
.el-facts dt { font-size: 13px; color: var(--muted); font-weight: 400; }
|
||||
.el-facts dd { margin: 2px 0 0; font-size: 15px; }
|
||||
.el-upl { display: flex; flex-wrap: wrap; gap: 10px 12px; align-items: flex-end; padding: 6px 24px 18px; }
|
||||
.el-upl .fld { flex: 1 1 22rem; min-width: 0; }
|
||||
.el-upl label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
|
||||
.el-prog { padding: 0 24px 18px; }
|
||||
.el-prog .bar { height: 8px; border-radius: 4px; background: var(--line); overflow: hidden; margin-top: 6px; }
|
||||
.el-prog .bar > div { height: 100%; background: var(--accent, #2563eb); width: 0; }
|
||||
.el-prog .bar > div.bad { background: var(--err, #dc2626); }
|
||||
.el-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 6px 14px; align-items: center;
|
||||
padding: 11px 24px; border-top: 1px solid var(--line); font-size: 14px; }
|
||||
.el-row form { margin: 0; }
|
||||
@media (max-width: 760px) {
|
||||
.el-facts { grid-template-columns: minmax(0, 1fr); }
|
||||
.el-row { grid-template-columns: minmax(0, 1fr); padding: 11px 16px; }
|
||||
.el-upl, .el-prog, .sec-h { padding-left: 16px; padding-right: 16px; }
|
||||
.cur { padding: 18px 16px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_image_header.html" %}
|
||||
|
||||
<div class="ui-stack">
|
||||
<section class="panel cur" aria-labelledby="el-h">
|
||||
<div class="cur-top">
|
||||
<div>
|
||||
<h2 id="el-h" class="sec" style="font-size: 15px; color: var(--muted)">Engineering Layer</h2>
|
||||
<div class="sub">Installed by the engineer image at first logon (Install-EngineeringLayer.ps1), from <span class="mono">Deploy\Applications\extra_engineering</span>. It installs the newest <span class="mono">Engineering_Install_v*.exe</span> it finds.</div>
|
||||
</div>
|
||||
<span style="margin-left: auto">
|
||||
{% if not st.image_exists %}{{ ui.chip('Engineer image not imported', 'warn') }}
|
||||
{% elif st.active and st.prereqs.present %}{{ ui.chip('Ready', 'ok') }}
|
||||
{% elif st.active %}{{ ui.chip('No prereqs', 'warn') }}
|
||||
{% else %}{{ ui.chip('Not uploaded', 'warn') }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<dl class="el-facts">
|
||||
<div><dt>Installer</dt><dd>{% if st.active %}<span class="mono">{{ st.active.name }}</span><div class="sub">{{ st.active.size | filesizeformat }}, {{ st.active.mtime | local_dt }}</div>{% else %}<span class="muted">none</span>{% endif %}</dd></div>
|
||||
<div><dt>Prerequisites (VC++ / .NET 3.5)</dt><dd>{% if st.prereqs.present %}{{ st.prereqs.files }} files, {{ st.prereqs.size | filesizeformat }}{% else %}<span class="muted">none</span>{% endif %}</dd></div>
|
||||
<div><dt>Setup script</dt><dd>{% if st.script %}present{% else %}<span class="muted">missing (the playbook deploys it)</span>{% endif %}</dd></div>
|
||||
</dl>
|
||||
{% if st.installers | length > 1 %}<div class="notice">More than one installer is in the folder ({{ st.installers | map(attribute='name') | join(', ') }}); the newest is used.</div>{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel ui-list" id="upload" aria-labelledby="up-h">
|
||||
<div class="sec-h"><div><h2 id="up-h" class="sec">Upload</h2>
|
||||
<div class="sub">Either the installer alone (<span class="mono">Engineering_Install_v<version>.exe</span>, keeps the prerequisites), or a .zip of the whole payload with <span class="mono">engineering_layer\</span> and <span class="mono">prereqs\</span> at its top (or inside one <span class="mono">extra_engineering\</span> folder). The current files are kept as a previous version.</div></div></div>
|
||||
<form id="elUpload" method="post" action="{{ url_for('engineering_layer.upload') }}" class="el-upl">
|
||||
<div class="fld">
|
||||
<label for="elFile">Installer (.exe) or payload (.zip)</label>
|
||||
<input type="file" class="field" id="elFile" name="payload" accept=".exe,.zip" required>
|
||||
</div>
|
||||
<button type="submit" class="ui-btn pri"{% if not st.image_exists %} disabled title="Import the engineer image first"{% endif %}>Upload</button>
|
||||
</form>
|
||||
<div id="elProg" class="el-prog" style="display: none;" role="status">
|
||||
<span id="elText">Uploading...</span>
|
||||
<div class="bar"><div id="elBar"></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if st.archives %}
|
||||
<section class="panel ui-list" aria-labelledby="arc-h">
|
||||
<div class="sec-h"><div><h2 id="arc-h" class="sec">Previous versions</h2>
|
||||
<div class="sub">The two most recent replaced payloads. Restoring swaps the current one into this list.</div></div></div>
|
||||
{% for a in st.archives %}
|
||||
<div class="el-row">
|
||||
<span><span class="mono">{{ a.name }}</span> <span class="sub">installer {{ ('v' ~ a.version) if a.version else 'none' }}{% if a.prereqs %}, with prerequisites{% endif %}</span></span>
|
||||
<span></span>
|
||||
<form method="post" action="{{ url_for('engineering_layer.restore', stamp=a.name) }}"
|
||||
onsubmit="return confirm('Restore the Engineering Layer from {{ a.name }}? The current files are kept as a previous version.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="ui-btn sm">Restore</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
var csrf = document.querySelector('meta[name="csrf-token"]').content;
|
||||
var form = document.getElementById('elUpload');
|
||||
form.addEventListener('submit', function (ev) {
|
||||
var input = document.getElementById('elFile');
|
||||
if (!input.files.length || !window.XMLHttpRequest) { ev.preventDefault(); return; }
|
||||
ev.preventDefault();
|
||||
var file = input.files[0];
|
||||
var box = document.getElementById('elProg');
|
||||
var bar = document.getElementById('elBar');
|
||||
var text = document.getElementById('elText');
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', form.action + '?name=' + encodeURIComponent(file.name));
|
||||
xhr.setRequestHeader('X-CSRF-Token', csrf);
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
xhr.upload.onprogress = function (e) {
|
||||
if (!e.lengthComputable) { return; }
|
||||
bar.style.width = (100 * e.loaded / e.total).toFixed(1) + '%';
|
||||
text.textContent = 'Uploading ' + file.name + ': ' + Math.round(e.loaded / 1048576) + ' of ' +
|
||||
Math.round(e.total / 1048576) + ' MB';
|
||||
if (e.loaded === e.total) { text.textContent = 'Checking and unpacking ' + file.name + '...'; }
|
||||
};
|
||||
xhr.onload = function () {
|
||||
var r = {};
|
||||
try { r = JSON.parse(xhr.responseText); } catch (e) {}
|
||||
if (xhr.status === 200 && r.ok) { location.reload(); return; }
|
||||
text.textContent = 'Upload failed: ' + (r.error || ('HTTP ' + xhr.status));
|
||||
bar.classList.add('bad');
|
||||
form.querySelector('button[type=submit]').disabled = false;
|
||||
};
|
||||
xhr.onerror = function () { text.textContent = 'Upload failed: connection lost.'; bar.classList.add('bad'); };
|
||||
box.style.display = '';
|
||||
form.querySelector('button[type=submit]').disabled = true;
|
||||
xhr.send(file);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user