Adopt the shopdb-flask visual language across the PXE webapp (presentation only, Flask/Jinja logic unchanged): - New static/pxe-theme.css: GE Aerospace palette (atmosphere-blue sidebar, sky-blue primary, avionics-green), Inter font stack, light/dark theming via data-theme + localStorage (key pxe-theme) with system-pref fallback, and card/button/table/form/badge/alert component styles layered over Bootstrap. - base.html: shopdb-style sidebar (logo + title, nav sections, footer light/dark toggle) + theme boot script. - All 13 content templates restyled to the new page-header + card/table/badge vocabulary; unattend_editor grouped per unattend-UX research. - Fixed a pre-existing CRITICAL bug found during review: nested <form>s in image_config.html made Adopt submit the delete form and Delete-selected post every orphan filename regardless of checkboxes; split into standalone forms wired via the form= attribute. Built by a Fable-orchestrated Opus workflow (17 agents). All 14 templates parse clean under Jinja2.
190 lines
8.1 KiB
HTML
190 lines
8.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ session.serial }} - Imaging Session{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h2>{{ session.serial }}
|
|
{% if session.hostname_target %}<span class="text-light" style="font-size:14px; font-weight:400;">{{ session.hostname_target }}</span>{% endif %}
|
|
</h2>
|
|
<div style="margin-top:0.5rem; display:flex; gap:0.4rem; flex-wrap:wrap;">
|
|
{% if session.pctype %}<span class="badge badge-info">{{ session.pctype }}</span>{% endif %}
|
|
{% if session.machinenumber %}<span class="badge badge-secondary">#{{ session.machinenumber }}</span>{% endif %}
|
|
<span class="badge badge-{{ 'success' if session.status == 'succeeded' else ('danger' if session.status == 'failed' else 'primary') }}">{{ session.status or 'in_progress' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<a href="{{ url_for('imaging_dashboard') }}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-arrow-left"></i> Back to dashboard
|
|
</a>
|
|
<button type="button" class="btn btn-primary btn-sm copy-summary-btn">
|
|
<i class="bi bi-clipboard"></i> Copy support summary
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-5">
|
|
|
|
<div class="section-card">
|
|
<div class="section-title">Session metadata</div>
|
|
<dl class="row mb-0">
|
|
<dt class="col-4 text-light">Serial</dt><dd class="col-8"><span class="mono">{{ session.serial }}</span></dd>
|
|
<dt class="col-4 text-light">Hostname</dt><dd class="col-8"><span class="mono">{{ session.hostname_target or '-' }}</span></dd>
|
|
<dt class="col-4 text-light">MAC</dt><dd class="col-8"><span class="mono">{{ session.mac or '-' }}</span></dd>
|
|
<dt class="col-4 text-light">PC type</dt><dd class="col-8">{{ session.pctype or '-' }}</dd>
|
|
<dt class="col-4 text-light">Machine #</dt><dd class="col-8">{{ session.machinenumber or '-' }}</dd>
|
|
<dt class="col-4 text-light">Intune ID</dt><dd class="col-8">
|
|
{% if session.intune_device_id %}
|
|
<span class="mono" style="word-break: break-all;">{{ session.intune_device_id }}</span>
|
|
{% else %}-{% endif %}
|
|
</dd>
|
|
<dt class="col-4 text-light">Started</dt><dd class="col-8"><span class="mono">{{ session.started_at or '-' }}</span></dd>
|
|
<dt class="col-4 text-light">Last update</dt><dd class="col-8"><span class="mono">{{ session.last_updated or '-' }}</span></dd>
|
|
<dt class="col-4 text-light">Stage</dt><dd class="col-8">
|
|
{{ session.stage_index or 0 }}/{{ session.stage_total or '?' }}
|
|
{% if session.current_stage %} - <span class="mono">{{ session.current_stage }}</span>{% endif %}
|
|
</dd>
|
|
{% if session.previous_run_at %}
|
|
<dt class="col-4 text-light">Prior run</dt><dd class="col-8"><span class="mono">{{ session.previous_run_at }}</span></dd>
|
|
{% endif %}
|
|
{% if session.error %}
|
|
<dt class="col-4 text-danger">Error</dt>
|
|
<dd class="col-8 text-danger">{{ session.error }}</dd>
|
|
{% endif %}
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="section-title">Stage timeline</div>
|
|
{% if session.stage_history %}
|
|
<div class="table-container">
|
|
<table class="data-table mb-0">
|
|
<thead>
|
|
<tr><th>Time</th><th>Stage</th><th>Status</th><th>Detail</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for h in session.stage_history %}
|
|
<tr>
|
|
<td><span class="mono">{{ h.ts }}</span></td>
|
|
<td><span class="badge badge-secondary">{{ h.stage_index }}</span></td>
|
|
<td>
|
|
<span class="badge badge-{{ 'success' if h.status == 'succeeded' else ('danger' if h.status == 'failed' else 'primary') }}">
|
|
{{ h.status }}
|
|
</span>
|
|
</td>
|
|
<td><span class="mono">{{ h.current_stage or '' }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="status-indicator">
|
|
<i class="bi bi-info-circle"></i>
|
|
<span>No stage transitions recorded yet. The client only logs a row when
|
|
stage_index advances or status flips to succeeded/failed.</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-lg-7">
|
|
|
|
<div class="section-card">
|
|
<div class="section-title" style="display:flex; justify-content:space-between; align-items:center;">
|
|
<span>Full log
|
|
{% if full_log_truncated %}
|
|
<span class="badge badge-warning" style="margin-left:0.5rem;">truncated (last 1 MB)</span>
|
|
{% endif %}
|
|
</span>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm copy-log-btn"><i class="bi bi-clipboard"></i> Copy</button>
|
|
</div>
|
|
{% if full_log %}
|
|
<pre id="full-log" class="mono mb-0" style="max-height: 36rem; overflow: auto; white-space: pre-wrap; background: var(--bg); padding: 0.75rem; border-radius: 0.25rem;">{{ full_log }}</pre>
|
|
{% else %}
|
|
<div class="status-indicator">
|
|
<i class="bi bi-info-circle"></i>
|
|
<span>No log content. The client has not pushed any <code>log_lines</code>
|
|
entries yet, or the sidecar file was cleared.</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if session.log_tail %}
|
|
<div class="section-card">
|
|
<div class="section-title">Recent tail ({{ session.log_tail | length }} line{{ 's' if session.log_tail | length != 1 }})</div>
|
|
<pre class="mono mb-0" style="max-height: 12rem; overflow: auto; background: var(--bg); padding: 0.75rem; border-radius: 0.25rem;">{% for line in session.log_tail %}{{ line }}
|
|
{% endfor %}</pre>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function copyText(text) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(text);
|
|
}
|
|
return new Promise(function(resolve, reject) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
|
document.body.appendChild(ta);
|
|
ta.focus(); ta.select();
|
|
try {
|
|
var ok = document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
if (ok) resolve(); else reject(new Error('execCommand returned false'));
|
|
} catch (err) {
|
|
document.body.removeChild(ta);
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
var logBtn = e.target.closest('.copy-log-btn');
|
|
var summaryBtn = e.target.closest('.copy-summary-btn');
|
|
if (logBtn) {
|
|
var pre = document.getElementById('full-log');
|
|
if (pre) {
|
|
copyText(pre.textContent).then(function() {
|
|
logBtn.innerHTML = '<i class="bi bi-check2"></i> copied!';
|
|
setTimeout(function() { logBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy'; }, 1200);
|
|
});
|
|
}
|
|
} else if (summaryBtn) {
|
|
var lines = [];
|
|
document.querySelectorAll('dl dt').forEach(function(dt) {
|
|
var dd = dt.nextElementSibling;
|
|
if (dd) {
|
|
lines.push(dt.textContent.trim() + ': ' + dd.textContent.trim().replace(/\s+/g, ' '));
|
|
}
|
|
});
|
|
var rows = document.querySelectorAll('table tbody tr');
|
|
if (rows.length) {
|
|
lines.push('');
|
|
lines.push('Stage timeline:');
|
|
rows.forEach(function(tr) {
|
|
var cells = tr.querySelectorAll('td');
|
|
if (cells.length === 4) {
|
|
lines.push(' - ' + cells[0].textContent.trim() + ' stage ' + cells[1].textContent.trim()
|
|
+ ' ' + cells[2].textContent.trim() + ' ' + cells[3].textContent.trim());
|
|
}
|
|
});
|
|
}
|
|
copyText(lines.join('\n')).then(function() {
|
|
summaryBtn.innerHTML = '<i class="bi bi-check2"></i> copied!';
|
|
setTimeout(function() { summaryBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy support summary'; }, 1200);
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|