From 14fa020b1b11f3a5f618b8b1fbaa80c4ab2af4a3 Mon Sep 17 00:00:00 2001 From: accius Date: Sun, 1 Feb 2026 14:39:20 -0500 Subject: [PATCH] fixing error logs --- index.html | 5664 ---------------------------------- iturhfprop-service/server.js | 161 +- public/index.html | 19 +- 3 files changed, 143 insertions(+), 5701 deletions(-) delete mode 100644 index.html diff --git a/index.html b/index.html deleted file mode 100644 index badc83b..0000000 --- a/index.html +++ /dev/null @@ -1,5664 +0,0 @@ - - - - - - OpenHamClock - Amateur Radio Dashboard - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - diff --git a/iturhfprop-service/server.js b/iturhfprop-service/server.js index 3b0c0c2..26cebc9 100644 --- a/iturhfprop-service/server.js +++ b/iturhfprop-service/server.js @@ -27,6 +27,78 @@ const ITURHFPROP_DATA = process.env.ITURHFPROP_DATA || '/opt/iturhfprop'; // Temp directory for input/output files const TEMP_DIR = '/tmp/iturhfprop'; +// === CACHING AND CIRCUIT BREAKER === +const predictionCache = new Map(); +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes +const CACHE_MAX_SIZE = 100; + +// Circuit breaker state +let circuitBreakerOpen = false; +let consecutiveFailures = 0; +let lastFailureTime = 0; +const FAILURE_THRESHOLD = 5; +const CIRCUIT_RESET_TIME = 60 * 1000; // 1 minute + +// Rate limiting - track last log time to avoid log spam +let lastErrorLogTime = 0; +const ERROR_LOG_INTERVAL = 10000; // Only log errors every 10 seconds + +function getCacheKey(params) { + // Round coordinates to 1 decimal place for better cache hits + const key = `${params.txLat.toFixed(1)},${params.txLon.toFixed(1)}-${params.rxLat.toFixed(1)},${params.rxLon.toFixed(1)}-${params.month}-${params.hour}-${params.ssn}`; + return key; +} + +function getFromCache(key) { + const cached = predictionCache.get(key); + if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) { + return cached.data; + } + predictionCache.delete(key); + return null; +} + +function setCache(key, data) { + // Enforce max cache size + if (predictionCache.size >= CACHE_MAX_SIZE) { + const oldestKey = predictionCache.keys().next().value; + predictionCache.delete(oldestKey); + } + predictionCache.set(key, { data, timestamp: Date.now() }); +} + +function checkCircuitBreaker() { + if (!circuitBreakerOpen) return false; + + // Check if enough time has passed to try again + if (Date.now() - lastFailureTime > CIRCUIT_RESET_TIME) { + circuitBreakerOpen = false; + consecutiveFailures = 0; + console.log('[Circuit Breaker] Reset - allowing requests'); + return false; + } + return true; +} + +function recordFailure() { + consecutiveFailures++; + lastFailureTime = Date.now(); + + if (consecutiveFailures >= FAILURE_THRESHOLD) { + circuitBreakerOpen = true; + // Only log this once + if (Date.now() - lastErrorLogTime > ERROR_LOG_INTERVAL) { + console.log(`[Circuit Breaker] OPEN - ${consecutiveFailures} consecutive failures, pausing for ${CIRCUIT_RESET_TIME/1000}s`); + lastErrorLogTime = Date.now(); + } + } +} + +function recordSuccess() { + consecutiveFailures = 0; + circuitBreakerOpen = false; +} + // Middleware app.use(cors()); app.use(express.json()); @@ -168,7 +240,7 @@ function parseOutputFile(outputPath) { snr: snr, reliability: bcr }); - console.log(`[Parse] Freq ${freq} MHz: SNR=${snr} dB, BCR=${bcr}%`); + // Removed per-frequency logging to reduce spam } } } @@ -180,10 +252,17 @@ function parseOutputFile(outputPath) { results.muf = parseFloat(mufMatch[1]); } - console.log(`[Parse] Found ${results.frequencies.length} frequency results`); + // Only log success occasionally + if (results.frequencies.length > 0 && Date.now() - lastErrorLogTime > ERROR_LOG_INTERVAL) { + console.log(`[Parse] Found ${results.frequencies.length} frequency results, MUF=${results.muf || 'N/A'}`); + } return results; } catch (err) { - console.error('[Parse Error]', err.message); + // Only log errors occasionally + if (Date.now() - lastErrorLogTime > ERROR_LOG_INTERVAL) { + console.error('[Parse Error]', err.message); + lastErrorLogTime = Date.now(); + } return { error: err.message, frequencies: [] }; } } @@ -192,6 +271,18 @@ function parseOutputFile(outputPath) { * Run ITURHFProp prediction */ async function runPrediction(params) { + // Check circuit breaker first + if (checkCircuitBreaker()) { + return { error: 'Circuit breaker open - service temporarily unavailable', frequencies: [] }; + } + + // Check cache + const cacheKey = getCacheKey(params); + const cached = getFromCache(cacheKey); + if (cached) { + return cached; + } + const id = crypto.randomBytes(8).toString('hex'); const inputPath = path.join(TEMP_DIR, `input_${id}.txt`); const outputPath = path.join(TEMP_DIR, `output_${id}.txt`); @@ -209,14 +300,15 @@ async function runPrediction(params) { const inputContent = generateInputFile(params); fs.writeFileSync(inputPath, inputContent); - console.log(`[ITURHFProp] Running prediction ${id}`); - console.log(`[ITURHFProp] TX: ${params.txLat}, ${params.txLon} -> RX: ${params.rxLat}, ${params.rxLon}`); - console.log(`[ITURHFProp] Input file:\n${inputContent}`); + // Only log occasionally to avoid spam + const shouldLog = Date.now() - lastErrorLogTime > ERROR_LOG_INTERVAL; + if (shouldLog) { + console.log(`[ITURHFProp] Running prediction ${id}: TX(${params.txLat.toFixed(1)},${params.txLon.toFixed(1)}) -> RX(${params.rxLat.toFixed(1)},${params.rxLon.toFixed(1)})`); + } // Run ITURHFProp const startTime = Date.now(); const cmd = `${ITURHFPROP_PATH} ${inputPath} ${outputPath}`; - console.log(`[ITURHFProp] Command: ${cmd}`); try { execStdout = execSync(cmd, { @@ -224,40 +316,39 @@ async function runPrediction(params) { encoding: 'utf8', env: { ...process.env, LD_LIBRARY_PATH: '/opt/iturhfprop:' + (process.env.LD_LIBRARY_PATH || '') } }); - console.log(`[ITURHFProp] stdout: ${execStdout}`); } catch (execError) { execStderr = execError.stderr?.toString() || ''; execStdout = execError.stdout?.toString() || ''; - console.error('[ITURHFProp] Execution error!'); - console.error('[ITURHFProp] Exit code:', execError.status); - console.error('[ITURHFProp] stderr:', execStderr); - console.error('[ITURHFProp] stdout:', execStdout); - // Don't throw - try to read output anyway + // Only log errors periodically to avoid spam + if (Date.now() - lastErrorLogTime > ERROR_LOG_INTERVAL) { + console.error(`[ITURHFProp] Error (exit ${execError.status}): ${execStdout.substring(0, 100)}`); + lastErrorLogTime = Date.now(); + } + + recordFailure(); + + // Return empty result but cache it to avoid repeated failures + const errorResult = { error: `Exit code ${execError.status}`, frequencies: [], cached: false }; + setCache(cacheKey, errorResult); + return errorResult; } const elapsed = Date.now() - startTime; - console.log(`[ITURHFProp] Completed in ${elapsed}ms`); - - // Check output file - if (fs.existsSync(outputPath)) { - const rawOutput = fs.readFileSync(outputPath, 'utf8'); - const stats = fs.statSync(outputPath); - console.log(`[ITURHFProp] Output file exists, size: ${stats.size} bytes`); - console.log(`[ITURHFProp] Raw output (first 2000 chars):\n${rawOutput.substring(0, 2000)}`); - } else { - console.log(`[ITURHFProp] Output file NOT FOUND at ${outputPath}`); - // Check if there's a report file in /tmp - const tmpFiles = fs.readdirSync('/tmp').filter(f => f.startsWith('RPT') || f.startsWith('PDD')); - console.log(`[ITURHFProp] Report files in /tmp: ${tmpFiles.join(', ') || 'none'}`); - } // Parse output const results = parseOutputFile(outputPath); + + if (results.frequencies.length === 0) { + recordFailure(); + const errorResult = { error: 'No frequency results', frequencies: [], elapsed }; + setCache(cacheKey, errorResult); + return errorResult; + } + + // Success! + recordSuccess(); results.elapsed = elapsed; - results.execStdout = execStdout; - results.execStderr = execStderr; - results.inputContent = inputContent; results.params = { txLat: params.txLat, txLon: params.txLon, @@ -268,6 +359,9 @@ async function runPrediction(params) { ssn: params.ssn }; + // Cache successful result + setCache(cacheKey, results); + return results; } finally { @@ -301,7 +395,7 @@ app.get('/api/health', (req, res) => { const ionosDataExists = fs.existsSync(ITURHFPROP_DATA + '/Data/ionos12.bin'); res.json({ - status: binaryExists && dataSubExists && libp533Exists && ionosDataExists ? 'healthy' : 'degraded', + status: binaryExists && dataSubExists && libp533Exists && ionosDataExists && !circuitBreakerOpen ? 'healthy' : 'degraded', service: 'iturhfprop', version: '1.0.0', engine: 'ITURHFProp (ITU-R P.533-14)', @@ -310,6 +404,11 @@ app.get('/api/health', (req, res) => { libp372: libp372Exists ? 'found' : 'missing', dataDir: dataSubExists ? 'found' : 'missing', ionosData: ionosDataExists ? 'found' : 'missing', + circuitBreaker: { + open: circuitBreakerOpen, + consecutiveFailures, + cacheSize: predictionCache.size + }, paths: { binary: ITURHFPROP_PATH, data: ITURHFPROP_DATA diff --git a/public/index.html b/public/index.html index 6fa2502..badc83b 100644 --- a/public/index.html +++ b/public/index.html @@ -1891,12 +1891,19 @@ {/* 24 hour cells */} {Array.from({ length: 24 }, (_, hour) => { - // For current hour, use currentBands (same hybrid data as bars view) - // For other hours, use hourlyPredictions - let rel; - if (hour === currentHour) { + // For current hour, try to use currentBands (same hybrid data as bars view) + // Fall back to hourlyPredictions if currentBands doesn't have this band + let rel = 0; + if (hour === currentHour && currentBands?.length > 0) { const currentBandData = currentBands.find(b => b.band === band); - rel = currentBandData?.reliability || 0; + if (currentBandData) { + rel = currentBandData.reliability || 0; + } else { + // Band not in currentBands, use hourlyPredictions + const bandData = hourlyPredictions?.[band]; + const hourData = bandData?.find(h => h.hour === hour); + rel = hourData?.reliability || 0; + } } else { const bandData = hourlyPredictions?.[band]; const hourData = bandData?.find(h => h.hour === hour); @@ -1976,7 +1983,7 @@ K = 4 ? '#ff4444' : '#00ff88' }}>{solarData.kIndex} - {currentBands.slice(0, 11).map((band, idx) => ( + {(currentBands || []).slice(0, 11).map((band, idx) => (