Add alerts/recording, WiFi/TSCM updates, optimize waterfall

This commit is contained in:
Smittix
2026-02-07 18:29:58 +00:00
parent 4bbc00b765
commit 86e4ba7e29
42 changed files with 2514 additions and 485 deletions

194
static/js/core/alerts.js Normal file
View File

@@ -0,0 +1,194 @@
const AlertCenter = (function() {
'use strict';
let alerts = [];
let rules = [];
let eventSource = null;
const TRACKER_RULE_NAME = 'Tracker Detected';
function init() {
loadRules();
loadFeed();
connect();
}
function connect() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/alerts/stream');
eventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
if (data.type === 'keepalive') return;
handleAlert(data);
} catch (err) {
console.error('[Alerts] SSE parse error', err);
}
};
eventSource.onerror = function() {
console.warn('[Alerts] SSE connection error');
};
}
function handleAlert(alert) {
alerts.unshift(alert);
alerts = alerts.slice(0, 50);
updateFeedUI();
if (typeof showNotification === 'function') {
const severity = (alert.severity || '').toLowerCase();
if (['high', 'critical'].includes(severity)) {
showNotification(alert.title || 'Alert', alert.message || 'Alert triggered');
}
}
}
function updateFeedUI() {
const list = document.getElementById('alertsFeedList');
const countEl = document.getElementById('alertsFeedCount');
if (countEl) countEl.textContent = `(${alerts.length})`;
if (!list) return;
if (alerts.length === 0) {
list.innerHTML = '<div class="settings-feed-empty">No alerts yet</div>';
return;
}
list.innerHTML = alerts.map(alert => {
const title = escapeHtml(alert.title || 'Alert');
const message = escapeHtml(alert.message || '');
const severity = escapeHtml(alert.severity || 'medium');
const createdAt = alert.created_at ? new Date(alert.created_at).toLocaleString() : '';
return `
<div class="settings-feed-item">
<div class="settings-feed-title">
<span>${title}</span>
<span style="color: var(--text-dim);">${severity.toUpperCase()}</span>
</div>
<div class="settings-feed-meta">${message}</div>
<div class="settings-feed-meta" style="margin-top: 4px;">${createdAt}</div>
</div>
`;
}).join('');
}
function loadFeed() {
fetch('/alerts/events?limit=20')
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
alerts = data.events || [];
updateFeedUI();
}
})
.catch(err => console.error('[Alerts] Load feed failed', err));
}
function loadRules() {
fetch('/alerts/rules?all=1')
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
rules = data.rules || [];
}
})
.catch(err => console.error('[Alerts] Load rules failed', err));
}
function enableTrackerAlerts() {
ensureTrackerRule(true);
}
function disableTrackerAlerts() {
ensureTrackerRule(false);
}
function ensureTrackerRule(enabled) {
loadRules();
setTimeout(() => {
const existing = rules.find(r => r.name === TRACKER_RULE_NAME);
if (existing) {
fetch(`/alerts/rules/${existing.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
}).then(() => loadRules());
} else if (enabled) {
fetch('/alerts/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: TRACKER_RULE_NAME,
mode: 'bluetooth',
event_type: 'device_update',
match: { is_tracker: true },
severity: 'high',
enabled: true,
notify: { webhook: true }
})
}).then(() => loadRules());
}
}, 150);
}
function addBluetoothWatchlist(address, name) {
if (!address) return;
const existing = rules.find(r => r.mode === 'bluetooth' && r.match && r.match.address === address);
if (existing) {
return;
}
fetch('/alerts/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name ? `Watchlist ${name}` : `Watchlist ${address}`,
mode: 'bluetooth',
event_type: 'device_update',
match: { address: address },
severity: 'medium',
enabled: true,
notify: { webhook: true }
})
}).then(() => loadRules());
}
function removeBluetoothWatchlist(address) {
if (!address) return;
const existing = rules.find(r => r.mode === 'bluetooth' && r.match && r.match.address === address);
if (!existing) return;
fetch(`/alerts/rules/${existing.id}`, { method: 'DELETE' })
.then(() => loadRules());
}
function isWatchlisted(address) {
return rules.some(r => r.mode === 'bluetooth' && r.match && r.match.address === address && r.enabled);
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
return {
init,
loadFeed,
enableTrackerAlerts,
disableTrackerAlerts,
addBluetoothWatchlist,
removeBluetoothWatchlist,
isWatchlisted,
};
})();
document.addEventListener('DOMContentLoaded', () => {
if (typeof AlertCenter !== 'undefined') {
AlertCenter.init();
}
});

View File

@@ -0,0 +1,136 @@
const RecordingUI = (function() {
'use strict';
let recordings = [];
let active = [];
function init() {
refresh();
}
function refresh() {
fetch('/recordings')
.then(r => r.json())
.then(data => {
if (data.status !== 'success') return;
recordings = data.recordings || [];
active = data.active || [];
renderActive();
renderRecordings();
})
.catch(err => console.error('[Recording] Load failed', err));
}
function start() {
const modeSelect = document.getElementById('recordingModeSelect');
const labelInput = document.getElementById('recordingLabelInput');
const mode = modeSelect ? modeSelect.value : '';
const label = labelInput ? labelInput.value : '';
if (!mode) return;
fetch('/recordings/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode, label })
})
.then(r => r.json())
.then(() => {
refresh();
})
.catch(err => console.error('[Recording] Start failed', err));
}
function stop() {
const modeSelect = document.getElementById('recordingModeSelect');
const mode = modeSelect ? modeSelect.value : '';
if (!mode) return;
fetch('/recordings/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode })
})
.then(r => r.json())
.then(() => refresh())
.catch(err => console.error('[Recording] Stop failed', err));
}
function stopById(sessionId) {
fetch('/recordings/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: sessionId })
}).then(() => refresh());
}
function renderActive() {
const container = document.getElementById('recordingActiveList');
if (!container) return;
if (!active.length) {
container.innerHTML = '<div class="settings-feed-empty">No active recordings</div>';
return;
}
container.innerHTML = active.map(session => {
return `
<div class="settings-feed-item">
<div class="settings-feed-title">
<span>${escapeHtml(session.mode)}</span>
<button class="preset-btn" style="font-size: 9px; padding: 2px 6px;" onclick="RecordingUI.stopById('${session.id}')">Stop</button>
</div>
<div class="settings-feed-meta">Started: ${new Date(session.started_at).toLocaleString()}</div>
<div class="settings-feed-meta">Events: ${session.event_count || 0}</div>
</div>
`;
}).join('');
}
function renderRecordings() {
const container = document.getElementById('recordingList');
if (!container) return;
if (!recordings.length) {
container.innerHTML = '<div class="settings-feed-empty">No recordings yet</div>';
return;
}
container.innerHTML = recordings.map(rec => {
return `
<div class="settings-feed-item">
<div class="settings-feed-title">
<span>${escapeHtml(rec.mode)}${rec.label ? `${escapeHtml(rec.label)}` : ''}</span>
<button class="preset-btn" style="font-size: 9px; padding: 2px 6px;" onclick="RecordingUI.download('${rec.id}')">Download</button>
</div>
<div class="settings-feed-meta">${new Date(rec.started_at).toLocaleString()}${rec.stopped_at ? `${new Date(rec.stopped_at).toLocaleString()}` : ''}</div>
<div class="settings-feed-meta">Events: ${rec.event_count || 0}${(rec.size_bytes || 0) / 1024.0 > 0 ? (rec.size_bytes / 1024).toFixed(1) + ' KB' : '0 KB'}</div>
</div>
`;
}).join('');
}
function download(sessionId) {
window.open(`/recordings/${sessionId}/download`, '_blank');
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
return {
init,
refresh,
start,
stop,
stopById,
download,
};
})();
document.addEventListener('DOMContentLoaded', () => {
if (typeof RecordingUI !== 'undefined') {
RecordingUI.init();
}
});

View File

@@ -922,5 +922,13 @@ function switchSettingsTab(tabName) {
loadUpdateStatus();
} else if (tabName === 'location') {
loadObserverLocation();
} else if (tabName === 'alerts') {
if (typeof AlertCenter !== 'undefined') {
AlertCenter.loadFeed();
}
} else if (tabName === 'recording') {
if (typeof RecordingUI !== 'undefined') {
RecordingUI.refresh();
}
}
}