fix(dashboard): liveness badge filter regression + list-view legibility

Two follow-ups from the alpha diagnosis:
- FIX A (regression): filterDevices() compared badge TEXT to the option values 'online'/'offline', but
  the badge text is now "Healthy"/"Reconnecting"/"Offline" — so selecting a status filter matched
  nothing and emptied the dashboard. Now compares the liveness STATE via a data-liveness attribute, and
  the filter is upgraded to All / Healthy / Reconnecting / Offline (an admin can filter TO reconnecting
  devices — the point of the Degraded distinction).
- FIX B (legibility): the list rendered liveness as a status-dot where healthy=green/offline=red were
  visually identical to the old indicator, so it didn't read as new. The list now renders the same
  device-status-badge PILL as device-detail (3 distinct colors; amber Reconnecting visible on the list),
  scoped with an is-liveness modifier so video-wall cards keep their dark "NxN wall" pill.

Frontend-only. 14/14 filter+render tests; full ES-module parse clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-07-08 13:56:18 -05:00
parent a458c8f96a
commit 2772d1fc4d
2 changed files with 27 additions and 10 deletions

View file

@ -421,6 +421,19 @@ body {
font-size: 11px;
font-weight: 500;
}
/* Device cards render the reused liveness pill (.device-status-badge from device-detail), which brings
its own bg/padding/shape so neutralize the dark wrapper for those. Wall cards keep the dark pill
above for their "NxN wall" label (they share .device-card-status but have no .is-liveness). */
.device-card-status.is-liveness {
background: none;
backdrop-filter: none;
padding: 0;
border-radius: 0;
}
/* Lift the list pill off the screenshot so it stays legible over any image. */
.device-card-status.is-liveness .device-status-badge {
box-shadow: 0 1px 4px rgba(0,0,0,0.6);
}
.device-card-select {
position: absolute;

View file

@ -100,8 +100,8 @@ function renderDeviceCard(device) {
<span>${t('dashboard.no_preview')}</span>
</div>`
}
<div class="device-card-status">
${(() => { const b = livenessBadge(device); return `<span class="status-dot ${b.state}"></span><span>${esc(b.label)}</span>`; })()}
<div class="device-card-status is-liveness">
${(() => { const b = livenessBadge(device); return `<span class="device-status-badge ${b.state}" data-liveness="${b.state}">${esc(b.label)}</span>`; })()}
</div>
${device.status === 'provisioning' && device.pairing_code ? `
<div style="position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#f59e0b;padding:4px 12px;border-radius:6px;font-size:13px;font-weight:600;letter-spacing:2px;font-family:monospace">
@ -268,10 +268,11 @@ export function render(container) {
<div id="dashStats" class="dash-stats-row" style="display:flex;gap:12px;margin-bottom:16px"></div>
<div style="display:flex;gap:12px;margin-bottom:16px;align-items:center">
<input type="text" id="deviceSearch" class="input" placeholder="${t('dashboard.search')}" style="max-width:300px">
<select id="deviceFilter" class="input" style="width:140px;background:var(--bg-input)">
<select id="deviceFilter" class="input" style="width:160px;background:var(--bg-input)">
<option value="">${t('dashboard.all_status')}</option>
<option value="online">${t('dashboard.online')}</option>
<option value="offline">${t('dashboard.offline')}</option>
<option value="healthy">${t('device.liveness.healthy')}</option>
<option value="degraded">${t('device.liveness.degraded')}</option>
<option value="offline">${t('device.liveness.offline')}</option>
</select>
</div>
<div id="groupedDevices"></div>
@ -291,13 +292,16 @@ export function render(container) {
function filterDevices() {
const search = document.getElementById('deviceSearch').value.toLowerCase();
const status = document.getElementById('deviceFilter').value;
// Compare against the liveness STATE ('healthy'|'degraded'|'offline'), NOT the display label:
// the badge text is now "Healthy"/"Reconnecting"/"Offline", so the old text-vs-'online' compare
// matched nothing and emptied the list. data-liveness carries the state for a robust match.
const state = document.getElementById('deviceFilter').value;
document.querySelectorAll('.device-card').forEach(card => {
const name = card.querySelector('.device-card-name')?.textContent.toLowerCase() || '';
const deviceStatus = card.querySelector('.device-card-status span:last-child')?.textContent || '';
const cardState = card.querySelector('.device-card-status [data-liveness]')?.dataset.liveness || '';
const matchSearch = !search || name.includes(search);
const matchStatus = !status || deviceStatus === status;
card.style.display = (matchSearch && matchStatus) ? '' : 'none';
const matchState = !state || cardState === state;
card.style.display = (matchSearch && matchState) ? '' : 'none';
});
}
@ -362,7 +366,7 @@ export function render(container) {
const cards = document.querySelectorAll(`[data-device-id="${data.device_id}"]`);
cards.forEach(card => {
const statusEl = card.querySelector('.device-card-status');
if (statusEl) statusEl.innerHTML = `<span class="status-dot ${b.state}"></span><span>${esc(b.label)}</span>`;
if (statusEl) statusEl.innerHTML = `<span class="device-status-badge ${b.state}" data-liveness="${b.state}">${esc(b.label)}</span>`;
});
};