mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Improve cross-app UX: accessibility, mode consistency, and render performance
This commit is contained in:
@@ -38,6 +38,11 @@ const BluetoothMode = (function() {
|
||||
let currentDeviceFilter = 'all';
|
||||
let currentSearchTerm = '';
|
||||
let visibleDeviceCount = 0;
|
||||
let pendingDeviceFlush = false;
|
||||
let selectedDeviceNeedsRefresh = false;
|
||||
let filterListenersBound = false;
|
||||
let listListenersBound = false;
|
||||
const pendingDeviceIds = new Set();
|
||||
|
||||
// Agent support
|
||||
let showAllAgentsMode = false;
|
||||
@@ -111,8 +116,9 @@ const BluetoothMode = (function() {
|
||||
// Initialize legacy heatmap (zone counts)
|
||||
initHeatmap();
|
||||
|
||||
// Initialize device list filters
|
||||
initDeviceFilters();
|
||||
// Initialize device list filters
|
||||
initDeviceFilters();
|
||||
initListInteractions();
|
||||
|
||||
// Set initial panel states
|
||||
updateVisualizationPanels();
|
||||
@@ -122,6 +128,7 @@ const BluetoothMode = (function() {
|
||||
* Initialize device list filter buttons
|
||||
*/
|
||||
function initDeviceFilters() {
|
||||
if (filterListenersBound) return;
|
||||
const filterContainer = document.getElementById('btDeviceFilters');
|
||||
if (filterContainer) {
|
||||
filterContainer.addEventListener('click', (e) => {
|
||||
@@ -148,6 +155,35 @@ const BluetoothMode = (function() {
|
||||
applyDeviceFilter();
|
||||
});
|
||||
}
|
||||
filterListenersBound = true;
|
||||
}
|
||||
|
||||
function initListInteractions() {
|
||||
if (listListenersBound) return;
|
||||
if (deviceContainer) {
|
||||
deviceContainer.addEventListener('click', (event) => {
|
||||
const locateBtn = event.target.closest('.bt-locate-btn[data-locate-id]');
|
||||
if (locateBtn) {
|
||||
event.preventDefault();
|
||||
locateById(locateBtn.dataset.locateId);
|
||||
return;
|
||||
}
|
||||
|
||||
const row = event.target.closest('.bt-device-row[data-bt-device-id]');
|
||||
if (!row) return;
|
||||
selectDevice(row.dataset.btDeviceId);
|
||||
});
|
||||
}
|
||||
|
||||
const trackerList = document.getElementById('btTrackerList');
|
||||
if (trackerList) {
|
||||
trackerList.addEventListener('click', (event) => {
|
||||
const row = event.target.closest('.bt-tracker-item[data-device-id]');
|
||||
if (!row) return;
|
||||
selectDevice(row.dataset.deviceId);
|
||||
});
|
||||
}
|
||||
listListenersBound = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,6 +228,18 @@ const BluetoothMode = (function() {
|
||||
|
||||
visibleDeviceCount = visibleCount;
|
||||
|
||||
let stateEl = deviceContainer.querySelector('.bt-device-filter-state');
|
||||
if (visibleCount === 0 && devices.size > 0) {
|
||||
if (!stateEl) {
|
||||
stateEl = document.createElement('div');
|
||||
stateEl.className = 'bt-device-filter-state app-collection-state is-empty';
|
||||
deviceContainer.appendChild(stateEl);
|
||||
}
|
||||
stateEl.textContent = 'No devices match current filters';
|
||||
} else if (stateEl) {
|
||||
stateEl.remove();
|
||||
}
|
||||
|
||||
// Update visible count
|
||||
updateFilteredCount();
|
||||
}
|
||||
@@ -915,14 +963,25 @@ const BluetoothMode = (function() {
|
||||
function setScanning(scanning) {
|
||||
isScanning = scanning;
|
||||
|
||||
if (startBtn) startBtn.style.display = scanning ? 'none' : 'block';
|
||||
if (stopBtn) stopBtn.style.display = scanning ? 'block' : 'none';
|
||||
|
||||
if (scanning && deviceContainer) {
|
||||
deviceContainer.innerHTML = '';
|
||||
devices.clear();
|
||||
resetStats();
|
||||
}
|
||||
if (startBtn) startBtn.style.display = scanning ? 'none' : 'block';
|
||||
if (stopBtn) stopBtn.style.display = scanning ? 'block' : 'none';
|
||||
|
||||
if (scanning && deviceContainer) {
|
||||
pendingDeviceIds.clear();
|
||||
selectedDeviceNeedsRefresh = false;
|
||||
pendingDeviceFlush = false;
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(deviceContainer, { type: 'loading', message: 'Scanning for Bluetooth devices...' });
|
||||
} else {
|
||||
deviceContainer.innerHTML = '';
|
||||
}
|
||||
devices.clear();
|
||||
resetStats();
|
||||
} else if (!scanning && deviceContainer && devices.size === 0) {
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(deviceContainer, { type: 'empty', message: 'Start scanning to discover Bluetooth devices' });
|
||||
}
|
||||
}
|
||||
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
@@ -1087,17 +1146,43 @@ const BluetoothMode = (function() {
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function handleDeviceUpdate(device) {
|
||||
devices.set(device.device_id, device);
|
||||
renderDevice(device);
|
||||
updateDeviceCount();
|
||||
updateStatsFromDevices();
|
||||
updateVisualizationPanels();
|
||||
updateProximityZones();
|
||||
|
||||
// Update new proximity radar
|
||||
updateRadar();
|
||||
}
|
||||
function handleDeviceUpdate(device) {
|
||||
devices.set(device.device_id, device);
|
||||
pendingDeviceIds.add(device.device_id);
|
||||
if (selectedDeviceId === device.device_id) {
|
||||
selectedDeviceNeedsRefresh = true;
|
||||
}
|
||||
scheduleDeviceFlush();
|
||||
}
|
||||
|
||||
function scheduleDeviceFlush() {
|
||||
if (pendingDeviceFlush) return;
|
||||
pendingDeviceFlush = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
pendingDeviceFlush = false;
|
||||
|
||||
pendingDeviceIds.forEach((deviceId) => {
|
||||
const device = devices.get(deviceId);
|
||||
if (device) {
|
||||
renderDevice(device, false);
|
||||
}
|
||||
});
|
||||
pendingDeviceIds.clear();
|
||||
|
||||
applyDeviceFilter();
|
||||
updateDeviceCount();
|
||||
updateStatsFromDevices();
|
||||
updateVisualizationPanels();
|
||||
updateProximityZones();
|
||||
updateRadar();
|
||||
|
||||
if (selectedDeviceNeedsRefresh && selectedDeviceId && devices.has(selectedDeviceId)) {
|
||||
showDeviceDetail(selectedDeviceId);
|
||||
}
|
||||
selectedDeviceNeedsRefresh = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stats from all devices
|
||||
@@ -1171,83 +1256,83 @@ const BluetoothMode = (function() {
|
||||
|
||||
// Tracker Detection - Enhanced display with confidence and evidence
|
||||
const trackerList = document.getElementById('btTrackerList');
|
||||
if (trackerList) {
|
||||
if (devices.size === 0) {
|
||||
trackerList.innerHTML = '<div style="color:#666;padding:10px;text-align:center;font-size:11px;">Start scanning to detect trackers</div>';
|
||||
} else if (deviceStats.trackers.length === 0) {
|
||||
trackerList.innerHTML = '<div style="color:#22c55e;padding:10px;text-align:center;font-size:11px;">No trackers detected</div>';
|
||||
} else {
|
||||
// Sort by risk score (highest first), then confidence
|
||||
const sortedTrackers = [...deviceStats.trackers].sort((a, b) => {
|
||||
const riskA = a.risk_score || 0;
|
||||
const riskB = b.risk_score || 0;
|
||||
if (riskB !== riskA) return riskB - riskA;
|
||||
const confA = a.tracker_confidence_score || 0;
|
||||
const confB = b.tracker_confidence_score || 0;
|
||||
return confB - confA;
|
||||
});
|
||||
|
||||
trackerList.innerHTML = sortedTrackers.map(t => {
|
||||
// Get tracker type badge color based on confidence
|
||||
const confidence = t.tracker_confidence || 'low';
|
||||
const confColor = confidence === 'high' ? '#ef4444' :
|
||||
confidence === 'medium' ? '#f97316' : '#eab308';
|
||||
const confBg = confidence === 'high' ? 'rgba(239,68,68,0.2)' :
|
||||
confidence === 'medium' ? 'rgba(249,115,22,0.2)' : 'rgba(234,179,8,0.2)';
|
||||
|
||||
// Risk score indicator
|
||||
const riskScore = t.risk_score || 0;
|
||||
const riskColor = riskScore >= 0.5 ? '#ef4444' : riskScore >= 0.3 ? '#f97316' : '#666';
|
||||
|
||||
// Tracker type label
|
||||
const trackerType = t.tracker_name || t.tracker_type || 'Unknown Tracker';
|
||||
|
||||
// Build evidence tooltip (first 2 items)
|
||||
const evidence = (t.tracker_evidence || []).slice(0, 2);
|
||||
const evidenceHtml = evidence.length > 0
|
||||
? '<div style="font-size:9px;color:#888;margin-top:3px;font-style:italic;">' +
|
||||
evidence.map(e => '• ' + escapeHtml(e)).join('<br>') +
|
||||
'</div>'
|
||||
: '';
|
||||
|
||||
const deviceIdEscaped = escapeHtml(t.device_id).replace(/'/g, "\\'");
|
||||
|
||||
return '<div class="bt-tracker-item" style="padding:8px;border-bottom:1px solid rgba(255,255,255,0.05);cursor:pointer;" onclick="BluetoothMode.selectDevice(\'' + deviceIdEscaped + '\')">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
||||
'<div style="display:flex;align-items:center;gap:6px;">' +
|
||||
'<span style="background:' + confBg + ';color:' + confColor + ';font-size:9px;padding:2px 5px;border-radius:3px;font-weight:600;">' + confidence.toUpperCase() + '</span>' +
|
||||
'<span style="color:#fff;font-size:11px;">' + escapeHtml(trackerType) + '</span>' +
|
||||
'</div>' +
|
||||
'<div style="display:flex;align-items:center;gap:8px;">' +
|
||||
(riskScore >= 0.3 ? '<span style="color:' + riskColor + ';font-size:9px;font-weight:600;">RISK ' + Math.round(riskScore * 100) + '%</span>' : '') +
|
||||
'<span style="color:#666;font-size:10px;">' + (t.rssi_current || '--') + ' dBm</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div style="display:flex;justify-content:space-between;margin-top:3px;">' +
|
||||
'<span style="font-size:9px;color:#888;font-family:monospace;">' + (t.address_type === 'uuid' ? formatAddress(t) : t.address) + '</span>' +
|
||||
'<span style="font-size:9px;color:#666;">Seen ' + (t.seen_count || 0) + 'x</span>' +
|
||||
'</div>' +
|
||||
evidenceHtml +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (trackerList) {
|
||||
if (devices.size === 0) {
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(trackerList, { type: 'empty', message: 'Start scanning to detect trackers' });
|
||||
} else {
|
||||
trackerList.innerHTML = '<div class="app-collection-state is-empty">Start scanning to detect trackers</div>';
|
||||
}
|
||||
} else if (deviceStats.trackers.length === 0) {
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(trackerList, { type: 'empty', message: 'No trackers detected' });
|
||||
} else {
|
||||
trackerList.innerHTML = '<div class="app-collection-state is-empty">No trackers detected</div>';
|
||||
}
|
||||
} else {
|
||||
// Sort by risk score (highest first), then confidence
|
||||
const sortedTrackers = [...deviceStats.trackers].sort((a, b) => {
|
||||
const riskA = a.risk_score || 0;
|
||||
const riskB = b.risk_score || 0;
|
||||
if (riskB !== riskA) return riskB - riskA;
|
||||
const confA = a.tracker_confidence_score || 0;
|
||||
const confB = b.tracker_confidence_score || 0;
|
||||
return confB - confA;
|
||||
});
|
||||
|
||||
trackerList.innerHTML = sortedTrackers.map((t) => {
|
||||
const confidence = t.tracker_confidence || 'low';
|
||||
const riskScore = t.risk_score || 0;
|
||||
const trackerType = t.tracker_name || t.tracker_type || 'Unknown Tracker';
|
||||
const evidence = (t.tracker_evidence || []).slice(0, 2);
|
||||
const evidenceHtml = evidence.length > 0
|
||||
? `<div class="bt-tracker-evidence">${evidence.map((e) => `• ${escapeHtml(e)}`).join('<br>')}</div>`
|
||||
: '';
|
||||
const riskClass = riskScore >= 0.5 ? 'high' : riskScore >= 0.3 ? 'medium' : 'low';
|
||||
const riskHtml = riskScore >= 0.3
|
||||
? `<span class="bt-tracker-risk bt-risk-${riskClass}">RISK ${Math.round(riskScore * 100)}%</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="bt-tracker-item bt-tracker-confidence-${escapeHtml(confidence)}" data-device-id="${escapeAttr(t.device_id)}" role="button" tabindex="0" data-keyboard-activate="true">
|
||||
<div class="bt-tracker-row-top">
|
||||
<div class="bt-tracker-left">
|
||||
<span class="bt-tracker-confidence">${escapeHtml(confidence.toUpperCase())}</span>
|
||||
<span class="bt-tracker-type">${escapeHtml(trackerType)}</span>
|
||||
</div>
|
||||
<div class="bt-tracker-right">
|
||||
${riskHtml}
|
||||
<span class="bt-tracker-rssi">${t.rssi_current != null ? t.rssi_current : '--'} dBm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bt-tracker-row-bottom">
|
||||
<span class="bt-tracker-address">${escapeHtml(t.address_type === 'uuid' ? formatAddress(t) : (t.address || '--'))}</span>
|
||||
<span class="bt-tracker-seen">Seen ${t.seen_count || 0}x</span>
|
||||
</div>
|
||||
${evidenceHtml}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateDeviceCount() {
|
||||
updateFilteredCount();
|
||||
}
|
||||
|
||||
function renderDevice(device) {
|
||||
function renderDevice(device, reapplyFilter = true) {
|
||||
if (!deviceContainer) {
|
||||
deviceContainer = document.getElementById('btDeviceListContent');
|
||||
if (!deviceContainer) return;
|
||||
}
|
||||
|
||||
const escapedId = CSS.escape(device.device_id);
|
||||
const existingCard = deviceContainer.querySelector('[data-bt-device-id="' + escapedId + '"]');
|
||||
const cardHtml = createSimpleDeviceCard(device);
|
||||
|
||||
deviceContainer.querySelectorAll('.app-collection-state, .bt-device-filter-state').forEach((el) => el.remove());
|
||||
|
||||
const escapedId = CSS.escape(device.device_id);
|
||||
const existingCard = deviceContainer.querySelector('[data-bt-device-id="' + escapedId + '"]');
|
||||
const cardHtml = createSimpleDeviceCard(device);
|
||||
|
||||
if (existingCard) {
|
||||
existingCard.outerHTML = cardHtml;
|
||||
@@ -1255,8 +1340,9 @@ const BluetoothMode = (function() {
|
||||
deviceContainer.insertAdjacentHTML('afterbegin', cardHtml);
|
||||
}
|
||||
|
||||
// Re-apply filter after rendering
|
||||
applyDeviceFilter();
|
||||
if (reapplyFilter) {
|
||||
applyDeviceFilter();
|
||||
}
|
||||
}
|
||||
|
||||
function createSimpleDeviceCard(device) {
|
||||
@@ -1277,12 +1363,11 @@ const BluetoothMode = (function() {
|
||||
// RSSI typically ranges from -100 (weak) to -30 (very strong)
|
||||
const rssiPercent = rssi != null ? Math.max(0, Math.min(100, ((rssi + 100) / 70) * 100)) : 0;
|
||||
|
||||
const displayName = device.name || formatDeviceId(device.address);
|
||||
const name = escapeHtml(displayName);
|
||||
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
|
||||
const displayName = device.name || formatDeviceId(device.address);
|
||||
const name = escapeHtml(displayName);
|
||||
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
|
||||
const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : '';
|
||||
const seenCount = device.seen_count || 0;
|
||||
const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'");
|
||||
const searchIndex = [
|
||||
displayName,
|
||||
device.address,
|
||||
@@ -1373,14 +1458,14 @@ const BluetoothMode = (function() {
|
||||
}
|
||||
const secondaryInfo = secondaryParts.join(' · ');
|
||||
|
||||
// Row border color - highlight trackers in red/orange
|
||||
const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444' :
|
||||
isTracker ? '#f97316' : rssiColor;
|
||||
|
||||
return '<div class="bt-device-row' + (isTracker ? ' is-tracker' : '') + '" data-bt-device-id="' + escapeHtml(device.device_id) + '" data-is-new="' + isNew + '" data-has-name="' + hasName + '" data-rssi="' + (rssi || -100) + '" data-is-tracker="' + isTracker + '" data-search="' + escapeAttr(searchIndex) + '" onclick="BluetoothMode.selectDevice(\'' + deviceIdEscaped + '\')" style="border-left-color:' + borderColor + ';">' +
|
||||
'<div class="bt-row-main">' +
|
||||
'<div class="bt-row-left">' +
|
||||
protoBadge +
|
||||
// Row border color - highlight trackers in red/orange
|
||||
const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444' :
|
||||
isTracker ? '#f97316' : rssiColor;
|
||||
|
||||
return '<div class="bt-device-row' + (isTracker ? ' is-tracker' : '') + '" data-bt-device-id="' + escapeAttr(device.device_id) + '" data-is-new="' + isNew + '" data-has-name="' + hasName + '" data-rssi="' + (rssi || -100) + '" data-is-tracker="' + isTracker + '" data-search="' + escapeAttr(searchIndex) + '" role="button" tabindex="0" data-keyboard-activate="true" style="border-left-color:' + borderColor + ';">' +
|
||||
'<div class="bt-row-main">' +
|
||||
'<div class="bt-row-left">' +
|
||||
protoBadge +
|
||||
'<span class="bt-device-name">' + name + '</span>' +
|
||||
trackerBadge +
|
||||
irkBadge +
|
||||
@@ -1395,13 +1480,13 @@ const BluetoothMode = (function() {
|
||||
'</div>' +
|
||||
statusDot +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="bt-row-secondary">' + secondaryInfo + '</div>' +
|
||||
'<div class="bt-row-actions">' +
|
||||
'<button class="bt-locate-btn" data-locate-id="' + escapeHtml(device.device_id) + '" onclick="event.stopPropagation(); BluetoothMode.locateById(this.dataset.locateId)">' +
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>' +
|
||||
'Locate</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="bt-row-secondary">' + secondaryInfo + '</div>' +
|
||||
'<div class="bt-row-actions">' +
|
||||
'<button type="button" class="bt-locate-btn" data-locate-id="' + escapeAttr(device.device_id) + '">' +
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>' +
|
||||
'Locate</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -1532,18 +1617,22 @@ const BluetoothMode = (function() {
|
||||
/**
|
||||
* Clear all collected data.
|
||||
*/
|
||||
function clearData() {
|
||||
devices.clear();
|
||||
resetStats();
|
||||
|
||||
if (deviceContainer) {
|
||||
deviceContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
updateDeviceCount();
|
||||
updateProximityZones();
|
||||
updateRadar();
|
||||
}
|
||||
function clearData() {
|
||||
devices.clear();
|
||||
pendingDeviceIds.clear();
|
||||
pendingDeviceFlush = false;
|
||||
selectedDeviceNeedsRefresh = false;
|
||||
resetStats();
|
||||
clearSelection();
|
||||
|
||||
if (deviceContainer) {
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(deviceContainer, { type: 'empty', message: 'Start scanning to discover Bluetooth devices' });
|
||||
} else {
|
||||
deviceContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle "Show All Agents" mode.
|
||||
@@ -1578,19 +1667,27 @@ const BluetoothMode = (function() {
|
||||
}
|
||||
});
|
||||
|
||||
toRemove.forEach(deviceId => devices.delete(deviceId));
|
||||
|
||||
// Re-render device list
|
||||
if (deviceContainer) {
|
||||
deviceContainer.innerHTML = '';
|
||||
devices.forEach(device => renderDevice(device));
|
||||
}
|
||||
|
||||
updateDeviceCount();
|
||||
updateStatsFromDevices();
|
||||
updateVisualizationPanels();
|
||||
updateProximityZones();
|
||||
updateRadar();
|
||||
toRemove.forEach(deviceId => devices.delete(deviceId));
|
||||
|
||||
// Re-render device list
|
||||
if (deviceContainer) {
|
||||
deviceContainer.innerHTML = '';
|
||||
devices.forEach(device => renderDevice(device, false));
|
||||
applyDeviceFilter();
|
||||
if (devices.size === 0 && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(deviceContainer, { type: 'empty', message: 'No devices for current agent' });
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedDeviceId && !devices.has(selectedDeviceId)) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
updateDeviceCount();
|
||||
updateStatsFromDevices();
|
||||
updateVisualizationPanels();
|
||||
updateProximityZones();
|
||||
updateRadar();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user