chore: commit all pending changes

This commit is contained in:
Smittix
2026-02-23 16:51:32 +00:00
parent 94b358f686
commit 7241dbed35
19 changed files with 946 additions and 308 deletions
+33 -23
View File
@@ -898,66 +898,76 @@ body {
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 650;
--target-x: 50%;
--target-y: 50%;
z-index: 1200;
--crosshair-x-start: 100%;
--crosshair-y-start: 100%;
--crosshair-x-end: 50%;
--crosshair-y-end: 50%;
--crosshair-duration: 1500ms;
}
.map-crosshair-line {
position: absolute;
opacity: 0;
background: var(--accent-cyan);
box-shadow: 0 0 10px var(--accent-cyan);
box-shadow: none;
will-change: transform, opacity;
}
.map-crosshair-vertical {
top: 0;
bottom: 0;
width: 2px;
right: 0;
transform: translateX(0);
width: 1px;
left: 0;
transform: translateX(var(--crosshair-x-start));
}
.map-crosshair-horizontal {
left: 0;
right: 0;
height: 2px;
bottom: 0;
transform: translateY(0);
height: 1px;
top: 0;
transform: translateY(var(--crosshair-y-start));
}
.map-crosshair-overlay.active .map-crosshair-vertical {
animation: mapCrosshairSweepX 620ms cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
animation: mapCrosshairSweepX var(--crosshair-duration) cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
}
.map-crosshair-overlay.active .map-crosshair-horizontal {
animation: mapCrosshairSweepY 620ms cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
animation: mapCrosshairSweepY var(--crosshair-duration) cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
}
@keyframes mapCrosshairSweepX {
0% {
transform: translateX(0);
opacity: 0.95;
transform: translateX(var(--crosshair-x-start));
opacity: 0;
}
75% {
opacity: 0.95;
12% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateX(calc(var(--target-x) - 100%));
transform: translateX(var(--crosshair-x-end));
opacity: 0;
}
}
@keyframes mapCrosshairSweepY {
0% {
transform: translateY(0);
opacity: 0.95;
transform: translateY(var(--crosshair-y-start));
opacity: 0;
}
75% {
opacity: 0.95;
12% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateY(calc(var(--target-y) - 100%));
transform: translateY(var(--crosshair-y-end));
opacity: 0;
}
}
@@ -965,7 +975,7 @@ body {
@media (prefers-reduced-motion: reduce) {
.map-crosshair-overlay.active .map-crosshair-vertical,
.map-crosshair-overlay.active .map-crosshair-horizontal {
animation-duration: 120ms;
animation-duration: 220ms;
}
}
-53
View File
@@ -479,10 +479,6 @@
filter: sepia(0.35) hue-rotate(185deg) saturate(1.75) brightness(1.06) contrast(1.05);
}
.tile-layer-flir {
filter: grayscale(1) sepia(1) hue-rotate(-18deg) saturate(4.85) brightness(0.96) contrast(1.34);
}
/* Global Leaflet map theme: cyber overlay */
.leaflet-container.map-theme-cyber {
position: relative;
@@ -531,55 +527,6 @@ html.map-cyber-enabled .leaflet-container::after {
background-size: 52px 52px, 52px 52px;
}
/* Global Leaflet map theme: FLIR thermal overlay */
.leaflet-container.map-theme-flir {
position: relative;
background: #090602;
isolation: isolate;
}
.leaflet-container.map-theme-flir .leaflet-tile-pane {
filter: grayscale(1) sepia(1) hue-rotate(-18deg) saturate(4.85) brightness(0.96) contrast(1.34);
opacity: 1;
}
/* Hard global fallback: enforce FLIR tint on all Leaflet tile images */
html.map-flir-enabled .leaflet-container .leaflet-tile {
filter: grayscale(1) sepia(1) hue-rotate(-18deg) saturate(4.85) brightness(0.96) contrast(1.34) !important;
}
/* Hard global fallback: thermal glow + scanline/grid overlay */
html.map-flir-enabled .leaflet-container {
position: relative;
isolation: isolate;
}
html.map-flir-enabled .leaflet-container::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 620;
background:
radial-gradient(115% 90% at 50% 40%, rgba(255, 132, 28, 0.22), rgba(255, 132, 28, 0) 63%),
linear-gradient(180deg, rgba(14, 229, 255, 0.08) 0%, rgba(255, 96, 18, 0.15) 58%, rgba(255, 233, 128, 0.11) 100%);
}
html.map-flir-enabled .leaflet-container::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 621;
opacity: 0.32;
mix-blend-mode: screen;
background-image:
repeating-linear-gradient(0deg, rgba(255, 188, 92, 0.08) 0 1px, transparent 1px 3px),
linear-gradient(90deg, rgba(255, 141, 66, 0.12) 1px, transparent 1px),
linear-gradient(rgba(0, 240, 255, 0.07) 1px, transparent 1px);
background-size: 100% 3px, 68px 68px, 68px 68px;
}
/* Responsive */
@media (max-width: 960px) {
.settings-tabs {
+1 -21
View File
@@ -33,15 +33,6 @@ const Settings = {
mapTheme: 'cyber',
options: {}
},
cartodb_dark_flir: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd',
mapTheme: 'flir',
options: {
className: 'tile-layer-flir'
}
},
cartodb_light: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
@@ -116,7 +107,6 @@ const Settings = {
const resolvedConfig = config || this.getTileConfig();
const themeClass = this._getMapThemeClass(resolvedConfig);
document.documentElement.classList.toggle('map-cyber-enabled', themeClass === 'map-theme-cyber');
document.documentElement.classList.toggle('map-flir-enabled', themeClass === 'map-theme-flir');
},
/**
@@ -351,7 +341,6 @@ const Settings = {
_getMapThemeClass(config) {
if (!config || !config.mapTheme) return null;
if (config.mapTheme === 'cyber') return 'map-theme-cyber';
if (config.mapTheme === 'flir') return 'map-theme-flir';
return null;
},
@@ -375,7 +364,7 @@ const Settings = {
container.style.background = '';
}
container.classList.remove('map-theme-cyber', 'map-theme-flir');
container.classList.remove('map-theme-cyber');
const resolvedConfig = config || this.getTileConfig();
const themeClass = this._getMapThemeClass(resolvedConfig);
@@ -392,15 +381,6 @@ const Settings = {
tilePane.style.opacity = '1';
tilePane.style.willChange = 'filter';
}
} else if (themeClass === 'map-theme-flir') {
if (container.style) {
container.style.background = '#090602';
}
if (tilePane && tilePane.style) {
tilePane.style.filter = 'grayscale(1) sepia(1) hue-rotate(-18deg) saturate(4.85) brightness(0.96) contrast(1.34)';
tilePane.style.opacity = '1';
tilePane.style.willChange = 'filter';
}
}
// Map overlays are rendered via CSS pseudo elements on
+141 -23
View File
@@ -15,13 +15,21 @@ const SSTVGeneral = (function() {
let sstvGeneralScopeCtx = null;
let sstvGeneralScopeAnim = null;
let sstvGeneralScopeHistory = [];
let sstvGeneralScopeWaveBuffer = [];
let sstvGeneralScopeDisplayWave = [];
const SSTV_GENERAL_SCOPE_LEN = 200;
const SSTV_GENERAL_SCOPE_WAVE_BUFFER_LEN = 2048;
const SSTV_GENERAL_SCOPE_WAVE_INPUT_SMOOTH_ALPHA = 0.55;
const SSTV_GENERAL_SCOPE_WAVE_DISPLAY_SMOOTH_ALPHA = 0.22;
const SSTV_GENERAL_SCOPE_WAVE_IDLE_DECAY = 0.96;
let sstvGeneralScopeRms = 0;
let sstvGeneralScopePeak = 0;
let sstvGeneralScopeTargetRms = 0;
let sstvGeneralScopeTargetPeak = 0;
let sstvGeneralScopeMsgBurst = 0;
let sstvGeneralScopeTone = null;
let sstvGeneralScopeLastWaveAt = 0;
let sstvGeneralScopeLastInputSample = 0;
/**
* Initialize the SSTV General mode
@@ -205,20 +213,64 @@ const SSTVGeneral = (function() {
/**
* Initialize signal scope canvas
*/
function resizeSstvGeneralScopeCanvas(canvas) {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const width = Math.max(1, Math.floor(rect.width * dpr));
const height = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
function applySstvGeneralScopeData(scopeData) {
if (!scopeData || typeof scopeData !== 'object') return;
sstvGeneralScopeTargetRms = Number(scopeData.rms) || 0;
sstvGeneralScopeTargetPeak = Number(scopeData.peak) || 0;
if (scopeData.tone !== undefined) {
sstvGeneralScopeTone = scopeData.tone;
}
if (Array.isArray(scopeData.waveform) && scopeData.waveform.length) {
for (const packedSample of scopeData.waveform) {
const sample = Number(packedSample);
if (!Number.isFinite(sample)) continue;
const normalized = Math.max(-127, Math.min(127, sample)) / 127;
sstvGeneralScopeLastInputSample += (normalized - sstvGeneralScopeLastInputSample) * SSTV_GENERAL_SCOPE_WAVE_INPUT_SMOOTH_ALPHA;
sstvGeneralScopeWaveBuffer.push(sstvGeneralScopeLastInputSample);
}
if (sstvGeneralScopeWaveBuffer.length > SSTV_GENERAL_SCOPE_WAVE_BUFFER_LEN) {
sstvGeneralScopeWaveBuffer.splice(0, sstvGeneralScopeWaveBuffer.length - SSTV_GENERAL_SCOPE_WAVE_BUFFER_LEN);
}
sstvGeneralScopeLastWaveAt = performance.now();
}
}
function initSstvGeneralScope() {
const canvas = document.getElementById('sstvGeneralScopeCanvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * (window.devicePixelRatio || 1);
canvas.height = rect.height * (window.devicePixelRatio || 1);
if (sstvGeneralScopeAnim) {
cancelAnimationFrame(sstvGeneralScopeAnim);
sstvGeneralScopeAnim = null;
}
resizeSstvGeneralScopeCanvas(canvas);
sstvGeneralScopeCtx = canvas.getContext('2d');
sstvGeneralScopeHistory = new Array(SSTV_GENERAL_SCOPE_LEN).fill(0);
sstvGeneralScopeWaveBuffer = [];
sstvGeneralScopeDisplayWave = [];
sstvGeneralScopeRms = 0;
sstvGeneralScopePeak = 0;
sstvGeneralScopeTargetRms = 0;
sstvGeneralScopeTargetPeak = 0;
sstvGeneralScopeMsgBurst = 0;
sstvGeneralScopeTone = null;
sstvGeneralScopeLastWaveAt = 0;
sstvGeneralScopeLastInputSample = 0;
drawSstvGeneralScope();
}
@@ -228,12 +280,14 @@ const SSTVGeneral = (function() {
function drawSstvGeneralScope() {
const ctx = sstvGeneralScopeCtx;
if (!ctx) return;
resizeSstvGeneralScopeCanvas(ctx.canvas);
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const midY = H / 2;
// Phosphor persistence
ctx.fillStyle = 'rgba(5, 5, 16, 0.3)';
ctx.fillStyle = 'rgba(5, 5, 16, 0.26)';
ctx.fillRect(0, 0, W, H);
// Smooth towards target
@@ -256,32 +310,84 @@ const SSTVGeneral = (function() {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
// Waveform
const stepX = W / (SSTV_GENERAL_SCOPE_LEN - 1);
ctx.strokeStyle = '#c080ff';
ctx.lineWidth = 1.5;
ctx.shadowColor = '#c080ff';
ctx.shadowBlur = 4;
// Upper half
// Envelope
const envStepX = W / (SSTV_GENERAL_SCOPE_LEN - 1);
ctx.strokeStyle = 'rgba(168, 110, 255, 0.45)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < sstvGeneralScopeHistory.length; i++) {
const x = i * stepX;
const amp = sstvGeneralScopeHistory[i] * midY * 0.9;
const x = i * envStepX;
const amp = sstvGeneralScopeHistory[i] * midY * 0.85;
const y = midY - amp;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
// Lower half (mirror)
ctx.beginPath();
for (let i = 0; i < sstvGeneralScopeHistory.length; i++) {
const x = i * stepX;
const amp = sstvGeneralScopeHistory[i] * midY * 0.9;
const x = i * envStepX;
const amp = sstvGeneralScopeHistory[i] * midY * 0.85;
const y = midY + amp;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
// Actual waveform trace
const waveformPointCount = Math.min(Math.max(120, Math.floor(W / 3.2)), 420);
if (sstvGeneralScopeWaveBuffer.length > 1) {
const waveIsFresh = (performance.now() - sstvGeneralScopeLastWaveAt) < 1000;
const sourceLen = sstvGeneralScopeWaveBuffer.length;
const sourceWindow = Math.min(sourceLen, 1536);
const sourceStart = sourceLen - sourceWindow;
if (sstvGeneralScopeDisplayWave.length !== waveformPointCount) {
sstvGeneralScopeDisplayWave = new Array(waveformPointCount).fill(0);
}
for (let i = 0; i < waveformPointCount; i++) {
const a = sourceStart + Math.floor((i / waveformPointCount) * sourceWindow);
const b = sourceStart + Math.floor(((i + 1) / waveformPointCount) * sourceWindow);
const start = Math.max(sourceStart, Math.min(sourceLen - 1, a));
const end = Math.max(start + 1, Math.min(sourceLen, b));
let sum = 0;
let count = 0;
for (let j = start; j < end; j++) {
sum += sstvGeneralScopeWaveBuffer[j];
count++;
}
const targetSample = count > 0 ? (sum / count) : 0;
sstvGeneralScopeDisplayWave[i] += (targetSample - sstvGeneralScopeDisplayWave[i]) * SSTV_GENERAL_SCOPE_WAVE_DISPLAY_SMOOTH_ALPHA;
}
ctx.strokeStyle = waveIsFresh ? '#c080ff' : 'rgba(192, 128, 255, 0.45)';
ctx.lineWidth = 1.7;
ctx.shadowColor = '#c080ff';
ctx.shadowBlur = waveIsFresh ? 6 : 2;
const stepX = waveformPointCount > 1 ? (W / (waveformPointCount - 1)) : W;
ctx.beginPath();
const firstY = midY - (sstvGeneralScopeDisplayWave[0] * midY * 0.9);
ctx.moveTo(0, firstY);
for (let i = 1; i < waveformPointCount - 1; i++) {
const x = i * stepX;
const y = midY - (sstvGeneralScopeDisplayWave[i] * midY * 0.9);
const nx = (i + 1) * stepX;
const ny = midY - (sstvGeneralScopeDisplayWave[i + 1] * midY * 0.9);
const cx = (x + nx) / 2;
const cy = (y + ny) / 2;
ctx.quadraticCurveTo(x, y, cx, cy);
}
const lastX = (waveformPointCount - 1) * stepX;
const lastY = midY - (sstvGeneralScopeDisplayWave[waveformPointCount - 1] * midY * 0.9);
ctx.lineTo(lastX, lastY);
ctx.stroke();
if (!waveIsFresh) {
for (let i = 0; i < sstvGeneralScopeDisplayWave.length; i++) {
sstvGeneralScopeDisplayWave[i] *= SSTV_GENERAL_SCOPE_WAVE_IDLE_DECAY;
}
}
}
ctx.shadowBlur = 0;
// Peak indicator
@@ -317,8 +423,17 @@ const SSTVGeneral = (function() {
else { toneLabel.textContent = 'QUIET'; toneLabel.style.color = '#444'; }
}
if (statusLabel) {
if (sstvGeneralScopeRms > 500) { statusLabel.textContent = 'SIGNAL'; statusLabel.style.color = '#0f0'; }
else { statusLabel.textContent = 'MONITORING'; statusLabel.style.color = '#555'; }
const waveIsFresh = (performance.now() - sstvGeneralScopeLastWaveAt) < 1000;
if (sstvGeneralScopeRms > 900 && waveIsFresh) {
statusLabel.textContent = 'DEMODULATING';
statusLabel.style.color = '#c080ff';
} else if (sstvGeneralScopeRms > 500) {
statusLabel.textContent = 'CARRIER';
statusLabel.style.color = '#e0b8ff';
} else {
statusLabel.textContent = 'QUIET';
statusLabel.style.color = '#555';
}
}
sstvGeneralScopeAnim = requestAnimationFrame(drawSstvGeneralScope);
@@ -330,6 +445,11 @@ const SSTVGeneral = (function() {
function stopSstvGeneralScope() {
if (sstvGeneralScopeAnim) { cancelAnimationFrame(sstvGeneralScopeAnim); sstvGeneralScopeAnim = null; }
sstvGeneralScopeCtx = null;
sstvGeneralScopeWaveBuffer = [];
sstvGeneralScopeDisplayWave = [];
sstvGeneralScopeHistory = [];
sstvGeneralScopeLastWaveAt = 0;
sstvGeneralScopeLastInputSample = 0;
}
/**
@@ -353,9 +473,7 @@ const SSTVGeneral = (function() {
if (data.type === 'sstv_progress') {
handleProgress(data);
} else if (data.type === 'sstv_scope') {
sstvGeneralScopeTargetRms = data.rms;
sstvGeneralScopeTargetPeak = data.peak;
if (data.tone !== undefined) sstvGeneralScopeTone = data.tone;
applySstvGeneralScopeData(data);
}
} catch (err) {
console.error('Failed to parse SSE message:', err);
+66 -29
View File
@@ -17,21 +17,51 @@ const STATIC_PREFIXES = [
'/static/fonts/',
];
const CACHE_EXACT = ['/manifest.json'];
function isNetworkOnly(req) {
if (req.method !== 'GET') return true;
const accept = req.headers.get('Accept') || '';
if (accept.includes('text/event-stream')) return true;
const url = new URL(req.url);
const CACHE_EXACT = ['/manifest.json'];
function isHttpRequest(req) {
const url = new URL(req.url);
return url.protocol === 'http:' || url.protocol === 'https:';
}
function isNetworkOnly(req) {
if (req.method !== 'GET') return true;
const accept = req.headers.get('Accept') || '';
if (accept.includes('text/event-stream')) return true;
const url = new URL(req.url);
return NETWORK_ONLY_PREFIXES.some(p => url.pathname.startsWith(p));
}
function isStaticAsset(req) {
const url = new URL(req.url);
if (CACHE_EXACT.includes(url.pathname)) return true;
return STATIC_PREFIXES.some(p => url.pathname.startsWith(p));
}
function isStaticAsset(req) {
const url = new URL(req.url);
if (CACHE_EXACT.includes(url.pathname)) return true;
return STATIC_PREFIXES.some(p => url.pathname.startsWith(p));
}
function fallbackResponse(req, status = 503) {
const accept = req.headers.get('Accept') || '';
if (accept.includes('application/json')) {
return new Response(
JSON.stringify({ status: 'error', message: 'Network unavailable' }),
{
status,
headers: { 'Content-Type': 'application/json' },
}
);
}
if (accept.includes('text/event-stream')) {
return new Response('', {
status,
headers: { 'Content-Type': 'text/event-stream' },
});
}
return new Response('Offline', {
status,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
self.addEventListener('install', (e) => {
self.skipWaiting();
@@ -45,14 +75,21 @@ self.addEventListener('activate', (e) => {
);
});
self.addEventListener('fetch', (e) => {
const req = e.request;
// Always bypass service worker for non-GET and streaming routes
if (isNetworkOnly(req)) {
e.respondWith(fetch(req));
return;
}
self.addEventListener('fetch', (e) => {
const req = e.request;
// Ignore non-HTTP(S) requests so extensions/browser-internal URLs are untouched.
if (!isHttpRequest(req)) {
return;
}
// Always bypass service worker for non-GET and streaming routes
if (isNetworkOnly(req)) {
e.respondWith(
fetch(req).catch(() => fallbackResponse(req, 503))
);
return;
}
// Cache-first for static assets
if (isStaticAsset(req)) {
@@ -65,15 +102,15 @@ self.addEventListener('fetch', (e) => {
if (res && res.status === 200) cache.put(req, res.clone());
}).catch(() => {});
return cached;
}
return fetch(req).then(res => {
if (res && res.status === 200) cache.put(req, res.clone());
return res;
});
})
)
);
return;
}
return fetch(req).then(res => {
if (res && res.status === 200) cache.put(req, res.clone());
return res;
}).catch(() => fallbackResponse(req, 504));
})
)
);
return;
}
// Network-first for HTML pages