@@ -1889,8 +2122,10 @@ } function setFlockExtraControls(visible) { - const el = document.getElementById('flockExtraControls'); - if (el) el.style.display = visible ? 'flex' : 'none'; + const el = document.getElementById('flockCommandBar'); + if (!el) return; + if (visible) el.classList.add('show'); + else el.classList.remove('show'); } function setFlockCmdButtonsBusy(busy, exceptId = null) { @@ -1902,26 +2137,64 @@ }); } + // Per-pull progress — bumped by every replay_detection socket event. + // The protocol serializes one CMD:* at a time, so at most one pull is + // in flight; a single global state is enough. + const pullProgress = { active: null, count: 0, btnId: null, label: null }; + + function pullProgressStart(source, btnId, label) { + pullProgress.active = source; + pullProgress.count = 0; + pullProgress.btnId = btnId; + pullProgress.label = label; + pullProgressRender(); + } + function pullProgressBump() { + if (!pullProgress.active) return; + pullProgress.count += 1; + pullProgressRender(); + } + function pullProgressEnd() { + pullProgress.active = null; + pullProgress.count = 0; + pullProgress.btnId = null; + pullProgress.label = null; + } + function pullProgressRender() { + const btn = document.getElementById(pullProgress.btnId); + if (!btn) return; + const labelEl = btn.querySelector('.fcb-btn-label'); + if (!labelEl) return; + const countBadge = pullProgress.count > 0 + ? `${pullProgress.count}` + : ''; + labelEl.innerHTML = `Pulling${countBadge}`; + } + function flockPullSession(source) { const isPrev = source === 'prev'; const btnId = isPrev ? 'dumpPrevBtn' : 'dumpLiveBtn'; const btn = document.getElementById(btnId); if (!btn) return; - const original = btn.textContent; + const labelEl = btn.querySelector('.fcb-btn-label'); + const originalLabel = labelEl ? labelEl.textContent : (isPrev ? 'Pull Prev' : 'Pull Live'); + setFlockCmdButtonsBusy(true, btnId); - btn.textContent = isPrev ? 'Pulling Prev…' : 'Pulling Live…'; - showFlockToast(`Pulling ${isPrev ? 'previous-session' : 'live'} table from device…`, 'info', 0); + pullProgressStart(source, btnId, originalLabel); + showFlockToast(`Pulling ${isPrev ? 'previous session' : 'live table'} from device…`, 'info', 0); fetch(`/api/flock/dump_${source}`, { method: 'POST' }) .then(r => r.json()) .then(data => { setFlockCmdButtonsBusy(false); - btn.textContent = original; + if (labelEl) labelEl.textContent = originalLabel; + const finalCount = data.count ?? pullProgress.count; + pullProgressEnd(); if (data.status === 'success') { const reason = data.reason ? ` (${data.reason})` : ''; showFlockToast( - `Pulled ${data.count ?? 0} detection(s) from device ${isPrev ? 'flash' : 'RAM'}${reason}`, - data.count > 0 ? 'success' : 'warning'); + `Pulled ${finalCount} detection${finalCount === 1 ? '' : 's'} from device ${isPrev ? 'flash' : 'RAM'}${reason}`, + finalCount > 0 ? 'success' : 'warning'); } else { const reason = data.reason || data.message || 'unknown error'; showFlockToast(`Pull failed: ${reason}`, 'error'); @@ -1929,7 +2202,8 @@ }) .catch(err => { setFlockCmdButtonsBusy(false); - btn.textContent = original; + if (labelEl) labelEl.textContent = originalLabel; + pullProgressEnd(); showFlockToast(`Pull failed: ${err.message}`, 'error'); }); } @@ -1937,23 +2211,29 @@ function flockDumpPrev() { flockPullSession('prev'); } function flockDumpLive() { flockPullSession('live'); } + function fcbBtnLabel(btnId) { + const btn = document.getElementById(btnId); + return btn ? btn.querySelector('.fcb-btn-label') : null; + } + function flockStatusQuery() { + const labelEl = fcbBtnLabel('flockStatusBtn'); const btn = document.getElementById('flockStatusBtn'); - const original = btn.textContent; + const original = labelEl ? labelEl.textContent : 'Status'; btn.disabled = true; - btn.textContent = '…'; + if (labelEl) labelEl.textContent = 'Querying…'; fetch('/api/flock/status') .then(r => r.json()) .then(data => { btn.disabled = false; - btn.textContent = original; + if (labelEl) labelEl.textContent = original; if (data.status === 'success' && data.firmware_status) { const s = data.firmware_status; const heapKB = s.free_heap ? Math.round(s.free_heap / 1024) : '?'; const uptimeSec = s.uptime_ms ? Math.round(s.uptime_ms / 1000) : 0; showFlockToast( - `det=${s.fy_det} ouis=${s.oui_count} prev=${s.prev_session ? 'yes' : 'no'} ` + - `ch=${s.channel} heap=${heapKB}KB up=${uptimeSec}s`, + `det=${s.fy_det} · ouis=${s.oui_count} · prev=${s.prev_session ? 'yes' : 'no'} · ` + + `ch=${s.channel} · heap=${heapKB}KB · up=${uptimeSec}s`, 'success', 6000); } else { showFlockToast(`Status failed: ${data.message || 'no response'}`, 'error'); @@ -1961,7 +2241,7 @@ }) .catch(err => { btn.disabled = false; - btn.textContent = original; + if (labelEl) labelEl.textContent = original; showFlockToast(`Status failed: ${err.message}`, 'error'); }); } @@ -1971,14 +2251,15 @@ if (!confirm(`Wipe the ${label}?`)) return; const btnId = target === 'prev' ? 'clearPrevBtn' : 'clearLiveBtn'; const btn = document.getElementById(btnId); - const original = btn.textContent; + const labelEl = fcbBtnLabel(btnId); + const original = labelEl ? labelEl.textContent : (target === 'prev' ? 'Clear Prev' : 'Clear Live'); btn.disabled = true; - btn.textContent = '…'; + if (labelEl) labelEl.textContent = 'Clearing…'; fetch(`/api/flock/clear_${target}`, { method: 'POST' }) .then(r => r.json()) .then(data => { btn.disabled = false; - btn.textContent = original; + if (labelEl) labelEl.textContent = original; if (data.status === 'success') { showFlockToast(`Cleared device ${target === 'prev' ? '/prev_session.json' : 'in-RAM table'}`, 'success'); } else { @@ -1987,7 +2268,7 @@ }) .catch(err => { btn.disabled = false; - btn.textContent = original; + if (labelEl) labelEl.textContent = original; showFlockToast(`Clear failed: ${err.message}`, 'error'); }); } @@ -2206,12 +2487,19 @@ const isReplay = detection.replay === true || detection.timestamp_source === 'device_replay'; const replaySource = detection.replay_source || (isReplay ? 'device' : null); + const isLiveSrc = replaySource === 'live'; + // Inline icons: a small chip glyph for FLASH, a wave for RAM. + const replayIcon = isLiveSrc + ? '' + : ''; const replayBadge = isReplay - ? `${replaySource === 'live' ? 'RAM' : 'FLASH'}` + ? `${replayIcon}${isLiveSrc ? 'RAM' : 'FLASH'}` : ''; + const replayClass = isReplay ? (isLiveSrc ? ' replay live-source' : ' replay') : ''; + return ` -
+
@@ -2457,6 +2745,8 @@ detections.push(detection); cumulativeDetections.push(detection); } + // Live progress counter on the in-flight Pull button + pullProgressBump(); updateStats(); renderDetections(); });