truenas-burnin/app/templates/job_print.html
Brandon Walter b73b5251ae Initial commit — TrueNAS Burn-In Dashboard v0.5.0
Full-stack burn-in orchestration dashboard (Stages 1–6d complete):
FastAPI backend, SQLite/WAL, SSE live dashboard, mock TrueNAS server,
SMTP/webhook notifications, batch burn-in, settings UI, audit log,
stats page, cancel SMART/burn-in, drag-to-reorder stages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 00:08:29 -05:00

304 lines
7.8 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burn-In Report — Job #{{ job.id }}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--green: #1a7431;
--red: #b91c1c;
--gray: #374151;
--border: #d1d5db;
--muted: #6b7280;
}
body {
background: #fff;
color: #111827;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 13px;
padding: 32px;
max-width: 800px;
margin: 0 auto;
}
/* ---- Header ---- */
.print-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid var(--border);
}
.print-brand {
font-size: 13px;
color: var(--muted);
}
.print-brand strong {
display: block;
font-size: 16px;
color: #111827;
}
.result-badge {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
padding: 6px 20px;
border-radius: 8px;
border: 3px solid;
}
.result-badge.passed {
color: var(--green);
border-color: var(--green);
background: #f0fdf4;
}
.result-badge.failed {
color: var(--red);
border-color: var(--red);
background: #fef2f2;
}
.result-badge.cancelled,
.result-badge.unknown {
color: var(--gray);
border-color: var(--gray);
background: #f9fafb;
}
/* ---- Info grid ---- */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.info-card {
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.info-card-title {
background: #f9fafb;
border-bottom: 1px solid var(--border);
padding: 6px 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.info-row {
display: flex;
justify-content: space-between;
padding: 6px 12px;
border-bottom: 1px solid #f3f4f6;
}
.info-row:last-child { border-bottom: none; }
.info-label { color: var(--muted); font-size: 12px; }
.info-value {
font-weight: 500;
text-align: right;
font-size: 12px;
}
/* ---- Error box ---- */
.error-box {
background: #fef2f2;
border: 1px solid #fca5a5;
border-radius: 6px;
padding: 10px 14px;
color: var(--red);
font-size: 12px;
margin-bottom: 16px;
}
/* ---- Stages table ---- */
h2 {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 8px;
}
table { width: 100%; border-collapse: collapse; }
th {
background: #f9fafb;
border: 1px solid var(--border);
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
text-align: left;
}
td {
border: 1px solid var(--border);
padding: 7px 10px;
font-size: 12px;
vertical-align: middle;
}
.s-passed { color: var(--green); font-weight: 600; }
.s-failed { color: var(--red); font-weight: 600; }
.s-other { color: var(--muted); }
/* ---- QR + footer ---- */
.print-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.print-footer-note {
font-size: 11px;
color: var(--muted);
line-height: 1.6;
}
#qrcode canvas, #qrcode img { display: block; }
/* ---- Print styles ---- */
@media print {
body { padding: 16px; }
a { color: inherit; }
.result-badge.passed { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.result-badge.failed { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.info-card-title { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.s-passed, .s-failed { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>
</head>
<body>
<div class="print-header">
<div class="print-brand">
<strong>TrueNAS Burn-In Dashboard</strong>
Job #{{ job.id }} &nbsp;·&nbsp; {{ job.created_at | format_dt_full }}
</div>
<div class="result-badge {{ job.state }}">
{{ job.state | upper }}
</div>
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-card-title">Drive</div>
<div class="info-row">
<span class="info-label">Device</span>
<span class="info-value" style="font-size:14px;font-weight:700">{{ job.devname }}</span>
</div>
<div class="info-row">
<span class="info-label">Model</span>
<span class="info-value">{{ job.model or '—' }}</span>
</div>
<div class="info-row">
<span class="info-label">Serial</span>
<span class="info-value" style="font-family:monospace">{{ job.serial or '—' }}</span>
</div>
<div class="info-row">
<span class="info-label">Size</span>
<span class="info-value">{{ job.size_bytes | format_bytes }}</span>
</div>
</div>
<div class="info-card">
<div class="info-card-title">Job</div>
<div class="info-row">
<span class="info-label">Profile</span>
<span class="info-value">{{ job.profile | title }}</span>
</div>
<div class="info-row">
<span class="info-label">Operator</span>
<span class="info-value">{{ job.operator or '—' }}</span>
</div>
<div class="info-row">
<span class="info-label">Started</span>
<span class="info-value" style="font-family:monospace">{{ job.started_at | format_dt_full }}</span>
</div>
<div class="info-row">
<span class="info-label">Finished</span>
<span class="info-value" style="font-family:monospace">{{ job.finished_at | format_dt_full }}</span>
</div>
<div class="info-row">
<span class="info-label">Duration</span>
<span class="info-value" style="font-family:monospace">{{ job.duration_seconds | format_duration }}</span>
</div>
</div>
</div>
{% if job.error_text %}
<div class="error-box">✕ {{ job.error_text }}</div>
{% endif %}
<h2>Stages</h2>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Result</th>
<th>Duration</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for s in job.stages %}
<tr>
<td style="font-weight:500">{{ s.stage_name.replace('_', ' ').title() }}</td>
<td class="s-{{ s.state if s.state in ('passed','failed') else 'other' }}">
{{ s.state | upper }}
</td>
<td style="font-family:monospace">{{ s.duration_seconds | format_duration }}</td>
<td style="color:#6b7280">{{ s.error_text or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="print-footer">
<div class="print-footer-note">
Generated by TrueNAS Burn-In Dashboard<br>
{{ job.finished_at | format_dt_full }}<br>
Scan QR code to view full job details online
</div>
<div id="qrcode"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"
integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAXAIn1G+hNMtOE4lK7QTqBgQRmB/gFgQiRp8iKg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
new QRCode(document.getElementById('qrcode'), {
text: window.location.origin + '/history/{{ job.id }}',
width: 96, height: 96,
colorDark: '#111827', colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
</script>
</body>
</html>