[feature/satellite] OMM data download, server background driven update, optional Space-Track#1007
[feature/satellite] OMM data download, server background driven update, optional Space-Track#1007MichaelWheeley wants to merge 25 commits into
Conversation
…ction
The socket idle timeout (60s) was the shortest of all failure timers, so
any 60s gap between spots — normal on quiet bands overnight — tore down a
healthy connection. Keepalive (120s) was too slow to prevent it and the
180s activity watchdog (the graceful node-failover) never got to run.
- socket timeout 60s -> 300s, as a last-resort TCP backstop; the 180s
activity watchdog now owns failover as designed
- keepalive 120s -> 60s so the connection stays warm
- destroy the socket in the 'timeout' handler — Node does not auto-close
on timeout, leaving a zombie socket that kept feeding data and
spuriously reset the failover counter after [RECONNECT]
- mark authenticated on first spot; the DXSpider prompt has no trailing
newline so prompt-based detection never matched, and sh/dx output lines
("...de Helmut<DF4IY>") false-matched the prompt regex
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/api/dxcluster/paths shared one AbortController across the proxy fetch and the HamQTH fallback fetch. When the dxspider-proxy hangs to the 10s limit, controller.abort() fires — and the fallback fetch then receives an already-aborted signal, rejecting instantly with AbortError before HamQTH is ever contacted. newSpots stays empty and the endpoint returns the stale path cache (empty on a fresh server start). Give each upstream its own AbortController + timeout, cleared in a finally block. The HamQTH fallback now actually runs when the proxy is down. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
accius
left a comment
There was a problem hiding this comment.
Thanks for taking this on, Michael — moving the satellite refresh to a background state machine and switching to OMM is a solid direction, and the CelesTrak 403 lockout handling is a clear improvement. CI is green and the diff is clean. A few things I'd like to see addressed before this merges, mostly around the network fetch correctness and a couple of latent bugs.
Blocking-ish / correctness
-
axios abort signals are passed in the wrong argument slot. In
fetchOmmFromSpaceTrack,client.post(url, body, controller.signal)passes the signal as the config object, andclient.get(url, {...}, controller.signal)passes it as a fourth arg that axios ignores. axios expects{ signal }inside the config object. As written the 20sAbortControllertimeouts never actually abort the Space-Track login/whoami/fetch calls. That matters a lot here: the state machine serializesrun()through a mutex, so a single hung Space-Track request with no working timeout will stall the entire background updater indefinitely (CelesTrak included). Please pass{ ..., signal: controller.signal }in the config object and verify abort actually fires. -
Two implicit global leaks. In
isLoggedIn,jsonBytes = new Uint8Array(whoami)and inappendDataToOmmCache,match = knownNoradIds.has(noradId)are both undeclared (noconst/let). There's no'use strict'in this file, so these become properties on the global object and are shared/overwritten across calls. Add declarations. -
celestrakSatsToDownloaddoess.data_source.startsWith('celestrak')with no guard, butsatellites-tracked.js's own header comment explicitly saysdata_sourcemay be absent. Any satellite without that field will throw a TypeError inside theSTARThandler. Guard withs.data_source?.startsWith('celestrak').
Error handling
-
The
try { ... } finally { return 'NEXT_STATE'; }pattern in the handlers swallows thrown exceptions — areturninsidefinallydiscards any in-flight throw. If a handler hits an unexpected error you'll silently advance state with no log. Prefer an explicitcatchthat logs, then return the next state. -
429 is no longer treated as a rate-limit signal. The CelesTrak fetch handlers only trip the backoff on
301 || 403; the old code keyed on429. CelesTrak does return 429 under load — please include it in the block condition so we keep the "no more getting kicked off their server" property the PR description claims. -
appendDataToOmmCachedoesommJson.forEach(...), which assumes an array.parseCsvTextreturnsnullon a parse failure, and thetypeof ommJson === 'object'guard also lets non-array objects through. A non-array would throw. Tighten the guard toArray.isArray(ommJson).
Caching
-
ommUnusedCachehas no size cap or TTL. Every unknown NORAD ID seen in theamateur/weathergroup CSVs gets recorded, and those groups are large. It's bounded by the group sizes so it won't grow without limit, but per the repo checklist ("caches have TTLs and size caps — we serve 2,000+ concurrent users") it's worth either capping it or noting why it's safe./api/satellites/debugexposing the full unused list is fine for debugging, just be aware it's now a non-trivial payload. -
Minor: the Space-Track login spoofs
User-Agent: Mozilla/5.0, and the login POST response status is never checked — auth success is inferred only from the laterwhoamicall. That works, but checking the login response too would fail faster and more clearly. Credentials themselves look handled correctly: kept server-side under_username/_password, never logged, sent in the form body — good.
Smaller notes
noradsToDownloadis a module-level variable reused as both an array (group/Space-Track paths) and a scalar (individual path). The mutex serializes states so it's safe today, but it's fragile; the thrown guard inCELESTRAK_INDIVIDUAL_FETCHthrows a bare string instead of anError.convert-csv-to-jsonis a fairly lightly-maintained dependency for what is essentially CSV-of-known-shape parsing — not blocking, just flagging the supply-chain footprint.- Nice catch on the
wheel{ passive: true }warning fix.
Happy to re-review once the signal-passing and the two implicit globals are sorted — those are the ones I'd genuinely worry about in production.
— K0CJH
* update satellites tracked // added #41866 GOES-16 celestrak_weather // added #55506 ELEKTRO-L4 celestrak_active // added #67756 ELEKTRO-L5 celestrak_active // removed #53106(payload) = #53109(rocket body) = IO-117(Greencube), satellite decommissioned for all entries generated data_source field showing celestrak group data source * minor name change correction
* fix require path * - fix state machine unit test - if invalid state is provided then state should gracefully reset - duplicate of same removed
Adds real-time "as you type" preview support to the N3FJP Logged QSOs layer, based on a working prototype by Ben, KC1UEK. - Server: /api/n3fjp/qso accepts status log|preview|clear; trusts bridge-supplied coords first, falls back to grid then HamQTH; stale previews self-expire after 5 min. - Layer: previews render in their own colour with a dashed arc, bypass the display-window filter, and show a "(preview)" popup. - DX target: layer emits a dedicated ohc-n3fjp-dx-target event; App.jsx moves the DX crosshair via handleDXChange (no WSJT-X channel reuse). - Settings: new Preview color picker in Integrations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…I toggle The satellite status box is appended to the map container rather than .leaflet-control-container, so the Hide UI style block never matched it. Add .sat-data-window to that selector list — same mechanism every other floating map panel already uses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… panel A solar flare outside the fixed 6h window was invisible in the panel and missing from the "peak" rating. Add a timeframe dropdown to the X-Ray Flux view; the displayed peak is recomputed for the selected window. Server: /api/noaa/xray now pulls the 3-day SWPC feed, keeps only the 0.1-0.8nm band the panel plots, and trims to the last 50h — so the per-client payload stays small instead of shipping the multi-MB raw feed every 5 min. Choice persists in localStorage; default stays 6h. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking a callsign always opened QRZ.com. Add a "Callsign Lookup" dropdown in Station Settings (QRZ.com / HamQTH / QRZCQ), matching the DX Cluster Source selector. New src/utils/callbook.js builds the lookup URL; both callsign-click paths in CallsignLink.jsx route through it. Choice persists in localStorage; default stays QRZ.com. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eley/openhamclock into feature/sat_tle_to_omm_merge2 # Conflicts: # server/utils/statemachine.js
- logError not a valid function, it's use was masked by an outer exception handler, replaced with logWarn - isCelestrakEnabled was not getting used, CelesTrak was always active backup for Space-Track even when disabled
- added `const` to `jsonBytes` - added `const` to `match`
…es and login fail
|
accius
left a comment
There was a problem hiding this comment.
Re-reviewed against 646f015a. Thanks for the thorough pass, Michael — every item from the previous review is addressed, and the fixes hold up on inspection.
Verified resolved:
- ✅ Axios signal now correctly passed inside the config object —
client.post(url, body, { signal: controller.signal })andclient.get(url, { ..., signal: controller.signal }). Timeouts will actually fire. - ✅
'use strict'added at file top —jsonBytesis now properly declared (const jsonBytes = new Uint8Array(whoami)), and thematchline inappendDataToOmmCacheno longer leaks since the file would throw under strict mode if it tried. - ✅
s.data_source?.startsWith('celestrak')— optional-chained. - ✅
try { ... } catch (ex) { logWarn(...) } finally { return ... }— exceptions are caught and logged before the next state is returned. No more silent swallow. - ✅
429added to the CelesTrak block condition in all three group/individual handlers. - ✅
appendDataToOmmCachenow guards with!Array.isArray(ommJson). - ✅ Space-Track login response is now checked — non-200 returns the upstream status, and the response body is scanned for
failed/deniedto catch the case where Space-Track returns 200 with an error message. Good defensive layering with the subsequentwhoamicheck. - ✅ Comment added explaining
ommUnusedCachesize is bounded by the group-file size minus tracked sats — fair enough not to add a hard cap.
One concern I'd like fixed before merge — cold-start latency:
The state machine runs purely on setInterval(..., 15 * 1000) with no initial kick (server/routes/satellites.js:571). On a fresh server boot:
- First
sm.run()fires 15s after startup. - The pipeline is multi-step (
START → AMATEUR_INIT → AMATEUR_FETCH → WEATHER_INIT → WEATHER_FETCH → INDIVIDUAL_INIT → INDIVIDUAL_FETCH → START) — with one transition per 15s tick,ommCachestays empty for at least 60–90 seconds, easily several minutes if any handler short-circuits back to START. - Meanwhile the frontend fetches
/api/satellites/dataonce on mount, then every 6 hours (src/hooks/useSatellites.js:38). With no retry on empty payload, a client that hits the server in the first few minutes after a restart sees no satellites — and won't retry for 6 hours.
That's a regression vs the prior endpoint, which blocked on cold-start to guarantee the first request returned data. Two small mitigations would close the gap:
// server/routes/satellites.js
const sm = new StateMachine(smStates, handlers);
sm.run(); // kick-start before the interval
setInterval(async () => { sm.run(); }, 15 * 1000);and / or, in useSatellites.js, retry with a shorter interval while the payload is empty:
const interval = setInterval(
fetchSatelliteData,
Object.keys(satelliteData).length === 0 ? 60 * 1000 : 6 * 60 * 60 * 1000,
);Either alone helps; both together is robust against deploys.
Minor polish (non-blocking):
logWarn('Error reading CSV:', err)atparseCsvTextpasses two arguments — most of the loggers in this codebase take a single formatted string, soerris likely dropped. Inline it:logWarn(\[Satellites] CSV parse failed: ${err?.message ?? err}`)`.- The handler catches log a static
"[Satellites] caught unknown exception occurred in <HANDLER_NAME> handler, advancing to next state"— would be more useful with: ${ex?.message ?? ex}interpolated, otherwise operators see "unknown exception" with no detail. - Space-Track login still uses
User-Agent: 'Mozilla/5.0'while every other request (including the Space-Track data fetch) usesOpenHamClock/${APP_VERSION}. If the browser-UA is required by Space-Track's auth endpoint, worth a comment saying so; otherwise consistency would be nicer.
CI is green, the branch is 0 behind / 12 ahead of Staging, and mergeable: CLEAN. With the cold-start kickstart added, this is ready to merge.
— K0CJH
|
I mostly agree with the Automated Triage Review I ran. See notes above. |
- `useSatellites.js` fetch data every minute if empty data set
|
What does this PR do?
.env- requires login and cookie support.env.examplemodified with details/api/satellites/debug/api/satellites/data(was/api/satellites/tle)useSatelliteLayerwheel={ passive: true }Type of change
How to test
.env.exampleon how to enable / disable CelesTrak, and Space-Track with a username/passwordLOG_LEVEL=debugfor additional messaging/api/satellites/dataand/api/satellites/debugfor statusChecklist
server.js: caches have TTLs and size caps (we serve 2,000+ concurrent users)var(--accent-cyan), etc.).bak,.old,console.logdebug lines, or test scripts included