pull/52/head
accius 2 days ago
parent f6e2e7889a
commit abdcf095df

@ -3667,6 +3667,7 @@ const wsjtxState = {
decodes: [], // decoded messages (ring buffer) decodes: [], // decoded messages (ring buffer)
qsos: [], // logged QSOs qsos: [], // logged QSOs
wspr: [], // WSPR decodes wspr: [], // WSPR decodes
relay: null, // { lastSeen, version, port } — set by relay heartbeat
}; };
/** /**
@ -4113,10 +4114,14 @@ app.get('/api/wsjtx', (req, res) => {
} }
} }
// Relay is "connected" if seen in last 60 seconds
const relayConnected = wsjtxState.relay && (Date.now() - wsjtxState.relay.lastSeen < 60000);
res.json({ res.json({
enabled: WSJTX_ENABLED, enabled: WSJTX_ENABLED,
port: WSJTX_UDP_PORT, port: WSJTX_UDP_PORT,
relayEnabled: !!WSJTX_RELAY_KEY, relayEnabled: !!WSJTX_RELAY_KEY,
relayConnected: !!relayConnected,
clients, clients,
decodes: wsjtxState.decodes.slice(-100), // last 100 decodes: wsjtxState.decodes.slice(-100), // last 100
qsos: wsjtxState.qsos.slice(-20), // last 20 qsos: wsjtxState.qsos.slice(-20), // last 20
@ -4155,11 +4160,25 @@ app.post('/api/wsjtx/relay', (req, res) => {
return res.status(401).json({ error: 'Invalid relay key' }); return res.status(401).json({ error: 'Invalid relay key' });
} }
// Relay heartbeat — just registers the relay as alive
if (req.body && req.body.relay === true) {
wsjtxState.relay = {
lastSeen: Date.now(),
version: req.body.version || '1.0.0',
port: req.body.port || 2237,
};
return res.json({ ok: true, timestamp: Date.now() });
}
// Regular message batch
const { messages } = req.body || {}; const { messages } = req.body || {};
if (!Array.isArray(messages) || messages.length === 0) { if (!Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ error: 'No messages provided' }); return res.status(400).json({ error: 'No messages provided' });
} }
// Update relay last seen on every batch too
wsjtxState.relay = { ...(wsjtxState.relay || {}), lastSeen: Date.now() };
// Rate limit: max 100 messages per request // Rate limit: max 100 messages per request
const batch = messages.slice(0, 100); const batch = messages.slice(0, 100);
let processed = 0; let processed = 0;
@ -4263,10 +4282,16 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => {
return res.send(script); return res.send(script);
} else if (platform === 'windows') { } else if (platform === 'windows') {
// Simple .bat that downloads relay.js then runs with node // .bat that auto-downloads portable Node.js if needed, then runs relay
// No PowerShell, no execution policy issues // No install, no admin, no PowerShell execution policy issues
const NODE_VERSION = 'v22.13.1'; // LTS
const NODE_ZIP = 'node-' + NODE_VERSION + '-win-x64.zip';
const NODE_DIR = 'node-' + NODE_VERSION + '-win-x64';
const NODE_URL = 'https://nodejs.org/dist/' + NODE_VERSION + '/' + NODE_ZIP;
const batLines = [ const batLines = [
'@echo off', '@echo off',
'setlocal',
'title OpenHamClock WSJT-X Relay', 'title OpenHamClock WSJT-X Relay',
'echo.', 'echo.',
'echo =========================================', 'echo =========================================',
@ -4274,25 +4299,61 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => {
'echo =========================================', 'echo =========================================',
'echo.', 'echo.',
'', '',
':: Check for Node.js', ':: Check for Node.js (system-installed or portable)',
'set "NODE_EXE=node"',
'set "PORTABLE_DIR=%TEMP%\\ohc-node"',
'',
'where node >nul 2>nul', 'where node >nul 2>nul',
'if not errorlevel 1 (',
' for /f "tokens=*" %%i in (\'node -v\') do echo Found Node.js %%i',
' goto :have_node',
')',
'',
':: Check for previously downloaded portable Node.js',
'if exist "%PORTABLE_DIR%\\' + NODE_DIR + '\\node.exe" (',
' set "NODE_EXE=%PORTABLE_DIR%\\' + NODE_DIR + '\\node.exe"',
' echo Found portable Node.js',
' goto :have_node',
')',
'',
':: Download portable Node.js',
'echo Node.js not found. Downloading portable version...',
'echo (This is a one-time ~30MB download^)',
'echo.',
'',
'if not exist "%PORTABLE_DIR%" mkdir "%PORTABLE_DIR%"',
'',
'powershell -Command "try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri \'' + NODE_URL + '\' -OutFile \'%PORTABLE_DIR%\\' + NODE_ZIP + '\' } catch { Write-Host $_.Exception.Message; exit 1 }"',
'if errorlevel 1 (', 'if errorlevel 1 (',
' echo Node.js is not installed!',
' echo.', ' echo.',
' echo Download it from: https://nodejs.org', ' echo Failed to download Node.js!',
' echo Install the LTS version, then run this script again.', ' echo Check your internet connection and try again.',
' echo.', ' echo.',
' pause', ' pause',
' exit /b 1', ' exit /b 1',
')', ')',
'', '',
'for /f "tokens=*" %%i in (\'node -v\') do echo Found Node.js %%i', 'echo Extracting...',
'powershell -Command "Expand-Archive -Path \'%PORTABLE_DIR%\\' + NODE_ZIP + '\' -DestinationPath \'%PORTABLE_DIR%\' -Force"',
'if errorlevel 1 (',
' echo Failed to extract Node.js!',
' echo.',
' pause',
' exit /b 1',
')',
'',
'del "%PORTABLE_DIR%\\' + NODE_ZIP + '" >nul 2>nul',
'set "NODE_EXE=%PORTABLE_DIR%\\' + NODE_DIR + '\\node.exe"',
'echo Portable Node.js ready.',
'echo.',
'',
':have_node',
'echo Server: ' + serverURL, 'echo Server: ' + serverURL,
'echo.', 'echo.',
'', '',
':: Download relay agent', ':: Download relay agent',
'echo Downloading relay agent...', 'echo Downloading relay agent...',
'powershell -Command "Invoke-WebRequest -Uri \'' + serverURL + '/api/wsjtx/relay/agent.js\' -OutFile \'%TEMP%\\ohc-relay.js\'"', 'powershell -Command "try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri \'' + serverURL + '/api/wsjtx/relay/agent.js\' -OutFile \'%TEMP%\\ohc-relay.js\' } catch { Write-Host $_.Exception.Message; exit 1 }"',
'if errorlevel 1 (', 'if errorlevel 1 (',
' echo Failed to download relay agent!', ' echo Failed to download relay agent!',
' echo Check your internet connection and try again.', ' echo Check your internet connection and try again.',
@ -4310,7 +4371,7 @@ app.get('/api/wsjtx/relay/download/:platform', (req, res) => {
'echo.', 'echo.',
'', '',
':: Run relay', ':: Run relay',
'node "%TEMP%\\ohc-relay.js" --url "' + serverURL + '" --key "' + WSJTX_RELAY_KEY + '"', '%NODE_EXE% "%TEMP%\\ohc-relay.js" --url "' + serverURL + '" --key "' + WSJTX_RELAY_KEY + '"',
'', '',
'echo.', 'echo.',
'echo Relay stopped.', 'echo Relay stopped.',

@ -717,6 +717,7 @@ const App = () => {
wsjtxEnabled={wsjtx.enabled} wsjtxEnabled={wsjtx.enabled}
wsjtxPort={wsjtx.port} wsjtxPort={wsjtx.port}
wsjtxRelayEnabled={wsjtx.relayEnabled} wsjtxRelayEnabled={wsjtx.relayEnabled}
wsjtxRelayConnected={wsjtx.relayConnected}
showWSJTXOnMap={mapLayers.showWSJTX} showWSJTXOnMap={mapLayers.showWSJTX}
onToggleWSJTXMap={toggleWSJTX} onToggleWSJTXMap={toggleWSJTX}
/> />

@ -27,6 +27,7 @@ const PSKReporterPanel = ({
wsjtxEnabled, wsjtxEnabled,
wsjtxPort, wsjtxPort,
wsjtxRelayEnabled, wsjtxRelayEnabled,
wsjtxRelayConnected,
showWSJTXOnMap, showWSJTXOnMap,
onToggleWSJTXMap onToggleWSJTXMap
}) => { }) => {
@ -347,6 +348,19 @@ const PSKReporterPanel = ({
}}> }}>
<div style={{ fontSize: '12px' }}>Waiting for WSJT-X...</div> <div style={{ fontSize: '12px' }}>Waiting for WSJT-X...</div>
{wsjtxRelayEnabled ? ( {wsjtxRelayEnabled ? (
wsjtxRelayConnected ? (
<div style={{ fontSize: '10px', opacity: 0.8, lineHeight: 1.6 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', marginBottom: '4px' }}>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: '#4ade80', boxShadow: '0 0 4px #4ade80' }} />
<span style={{ color: '#4ade80', fontWeight: 600 }}>Relay connected</span>
</div>
<div style={{ fontSize: '9px', opacity: 0.5 }}>
Waiting for WSJT-X decodes...
<br />
In WSJT-X: Settings Reporting UDP 127.0.0.1:2237
</div>
</div>
) : (
<div style={{ fontSize: '10px', opacity: 0.8, lineHeight: 1.6 }}> <div style={{ fontSize: '10px', opacity: 0.8, lineHeight: 1.6 }}>
<div style={{ marginBottom: '8px' }}> <div style={{ marginBottom: '8px' }}>
Download the relay agent for your PC: Download the relay agent for your PC:
@ -375,6 +389,7 @@ const PSKReporterPanel = ({
Requires Node.js · Run the script, then start WSJT-X Requires Node.js · Run the script, then start WSJT-X
</div> </div>
</div> </div>
)
) : ( ) : (
<div style={{ fontSize: '10px', opacity: 0.6, lineHeight: 1.5 }}> <div style={{ fontSize: '10px', opacity: 0.6, lineHeight: 1.5 }}>
In WSJT-X: Settings Reporting UDP Server In WSJT-X: Settings Reporting UDP Server

@ -430,6 +430,43 @@ socket.on('listening', () => {
// Start batch relay loop // Start batch relay loop
scheduleBatch(); scheduleBatch();
// Send relay heartbeat immediately, then every 30s
// This tells the server the relay is alive even before WSJT-X sends any packets
function sendHeartbeat() {
const body = JSON.stringify({ relay: true, version: '1.0.0', port: config.port });
const parsed = new URL(relayEndpoint);
const transport = parsed.protocol === 'https:' ? https : http;
const reqOpts = {
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
path: parsed.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
'Authorization': `Bearer ${config.key}`,
'X-Relay-Heartbeat': 'true',
},
timeout: 10000,
};
const req = transport.request(reqOpts, (res) => {
res.resume();
if (res.statusCode === 200 && consecutiveErrors > 0) {
console.log('\n ✅ Server connection restored');
consecutiveErrors = 0;
}
});
req.on('error', () => {});
req.on('timeout', () => req.destroy());
req.write(body);
req.end();
}
sendHeartbeat();
setInterval(sendHeartbeat, 30000);
// Periodic health check — verify server is reachable // Periodic health check — verify server is reachable
setInterval(() => { setInterval(() => {
const parsed = new URL(`${serverUrl}/api/wsjtx`); const parsed = new URL(`${serverUrl}/api/wsjtx`);

Loading…
Cancel
Save

Powered by TurnKey Linux.