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 %} {% 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> <div>
<h2 class="mb-0">Imaging Progress</h2> <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> <small class="text-muted">Auto-refresh 15s. POST updates from imaging clients arrive at <code>/imaging/status</code>.</small>
</div> </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> </div>
{% if not sessions %} {% if not sessions %}
@@ -43,8 +49,10 @@ window.addEventListener('DOMContentLoaded', scheduleImagingReload);
4: ('Apps installed', 'Type-specific scripts complete. Preparing for Intune enrollment.'), 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.'), 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.'), 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.'), 7: ('Registered - awaiting category / lockdown',
8: ('Imaging complete', 'Lockdown applied. Bay rebooted into ShopFloor session. Ready for production.') '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 %} {% for s in sessions %}
@@ -55,64 +63,57 @@ window.addEventListener('DOMContentLoaded', scheduleImagingReload);
{% set is_done = s.status == 'succeeded' %} {% set is_done = s.status == 'succeeded' %}
{% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %} {% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %}
{% set friendly = stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %} {% set friendly = stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
<div class="card border-{{ border }} mb-4 shadow-sm"> <div class="card border-{{ border }} mb-2 shadow-sm imaging-card"
<div class="card-body"> 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="d-flex flex-wrap gap-3 align-items-start"> <div class="card-body py-2">
<div class="d-flex flex-wrap gap-3 align-items-center">
{% if s.intune_device_id %} {% 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;" style="line-height:0; flex-shrink:0;"
title="Intune Device ID: {{ s.intune_device_id }}"></div> title="Intune Device ID: {{ s.intune_device_id }}"></div>
{% else %} {% else %}
<div class="d-flex align-items-center justify-content-center bg-light text-muted" <div class="d-flex align-items-center justify-content-center bg-light text-muted small"
style="width:160px; height:160px; border-radius:0.25rem; flex-shrink:0; font-size:0.85rem; text-align:center; padding:0.5rem;"> style="width:96px; height:96px; border-radius:0.25rem; flex-shrink:0; text-align:center; padding:0.25rem;">
waiting for Intune Device ID no DeviceId
</div> </div>
{% endif %} {% endif %}
<div class="flex-grow-1" style="min-width:0;"> <div class="flex-grow-1" style="min-width:0;">
<div class="d-flex flex-wrap align-items-center gap-2 mb-2"> <div class="d-flex flex-wrap align-items-center gap-2">
<h4 class="mb-0">{{ s.serial or '(no serial)' }}</h4> <strong class="fs-6">{{ s.serial or '(no serial)' }}</strong>
{% if s.hostname_target %}<code class="text-muted">{{ s.hostname_target }}</code>{% endif %} {% 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.pctype %}<span class="badge bg-info text-dark">{{ s.pctype }}</span>{% endif %}
{% if s.machinenumber %}<span class="badge bg-secondary">#{{ s.machinenumber }}</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> <span class="badge bg-{{ border }} ms-auto">{{ s.status or 'in_progress' }}</span>
</div> </div>
<div class="mb-2"> <div class="d-flex justify-content-between align-items-baseline mt-1">
<div class="d-flex justify-content-between align-items-baseline mb-1">
<div> <div>
<strong class="fs-5">{{ friendly[0] }}</strong> <strong>{{ friendly[0] }}</strong>
<span class="badge bg-secondary ms-1">{{ stage_idx }}/{{ stage_total or '?' }}</span> <span class="badge bg-secondary ms-1">{{ stage_idx }}/{{ stage_total or '?' }}</span>
</div> </div>
<span class="text-muted small">{{ pct }}%</span> <span class="text-muted small">{{ pct }}% &middot; last <code>{{ s.last_updated or '-' }}</code></span>
</div> </div>
<div class="progress" style="height:1.2rem;"> <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 %}" <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 }}%;" role="progressbar" style="width: {{ pct }}%;"
aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"></div> aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
{% if friendly[1] %}<div class="small text-muted mt-1">{{ friendly[1] }}</div>{% endif %} {% if friendly[1] %}<div class="small text-muted mt-1">{{ friendly[1] }}</div>{% endif %}
</div>
{% if s.intune_device_id %} {% if s.intune_device_id %}
<div class="small mb-2"> <div class="small mt-1" style="font-size:0.75rem;">
<span class="text-muted">Intune:</span> <code>{{ s.intune_device_id }}</code> <code>{{ s.intune_device_id }}</code>
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 ms-1 copy-btn" <button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 copy-btn"
style="font-size:0.7rem; line-height:1; transition: all 0.2s;" style="font-size:0.65rem; line-height:1; transition: all 0.2s;"
data-copy-text="{{ s.intune_device_id }}">copy</button> data-copy-text="{{ s.intune_device_id }}">copy</button>
<a class="btn btn-sm btn-outline-primary py-0 px-1 ms-1" <a class="btn btn-sm btn-outline-primary py-0 px-1"
style="font-size:0.7rem; line-height:1;" style="font-size:0.65rem; line-height:1;"
target="_blank" rel="noopener" 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> 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> </div>
{% endif %} {% 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>
</div> </div>
@@ -325,5 +326,33 @@ window.addEventListener('DOMContentLoaded', function() {
if (input && input.value) renderLapsQR(card, { skipPersist: true }); 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> </script>
{% endblock %} {% endblock %}