Files
pxe-server/webapp/templates/imaging.html
cproudlock 6275a6a2b3 imaging: add visual feedback to device-id copy button
Click effect: button flashes green with "copied!" text and 1.15x
scale pulse, reverts after 1.2s. Failure case (clipboard API blocked
or HTTP context) shows red "failed" for 1.5s. Handler moved out of
inline onclick into a single delegated click listener at the doc
level so future copy buttons just need the .copy-btn class +
data-copy-text attribute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:29:00 -04:00

172 lines
6.6 KiB
HTML

{% extends "base.html" %}
{% block title %}Imaging Progress - PXE Server Manager{% endblock %}
{% block extra_head %}
<meta http-equiv="refresh" content="5">
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="mb-0">Imaging Progress</h2>
<small class="text-muted">Auto-refresh 5s. POST updates from imaging clients arrive at <code>/imaging/status</code>.</small>
</div>
<span class="badge bg-secondary fs-6">{{ sessions|length }} session{{ 's' if sessions|length != 1 }}</span>
</div>
{% if not sessions %}
<div class="card">
<div class="card-body text-center text-muted py-5">
<p class="mb-1">No imaging sessions yet.</p>
<p class="small mb-0">A PC being imaged will post status here.
Until then, nothing to show.</p>
</div>
</div>
{% endif %}
{% for s in sessions %}
{% set stage_idx = s.stage_index | int(0) %}
{% set stage_total = s.stage_total | int(0) %}
{% set pct = 100 if s.status == 'succeeded' else ((stage_idx / stage_total * 100) | round(0, 'floor')) if stage_total > 0 else 0 %}
{% set is_failed = s.status == 'failed' %}
{% set is_done = s.status == 'succeeded' %}
{% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %}
<div class="card border-{{ border }} mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
{% if s.intune_device_id %}
<div data-qr="{{ s.intune_device_id }}" data-qr-size="160" data-qr-ec="M" style="line-height:0;"
title="Intune Device ID: {{ s.intune_device_id }}"></div>
{% endif %}
<div>
<strong>{{ s.serial or '(no serial)' }}</strong>
{% if s.hostname_target %}<code class="ms-2 small text-muted">{{ s.hostname_target }}</code>{% endif %}
{% if s.pctype %}<span class="badge bg-info text-dark ms-2">{{ s.pctype }}</span>{% endif %}
{% if s.machinenumber %}<span class="badge bg-secondary ms-1">#{{ s.machinenumber }}</span>{% endif %}
{% if s.intune_device_id %}<div class="small text-muted mt-1">Intune: <code>{{ s.intune_device_id }}</code>
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 ms-1 copy-btn"
style="font-size:0.7rem; line-height:1; transition: all 0.2s;"
data-copy-text="{{ s.intune_device_id }}">copy</button>
</div>{% endif %}
</div>
</div>
<span class="badge bg-{{ border }}">{{ s.status or 'in_progress' }}</span>
</div>
<div class="card-body">
<div class="row g-3 mb-3 small">
<div class="col-md-3">
<div class="text-muted">Current stage</div>
<div><strong>{{ s.current_stage or '-' }}</strong></div>
</div>
<div class="col-md-2">
<div class="text-muted">Stage</div>
<div>{{ stage_idx }} / {{ stage_total or '?' }}</div>
</div>
<div class="col-md-2">
<div class="text-muted">Started</div>
<div><code>{{ s.started_at or '-' }}</code></div>
</div>
<div class="col-md-3">
<div class="text-muted">Last update</div>
<div><code>{{ s.last_updated or '-' }}</code></div>
</div>
<div class="col-md-2">
<div class="text-muted">MAC</div>
<div><code class="small">{{ s.mac or '-' }}</code></div>
</div>
</div>
<div class="progress mb-2" style="height: 1.4rem;">
<div class="progress-bar bg-{{ border }} {% if not is_done and not is_failed %}progress-bar-striped progress-bar-animated{% endif %}"
role="progressbar"
style="width: {{ pct }}%;"
aria-valuenow="{{ pct }}"
aria-valuemin="0"
aria-valuemax="100">{{ pct }}%</div>
</div>
{% if s.error %}
<div class="alert alert-danger small py-2 mb-2">
<strong>Error:</strong> {{ s.error }}
</div>
{% endif %}
{% if s.log_tail %}
<details>
<summary class="text-muted small">Log tail ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }})</summary>
<pre class="bg-light p-2 mt-2 small mb-0" style="max-height: 12rem; overflow-y: auto;">{% for line in s.log_tail %}{{ line }}
{% endfor %}</pre>
</details>
{% endif %}
<div class="mt-3 text-end">
<form method="post" action="{{ url_for('imaging_delete_session', serial=s.serial) }}" style="display: inline;">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-secondary"
onclick="return confirm('Clear session {{ s.serial }}?');">
Clear
</button>
</form>
</div>
</div>
</div>
{% endfor %}
<div class="card mt-3">
<div class="card-body small text-muted">
<strong>How to push status from an imaging client:</strong>
<pre class="mb-0 mt-2">POST http://10.9.100.1:9009/imaging/status
Content-Type: application/json
{
"serial": "4HBLF33",
"mac": "e4:54:e8:dc:b1:f0",
"hostname_target": "EDNMG3D4",
"pctype": "gea-shopfloor-keyence",
"machinenumber": "9999",
"current_stage": "Run-ShopfloorSetup: 09-Setup-Keyence",
"stage_index": 7,
"stage_total": 9,
"status": "in_progress",
"log_lines": ["last few log lines from the stage"]
}</pre>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('click', function(e) {
var btn = e.target.closest('.copy-btn');
if (!btn) return;
var text = btn.getAttribute('data-copy-text');
if (!text) return;
navigator.clipboard.writeText(text).then(function() {
var origText = btn.dataset.origText || btn.textContent;
btn.dataset.origText = origText;
btn.textContent = 'copied!';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-success');
btn.style.transform = 'scale(1.15)';
setTimeout(function() {
btn.textContent = origText;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-secondary');
btn.style.transform = 'scale(1)';
}, 1200);
}).catch(function(err) {
btn.textContent = 'failed';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-danger');
setTimeout(function() {
btn.textContent = btn.dataset.origText || 'copy';
btn.classList.remove('btn-danger');
btn.classList.add('btn-outline-secondary');
}, 1500);
});
});
</script>
{% endblock %}