User correction: AO-123 is ASRTU-1 (NORAD 61781, launched 2024-11-04,
in orbit), not Fox-1D (43137, re-entered 2024-02). Update the hardcoded
FM set and its tests to include ASRTU-1.
Root cause of the recurring empty AMSAT Live filters: OkHttp defaults to a
10s read timeout and amsat.org's live pages take 7-10s+ from many
networks — every sync timed out, fmNames/linearNames stayed empty, and
(with no previous list, e.g. right after clearing data) the filters were
written as EMPTY. The stale pre-whitelist FM list survived only because
older syncs had occasionally succeeded and the failure path preserved it.
Fix:
1. OkHttp timeouts raised (connect 20s / read 30s / write 30s / call 60s)
in MainContainer.provideRemoteSource so slow AMSAT pages actually load.
2. updateAmSatLiveLists gains a last-resort fallback: when the AMSAT page
is down AND there is no previous list to clean, derive FM/Linear from
the local transceivers (FM / SSB,CW,USB,LSB modes) intersected with
the amateur whitelist, so a network timeout never wipes the filters.
ISS module aliases stay excluded by the whitelist intersection.
Regression test: page fails + empty previous list -> fallback yields
{25544} (26700 excluded by whitelist).
resolvePerName crashed with NoSuchElementException (all.first()) when an
AMSAT live-page name had no matching local entry (TEVEL2-1..9, RS95S, any
brand-new sat not yet in the local TLE). The exception was swallowed by
updateAmSatLiveLists' runCatching, so the FM/Linear lists were NEVER
rewritten — they stayed at the stale pre-whitelist snapshot containing
all five ISS modules, which is why ISS kept showing as DESTINY despite the
15.4/15.5/15.6 disambiguation fixes.
Fix: return an empty set for names with no local match (correct skip
semantics) so the rest of the page still resolves and the ISS module
collapse runs. Regression test covers the real AMSAT page mix (ISS +
unknown names).
Root cause of "All filter is empty": 'All' is a real TLE source type
(CelesTrak active group) in satelliteDataUrls, but setSatelliteTypeIds
had an upstream-legacy 'if (type == "All") return' that silently dropped
its ids on every sync. Selecting 'All' in the type dialog then resolved
to an empty catnum set -> empty list, while entries stayed populated
(which is why satellite counts looked normal).
Also fix getTypesList: removeAt(0) removed the alphabetically-first real
source ('ARISS') instead of the intended placeholder; now excludes only
'Other' (empty URL), so all real source types incl. 'All'/'ARISS' are
selectable.
Tests: remote update persists All ids; All type resolves to persisted
CelesTrak active ids; getTypesList excludes only Other.
Two independent defects kept ISS (DESTINY) visible in the satellite-page
AMSAT Live FM filter even after installing the fix and syncing data:
1. UI-layer staleness (SelectionRepo): resolveTypeIds ran only when the
filter type changed and snapshotted the catnum set. A background data
sync rewriting the FM/Linear lists never re-triggered resolution, so
the UI kept filtering with the pre-sync (dirty) set until restart or a
type toggle. ISettingsRepo now exposes a monotonic amSatListsVersion
StateFlow, bumped by SettingsRepo.setAmSatCatnums; SelectionRepo
combines it with currentTypes so every rewrite re-resolves instantly.
2. Data-layer staleness (DatabaseRepo): when the AMSAT FM page fetch
failed, the previous list was kept verbatim — including pre-whitelist
era lists containing all five ISS module entries. The stale list is now
re-disambiguated: previous catnums are looked up by local name and run
through the same whitelist / smallest-catnum collapse, so ISS modules
always collapse to the primary ZARYA (25544).
- Failed whitelist fetch (raw.githubusercontent.com unreachable on some
networks) no longer wipes the persisted whitelist or admits every ISS
module alias: fall back to the persisted whitelist, and only persist
when a stream was actually fetched.
- Multi-match collapse without any whitelist now keeps the primary entry
(smallest catnum, ISS ZARYA=25544) instead of every alias (DESTINY etc.).
- Failed FM/Linear page fetch keeps the previous lists instead of
clearing the filters.
- Amateur whitelist gets a jsDelivr CDN mirror as fallback source.
- Resolve ISS station modules (ZARYA/UNITY/ZVEZDA/DESTINY/NAUKA) to the
primary 25544 instead of whichever module appears first in the TLE source
- Match OSCAR designators to local names spelled 'OSCAR N'/'AMSAT-OSCAR N'
(AO-7 -> OSCAR 7) so Linear lists keep AO-7 on SatNOGS-sourced data
- SSTV virtual type now uses the transceiver service class (Amateur only)
plus launcher-debris name exclusion, dropping retired weather birds
(TIROS) and rocket bodies (ARIANE 6 R/B) while keeping ISS/CSS/TEIKYOSAT
- Persist the AMSAT active-frequency whitelist (palewire mirror) on sync;
used only to disambiguate multi-match names, never as a hard filter
- matchesAmSatName: exact token match instead of substring contains,
so 'ISS' no longer matches AISSAT-1 and 'AO-7' no longer matches
AO-73 (FUNCUBE-1); resolves missing ISS in FM and missing AO-7 in
Linear lists
- resolveTypeIds: intersect SSTV catnums with current in-orbit TLE set,
excluding retired satellites (e.g. TIROS) whose radios are tagged SSTV
- add regression tests for token matching
Two correctness fixes for the FM/Linear/SSTV filters:
1. Satellite type picker: virtual transponder types (AMSAT Live FM,
AMSAT Live Linear, Live SSTV) are now mutually exclusive with the
regular TLE-source types - ticking a virtual type clears Amateur/
CubeSat/etc and vice versa, so selecting 'AMSAT Live FM' no longer
unions in hundreds of non-transponder satellites. Virtual types
still combine with each other. Also: an empty virtual-type resolve
now yields an empty list instead of falling back to 'show all'.
2. Mutual-match fallback: when the AMSAT lists are empty (first run /
sync failure) the DB mode filter used downlinkMode alone, so any
downlink-only beacon sharing a mode label was misclassified
(ISS/IO-86 CW beacons into Linear, FO-29's FM Digitalker into FM).
New getIdsWithModesAndUplink() requires uplinkLow IS NOT NULL,
matching real transponders only. SSTV filter keeps the old query
(SSTV sats are often downlink-only).
Shared virtual-type names moved to Sources.virtualTypeNames.
The satellite selection screen's 'Select satellite type' dialog now lists
three virtual types first (AMSAT Live FM, AMSAT Live Linear, Live SSTV)
before the TLE-source types. Selecting one filters the satellite list to
that transponder/activity set; multiple selections union. 'All' is kept.
- SelectionRepo.getTypesList: virtual types prepended (All preserved)
- SelectionRepo.resolveTypeIds: FM/Linear -> AMSAT catnum sets,
Live SSTV -> mode=SSTV radios; other types via persisted IDs
- Tests: virtual-type filtering, union, clear, list-order assertion
Mutual match page gains FM/Linear transponder-type filters. The
authoritative source is AMSAT's human-maintained Live FM/Linear
Satellites pages, fetched during data sync and stored as catnum sets;
filters resolve to those sets (falling back to DB mode filter when
empty), and unchecking both shows a prompt instead of querying.
- DataParser.parseAmSatLivePage: table rows to satellite names,
TEVEL2-1 thru TEVEL2-9 range expansion, bracket-alias matching
- SettingsRepo: AMSAT FM/Linear catnum sets persisted in prefs
- DatabaseRepo: fetch AMSAT pages in updateFromRemote, best-effort
(failure keeps previous list, never blocks the update)
- getIdsWithModes: AND isAlive = 1
- MutualScreen: FilterChip row inside MatchSearchCard
- Tests: parser (4) + viewmodel (3) + repo fakes
- MarkedStation storage upgraded to an ordered list per grid
(Map<String, List<MarkedStation>>); JSON array shape with legacy
v14.1 single-object migration on read
- MapAction: SetMarkedStation appends (duplicate callsign ignored),
RemoveMarkedStation deletes by callsign, PinMarkedStation moves a
mark to the top of the list
- MaidenheadGridOverlay: red fill for any marked grid unchanged;
first-call label mode shows the FIRST (pinned) marked callsign
- MapScreen: one row per mark (pale-yellow callsign), pin icon
(冖 + stroke ↑, drawn in-app) left of the Marked label — shown only
when the grid has 2+ marks; pinned row's icon grey, others pale
yellow; delete icon per row
- New ic_pin.xml vector drawable; strings grid_mark_pin
- MapScreen: mark-a-station callsign input uppercases as you type
- MutualViewModel: read passesSettings.hoursBefore, shift the mutual
match query start to now-hoursBefore so historical passes are
included when the main Passes page has a lookback window set;
endTime stays at now+hoursAhead
- New MarkedStation model + ISettingsRepo/SettingsRepo persistence
(single JSON key markedGridStations, org.json only)
- MapState/MapViewModel: markedGrids state, Set/RemoveMarkedStation
actions; marks auto-clear once the grid becomes worked
- MaidenheadGridOverlay: red fill (alpha 90, same as green worked
fill) for marked grids at both zoom levels; first-call label mode
shows the marked callsign in pale yellow on red cells
- MapScreen: unworked-grid dialog gains a 'mark a station you want
to contact' button + callsign input dialog; marked row shows red
callsign, 'Marked' in the count column, marking date, delete icon
- i18n: values/ + values-zh/ (grid_mark_*)
Tapping the grid-QSO dialog "Match" button now starts the pass query on
the map page and only navigates to the match page once it finishes, so the
page's first frame already contains the results — the time-range card at
the top and the pass curves right below, with no scroll flicker. The dialog
button shows a spinner and is disabled while calculating. A uiState
collector in MainScreen performs the actual navigation (still a push, so
the back gesture returns to the map). Failures/exceptions still navigate so
the user is never stuck on the map page.
queryMutualPasses() reset listScrollIndex to 0 on every query, so tapping
"query" on the match page jumped the list back to the top. Now the default
keeps the current position; the stale results stay on screen while the new
query runs (clearing them shrank the list below a screen and clamped the
position away) and are replaced when the new results arrive. New VM test
covers position + stale-results behaviour through a settled re-query.
On a device the first frame of the match page only contains the station
cards and the time-range card, which can be shorter than the viewport —
scrollToItem(1) then has no scroll range and silently does nothing, and
by the time the async query results grow the list past one screen the
one-shot flag is already consumed. Retry every 100ms (up to 2s) until
firstVisibleItemIndex reaches the time-range card. Verified with a new
Robolectric test that grows the content after a delay. Bump 12.6.
Temporary Log.d probes at prefill, list-layout gate, scroll effect entry,
scrollToItem outcome and consume, to locate why the map Match prefill
scroll does not land on the device (Robolectric covers it). Bump 12.5.
Run the scroll tests at 411x891dp portrait (device-like layout) and add
a NavDisplay entryProvider scenario that composes MutualScreen the way
the app does, so the prefill scroll is exercised end-to-end.
On a device the Mutual entry is composed inside the NavDisplay transition;
LaunchedEffect could run before the LazyColumn is measurable and the
scrollToItem then never lands, leaving the page at the top. Only scroll
once onGloballyPositioned confirms the list has actually been laid out.
The grid-QSO dialog Match button now adds Mutual on top of the Map entry
instead of clearing the stack, so the system back gesture pops back to
the map page (the page the user came from). Bottom-nav tab switches keep
their replace semantics.
MutualMatchPrefillScrollTest verifies the map grid-QSO Match prefill:
time-range card lands at the top after scrollToItem, default open stays
at station inputs, and the query auto-starts with grid+24h pre-filled.
Adds Robolectric + compose ui-test-junit4 to the mutual test classpath.
Compose ui 1.11.x (BOM 2026.06.01) ignores the LazyListState
firstVisibleItemIndex constructor parameter - the list always starts at
item 0. Restore the scrollToItem approach (verified effective by
Robolectric Compose UI tests) and keep the auto-started query.
- Match prefill now positions the LazyColumn via its initial state
(queryMutualPasses(initialScrollIndex = 1)) instead of a post-layout
scrollToItem, so the page reliably opens with the time-range card at the
top while the 24h query runs.
- The install-time what's new dialog text now scrolls inside a bounded box
(max 360dp) instead of overflowing the fixed dialog frame.
- Grid-QSO dialog gains a Match action (radio-tower icon + label) in its
header; tapping it navigates to the match page pre-filled with the tapped
grid as the opposite station and a 24h time range, auto-starts the query,
and scrolls the page straight to the time-range card.
- Every grid is now tappable (previously only worked grids opened the
dialog); unworked grids show an empty QSO list instead of a hint.