Files
pxe-server/webapp/templates/imaging_detail.html
cproudlock 18db077475 webapp: reskin to shopdb-flask design system
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.
2026-07-23 10:05:09 -04:00

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 %}