mirror of
https://github.com/smittix/intercept.git
synced 2026-06-30 22:08:59 -07:00
chore: commit all pending changes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <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: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user