imaging: compact tile + search filter + stage 7 label tweak

Tile shrunk for fleet density:
 - QR: 160px -> 96px
 - Drop big h4 for serial, use fs-6 strong instead
 - DeviceId + buttons + MAC + started time consolidated into one
   small grey row instead of three separate sections
 - Progress bar 1.2rem -> 0.7rem
 - mb-4 -> mb-2 between cards
 - card-body py-2 for tighter vertical rhythm

Search:
 - Sticky search input above the card list
 - Filters live on serial, hostname, pctype, machinenumber,
   intune_device_id via lowercase substring match on a data-filter
   attribute
 - Visible-count badge updates as you type ("3/12")
 - Auto-refresh paused while query has text or while input is focused

Stage 7 label: was "assign category" only, now "awaiting category /
lockdown" to reflect that bays past category assignment are still
waiting on the Intune-driven LAPS-prompt reboot before lockdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-14 20:14:51 -04:00
parent ca647cb690
commit 1c361e138b

View File

@@ -18,12 +18,18 @@ window.addEventListener('DOMContentLoaded', scheduleImagingReload);
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<h2 class="mb-0">Imaging Progress</h2>
<small class="text-muted">Auto-refresh 15s. 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>
<span class="badge bg-secondary fs-6"><span id="visible-count">{{ sessions|length }}</span>/{{ sessions|length }}</span>
</div>
<div class="mb-3">
<input id="imaging-search" type="search" class="form-control form-control-sm"
placeholder="Filter by serial, hostname, pctype, machine#, or Intune device id - typing pauses auto-refresh"
autocomplete="off">
</div>
{% if not sessions %}
@@ -43,8 +49,10 @@ window.addEventListener('DOMContentLoaded', scheduleImagingReload);
4: ('Apps installed', 'Type-specific scripts complete. Preparing for Intune enrollment.'),
5: ('Enrolling in Intune', 'PPKG installing - device joining Azure AD + Intune. ~5-10 min, reboot to follow.'),
6: ('Waiting on first Intune sync','Post-PPKG settle (~120s). Triggering Schedule #3 sync repeatedly.'),
7: ('Registered - assign category','Device ID captured. Click "set category" to put bay in the right Intune group. Then wait for LAPS reboot.'),
8: ('Imaging complete', 'Lockdown applied. Bay rebooted into ShopFloor session. Ready for production.')
7: ('Registered - awaiting category / lockdown',
'Device ID captured. If category not yet set in Intune, click "set category". Once set, bay waits for the Intune-driven LAPS-prompt reboot to apply lockdown.'),
8: ('Imaging complete',
'Lockdown applied. Bay rebooted into ShopFloor session. Ready for production.')
} %}
{% for s in sessions %}
@@ -55,64 +63,57 @@ window.addEventListener('DOMContentLoaded', scheduleImagingReload);
{% set is_done = s.status == 'succeeded' %}
{% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %}
{% set friendly = stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
<div class="card border-{{ border }} mb-4 shadow-sm">
<div class="card-body">
<div class="d-flex flex-wrap gap-3 align-items-start">
<div class="card border-{{ border }} mb-2 shadow-sm imaging-card"
data-filter="{{ s.serial|lower }} {{ (s.hostname_target or '')|lower }} {{ (s.pctype or '')|lower }} {{ (s.machinenumber or '')|lower }} {{ (s.intune_device_id or '')|lower }}">
<div class="card-body py-2">
<div class="d-flex flex-wrap gap-3 align-items-center">
{% if s.intune_device_id %}
<div data-qr="{{ s.intune_device_id }}" data-qr-size="160" data-qr-ec="M"
<div data-qr="{{ s.intune_device_id }}" data-qr-size="96" data-qr-ec="M"
style="line-height:0; flex-shrink:0;"
title="Intune Device ID: {{ s.intune_device_id }}"></div>
{% else %}
<div class="d-flex align-items-center justify-content-center bg-light text-muted"
style="width:160px; height:160px; border-radius:0.25rem; flex-shrink:0; font-size:0.85rem; text-align:center; padding:0.5rem;">
waiting for Intune Device ID
<div class="d-flex align-items-center justify-content-center bg-light text-muted small"
style="width:96px; height:96px; border-radius:0.25rem; flex-shrink:0; text-align:center; padding:0.25rem;">
no DeviceId
</div>
{% endif %}
<div class="flex-grow-1" style="min-width:0;">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<h4 class="mb-0">{{ s.serial or '(no serial)' }}</h4>
{% if s.hostname_target %}<code class="text-muted">{{ s.hostname_target }}</code>{% endif %}
<div class="d-flex flex-wrap align-items-center gap-2">
<strong class="fs-6">{{ s.serial or '(no serial)' }}</strong>
{% if s.hostname_target %}<code class="text-muted small">{{ s.hostname_target }}</code>{% endif %}
{% if s.pctype %}<span class="badge bg-info text-dark">{{ s.pctype }}</span>{% endif %}
{% if s.machinenumber %}<span class="badge bg-secondary">#{{ s.machinenumber }}</span>{% endif %}
<span class="badge bg-{{ border }} ms-auto">{{ s.status or 'in_progress' }}</span>
</div>
<div class="mb-2">
<div class="d-flex justify-content-between align-items-baseline mb-1">
<div>
<strong class="fs-5">{{ friendly[0] }}</strong>
<span class="badge bg-secondary ms-1">{{ stage_idx }}/{{ stage_total or '?' }}</span>
</div>
<span class="text-muted small">{{ pct }}%</span>
<div class="d-flex justify-content-between align-items-baseline mt-1">
<div>
<strong>{{ friendly[0] }}</strong>
<span class="badge bg-secondary ms-1">{{ stage_idx }}/{{ stage_total or '?' }}</span>
</div>
<div class="progress" style="height:1.2rem;">
<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"></div>
</div>
{% if friendly[1] %}<div class="small text-muted mt-1">{{ friendly[1] }}</div>{% endif %}
<span class="text-muted small">{{ pct }}% &middot; last <code>{{ s.last_updated or '-' }}</code></span>
</div>
<div class="progress mt-1" style="height:0.7rem;">
<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"></div>
</div>
{% if friendly[1] %}<div class="small text-muted mt-1">{{ friendly[1] }}</div>{% endif %}
{% if s.intune_device_id %}
<div class="small mb-2">
<span class="text-muted">Intune:</span> <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;"
<div class="small mt-1" style="font-size:0.75rem;">
<code>{{ s.intune_device_id }}</code>
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 copy-btn"
style="font-size:0.65rem; line-height:1; transition: all 0.2s;"
data-copy-text="{{ s.intune_device_id }}">copy</button>
<a class="btn btn-sm btn-outline-primary py-0 px-1 ms-1"
style="font-size:0.7rem; line-height:1;"
<a class="btn btn-sm btn-outline-primary py-0 px-1"
style="font-size:0.65rem; line-height:1;"
target="_blank" rel="noopener"
href="https://portal.azure.us/?feature.msaljs=false#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/properties/aadDeviceId/{{ s.intune_device_id }}">set category</a>
<span class="text-muted ms-2">MAC <code>{{ s.mac or '-' }}</code> &middot; started <code>{{ s.started_at or '-' }}</code></span>
</div>
{% endif %}
<div class="text-muted" style="font-size:0.75rem;">
started <code>{{ s.started_at or '-' }}</code>
&middot; last <code>{{ s.last_updated or '-' }}</code>
{% if s.mac %}&middot; MAC <code>{{ s.mac }}</code>{% endif %}
{% if s.current_stage %}&middot; <span style="font-family:monospace;">{{ s.current_stage }}</span>{% endif %}
</div>
</div>
</div>
@@ -325,5 +326,33 @@ window.addEventListener('DOMContentLoaded', function() {
if (input && input.value) renderLapsQR(card, { skipPersist: true });
});
});
// Client-side filter: hide imaging-card elements whose data-filter doesn't
// match the search query. Live as user types. Pauses auto-reload while
// the input is focused or non-empty so typing isn't interrupted by refresh.
window.addEventListener('DOMContentLoaded', function() {
var search = document.getElementById('imaging-search');
var counter = document.getElementById('visible-count');
if (!search) return;
function applyFilter() {
var q = search.value.trim().toLowerCase();
var visible = 0;
document.querySelectorAll('.imaging-card').forEach(function(card) {
var hay = card.getAttribute('data-filter') || '';
var match = (q === '') || hay.indexOf(q) !== -1;
card.style.display = match ? '' : 'none';
if (match) visible++;
});
if (counter) counter.textContent = visible;
if (q !== '') { cancelImagingReload(); }
else { cancelImagingReload(); scheduleImagingReload(); }
}
search.addEventListener('input', applyFilter);
search.addEventListener('focus', cancelImagingReload);
search.addEventListener('blur', function() {
if (search.value.trim() === '') { cancelImagingReload(); scheduleImagingReload(); }
});
applyFilter();
});
</script>
{% endblock %}