diff --git a/frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js b/frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js rename to frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js index 1ed337f2..6fe75b68 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js and b/frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js.map b/frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js.map similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js.map rename to frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js.map index 67544f59..2b3bfa80 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js.map and b/frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js.map differ diff --git a/frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js b/frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js rename to frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js index 5d9d929c..51e0cacf 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js and b/frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js differ diff --git a/frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js.map b/frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js.map similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js.map rename to frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js.map index babae990..be61082e 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js.map and b/frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js.map differ diff --git a/frontend/prebuilt/assets/MapView-CIrx3Q-Q.js b/frontend/prebuilt/assets/MapView-CCM06mdk.js similarity index 92% rename from frontend/prebuilt/assets/MapView-CIrx3Q-Q.js rename to frontend/prebuilt/assets/MapView-CCM06mdk.js index a92faa56..e589d418 100644 Binary files a/frontend/prebuilt/assets/MapView-CIrx3Q-Q.js and b/frontend/prebuilt/assets/MapView-CCM06mdk.js differ diff --git a/frontend/prebuilt/assets/MapView-CIrx3Q-Q.js.map b/frontend/prebuilt/assets/MapView-CCM06mdk.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-CIrx3Q-Q.js.map rename to frontend/prebuilt/assets/MapView-CCM06mdk.js.map index 5d7dea6e..b01df0c9 100644 Binary files a/frontend/prebuilt/assets/MapView-CIrx3Q-Q.js.map and b/frontend/prebuilt/assets/MapView-CCM06mdk.js.map differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js b/frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js rename to frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js index 5f3a4227..1693c641 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js and b/frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js.map similarity index 97% rename from frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js.map rename to frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js.map index 49ebb549..b2cf6c5b 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js.map and b/frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js.map differ diff --git a/frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js b/frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js similarity index 95% rename from frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js rename to frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js index 3b3eedde..05201c5b 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js and b/frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js differ diff --git a/frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js.map b/frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js.map rename to frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js.map index ac6bc6e6..bce486ca 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js.map and b/frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js.map differ diff --git a/frontend/prebuilt/assets/Popup-CPQBLPRZ.js b/frontend/prebuilt/assets/Popup-s69FReak.js similarity index 81% rename from frontend/prebuilt/assets/Popup-CPQBLPRZ.js rename to frontend/prebuilt/assets/Popup-s69FReak.js index f6bb377b..22661328 100644 Binary files a/frontend/prebuilt/assets/Popup-CPQBLPRZ.js and b/frontend/prebuilt/assets/Popup-s69FReak.js differ diff --git a/frontend/prebuilt/assets/Popup-CPQBLPRZ.js.map b/frontend/prebuilt/assets/Popup-s69FReak.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-CPQBLPRZ.js.map rename to frontend/prebuilt/assets/Popup-s69FReak.js.map index 5af762cb..d9449f75 100644 Binary files a/frontend/prebuilt/assets/Popup-CPQBLPRZ.js.map and b/frontend/prebuilt/assets/Popup-s69FReak.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js b/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js new file mode 100644 index 00000000..6639056c Binary files /dev/null and b/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js.map b/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js.map new file mode 100644 index 00000000..456290e3 Binary files /dev/null and b/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js b/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js deleted file mode 100644 index 93583cbc..00000000 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js and /dev/null differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js.map b/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js.map deleted file mode 100644 index f8fd507f..00000000 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js.map and /dev/null differ diff --git a/frontend/prebuilt/assets/SearchView-WhBxZXuQ.js b/frontend/prebuilt/assets/SearchView-4aOlPoPL.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-WhBxZXuQ.js rename to frontend/prebuilt/assets/SearchView-4aOlPoPL.js index ed5dd986..a6fd9a69 100644 Binary files a/frontend/prebuilt/assets/SearchView-WhBxZXuQ.js and b/frontend/prebuilt/assets/SearchView-4aOlPoPL.js differ diff --git a/frontend/prebuilt/assets/SearchView-WhBxZXuQ.js.map b/frontend/prebuilt/assets/SearchView-4aOlPoPL.js.map similarity index 99% rename from frontend/prebuilt/assets/SearchView-WhBxZXuQ.js.map rename to frontend/prebuilt/assets/SearchView-4aOlPoPL.js.map index 0e6d772d..a70278c2 100644 Binary files a/frontend/prebuilt/assets/SearchView-WhBxZXuQ.js.map and b/frontend/prebuilt/assets/SearchView-4aOlPoPL.js.map differ diff --git a/frontend/prebuilt/assets/SettingsModal-Ds7A10ji.js b/frontend/prebuilt/assets/SettingsModal-Bje6NQB0.js similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-Ds7A10ji.js rename to frontend/prebuilt/assets/SettingsModal-Bje6NQB0.js index 6924ef15..18b597e2 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-Ds7A10ji.js and b/frontend/prebuilt/assets/SettingsModal-Bje6NQB0.js differ diff --git a/frontend/prebuilt/assets/SettingsModal-Ds7A10ji.js.map b/frontend/prebuilt/assets/SettingsModal-Bje6NQB0.js.map similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-Ds7A10ji.js.map rename to frontend/prebuilt/assets/SettingsModal-Bje6NQB0.js.map index 626a5c48..acb8ebba 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-Ds7A10ji.js.map and b/frontend/prebuilt/assets/SettingsModal-Bje6NQB0.js.map differ diff --git a/frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js b/frontend/prebuilt/assets/VisualizerView-DClWBHTO.js similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js rename to frontend/prebuilt/assets/VisualizerView-DClWBHTO.js index 228a80e7..b34b338a 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js and b/frontend/prebuilt/assets/VisualizerView-DClWBHTO.js differ diff --git a/frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js.map b/frontend/prebuilt/assets/VisualizerView-DClWBHTO.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js.map rename to frontend/prebuilt/assets/VisualizerView-DClWBHTO.js.map index 275783d8..d0d7dfab 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js.map and b/frontend/prebuilt/assets/VisualizerView-DClWBHTO.js.map differ diff --git a/frontend/prebuilt/assets/hooks-29eXpyfK.js b/frontend/prebuilt/assets/hooks-29eXpyfK.js deleted file mode 100644 index 8bd8fff4..00000000 Binary files a/frontend/prebuilt/assets/hooks-29eXpyfK.js and /dev/null differ diff --git a/frontend/prebuilt/assets/hooks-DtoQuSJA.js b/frontend/prebuilt/assets/hooks-DtoQuSJA.js new file mode 100644 index 00000000..e3594f31 Binary files /dev/null and b/frontend/prebuilt/assets/hooks-DtoQuSJA.js differ diff --git a/frontend/prebuilt/assets/hooks-29eXpyfK.js.map b/frontend/prebuilt/assets/hooks-DtoQuSJA.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-29eXpyfK.js.map rename to frontend/prebuilt/assets/hooks-DtoQuSJA.js.map index b448288f..573bb41c 100644 Binary files a/frontend/prebuilt/assets/hooks-29eXpyfK.js.map and b/frontend/prebuilt/assets/hooks-DtoQuSJA.js.map differ diff --git a/frontend/prebuilt/assets/index-DxvL_BZd.js b/frontend/prebuilt/assets/index-CM9d1p2a.js similarity index 99% rename from frontend/prebuilt/assets/index-DxvL_BZd.js rename to frontend/prebuilt/assets/index-CM9d1p2a.js index ddf65656..5f54af96 100644 Binary files a/frontend/prebuilt/assets/index-DxvL_BZd.js and b/frontend/prebuilt/assets/index-CM9d1p2a.js differ diff --git a/frontend/prebuilt/assets/index-DxvL_BZd.js.map b/frontend/prebuilt/assets/index-CM9d1p2a.js.map similarity index 99% rename from frontend/prebuilt/assets/index-DxvL_BZd.js.map rename to frontend/prebuilt/assets/index-CM9d1p2a.js.map index 0b143aab..25ed43d2 100644 Binary files a/frontend/prebuilt/assets/index-DxvL_BZd.js.map and b/frontend/prebuilt/assets/index-CM9d1p2a.js.map differ diff --git a/frontend/prebuilt/assets/leaflet-DtWtaoJ1.js b/frontend/prebuilt/assets/leaflet-CetcwTHt.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-DtWtaoJ1.js rename to frontend/prebuilt/assets/leaflet-CetcwTHt.js index 2cfac8ca..9517f74d 100644 Binary files a/frontend/prebuilt/assets/leaflet-DtWtaoJ1.js and b/frontend/prebuilt/assets/leaflet-CetcwTHt.js differ diff --git a/frontend/prebuilt/assets/leaflet-DtWtaoJ1.js.map b/frontend/prebuilt/assets/leaflet-CetcwTHt.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-DtWtaoJ1.js.map rename to frontend/prebuilt/assets/leaflet-CetcwTHt.js.map index a09b1b4d..01f7bc05 100644 Binary files a/frontend/prebuilt/assets/leaflet-DtWtaoJ1.js.map and b/frontend/prebuilt/assets/leaflet-CetcwTHt.js.map differ diff --git a/frontend/prebuilt/assets/separator-nd9KgFnt.js b/frontend/prebuilt/assets/separator-DiyBVm1x.js similarity index 89% rename from frontend/prebuilt/assets/separator-nd9KgFnt.js rename to frontend/prebuilt/assets/separator-DiyBVm1x.js index 7dccdaab..f28fff43 100644 Binary files a/frontend/prebuilt/assets/separator-nd9KgFnt.js and b/frontend/prebuilt/assets/separator-DiyBVm1x.js differ diff --git a/frontend/prebuilt/assets/separator-nd9KgFnt.js.map b/frontend/prebuilt/assets/separator-DiyBVm1x.js.map similarity index 98% rename from frontend/prebuilt/assets/separator-nd9KgFnt.js.map rename to frontend/prebuilt/assets/separator-DiyBVm1x.js.map index 1d8fe3db..f80d1763 100644 Binary files a/frontend/prebuilt/assets/separator-nd9KgFnt.js.map and b/frontend/prebuilt/assets/separator-DiyBVm1x.js.map differ diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html index 8cf690dc..f31ee8f4 100644 Binary files a/frontend/prebuilt/index.html and b/frontend/prebuilt/index.html differ diff --git a/frontend/src/components/RepeaterDashboard.tsx b/frontend/src/components/RepeaterDashboard.tsx index 46c0a225..c020778c 100644 --- a/frontend/src/components/RepeaterDashboard.tsx +++ b/frontend/src/components/RepeaterDashboard.tsx @@ -8,6 +8,7 @@ import { RepeaterLogin } from './RepeaterLogin'; import { useRepeaterDashboard } from '../hooks/useRepeaterDashboard'; import { isFavorite } from '../utils/favorites'; import { handleKeyboardActivate } from '../utils/a11y'; +import { isValidLocation } from '../utils/pathUtils'; import { ContactStatusInfo } from './ContactStatusInfo'; import type { Contact, Conversation, Favorite, PathDiscoveryResponse } from '../types'; import { TelemetryPane } from './repeater/RepeaterTelemetryPane'; @@ -60,6 +61,8 @@ export function RepeaterDashboard({ onDeleteContact, }: RepeaterDashboardProps) { const [pathDiscoveryOpen, setPathDiscoveryOpen] = useState(false); + const contact = contacts.find((c) => c.public_key === conversation.id) ?? null; + const hasAdvertLocation = isValidLocation(contact?.lat ?? null, contact?.lon ?? null); const { loggedIn, loginLoading, @@ -77,9 +80,8 @@ export function RepeaterDashboard({ sendFloodAdvert, rebootRepeater, syncClock, - } = useRepeaterDashboard(conversation); + } = useRepeaterDashboard(conversation, { hasAdvertLocation }); - const contact = contacts.find((c) => c.public_key === conversation.id); const isFav = isFavorite(favorites, 'contact', conversation.id); // Loading all panes indicator @@ -261,6 +263,7 @@ export function RepeaterDashboard({ state={paneStates.neighbors} onRefresh={() => refreshPane('neighbors')} disabled={anyLoading} + repeaterContact={contact} contacts={contacts} nodeInfo={paneData.nodeInfo} nodeInfoState={paneStates.nodeInfo} diff --git a/frontend/src/components/repeater/RepeaterNeighborsPane.tsx b/frontend/src/components/repeater/RepeaterNeighborsPane.tsx index 6812f90e..0c2d5184 100644 --- a/frontend/src/components/repeater/RepeaterNeighborsPane.tsx +++ b/frontend/src/components/repeater/RepeaterNeighborsPane.tsx @@ -19,6 +19,7 @@ export function NeighborsPane({ state, onRefresh, disabled, + repeaterContact, contacts, nodeInfo, nodeInfoState, @@ -28,11 +29,15 @@ export function NeighborsPane({ state: PaneState; onRefresh: () => void; disabled?: boolean; + repeaterContact: Contact | null; contacts: Contact[]; nodeInfo: RepeaterNodeInfoResponse | null; nodeInfoState: PaneState; repeaterName: string | null; }) { + const advertLat = repeaterContact?.lat ?? null; + const advertLon = repeaterContact?.lon ?? null; + const radioLat = useMemo(() => { const parsed = nodeInfo?.lat != null ? parseFloat(nodeInfo.lat) : null; return Number.isFinite(parsed) ? parsed : null; @@ -43,11 +48,26 @@ export function NeighborsPane({ return Number.isFinite(parsed) ? parsed : null; }, [nodeInfo?.lon]); - const radioName = nodeInfo?.name || repeaterName; - const hasValidRepeaterGps = isValidLocation(radioLat, radioLon); - const showGpsUnavailableMessage = - !hasValidRepeaterGps && - (nodeInfoState.error !== null || nodeInfoState.fetched_at != null || nodeInfo !== null); + const positionSource = useMemo(() => { + if (isValidLocation(radioLat, radioLon)) { + return { lat: radioLat, lon: radioLon, source: 'reported' as const }; + } + if (isValidLocation(advertLat, advertLon)) { + return { lat: advertLat, lon: advertLon, source: 'advert' as const }; + } + return { lat: null, lon: null, source: null }; + }, [advertLat, advertLon, radioLat, radioLon]); + + const radioName = nodeInfo?.name || repeaterContact?.name || repeaterName; + const hasValidRepeaterGps = positionSource.source !== null; + const headerNote = + positionSource.source === 'reported' + ? 'Using repeater-reported position' + : positionSource.source === 'advert' + ? 'Using advert position' + : nodeInfoState.loading + ? 'Waiting for repeater position' + : 'No repeater position available'; // Resolve contact data for each neighbor in a single pass — used for // coords (mini-map), distances (table column), and sorted display order. @@ -71,7 +91,7 @@ export function NeighborsPane({ let dist: string | null = null; if (hasValidRepeaterGps && isValidLocation(nLat, nLon)) { - const distKm = calculateDistance(radioLat, radioLon, nLat, nLon); + const distKm = calculateDistance(positionSource.lat, positionSource.lon, nLat, nLon); if (distKm != null) { dist = formatDistance(distKm); anyDist = true; @@ -91,11 +111,12 @@ export function NeighborsPane({ sorted: enriched, hasDistances: anyDist, }; - }, [contacts, data, hasValidRepeaterGps, radioLat, radioLon]); + }, [contacts, data, hasValidRepeaterGps, positionSource.lat, positionSource.lon]); return ( n.pubkey_prefix).join(',')} neighbors={neighborsWithCoords} - radioLat={radioLat} - radioLon={radioLon} + radioLat={positionSource.lat} + radioLon={positionSource.lon} radioName={radioName} /> - ) : showGpsUnavailableMessage ? ( + ) : (
- GPS info failed to fetch; map and distance data not available. This may be due to - missing or zero-zero GPS data on the repeater, or due to transient fetch failure. Try - refreshing. + Map and distance data are unavailable until this repeater has a valid position from + either its advert or a Node Info fetch.
- ) : null} + )} )}
diff --git a/frontend/src/components/repeater/repeaterPaneShared.tsx b/frontend/src/components/repeater/repeaterPaneShared.tsx index 21cb6eec..d8817b4f 100644 --- a/frontend/src/components/repeater/repeaterPaneShared.tsx +++ b/frontend/src/components/repeater/repeaterPaneShared.tsx @@ -109,6 +109,7 @@ function formatFetchedTime(fetchedAt: number): string { export function RepeaterPane({ title, + headerNote, state, onRefresh, disabled, @@ -117,6 +118,7 @@ export function RepeaterPane({ contentClassName, }: { title: string; + headerNote?: ReactNode; state: PaneState; onRefresh?: () => void; disabled?: boolean; @@ -131,6 +133,7 @@ export function RepeaterPane({

{title}

+ {headerNote &&

{headerNote}

} {fetchedAt && (

Promise; } +interface UseRepeaterDashboardOptions { + hasAdvertLocation?: boolean; +} + export function useRepeaterDashboard( - activeConversation: Conversation | null + activeConversation: Conversation | null, + options: UseRepeaterDashboardOptions = {} ): UseRepeaterDashboardResult { const conversationId = activeConversation && activeConversation.type === 'contact' ? activeConversation.id : null; @@ -301,7 +306,7 @@ export function useRepeaterDashboard( if (!publicKey) return; const conversationId = publicKey; - if (pane === 'neighbors') { + if (pane === 'neighbors' && !options.hasAdvertLocation) { const nodeInfoState = paneStatesRef.current.nodeInfo; const nodeInfoData = paneDataRef.current.nodeInfo; const needsNodeInfoPrefetch = @@ -385,7 +390,7 @@ export function useRepeaterDashboard( } } }, - [getPublicKey] + [getPublicKey, options.hasAdvertLocation] ); const loadAll = useCallback(async () => { diff --git a/frontend/src/test/repeaterDashboard.test.tsx b/frontend/src/test/repeaterDashboard.test.tsx index b656e35f..46bbd92f 100644 --- a/frontend/src/test/repeaterDashboard.test.tsx +++ b/frontend/src/test/repeaterDashboard.test.tsx @@ -265,13 +265,14 @@ describe('RepeaterDashboard', () => { expect( screen.getByText( - 'GPS info failed to fetch; map and distance data not available. This may be due to missing or zero-zero GPS data on the repeater, or due to transient fetch failure. Try refreshing.' + 'Map and distance data are unavailable until this repeater has a valid position from either its advert or a Node Info fetch.' ) ).toBeInTheDocument(); + expect(screen.getByText('No repeater position available')).toBeInTheDocument(); expect(screen.queryByText('Dist')).not.toBeInTheDocument(); }); - it('shows neighbor distance when repeater radio settings include valid coords', () => { + it('shows neighbor distance when repeater node info includes valid coords', () => { mockHook.loggedIn = true; mockHook.paneData.neighbors = { neighbors: [ @@ -324,13 +325,69 @@ describe('RepeaterDashboard', () => { render(); expect(screen.getByText('Dist')).toBeInTheDocument(); + expect(screen.getByText('Using repeater-reported position')).toBeInTheDocument(); expect( screen.queryByText( - 'GPS info failed to fetch; map and distance data not available. This may be due to missing or zero-zero GPS data on the repeater, or due to transient fetch failure. Try refreshing.' + 'Map and distance data are unavailable until this repeater has a valid position from either its advert or a Node Info fetch.' ) ).not.toBeInTheDocument(); }); + it('uses advert coords for neighbor distance when node info is unavailable', () => { + mockHook.loggedIn = true; + mockHook.paneData.neighbors = { + neighbors: [ + { pubkey_prefix: 'bbbbbbbbbbbb', name: 'Neighbor', snr: 7.2, last_heard_seconds: 9 }, + ], + }; + mockHook.paneData.nodeInfo = null; + mockHook.paneStates.neighbors = { + loading: false, + attempt: 1, + error: null, + fetched_at: Date.now(), + }; + mockHook.paneStates.nodeInfo = { + loading: false, + attempt: 0, + error: null, + fetched_at: null, + }; + + const contactsWithAdvertAndNeighbor = [ + { + ...contacts[0], + lat: -31.95, + lon: 115.86, + }, + { + public_key: 'bbbbbbbbbbbb0000000000000000000000000000000000000000000000000000', + name: 'Neighbor', + type: 1, + flags: 0, + last_path: null, + last_path_len: 0, + out_path_hash_mode: 0, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: -31.94, + lon: 115.87, + last_seen: null, + on_radio: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }, + ]; + + render(); + + expect(screen.getByText('Dist')).toBeInTheDocument(); + expect(screen.getByText('Using advert position')).toBeInTheDocument(); + }); + it('shows fetching state with attempt counter', () => { mockHook.loggedIn = true; mockHook.paneStates.status = { loading: true, attempt: 2, error: null }; diff --git a/frontend/src/test/useRepeaterDashboard.test.ts b/frontend/src/test/useRepeaterDashboard.test.ts index 63bfd7b1..0583892b 100644 --- a/frontend/src/test/useRepeaterDashboard.test.ts +++ b/frontend/src/test/useRepeaterDashboard.test.ts @@ -408,6 +408,22 @@ describe('useRepeaterDashboard', () => { expect(mockApi.repeaterNeighbors).toHaveBeenCalledTimes(2); }); + it('refreshing neighbors skips node info prefetch when advert location already exists', async () => { + mockApi.repeaterNeighbors.mockResolvedValueOnce({ neighbors: [] }); + + const { result } = renderHook(() => + useRepeaterDashboard(repeaterConversation, { hasAdvertLocation: true }) + ); + + await act(async () => { + await result.current.refreshPane('neighbors'); + }); + + expect(mockApi.repeaterNodeInfo).not.toHaveBeenCalled(); + expect(mockApi.repeaterNeighbors).toHaveBeenCalledTimes(1); + expect(result.current.paneData.neighbors).toEqual({ neighbors: [] }); + }); + it('restores dashboard state when navigating away and back to the same repeater', async () => { const statusData = { battery_volts: 4.2 }; mockApi.repeaterLogin.mockResolvedValueOnce({