mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Add alerts/recording, WiFi/TSCM updates, optimize waterfall
This commit is contained in:
@@ -366,7 +366,10 @@ const BluetoothMode = (function() {
|
||||
// Badges
|
||||
const badgesEl = document.getElementById('btDetailBadges');
|
||||
let badgesHtml = `<span class="bt-detail-badge ${protocol}">${protocol.toUpperCase()}</span>`;
|
||||
badgesHtml += `<span class="bt-detail-badge ${device.in_baseline ? 'baseline' : 'new'}">${device.in_baseline ? '✓ KNOWN' : '● NEW'}</span>`;
|
||||
badgesHtml += `<span class="bt-detail-badge ${device.in_baseline ? 'baseline' : 'new'}">${device.in_baseline ? '✓ KNOWN' : '● NEW'}</span>`;
|
||||
if (device.seen_before) {
|
||||
badgesHtml += `<span class="bt-detail-badge flag">SEEN BEFORE</span>`;
|
||||
}
|
||||
|
||||
// Tracker badge
|
||||
if (device.is_tracker) {
|
||||
@@ -448,12 +451,14 @@ const BluetoothMode = (function() {
|
||||
? minMax[0] + '/' + minMax[1]
|
||||
: '--';
|
||||
|
||||
document.getElementById('btDetailFirstSeen').textContent = device.first_seen
|
||||
? new Date(device.first_seen).toLocaleTimeString()
|
||||
: '--';
|
||||
document.getElementById('btDetailLastSeen').textContent = device.last_seen
|
||||
? new Date(device.last_seen).toLocaleTimeString()
|
||||
: '--';
|
||||
document.getElementById('btDetailFirstSeen').textContent = device.first_seen
|
||||
? new Date(device.first_seen).toLocaleTimeString()
|
||||
: '--';
|
||||
document.getElementById('btDetailLastSeen').textContent = device.last_seen
|
||||
? new Date(device.last_seen).toLocaleTimeString()
|
||||
: '--';
|
||||
|
||||
updateWatchlistButton(device);
|
||||
|
||||
// Services
|
||||
const servicesContainer = document.getElementById('btDetailServices');
|
||||
@@ -465,13 +470,29 @@ const BluetoothMode = (function() {
|
||||
servicesContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show content, hide placeholder
|
||||
placeholder.style.display = 'none';
|
||||
content.style.display = 'block';
|
||||
// Show content, hide placeholder
|
||||
placeholder.style.display = 'none';
|
||||
content.style.display = 'block';
|
||||
|
||||
// Highlight selected device in list
|
||||
highlightSelectedDevice(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update watchlist button state
|
||||
*/
|
||||
function updateWatchlistButton(device) {
|
||||
const btn = document.getElementById('btDetailWatchBtn');
|
||||
if (!btn) return;
|
||||
if (typeof AlertCenter === 'undefined') {
|
||||
btn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
btn.style.display = '';
|
||||
const watchlisted = AlertCenter.isWatchlisted(device.address);
|
||||
btn.textContent = watchlisted ? 'Watching' : 'Watchlist';
|
||||
btn.classList.toggle('active', watchlisted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear device selection
|
||||
@@ -525,24 +546,43 @@ const BluetoothMode = (function() {
|
||||
/**
|
||||
* Copy selected device address to clipboard
|
||||
*/
|
||||
function copyAddress() {
|
||||
if (!selectedDeviceId) return;
|
||||
const device = devices.get(selectedDeviceId);
|
||||
if (!device) return;
|
||||
function copyAddress() {
|
||||
if (!selectedDeviceId) return;
|
||||
const device = devices.get(selectedDeviceId);
|
||||
if (!device) return;
|
||||
|
||||
navigator.clipboard.writeText(device.address).then(() => {
|
||||
const btn = document.querySelector('.bt-detail-btn');
|
||||
if (btn) {
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#22c55e';
|
||||
navigator.clipboard.writeText(device.address).then(() => {
|
||||
const btn = document.getElementById('btDetailCopyBtn');
|
||||
if (btn) {
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#22c55e';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.style.background = '';
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle Bluetooth watchlist for selected device
|
||||
*/
|
||||
function toggleWatchlist() {
|
||||
if (!selectedDeviceId) return;
|
||||
const device = devices.get(selectedDeviceId);
|
||||
if (!device || typeof AlertCenter === 'undefined') return;
|
||||
|
||||
if (AlertCenter.isWatchlisted(device.address)) {
|
||||
AlertCenter.removeBluetoothWatchlist(device.address);
|
||||
showInfo('Removed from watchlist');
|
||||
} else {
|
||||
AlertCenter.addBluetoothWatchlist(device.address, device.name || device.address);
|
||||
showInfo('Added to watchlist');
|
||||
}
|
||||
|
||||
setTimeout(() => updateWatchlistButton(device), 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a device - opens modal with details
|
||||
@@ -1090,10 +1130,11 @@ const BluetoothMode = (function() {
|
||||
const isNew = !inBaseline;
|
||||
const hasName = !!device.name;
|
||||
const isTracker = device.is_tracker === true;
|
||||
const trackerType = device.tracker_type;
|
||||
const trackerConfidence = device.tracker_confidence;
|
||||
const riskScore = device.risk_score || 0;
|
||||
const agentName = device._agent || 'Local';
|
||||
const trackerType = device.tracker_type;
|
||||
const trackerConfidence = device.tracker_confidence;
|
||||
const riskScore = device.risk_score || 0;
|
||||
const agentName = device._agent || 'Local';
|
||||
const seenBefore = device.seen_before === true;
|
||||
|
||||
// Calculate RSSI bar width (0-100%)
|
||||
// RSSI typically ranges from -100 (weak) to -30 (very strong)
|
||||
@@ -1145,8 +1186,9 @@ const BluetoothMode = (function() {
|
||||
|
||||
// Build secondary info line
|
||||
let secondaryParts = [addr];
|
||||
if (mfr) secondaryParts.push(mfr);
|
||||
secondaryParts.push('Seen ' + seenCount + '×');
|
||||
if (mfr) secondaryParts.push(mfr);
|
||||
secondaryParts.push('Seen ' + seenCount + '×');
|
||||
if (seenBefore) secondaryParts.push('<span class="bt-history-badge">SEEN BEFORE</span>');
|
||||
// Add agent name if not Local
|
||||
if (agentName !== 'Local') {
|
||||
secondaryParts.push('<span class="agent-badge agent-remote" style="font-size:8px;padding:1px 4px;">' + escapeHtml(agentName) + '</span>');
|
||||
@@ -1358,9 +1400,10 @@ const BluetoothMode = (function() {
|
||||
setBaseline,
|
||||
clearBaseline,
|
||||
exportData,
|
||||
selectDevice,
|
||||
clearSelection,
|
||||
copyAddress,
|
||||
selectDevice,
|
||||
clearSelection,
|
||||
copyAddress,
|
||||
toggleWatchlist,
|
||||
|
||||
// Agent handling
|
||||
handleAgentChange,
|
||||
|
||||
@@ -3021,15 +3021,23 @@ let spectrumCanvas = null;
|
||||
let spectrumCtx = null;
|
||||
let waterfallStartFreq = 88;
|
||||
let waterfallEndFreq = 108;
|
||||
let waterfallRowImage = null;
|
||||
let waterfallPalette = null;
|
||||
let lastWaterfallDraw = 0;
|
||||
const WATERFALL_MIN_INTERVAL_MS = 80;
|
||||
|
||||
function initWaterfallCanvas() {
|
||||
waterfallCanvas = document.getElementById('waterfallCanvas');
|
||||
spectrumCanvas = document.getElementById('spectrumCanvas');
|
||||
if (waterfallCanvas) waterfallCtx = waterfallCanvas.getContext('2d');
|
||||
if (spectrumCanvas) spectrumCtx = spectrumCanvas.getContext('2d');
|
||||
if (waterfallCtx && waterfallCanvas) {
|
||||
waterfallRowImage = waterfallCtx.createImageData(waterfallCanvas.width, 1);
|
||||
if (!waterfallPalette) waterfallPalette = buildWaterfallPalette();
|
||||
}
|
||||
}
|
||||
|
||||
function dBmToColor(normalized) {
|
||||
function dBmToRgb(normalized) {
|
||||
// Viridis-inspired: dark blue -> cyan -> green -> yellow
|
||||
const n = Math.max(0, Math.min(1, normalized));
|
||||
let r, g, b;
|
||||
@@ -3054,7 +3062,15 @@ function dBmToColor(normalized) {
|
||||
g = Math.round(255 - t * 55);
|
||||
b = Math.round(20 - t * 20);
|
||||
}
|
||||
return `rgb(${r},${g},${b})`;
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
function buildWaterfallPalette() {
|
||||
const palette = new Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
palette[i] = dBmToRgb(i / 255);
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
function drawWaterfallRow(bins) {
|
||||
@@ -3062,9 +3078,8 @@ function drawWaterfallRow(bins) {
|
||||
const w = waterfallCanvas.width;
|
||||
const h = waterfallCanvas.height;
|
||||
|
||||
// Scroll existing content down by 1 pixel
|
||||
const imageData = waterfallCtx.getImageData(0, 0, w, h - 1);
|
||||
waterfallCtx.putImageData(imageData, 0, 1);
|
||||
// Scroll existing content down by 1 pixel (GPU-accelerated)
|
||||
waterfallCtx.drawImage(waterfallCanvas, 0, 0, w, h - 1, 0, 1, w, h - 1);
|
||||
|
||||
// Find min/max for normalization
|
||||
let minVal = Infinity, maxVal = -Infinity;
|
||||
@@ -3074,13 +3089,24 @@ function drawWaterfallRow(bins) {
|
||||
}
|
||||
const range = maxVal - minVal || 1;
|
||||
|
||||
// Draw new row at top
|
||||
const binWidth = w / bins.length;
|
||||
for (let i = 0; i < bins.length; i++) {
|
||||
const normalized = (bins[i] - minVal) / range;
|
||||
waterfallCtx.fillStyle = dBmToColor(normalized);
|
||||
waterfallCtx.fillRect(Math.floor(i * binWidth), 0, Math.ceil(binWidth) + 1, 1);
|
||||
// Draw new row at top using ImageData
|
||||
if (!waterfallRowImage || waterfallRowImage.width !== w) {
|
||||
waterfallRowImage = waterfallCtx.createImageData(w, 1);
|
||||
}
|
||||
const rowData = waterfallRowImage.data;
|
||||
const palette = waterfallPalette || buildWaterfallPalette();
|
||||
const binCount = bins.length;
|
||||
for (let x = 0; x < w; x++) {
|
||||
const idx = Math.min(binCount - 1, Math.floor((x / w) * binCount));
|
||||
const normalized = (bins[idx] - minVal) / range;
|
||||
const color = palette[Math.max(0, Math.min(255, Math.floor(normalized * 255)))] || [0, 0, 0];
|
||||
const offset = x * 4;
|
||||
rowData[offset] = color[0];
|
||||
rowData[offset + 1] = color[1];
|
||||
rowData[offset + 2] = color[2];
|
||||
rowData[offset + 3] = 255;
|
||||
}
|
||||
waterfallCtx.putImageData(waterfallRowImage, 0, 0);
|
||||
}
|
||||
|
||||
function drawSpectrumLine(bins, startFreq, endFreq) {
|
||||
@@ -3154,6 +3180,7 @@ function startWaterfall() {
|
||||
const binSize = parseInt(document.getElementById('waterfallBinSize')?.value || 10000);
|
||||
const gain = parseInt(document.getElementById('waterfallGain')?.value || 40);
|
||||
const device = typeof getSelectedDevice === 'function' ? getSelectedDevice() : 0;
|
||||
const maxBins = document.getElementById('waterfallCanvas')?.width || 800;
|
||||
|
||||
if (startFreq >= endFreq) {
|
||||
if (typeof showNotification === 'function') showNotification('Error', 'End frequency must be greater than start');
|
||||
@@ -3166,7 +3193,14 @@ function startWaterfall() {
|
||||
fetch('/listening/waterfall/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ start_freq: startFreq, end_freq: endFreq, bin_size: binSize, gain: gain, device: device })
|
||||
body: JSON.stringify({
|
||||
start_freq: startFreq,
|
||||
end_freq: endFreq,
|
||||
bin_size: binSize,
|
||||
gain: gain,
|
||||
device: device,
|
||||
max_bins: maxBins,
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
@@ -3176,6 +3210,7 @@ function startWaterfall() {
|
||||
document.getElementById('stopWaterfallBtn').style.display = 'block';
|
||||
const waterfallPanel = document.getElementById('waterfallPanel');
|
||||
if (waterfallPanel) waterfallPanel.style.display = 'block';
|
||||
lastWaterfallDraw = 0;
|
||||
initWaterfallCanvas();
|
||||
connectWaterfallSSE();
|
||||
} else {
|
||||
@@ -3204,6 +3239,9 @@ function connectWaterfallSSE() {
|
||||
waterfallEventSource.onmessage = function(event) {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'waterfall_sweep') {
|
||||
const now = Date.now();
|
||||
if (now - lastWaterfallDraw < WATERFALL_MIN_INTERVAL_MS) return;
|
||||
lastWaterfallDraw = now;
|
||||
drawWaterfallRow(msg.bins);
|
||||
drawSpectrumLine(msg.bins, msg.start_freq, msg.end_freq);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ const WiFiMode = (function() {
|
||||
maxProbes: 1000,
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Agent Support
|
||||
// ==========================================================================
|
||||
// ==========================================================================
|
||||
// Agent Support
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get the API base URL, routing through agent proxy if agent is selected.
|
||||
@@ -59,15 +59,49 @@ const WiFiMode = (function() {
|
||||
/**
|
||||
* Check for agent mode conflicts before starting WiFi scan.
|
||||
*/
|
||||
function checkAgentConflicts() {
|
||||
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
|
||||
return true;
|
||||
}
|
||||
if (typeof checkAgentModeConflict === 'function') {
|
||||
return checkAgentModeConflict('wifi');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function checkAgentConflicts() {
|
||||
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
|
||||
return true;
|
||||
}
|
||||
if (typeof checkAgentModeConflict === 'function') {
|
||||
return checkAgentModeConflict('wifi');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getChannelPresetList(preset) {
|
||||
switch (preset) {
|
||||
case '2.4-common':
|
||||
return '1,6,11';
|
||||
case '2.4-all':
|
||||
return '1,2,3,4,5,6,7,8,9,10,11,12,13';
|
||||
case '5-low':
|
||||
return '36,40,44,48';
|
||||
case '5-mid':
|
||||
return '52,56,60,64';
|
||||
case '5-high':
|
||||
return '149,153,157,161,165';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildChannelConfig() {
|
||||
const preset = document.getElementById('wifiChannelPreset')?.value || '';
|
||||
const listInput = document.getElementById('wifiChannelList')?.value || '';
|
||||
const singleInput = document.getElementById('wifiChannel')?.value || '';
|
||||
|
||||
const listValue = listInput.trim();
|
||||
const presetValue = getChannelPresetList(preset);
|
||||
|
||||
const channels = listValue || presetValue || '';
|
||||
const channel = channels ? null : (singleInput.trim() ? parseInt(singleInput.trim()) : null);
|
||||
|
||||
return {
|
||||
channels: channels || null,
|
||||
channel: Number.isFinite(channel) ? channel : null,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// State
|
||||
@@ -461,10 +495,10 @@ const WiFiMode = (function() {
|
||||
setScanning(true, 'deep');
|
||||
|
||||
try {
|
||||
const iface = elements.interfaceSelect?.value || null;
|
||||
const band = document.getElementById('wifiBand')?.value || 'all';
|
||||
const channel = document.getElementById('wifiChannel')?.value || null;
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const iface = elements.interfaceSelect?.value || null;
|
||||
const band = document.getElementById('wifiBand')?.value || 'all';
|
||||
const channelConfig = buildChannelConfig();
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
let response;
|
||||
if (isAgentMode) {
|
||||
@@ -473,23 +507,25 @@ const WiFiMode = (function() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interface: iface,
|
||||
scan_type: 'deep',
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channel ? parseInt(channel) : null,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${CONFIG.apiBase}/scan/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interface: iface,
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channel ? parseInt(channel) : null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
interface: iface,
|
||||
scan_type: 'deep',
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channelConfig.channel,
|
||||
channels: channelConfig.channels,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${CONFIG.apiBase}/scan/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interface: iface,
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channelConfig.channel,
|
||||
channels: channelConfig.channels,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user