mirror of
https://github.com/smittix/intercept.git
synced 2026-06-13 00:03:33 -07:00
Merge upstream/main: sync fork with conflict resolution
Resolve conflicts keeping local GSM tools in kill_all() process list and weather satellite config settings while merging upstream changes including GSM spy removal, DMR fixes, USB device probe, APRS crash fix, and cross-module frequency routing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,9 @@
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
@@ -115,6 +118,9 @@
|
||||
|
||||
.settings-section.active {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
|
||||
@@ -930,5 +930,56 @@ function switchSettingsTab(tabName) {
|
||||
if (typeof RecordingUI !== 'undefined') {
|
||||
RecordingUI.refresh();
|
||||
}
|
||||
} else if (tabName === 'apikeys') {
|
||||
loadApiKeyStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load API key status into the API Keys settings tab
|
||||
*/
|
||||
function loadApiKeyStatus() {
|
||||
const badge = document.getElementById('apiKeyStatusBadge');
|
||||
const desc = document.getElementById('apiKeyStatusDesc');
|
||||
const usage = document.getElementById('apiKeyUsageCount');
|
||||
const bar = document.getElementById('apiKeyUsageBar');
|
||||
|
||||
if (!badge) return;
|
||||
|
||||
badge.textContent = 'Not available';
|
||||
badge.className = 'asset-badge missing';
|
||||
desc.textContent = 'GSM feature removed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Save API key from the settings input
|
||||
*/
|
||||
function saveApiKey() {
|
||||
const input = document.getElementById('apiKeyInput');
|
||||
const result = document.getElementById('apiKeySaveResult');
|
||||
if (!input || !result) return;
|
||||
|
||||
const key = input.value.trim();
|
||||
if (!key) {
|
||||
result.style.display = 'block';
|
||||
result.style.color = 'var(--accent-red)';
|
||||
result.textContent = 'Please enter an API key.';
|
||||
return;
|
||||
}
|
||||
|
||||
result.style.display = 'block';
|
||||
result.style.color = 'var(--text-dim)';
|
||||
result.textContent = 'Saving...';
|
||||
|
||||
result.style.color = 'var(--accent-red)';
|
||||
result.textContent = 'GSM feature has been removed.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle API key input visibility
|
||||
*/
|
||||
function toggleApiKeyVisibility() {
|
||||
const input = document.getElementById('apiKeyInput');
|
||||
if (!input) return;
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
@@ -91,6 +91,21 @@ function startDmr() {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('DMR', `Decoding ${frequency} MHz (${protocol.toUpperCase()})`);
|
||||
}
|
||||
} else if (data.status === 'error' && data.message === 'Already running') {
|
||||
// Backend has an active session the frontend lost track of — resync
|
||||
isDmrRunning = true;
|
||||
updateDmrUI();
|
||||
connectDmrSSE();
|
||||
if (!dmrSynthInitialized) initDmrSynthesizer();
|
||||
dmrEventType = 'idle';
|
||||
dmrActivityTarget = 0.1;
|
||||
dmrLastEventTime = Date.now();
|
||||
updateDmrSynthStatus();
|
||||
const statusEl = document.getElementById('dmrStatus');
|
||||
if (statusEl) statusEl.textContent = 'DECODING';
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('DMR', 'Reconnected to active session');
|
||||
}
|
||||
} else {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Error', data.message || 'Failed to start DMR');
|
||||
@@ -496,9 +511,43 @@ function stopDmrSynthesizer() {
|
||||
|
||||
window.addEventListener('resize', resizeDmrSynthesizer);
|
||||
|
||||
// ============== STATUS SYNC ==============
|
||||
|
||||
function checkDmrStatus() {
|
||||
fetch('/dmr/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.running && !isDmrRunning) {
|
||||
// Backend is running but frontend lost track — resync
|
||||
isDmrRunning = true;
|
||||
updateDmrUI();
|
||||
connectDmrSSE();
|
||||
if (!dmrSynthInitialized) initDmrSynthesizer();
|
||||
dmrEventType = 'idle';
|
||||
dmrActivityTarget = 0.1;
|
||||
dmrLastEventTime = Date.now();
|
||||
updateDmrSynthStatus();
|
||||
const statusEl = document.getElementById('dmrStatus');
|
||||
if (statusEl) statusEl.textContent = 'DECODING';
|
||||
} else if (!data.running && isDmrRunning) {
|
||||
// Backend stopped but frontend didn't know
|
||||
isDmrRunning = false;
|
||||
if (dmrEventSource) { dmrEventSource.close(); dmrEventSource = null; }
|
||||
updateDmrUI();
|
||||
dmrEventType = 'stopped';
|
||||
dmrActivityTarget = 0;
|
||||
updateDmrSynthStatus();
|
||||
const statusEl = document.getElementById('dmrStatus');
|
||||
if (statusEl) statusEl.textContent = 'STOPPED';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// ============== EXPORTS ==============
|
||||
|
||||
window.startDmr = startDmr;
|
||||
window.stopDmr = stopDmr;
|
||||
window.checkDmrTools = checkDmrTools;
|
||||
window.checkDmrStatus = checkDmrStatus;
|
||||
window.initDmrSynthesizer = initDmrSynthesizer;
|
||||
|
||||
@@ -1018,8 +1018,16 @@ function addSignalHit(data) {
|
||||
<td style="padding: 4px; color: var(--accent-green); font-weight: bold;">${data.frequency.toFixed(3)}</td>
|
||||
<td style="padding: 4px; color: ${snrColor}; font-weight: bold; font-size: 9px;">${snrText}</td>
|
||||
<td style="padding: 4px; color: var(--text-secondary);">${mod.toUpperCase()}</td>
|
||||
<td style="padding: 4px; text-align: center;">
|
||||
<td style="padding: 4px; text-align: center; white-space: nowrap;">
|
||||
<button class="preset-btn" onclick="tuneToFrequency(${data.frequency}, '${mod}')" style="padding: 2px 6px; font-size: 9px; background: var(--accent-green); border: none; color: #000; cursor: pointer; border-radius: 3px;">Listen</button>
|
||||
<span style="position:relative;display:inline-block;">
|
||||
<button class="preset-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'block' ? 'none' : 'block'" style="padding:2px 5px; font-size:9px; background:var(--accent-cyan); border:none; color:#000; cursor:pointer; border-radius:3px; margin-left:3px;" title="Send frequency to decoder">▶</button>
|
||||
<div style="display:none; position:absolute; right:0; top:100%; background:var(--bg-primary); border:1px solid var(--border-color); border-radius:4px; z-index:100; min-width:90px; padding:2px; box-shadow:0 2px 8px rgba(0,0,0,0.4);">
|
||||
<div onclick="sendFrequencyToMode(${data.frequency}, 'pager'); this.parentElement.style.display='none'" style="padding:3px 8px; cursor:pointer; font-size:9px; color:var(--text-primary); border-radius:3px;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='transparent'">Pager</div>
|
||||
<div onclick="sendFrequencyToMode(${data.frequency}, 'sensor'); this.parentElement.style.display='none'" style="padding:3px 8px; cursor:pointer; font-size:9px; color:var(--text-primary); border-radius:3px;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='transparent'">433 Sensor</div>
|
||||
<div onclick="sendFrequencyToMode(${data.frequency}, 'rtlamr'); this.parentElement.style.display='none'" style="padding:3px 8px; cursor:pointer; font-size:9px; color:var(--text-primary); border-radius:3px;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='transparent'">RTLAMR</div>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
`;
|
||||
tbody.insertBefore(row, tbody.firstChild);
|
||||
@@ -3056,6 +3064,27 @@ function renderSignalGuess(result) {
|
||||
altsEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
const sendToEl = document.getElementById('signalGuessSendTo');
|
||||
if (sendToEl) {
|
||||
const freqInput = document.getElementById('signalGuessFreqInput');
|
||||
const freq = freqInput ? parseFloat(freqInput.value) : NaN;
|
||||
if (!isNaN(freq) && freq > 0) {
|
||||
const tags = (result.tags || []).map(t => t.toLowerCase());
|
||||
const modes = [
|
||||
{ key: 'pager', label: 'Pager', highlight: tags.some(t => t.includes('pager') || t.includes('pocsag') || t.includes('flex')) },
|
||||
{ key: 'sensor', label: '433 Sensor', highlight: tags.some(t => t.includes('ism') || t.includes('433') || t.includes('sensor') || t.includes('iot')) },
|
||||
{ key: 'rtlamr', label: 'RTLAMR', highlight: tags.some(t => t.includes('meter') || t.includes('amr') || t.includes('utility')) }
|
||||
];
|
||||
sendToEl.style.display = 'block';
|
||||
sendToEl.innerHTML = '<div style="font-size:9px; color:var(--text-muted); margin-bottom:4px;">Send to:</div><div style="display:flex; gap:4px;">' +
|
||||
modes.map(m =>
|
||||
`<button class="preset-btn" onclick="sendFrequencyToMode(${freq}, '${m.key}')" style="padding:2px 8px; font-size:9px; border:none; color:#000; cursor:pointer; border-radius:3px; background:${m.highlight ? 'var(--accent-green)' : 'var(--accent-cyan)'}; ${m.highlight ? 'font-weight:bold;' : ''}">${m.label}</button>`
|
||||
).join('') + '</div>';
|
||||
} else {
|
||||
sendToEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function manualSignalGuess() {
|
||||
@@ -4023,21 +4052,88 @@ function bindWaterfallInteraction() {
|
||||
tooltip.style.display = 'none';
|
||||
};
|
||||
|
||||
// Right-click context menu for "Send to" decoder
|
||||
let ctxMenu = document.getElementById('waterfallCtxMenu');
|
||||
if (!ctxMenu) {
|
||||
ctxMenu = document.createElement('div');
|
||||
ctxMenu.id = 'waterfallCtxMenu';
|
||||
ctxMenu.style.cssText = 'position:fixed;display:none;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:4px;z-index:10000;min-width:120px;padding:4px 0;box-shadow:0 4px 12px rgba(0,0,0,0.5);font-size:11px;';
|
||||
document.body.appendChild(ctxMenu);
|
||||
document.addEventListener('click', () => { ctxMenu.style.display = 'none'; });
|
||||
}
|
||||
|
||||
const contextHandler = (event) => {
|
||||
if (waterfallMode === 'audio') return;
|
||||
event.preventDefault();
|
||||
const canvas = event.currentTarget;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const ratio = Math.max(0, Math.min(1, x / rect.width));
|
||||
const freq = waterfallStartFreq + ratio * (waterfallEndFreq - waterfallStartFreq);
|
||||
|
||||
const modes = [
|
||||
{ key: 'pager', label: 'Pager' },
|
||||
{ key: 'sensor', label: '433 Sensor' },
|
||||
{ key: 'rtlamr', label: 'RTLAMR' }
|
||||
];
|
||||
|
||||
ctxMenu.innerHTML = `<div style="padding:4px 10px; color:var(--text-muted); font-size:9px; border-bottom:1px solid var(--border-color); margin-bottom:2px;">${freq.toFixed(3)} MHz →</div>` +
|
||||
modes.map(m =>
|
||||
`<div onclick="sendFrequencyToMode(${freq}, '${m.key}')" style="padding:4px 10px; cursor:pointer; color:var(--text-primary);" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='transparent'">Send to ${m.label}</div>`
|
||||
).join('');
|
||||
|
||||
ctxMenu.style.left = event.clientX + 'px';
|
||||
ctxMenu.style.top = event.clientY + 'px';
|
||||
ctxMenu.style.display = 'block';
|
||||
};
|
||||
|
||||
if (waterfallCanvas) {
|
||||
waterfallCanvas.style.cursor = 'crosshair';
|
||||
waterfallCanvas.addEventListener('click', handler);
|
||||
waterfallCanvas.addEventListener('mousemove', hoverHandler);
|
||||
waterfallCanvas.addEventListener('mouseleave', leaveHandler);
|
||||
waterfallCanvas.addEventListener('contextmenu', contextHandler);
|
||||
}
|
||||
if (spectrumCanvas) {
|
||||
spectrumCanvas.style.cursor = 'crosshair';
|
||||
spectrumCanvas.addEventListener('click', handler);
|
||||
spectrumCanvas.addEventListener('mousemove', hoverHandler);
|
||||
spectrumCanvas.addEventListener('mouseleave', leaveHandler);
|
||||
spectrumCanvas.addEventListener('contextmenu', contextHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============== CROSS-MODULE FREQUENCY ROUTING ==============
|
||||
|
||||
function sendFrequencyToMode(freqMhz, targetMode) {
|
||||
const inputMap = {
|
||||
pager: 'frequency',
|
||||
sensor: 'sensorFrequency',
|
||||
rtlamr: 'rtlamrFrequency'
|
||||
};
|
||||
|
||||
const inputId = inputMap[targetMode];
|
||||
if (!inputId) return;
|
||||
|
||||
if (typeof switchMode === 'function') {
|
||||
switchMode(targetMode);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.value = freqMhz.toFixed(4);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
if (typeof showNotification === 'function') {
|
||||
const modeLabels = { pager: 'Pager', sensor: '433 Sensor', rtlamr: 'RTLAMR' };
|
||||
showNotification('Frequency Sent', `${freqMhz.toFixed(3)} MHz → ${modeLabels[targetMode] || targetMode}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.sendFrequencyToMode = sendFrequencyToMode;
|
||||
window.stopDirectListen = stopDirectListen;
|
||||
window.toggleScanner = toggleScanner;
|
||||
window.startScanner = startScanner;
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
(c) 2014, Vladimir Agafonkin
|
||||
simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas
|
||||
https://github.com/mourner/simpleheat
|
||||
*/
|
||||
!function(){"use strict";function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;e>s;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;e>s;s+=4)a=4*t[s],a&&(t[s-3]=i[a],t[s-2]=i[a+1],t[s-1]=i[a+2])}},window.simpleheat=t}(),/*
|
||||
(c) 2014, Vladimir Agafonkin
|
||||
Leaflet.heat, a tiny and fast heatmap plugin for Leaflet.
|
||||
https://github.com/Leaflet/Leaflet.heat
|
||||
*/
|
||||
L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!this._heat||this._frame||this._map._animating||(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient),this.options.max&&this._heat.max(this.options.max)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){var t,i,a,s,e,n,h,o,r,d=[],_=this._heat._r,l=this._map.getSize(),m=new L.Bounds(L.point([-_,-_]),l.add([_,_])),c=void 0===this.options.max?1:this.options.max,u=void 0===this.options.maxZoom?this._map.getMaxZoom():this.options.maxZoom,f=1/Math.pow(2,Math.max(0,Math.min(u-this._map.getZoom(),12))),g=_/2,p=[],v=this._map._getMapPanePos(),w=v.x%g,y=v.y%g;for(t=0,i=this._latlngs.length;i>t;t++)if(a=this._map.latLngToContainerPoint(this._latlngs[t]),m.contains(a)){e=Math.floor((a.x-w)/g)+2,n=Math.floor((a.y-y)/g)+2;var x=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;r=x*f,p[n]=p[n]||[],s=p[n][e],s?(s[0]=(s[0]*s[2]+a.x*r)/(s[2]+r),s[1]=(s[1]*s[2]+a.y*r)/(s[2]+r),s[2]+=r):p[n][e]=[a.x,a.y,r]}for(t=0,i=p.length;i>t;t++)if(p[t])for(h=0,o=p[t].length;o>h;h++)s=p[t][h],s&&d.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],c)]);this._heat.data(d).draw(this.options.minOpacity),this._frame=null},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)};
|
||||
Reference in New Issue
Block a user