(root)
| 24 | icon: icons.sensing, |
| 25 | |
| 26 | async mount(root) { |
| 27 | // Hold vitals through brief confidence dips / dropped frames; a steady tick |
| 28 | // (below) clears them to "—" after holdMs so a stalled stream can't freeze. |
| 29 | const hrHold = makeVitalHold({ holdMs: 30000, decimals: 0 }); |
| 30 | const brHold = makeVitalHold({ holdMs: 30000, decimals: 1 }); |
| 31 | // Presence flickers 0↔1 at ~46 Hz; smooth it (duty-cycle hysteresis) from a |
| 32 | // full-rate listener below. smPresent is the smoothed state the rest reads. |
| 33 | const presence = makePresenceHold(); |
| 34 | let smPresent = false, lastMotion = '', presSig = null; |
| 35 | |
| 36 | root.appendChild(html` |
| 37 | <section class="space-y-5"> |
| 38 | <div class="card card-pad space-y-3"> |
| 39 | <div class="flex items-center justify-between"> |
| 40 | <h2 class="card-title">Signal field</h2> |
| 41 | <span id="sf-status" class="badge-mut">no data</span> |
| 42 | </div> |
| 43 | <canvas id="sf-canvas" class="w-full rounded-lg bg-ink-0 aspect-[2/1]" aria-label="Signal field heatmap"></canvas> |
| 44 | <p class="text-xs text-ink-muted">CSI energy map across the sensing grid. Brighter = stronger perturbation.</p> |
| 45 | </div> |
| 46 | |
| 47 | <div class="card card-pad space-y-3"> |
| 48 | <div class="flex items-center justify-between gap-2"> |
| 49 | <h2 class="card-title">Perimeter geofence <span class="text-xs text-ink-muted font-normal">prototype</span></h2> |
| 50 | <span id="gf-verdict" class="badge-mut shrink-0" style="min-width:7.5rem;justify-content:center">—</span> |
| 51 | </div> |
| 52 | <canvas id="gf-canvas" class="w-full rounded-lg bg-ink-0 aspect-[3/2]" aria-label="Room geofence plan"></canvas> |
| 53 | <div class="grid grid-cols-3 gap-3 text-sm"> |
| 54 | <div class="stat"><span class="stat-label">Disturbed corners</span><span class="stat-value" id="gf-hot">—</span></div> |
| 55 | <div class="stat"><span class="stat-label">Disturbance</span><span class="stat-value" id="gf-total">—</span></div> |
| 56 | <div class="stat"><span class="stat-label">Inside score</span><span class="stat-value" id="gf-score">—</span></div> |
| 57 | </div> |
| 58 | <p class="text-xs text-ink-muted truncate" id="gf-reason" title="">—</p> |
| 59 | <p class="text-xs text-ink-muted" id="gf-note"> |
| 60 | Motion is confined to the polygon of mapped node corners (Settings → node X/Y). |
| 61 | A disturbance that lights up only one corner — a hallway walk-by — is rejected as outside. |
| 62 | Coarse zone-level filter, not a hard RF wall. |
| 63 | </p> |
| 64 | </div> |
| 65 | |
| 66 | <div class="grid gap-4 sm:grid-cols-2"> |
| 67 | <div class="card card-pad space-y-3"> |
| 68 | <h2 class="card-title">Classification</h2> |
| 69 | <dl class="space-y-2 text-sm" id="cls-list"> |
| 70 | ${[['Presence', 'cls-presence'], ['Motion level', 'cls-motion'], ['Confidence', 'cls-conf'], ['Posture', 'cls-posture'], ['Signal quality', 'cls-quality']] |
| 71 | .map(([l, id]) => `<div class="flex justify-between border-b border-ink-3 pb-2 last:border-0"><dt class="text-ink-muted">${l}</dt><dd id="${id}" class="font-mono">—</dd></div>`).join('')} |
| 72 | </dl> |
| 73 | </div> |
| 74 | |
| 75 | <div class="card card-pad space-y-3"> |
| 76 | <h2 class="card-title">Vital signs</h2> |
| 77 | <div class="grid grid-cols-2 gap-3"> |
| 78 | <div class="stat"><span class="stat-label">Breathing</span><span class="stat-value" id="vs-br">—</span><span class="text-xs text-ink-muted" id="vs-br-c">confidence —</span></div> |
| 79 | <div class="stat"><span class="stat-label">Heart rate</span><span class="stat-value" id="vs-hr">—</span><span class="text-xs text-ink-muted" id="vs-hr-c">confidence —</span></div> |
| 80 | </div> |
| 81 | <div class="text-xs text-ink-muted" id="vs-buf">buffer —</div> |
| 82 | </div> |
| 83 | </div> |
nothing calls this directly
no test coverage detected