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:
@@ -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>{{ friendly[0] }}</strong>
|
||||||
<strong class="fs-5">{{ 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>
|
|
||||||
<span class="text-muted small">{{ pct }}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="progress" style="height:1.2rem;">
|
<span class="text-muted small">{{ pct }}% · last <code>{{ s.last_updated or '-' }}</code></span>
|
||||||
<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 %}
|
|
||||||
</div>
|
</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 %}
|
{% 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> · 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>
|
|
||||||
· last <code>{{ s.last_updated or '-' }}</code>
|
|
||||||
{% if s.mac %}· MAC <code>{{ s.mac }}</code>{% endif %}
|
|
||||||
{% if s.current_stage %}· <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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user