Fix app becoming unresponsive when two browser windows are open: the
root cause was HTTP/1.1 connection pool exhaustion (6-connection limit
per origin). VoiceAlerts was opening 3 SSE streams per window by
default, so two windows produced 8 connections and permanently starved
all regular HTTP requests.
- voice-alerts.js: default all streams to false (opt-in) to stay within
the browser connection limit; existing user preferences in localStorage
are preserved
- routes/alerts.py: replace direct AlertManager.stream_events() with
sse_stream_fanout so both windows receive every alert instead of
competing for the same queue
- routes/bluetooth_v2.py: same fanout fix via subscribe_fanout_queue,
preserving named SSE events (device_update, scan_started, etc.)
Also includes accumulated UI/theming changes: accent-cyan CSS variable
sweep across mode CSS/JS files, standalone dashboard pages, template
updates, satellite TLE data refresh, and tile provider default rename.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each dashboard is a separate HTML page that doesn't inherit the main SPA's
localStorage restore. Add a synchronous tier-restore script before CSS loads
so html[data-ui-tier] selectors fire on first paint.
Also add enhanced/lean tier override blocks to each dashboard CSS to remap
the dashboard-local variables (--bg-dark, --bg-panel, --radar-cyan, etc.)
that variables.css doesn't cover, and add lean-mode scanline/bg hide rules
since components.css is not loaded on these pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove 'METEOR-M2' (NORAD 40069) from WEATHER_SAT_KEYS — it has no
entry in WEATHER_SATELLITES and no dropdown option, so the Capture
button was silently scheduling against the wrong satellite
- Dispatch a 'change' event after setting satSelect.value in preSelect()
and startPass() so any UI listeners (frequency display, mode info)
update correctly when the satellite is set programmatically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fills the empty space below the sky view circle with a compact
three-column AOS / TCA / LOS readout (time + azimuth/elevation)
and a duration + max elevation footer line.
Populated by drawPolarPlot() when a pass is selected; shows a
placeholder prompt otherwise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setting both width:100% and height:100% made CSS ignore aspect-ratio,
stretching the drawing buffer non-uniformly into the tall container.
Fixed by keeping only width:100% + max-height:100% so aspect-ratio:1/1
clamps the height and the element stays square.
Draw functions now use canvas.offsetWidth for the square buffer size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove align-self: start from .polar-container so the grid row's
minmax(260px, 340px) height is actually respected
- Switch #polarPlot to aspect-ratio: 1/1 so the canvas is always square
- Fix both draw functions to size canvas from getBoundingClientRect on
the canvas itself (not parent) using min(width, height) for a square plot
- Remove min-height from .dashboard to prevent empty space below content
on narrow/mobile screens where stacked panels are shorter than 720px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move _passAbortController = null to after response.json() so the retry
scheduler cannot see a false idle state mid-parse, increment
_passRequestId, and discard the in-flight response — this was causing
non-ISS satellites to show no passes intermittently
- Add _computeSlantRange() helper using 3D ECEF geometry
- Update applyTelemetryPosition to compute slant range from SSE lat/lon/
altitude, giving distance updates at 1Hz instead of 5s HTTP poll rate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
updateCountdown fell back to passes[0] even when it was in the past,
showing 00:00:00:00 with a stale satellite name indefinitely. Now
displays a clear 'NO UPCOMING PASSES' state with '--' for all fields
when no future pass exists in the current prediction window.
Consolidated to a single active-request guard with cleanup in finally.
The previous pattern had redundant null-checks across try and catch, and
an always-false check on a controller that was already null. Cancel-on-
new-request is now explicit before creating the new controller.
METEOR-M2 (NORAD 40069) is a weather satellite with LRPT downlink but
was missing from WEATHER_SAT_KEYS, so no capture button appeared in
the pass list. Adds it alongside M2-3 and M2-4.
Previously currentPos only had lat/lon, so the updateTelemetry fallback
(used before first live position arrives) always showed '---' for
altitude/elevation/azimuth/distance. currentPos now includes all fields
computed from the request observer location. updateTelemetry simplified
to delegate to applyTelemetryPosition.
Adds a 'source' param to handleLivePositions. The SSE path ('sse') only
applies lat/lon/altitude/groundTrack since the server-side tracker has
no per-client location. The HTTP poll path ('poll') owns all observer-
relative data and the visible-count badge.
- OBSERVATION PROFILES section with list of configured satellites
- + ADD button opens inline form pre-filled from currently selected satellite
and SatNOGS transmitter data (frequency, decoder type auto-detected)
- EDIT / ✕ buttons per profile row
- Form fields: frequency, decoder (FM/AFSK/GMSK/BPSK/IQ-only), min elevation,
gain, record IQ checkbox
- UPCOMING PASSES section below profiles with friendlier empty-state message
- gsOnSatelliteChange hook updates form when satellite dropdown changes
- CSS for .gs-form-row, .gs-profile-item, .gs-form-label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>