diff --git a/rig-listener/rig-listener.js b/rig-listener/rig-listener.js index bffe32d3..d2eac984 100644 --- a/rig-listener/rig-listener.js +++ b/rig-listener/rig-listener.js @@ -700,10 +700,15 @@ async function initTci(cfg) { tciSocket.addEventListener('error', (evt) => { // 'error' fires before 'close' — just log it, reconnect happens on 'close' const err = evt.error || evt; - if (err.code === 'ECONNREFUSED') { - console.error(`[TCI] Connection refused — is Thetis/ExpertSDR running with TCI enabled?`); + const msg = (err && err.message) || ''; + if (err && err.code === 'ECONNREFUSED') { + console.error(`[TCI] Connection refused — is the SDR app running with TCI enabled?`); + } else if (msg.toLowerCase().includes('sec-websocket-accept') || msg.toLowerCase().includes('incorrect hash')) { + console.error('[TCI] WebSocket handshake rejected (invalid Sec-WebSocket-Accept).'); + console.error("[TCI] This usually means the SDR app's TCI server has a non-standard WebSocket implementation."); + console.error('[TCI] Try updating your SDR software, or check that TCI is enabled (not just CAT).'); } else { - console.error(`[TCI] Error: ${err.message || 'connection error'}`); + console.error(`[TCI] Error: ${msg || 'connection error'}`); } }); diff --git a/server.js b/server.js index 977dac33..63060e7b 100644 --- a/server.js +++ b/server.js @@ -182,6 +182,7 @@ const staticOptions = { setHeaders: (res, filePath) => { if (filePath.endsWith('index.html') || filePath.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('CDN-Cache-Control', 'no-store'); // Cloudflare-specific: never cache at edge res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } @@ -267,6 +268,7 @@ app.get('*', (req, res) => { const publicIndex = path.join(ROOT_DIR, 'public', 'index.html'); const indexPath = fs.existsSync(distIndex) ? distIndex : publicIndex; res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('CDN-Cache-Control', 'no-store'); // Cloudflare: never cache at edge res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.sendFile(indexPath); diff --git a/server/routes/presence.js b/server/routes/presence.js index bbafe2f5..75638554 100644 --- a/server/routes/presence.js +++ b/server/routes/presence.js @@ -53,6 +53,19 @@ module.exports = function (app, ctx) { res.json({ ok: true, active: activeUsers.size }); }); + // POST /api/presence/leave — user closing tab + app.post('/api/presence/leave', (req, res) => { + const { callsign } = req.body || {}; + if (!callsign) return res.status(400).json({ error: 'callsign required' }); + const call = String(callsign) + .toUpperCase() + .replace(/[^A-Z0-9/\-]/g, ''); + if (activeUsers.delete(call)) { + logDebug(`[Presence] ${call} left`); + } + res.json({ ok: true }); + }); + // GET /api/presence — get all active users app.get('/api/presence', (req, res) => { const cutoff = Date.now() - PRESENCE_TTL; diff --git a/server/routes/satellites.js b/server/routes/satellites.js index 8e7236cc..4cb1b905 100644 --- a/server/routes/satellites.js +++ b/server/routes/satellites.js @@ -411,6 +411,15 @@ module.exports = function (app, ctx) { }, // ── Digipeaters ──────────────────────────────────────────────── + 'IO-86': { + norad: 40931, + name: 'IO-86 (LAPAN-A2/ORARI)', + color: '#33ccaa', + priority: 2, + mode: 'APRS Digipeater', + downlink: '145.825 MHz', + uplink: '145.825 MHz', + }, 'IO-117': { norad: 53106, name: 'IO-117 (GreenCube)', diff --git a/server/routes/wsjtx.js b/server/routes/wsjtx.js index 42bed652..57ff00e4 100644 --- a/server/routes/wsjtx.js +++ b/server/routes/wsjtx.js @@ -1323,15 +1323,13 @@ module.exports = function (app, ctx) { '', ':: Run relay', '%NODE_EXE% "%TEMP%\\ohc-relay.js" --url "' + - safeServerURL + - '" --key "' + - safeRelayKey + - '" --session "' + - safeSessionId + - '"' + - multicastAddress - ? ' --multicast "' + safeMulticastAddress + "'" - : '', + safeServerURL + + '" --key "' + + safeRelayKey + + '" --session "' + + safeSessionId + + '"' + + (multicastAddress ? ' --multicast "' + safeMulticastAddress + '"' : ''), '', 'echo.', 'echo Relay stopped.', diff --git a/src/hooks/app/usePresence.js b/src/hooks/app/usePresence.js index aee0d1fe..e3e1cade 100644 --- a/src/hooks/app/usePresence.js +++ b/src/hooks/app/usePresence.js @@ -58,6 +58,18 @@ export default function usePresence({ callsign, locator }) { sendHeartbeat(); const interval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); - return () => clearInterval(interval); + + // Remove presence immediately when the tab closes + const handleUnload = () => { + // navigator.sendBeacon is fire-and-forget — works even during unload + const payload = JSON.stringify({ callsign }); + navigator.sendBeacon('/api/presence/leave', payload); + }; + window.addEventListener('beforeunload', handleUnload); + + return () => { + clearInterval(interval); + window.removeEventListener('beforeunload', handleUnload); + }; }, [callsign, locator]); }