From 749fb43fd09236302b190c06215b92e71c0ee3be Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Mon, 16 Mar 2026 14:24:58 -0700 Subject: [PATCH] Ditch garbage data ingest for lat/lon and extend map. Closes #63 --- app/decoder.py | 18 +++++++++- app/packet_processor.py | 2 +- ...-CwRSdUcr.js => BotCodeEditor-D9DJubdS.js} | Bin 467083 -> 467083 bytes ...r.js.map => BotCodeEditor-D9DJubdS.js.map} | Bin 1956915 -> 1956915 bytes ...l-F1IZPbqZ.js => CrackerPanel-XySvf2Tf.js} | Bin 129410 -> 129410 bytes ...qZ.js.map => CrackerPanel-XySvf2Tf.js.map} | Bin 396562 -> 396562 bytes ...apView-CpdCCT8F.js => MapView-CIrx3Q-Q.js} | Bin 4103 -> 4103 bytes ...pdCCT8F.js.map => MapView-CIrx3Q-Q.js.map} | Bin 14925 -> 14925 bytes ...lNdNzL.js => NeighborsMiniMap-BKFex7LO.js} | Bin 1670 -> 1670 bytes ...s.map => NeighborsMiniMap-BKFex7LO.js.map} | Bin 5886 -> 5886 bytes ...p-CI0Ccl1k.js => PathRouteMap-DngkQYtL.js} | Bin 3868 -> 3868 bytes ...1k.js.map => PathRouteMap-DngkQYtL.js.map} | Bin 14070 -> 14070 bytes .../{Popup-afIfyuqS.js => Popup-CPQBLPRZ.js} | Bin 920 -> 920 bytes ...-afIfyuqS.js.map => Popup-CPQBLPRZ.js.map} | Bin 4181 -> 4181 bytes .../assets/RepeaterDashboard-CNYTKg7R.js | Bin 28529 -> 0 bytes .../assets/RepeaterDashboard-CQiVVwkX.js | Bin 0 -> 28605 bytes ....map => RepeaterDashboard-CQiVVwkX.js.map} | Bin 89995 -> 90071 bytes ...iew-CITGP0uG.js => SearchView-WhBxZXuQ.js} | Bin 5106 -> 5106 bytes ...P0uG.js.map => SearchView-WhBxZXuQ.js.map} | Bin 16886 -> 16886 bytes ...-B-RRzLqB.js => SettingsModal-Ds7A10ji.js} | Bin 77263 -> 77263 bytes ...B.js.map => SettingsModal-Ds7A10ji.js.map} | Bin 196690 -> 196690 bytes ...6NygNgga.js => VisualizerView-DS6ZM6Ne.js} | Bin 590321 -> 590321 bytes ....js.map => VisualizerView-DS6ZM6Ne.js.map} | Bin 3112795 -> 3112795 bytes frontend/prebuilt/assets/hooks-29eXpyfK.js | Bin 0 -> 157 bytes ...-B3DSOzgN.js.map => hooks-29eXpyfK.js.map} | Bin 1128 -> 1128 bytes frontend/prebuilt/assets/hooks-B3DSOzgN.js | Bin 157 -> 0 bytes ...{index-Kkb-Bio9.css => index-Cx5ENk0V.css} | Bin 64734 -> 64773 bytes .../{index-GLdV3E1N.js => index-DxvL_BZd.js} | Bin 583357 -> 583385 bytes ...-GLdV3E1N.js.map => index-DxvL_BZd.js.map} | Bin 2117399 -> 2117527 bytes ...eaflet-Df2K48et.js => leaflet-DtWtaoJ1.js} | Bin 153764 -> 153764 bytes ...f2K48et.js.map => leaflet-DtWtaoJ1.js.map} | Bin 700833 -> 700833 bytes ...ator-iZHk50Ep.js => separator-nd9KgFnt.js} | Bin 1068 -> 1068 bytes ...k50Ep.js.map => separator-nd9KgFnt.js.map} | Bin 5058 -> 5058 bytes frontend/prebuilt/index.html | Bin 2761 -> 2761 bytes frontend/src/components/RepeaterDashboard.tsx | 4 +-- .../repeater/RepeaterNeighborsPane.tsx | 10 +++--- frontend/src/test/pathUtils.test.ts | 19 +++++++++++ frontend/src/utils/pathUtils.ts | 3 ++ tests/test_decoder.py | 32 ++++++++++++++++++ 39 files changed, 79 insertions(+), 9 deletions(-) rename frontend/prebuilt/assets/{BotCodeEditor-CwRSdUcr.js => BotCodeEditor-D9DJubdS.js} (99%) rename frontend/prebuilt/assets/{BotCodeEditor-CwRSdUcr.js.map => BotCodeEditor-D9DJubdS.js.map} (99%) rename frontend/prebuilt/assets/{CrackerPanel-F1IZPbqZ.js => CrackerPanel-XySvf2Tf.js} (99%) rename frontend/prebuilt/assets/{CrackerPanel-F1IZPbqZ.js.map => CrackerPanel-XySvf2Tf.js.map} (99%) rename frontend/prebuilt/assets/{MapView-CpdCCT8F.js => MapView-CIrx3Q-Q.js} (92%) rename frontend/prebuilt/assets/{MapView-CpdCCT8F.js.map => MapView-CIrx3Q-Q.js.map} (99%) rename frontend/prebuilt/assets/{NeighborsMiniMap-BrlNdNzL.js => NeighborsMiniMap-BKFex7LO.js} (85%) rename frontend/prebuilt/assets/{NeighborsMiniMap-BrlNdNzL.js.map => NeighborsMiniMap-BKFex7LO.js.map} (97%) rename frontend/prebuilt/assets/{PathRouteMap-CI0Ccl1k.js => PathRouteMap-DngkQYtL.js} (95%) rename frontend/prebuilt/assets/{PathRouteMap-CI0Ccl1k.js.map => PathRouteMap-DngkQYtL.js.map} (99%) rename frontend/prebuilt/assets/{Popup-afIfyuqS.js => Popup-CPQBLPRZ.js} (81%) rename frontend/prebuilt/assets/{Popup-afIfyuqS.js.map => Popup-CPQBLPRZ.js.map} (97%) delete mode 100644 frontend/prebuilt/assets/RepeaterDashboard-CNYTKg7R.js create mode 100644 frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js rename frontend/prebuilt/assets/{RepeaterDashboard-CNYTKg7R.js.map => RepeaterDashboard-CQiVVwkX.js.map} (66%) rename frontend/prebuilt/assets/{SearchView-CITGP0uG.js => SearchView-WhBxZXuQ.js} (97%) rename frontend/prebuilt/assets/{SearchView-CITGP0uG.js.map => SearchView-WhBxZXuQ.js.map} (99%) rename frontend/prebuilt/assets/{SettingsModal-B-RRzLqB.js => SettingsModal-Ds7A10ji.js} (99%) rename frontend/prebuilt/assets/{SettingsModal-B-RRzLqB.js.map => SettingsModal-Ds7A10ji.js.map} (99%) rename frontend/prebuilt/assets/{VisualizerView-6NygNgga.js => VisualizerView-DS6ZM6Ne.js} (99%) rename frontend/prebuilt/assets/{VisualizerView-6NygNgga.js.map => VisualizerView-DS6ZM6Ne.js.map} (99%) create mode 100644 frontend/prebuilt/assets/hooks-29eXpyfK.js rename frontend/prebuilt/assets/{hooks-B3DSOzgN.js.map => hooks-29eXpyfK.js.map} (90%) delete mode 100644 frontend/prebuilt/assets/hooks-B3DSOzgN.js rename frontend/prebuilt/assets/{index-Kkb-Bio9.css => index-Cx5ENk0V.css} (60%) rename frontend/prebuilt/assets/{index-GLdV3E1N.js => index-DxvL_BZd.js} (96%) rename frontend/prebuilt/assets/{index-GLdV3E1N.js.map => index-DxvL_BZd.js.map} (60%) rename frontend/prebuilt/assets/{leaflet-Df2K48et.js => leaflet-DtWtaoJ1.js} (99%) rename frontend/prebuilt/assets/{leaflet-Df2K48et.js.map => leaflet-DtWtaoJ1.js.map} (99%) rename frontend/prebuilt/assets/{separator-iZHk50Ep.js => separator-nd9KgFnt.js} (89%) rename frontend/prebuilt/assets/{separator-iZHk50Ep.js.map => separator-nd9KgFnt.js.map} (98%) diff --git a/app/decoder.py b/app/decoder.py index d2a83fbc..e0352ae9 100644 --- a/app/decoder.py +++ b/app/decoder.py @@ -85,6 +85,10 @@ class PacketInfo: path_hash_size: int = 1 # Bytes per hop: 1, 2, or 3 +def _is_valid_advert_location(lat: float, lon: float) -> bool: + return -90 <= lat <= 90 and -180 <= lon <= 180 + + def extract_payload(raw_packet: bytes) -> bytes | None: """ Extract just the payload from a raw packet, skipping header and path. @@ -243,7 +247,9 @@ def get_packet_payload_type(raw_packet: bytes) -> PayloadType | None: return None -def parse_advertisement(payload: bytes) -> ParsedAdvertisement | None: +def parse_advertisement( + payload: bytes, raw_packet: bytes | None = None +) -> ParsedAdvertisement | None: """ Parse an advertisement payload. @@ -299,6 +305,16 @@ def parse_advertisement(payload: bytes) -> ParsedAdvertisement | None: lon_raw = int.from_bytes(payload[offset + 4 : offset + 8], byteorder="little", signed=True) lat = lat_raw / 1_000_000 lon = lon_raw / 1_000_000 + if not _is_valid_advert_location(lat, lon): + packet_hex = (raw_packet if raw_packet is not None else payload).hex().upper() + logger.warning( + "Dropping location data for nonsensical packet -- packet %s implies lat/lon %s/%s. Outta this world!", + packet_hex, + lat, + lon, + ) + lat = None + lon = None offset += 8 # Skip feature fields if present diff --git a/app/packet_processor.py b/app/packet_processor.py index afeb41b4..2a398503 100644 --- a/app/packet_processor.py +++ b/app/packet_processor.py @@ -425,7 +425,7 @@ async def _process_advertisement( logger.debug("Failed to parse advertisement packet") return - advert = parse_advertisement(packet_info.payload) + advert = parse_advertisement(packet_info.payload, raw_packet=raw_bytes) if not advert: logger.debug("Failed to parse advertisement payload") return diff --git a/frontend/prebuilt/assets/BotCodeEditor-CwRSdUcr.js b/frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-CwRSdUcr.js rename to frontend/prebuilt/assets/BotCodeEditor-D9DJubdS.js index 08cfd18c337505fb5affcfb17ec43714bda696ac..1ed337f28cbd9f9ac8c4cb749ba6c5a5e8724001 100644 GIT binary patch delta 59 zcmeA^DARpVW`YWbOGTMayi-(4qfV<1W2+8Rs}6Ik4oj;JYpV|1RvmVK1pyaJ7q8N! OlwiHAV!hnN0xkeJkP*`W delta 59 zcmeA^DARpVW`YWbyH84(v8$n9qfV<1W2+8Rs}6Ik4oj;JYpV|1RvmVK1p(*spx~6y OS1{BTw`Uh(!Y~fwIm6fuW7pq8b8p)B w^Y!>36Pe0P<`T(5VhOU8l_ZkNS~il&R&pt1Cwn=_QA#<%S|lc0swT(5U2nE delta 41 xcmZqr%--~weL@$9yH84(v8$n9u9(#tB=%S|lc0svlp54r#V diff --git a/frontend/prebuilt/assets/CrackerPanel-F1IZPbqZ.js.map b/frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js.map similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-F1IZPbqZ.js.map rename to frontend/prebuilt/assets/CrackerPanel-XySvf2Tf.js.map index 01f0ffc22e3818d6920c74d15a7d474ea2c7bc8d..babae990c80df56cfa0097bbf596d39ade1c94a6 100644 GIT binary patch delta 35 qcmbOF7M2#)Eo`j3yb+bbWobqsX?j`3O0@vcrVASY delta 35 qcmbOF7M2#)Eo`j3yl#e`Q2|MXQF>X$O0@vY6AH!v diff --git a/frontend/prebuilt/assets/MapView-CpdCCT8F.js b/frontend/prebuilt/assets/MapView-CIrx3Q-Q.js similarity index 92% rename from frontend/prebuilt/assets/MapView-CpdCCT8F.js rename to frontend/prebuilt/assets/MapView-CIrx3Q-Q.js index fb36cd6944192877e51cc504c5a3344757b96de7..a92faa5698419ecf7fc9c2c1714b1fac76bd26b3 100644 GIT binary patch delta 70 zcmZoyXjhnE&f!u~<`eG}l`_#@i@hYgBr)I1aAJWvhjT!nlTScU)WrD;97dL@5e1cL a-W!h_^7DHZRTu~A2I^%M>*XdEZ~*`;N*22S delta 70 zcmZoyXjhnE&f)Hp5@zgb=r_?`i#^TA+r%QZWMY9jM`D_1T4iZr@WlBF98ShA!Twe0 aejAS)^79v@I6H?}xanmT>*XdEZ~*`cL>4&! diff --git a/frontend/prebuilt/assets/MapView-CpdCCT8F.js.map b/frontend/prebuilt/assets/MapView-CIrx3Q-Q.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-CpdCCT8F.js.map rename to frontend/prebuilt/assets/MapView-CIrx3Q-Q.js.map index f29de66a137015c8ad536d17df23d69ad438cfd4..5d7dea6e9d9c0a1922a67262632b87325f6173a0 100644 GIT binary patch delta 18 ZcmX?Ga<*iG9J^;xg>j&6;6_yqD*#D^2BiQ1 delta 18 ZcmX?Ga<*iG9D6~EvvY`r+eTFlD*#Do2Alu@ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-BrlNdNzL.js b/frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-BrlNdNzL.js rename to frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js index 8a0c72c0ad6e67298a20d041dc8cfd6d94e9e378..5f3a4227ca6ad7d66be2b983d72ac42242502edc 100644 GIT binary patch delta 58 zcmZqUZR4FF&EZl}<`eG}l`>J;jlCqiBr)I1aAJWvhjT!nlTScU)W-QYSoyu(QY*}T N{PnVm^>PynxBw?s65Rj* delta 58 zcmZqUZR4FF&Ef8o5@zgb=r>W>jXllC+r%QZWMY9jM`D_1T4iZr@W%N!Sow=`{8Id? NeDt!4^>PynxBwFI6951J diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-BrlNdNzL.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js.map similarity index 97% rename from frontend/prebuilt/assets/NeighborsMiniMap-BrlNdNzL.js.map rename to frontend/prebuilt/assets/NeighborsMiniMap-BKFex7LO.js.map index 9e5b8ba998e6e6cf329e95ede9e4ca00c3c7321f..49ebb5497196f6452787a11610d8754e5349ca5b 100644 GIT binary patch delta 18 ZcmeyT`%ia*I=i=9YK6Iv|3@Cmr&rDq35 PmiXvp73<|D7H|OoTUitf delta 59 zcmbOuH%D%QCWpIEN|>>$q2ENq5cV`9Zxf5ul8HSE98ShA!Twe0ejB$$@Ci738aOBC P7-s8b73<|D7H|OoB3Tkt diff --git a/frontend/prebuilt/assets/PathRouteMap-CI0Ccl1k.js.map b/frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap-CI0Ccl1k.js.map rename to frontend/prebuilt/assets/PathRouteMap-DngkQYtL.js.map index 6aa3c11146870b40c30760951d87d3c046327f8b..ac6bc6e653d811b23312faae38fb29da7f2a1cf7 100644 GIT binary patch delta 19 acmeyC`z?2Z5{FA(dUjxBiO)u@GiCr;#|TUS delta 19 acmeyC`z?2Z5{I*=fpc<>VfIF?GiCr(v%mRsNo@te(g~57R#d^7k1zZ48 C1`c5W diff --git a/frontend/prebuilt/assets/Popup-afIfyuqS.js.map b/frontend/prebuilt/assets/Popup-CPQBLPRZ.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-afIfyuqS.js.map rename to frontend/prebuilt/assets/Popup-CPQBLPRZ.js.map index 38e6b763da207063975aa2657d8121bdff637d07..5af762cb59cedd746bb2dbb66a5af73d85d3cd64 100644 GIT binary patch delta 19 acmcbra8+S~G>3CQpp#EPP}D|cBLM(Jhz246 delta 19 acmcbra8+S~G)H2ZXIf=xVem#}BLM(P*aqAH diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CNYTKg7R.js b/frontend/prebuilt/assets/RepeaterDashboard-CNYTKg7R.js deleted file mode 100644 index e6a0baba10c4f947ba2cbaafec2f18bf0f97d98a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28529 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 G%l`$7;6MWa diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js b/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js new file mode 100644 index 0000000000000000000000000000000000000000..93583cbc7c9c8aef2cb31687adb9383628cdf813 GIT binary patch literal 28605 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 literal 0 HcmV?d00001 diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CNYTKg7R.js.map b/frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js.map similarity index 66% rename from frontend/prebuilt/assets/RepeaterDashboard-CNYTKg7R.js.map rename to frontend/prebuilt/assets/RepeaterDashboard-CQiVVwkX.js.map index 1ab943b83865d685be2288fd8ea2d5e7f3f690a4..f8fd507fd26b68ce2af5c9ff7415ea2c2a23071f 100644 GIT binary patch delta 270 zcmeC)&w717>xN4feA-Tq&S5&4PL7V5PCD9?A6P^(6*^C@4U%Lu+8k=B&CHwW29$Es zF>rEp&2Zn`YonvcROzyLcPuv(qs8Xq@!Z|h4_Gm>aAoKkD5T}2R_Gc|zW7RO`hF`$ zR%9`z*LN|*er=JP9udqa$XT3Gl$nxN4feCbY(&S5&4PL7V5PCBKNA6P^(nK@0a4U%Nc-W+PF&CFZu>gZVP zs$<~f=vw5qxz|QVaq@%&uFZ#InVBZ@C9-Wk6VKK?T`z`_WAe*aiqrpCG73yqdVK}N z5S+}iRb=|_C`Q4}wr@PxSu%7@EGDnpptO10=XLSZWuqB2w$BY_6krAER@r_tg7Fg@ Rk6&bnce;6yURJSEEdXkbJRtx8 diff --git a/frontend/prebuilt/assets/SearchView-CITGP0uG.js b/frontend/prebuilt/assets/SearchView-WhBxZXuQ.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-CITGP0uG.js rename to frontend/prebuilt/assets/SearchView-WhBxZXuQ.js index f9959a3a3bc3e02ab2a1e11b2c5781895e123ae8..ed5dd986a7d3376bd67c74b7a5889332f9d1d092 100644 GIT binary patch delta 36 rcmeyQ{z-j;JBLd}nNPe^RLVyG$-)BR8BP^Z5v758S;cy}i3MB$1HBCw delta 36 rcmeyQ{z-j;JBPbZN|>>$q2EUT$-)B8o+0i52Bq$LS;cy}i3MB$?Vt+ delta 159 zcmX?qo8|m%mI?lL&gDVDDWS_E&h Z-ItMbihxV7S(LAtUn)?eUT$Im7XUbD7rOue delta 82 zcmey^tn#s0WkMg1yH84(v8$n_E&h Z-ItMbih!A4Wx8K_dLmGxUT$Im7XTGc7p4FJ diff --git a/frontend/prebuilt/assets/VisualizerView-6NygNgga.js.map b/frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-6NygNgga.js.map rename to frontend/prebuilt/assets/VisualizerView-DS6ZM6Ne.js.map index 3e7a45a8d000706b5483cb8b29603c7caa9024fe..275783d81c9d8680e1ee3843b9c12f466cd5a59b 100644 GIT binary patch delta 164 zcmWO0rx5}H006-u=bUrS2k*WDEEpV~IvB)xnn6+ty)Yow@8|oDeEy#E^zjm2PvQCY zKq3;An8YO^Nl8gsGLn^?Qk9z2r6EmeNn1M7m7ervAVV3+SSB)+napJ& UOIgWUHnNqS{11CMya!YH~u zk%S~AC27e>R&o+ZUJ6o_l9Z()RjEl`8q$=Ow520m=}BJ(GL(^wWg=6V$y^q)l$ESy UBU{C#V{!@CPw388hF(z^fj-q21EFFf%%?$j_Di{$1*pz zfj%qyZ}r&U8~OHeVU}$OLV3ZXChR7z1*KcytmG>zJggT|2V1j2D=0|^0uE#aC#md| r8=;>I1I?B&7EzwBgvUTNoRo9MJ}>8L_d@*mY_o-%`|0E4oUhRzMsGKE literal 0 HcmV?d00001 diff --git a/frontend/prebuilt/assets/hooks-B3DSOzgN.js.map b/frontend/prebuilt/assets/hooks-29eXpyfK.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-B3DSOzgN.js.map rename to frontend/prebuilt/assets/hooks-29eXpyfK.js.map index e0f9166294ab2d1e37f0d7d270d2fa74e422ed36..b448288f757e6fd123180f071307809ada403cc4 100644 GIT binary patch delta 19 acmaFC@q%N5G>4I8YD7V0n)gO!R~7(9PX==U delta 19 acmaFC@q%N5G>4P1OR#@cy5B}+R~7(7Y6d$1 diff --git a/frontend/prebuilt/assets/hooks-B3DSOzgN.js b/frontend/prebuilt/assets/hooks-B3DSOzgN.js deleted file mode 100644 index df72b4547f6e6691865b22b0cc593ab021b63287..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmW-au?~VT6h(KwqQM0QAc}(tVKEp7Q4@{s4ZLC%+UE5&Ark&w%Y4s0M-E0Kg=r>L z3uAJu+h=}QU(&Y?3%P8QW85;D1?u6?^}t0BlvHAK+U|fBQaII+)QVvgPYC2tF^b}J s;AV!=7bf%;Hy1&ct(esg8#r@gr0O2Gx_oKYU>;4-20LuF|LjV8( diff --git a/frontend/prebuilt/assets/index-Kkb-Bio9.css b/frontend/prebuilt/assets/index-Cx5ENk0V.css similarity index 60% rename from frontend/prebuilt/assets/index-Kkb-Bio9.css rename to frontend/prebuilt/assets/index-Cx5ENk0V.css index 8421d558659d2e84b03c59c9f8a35e752538c9aa..92492d9a51c66289248d7cfb1555d71ca1ef467a 100644 GIT binary patch delta 48 vcmccjlezU5^9Jo1ikT&;xy8E0B}J(v$r;s&IhpBsx?nLYsMu!J89P$|2^bXC delta 14 WcmZqu#eDB4^9Jo1n}cR-P5}Tq9|wd0 diff --git a/frontend/prebuilt/assets/index-GLdV3E1N.js b/frontend/prebuilt/assets/index-DxvL_BZd.js similarity index 96% rename from frontend/prebuilt/assets/index-GLdV3E1N.js rename to frontend/prebuilt/assets/index-DxvL_BZd.js index ed05510cacd494ee24a675b5a09f4e099b9395f3..ddf6565632dbff4bf815790c974bfd7cb82075ff 100644 GIT binary patch delta 371 zcmdmcR{7>x$Gc zIh>4Lg8i$~{U)wai1UjK@lH1nLeW#4T98bfVsVWS~)8avu&SN&hFpD4t3r1H$T}6*rC=u9nhundo4#=)hc(cG5cdFsQg^V>cA-ri+f4-AeNw`V PT@C$!9?;88EZ_nFIKy+4 diff --git a/frontend/prebuilt/assets/index-GLdV3E1N.js.map b/frontend/prebuilt/assets/index-DxvL_BZd.js.map similarity index 60% rename from frontend/prebuilt/assets/index-GLdV3E1N.js.map rename to frontend/prebuilt/assets/index-DxvL_BZd.js.map index 1a8e5bc5d85876e750b8a34cb6b9509cadd4351f..0b143aab9ca4408e577d0a24dfb369598c92fd19 100644 GIT binary patch delta 299 zcmbO}uaR;3{6@xx7RDB)7UmX~Ev$JCfr0w2I=+qpo;t3Mj*c!mo{o-zK)z$3GmPn8 z;p766^wsfpbo6(GFoDW^ogqw+VyLLMqo=v^_DK$`3j`SR+KmHQftU@5*@2h?h&h3n z3y8UamGrHSQLoGfLI)eC4g8Gh^2s78i-|pSQd!ow(Fmfk8b95_elveb~W_V%PLl? F1pqNPLDK*L diff --git a/frontend/prebuilt/assets/leaflet-Df2K48et.js b/frontend/prebuilt/assets/leaflet-DtWtaoJ1.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-Df2K48et.js rename to frontend/prebuilt/assets/leaflet-DtWtaoJ1.js index f571bcb8f03d4994acd105871133c46d026b5695..2cfac8ca43146cf561d82a3c551a03c2e163482e 100644 GIT binary patch delta 43 ycmZ3ok#osL&IzU*E)``y@lH`Gjkc|}j9YD)A|m)p!b=kKy$toTiuH063%CGP;0~q$ delta 43 ycmZ3ok#osL&IzU*?mj7D#;%5bjkc|}j9YD)A|m+HjJ!=OQcLu*iuH063%CG1^bM;3 diff --git a/frontend/prebuilt/assets/leaflet-Df2K48et.js.map b/frontend/prebuilt/assets/leaflet-DtWtaoJ1.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-Df2K48et.js.map rename to frontend/prebuilt/assets/leaflet-DtWtaoJ1.js.map index 54e7b98cffb6dbe2ce78df906aede8d46c53c52c..a09b1b4d193a650c0dad44d65edd3b0e07b9a781 100644 GIT binary patch delta 50 zcmZ4ZSZm>9tqF4MCE+EB`Cf*Ns;#Pwt*T6|s?4pbEUl`nt*UITs_d<*9IdLHTUEJg GHUa>(kq>$R delta 50 zcmZ4ZSZm>9tqF4MX-3{A7O5qTs;#Pwt*T6|s?4pbEUl`nt*UITs_d<*9IdLHTUEJg GHUa>ppbr=T diff --git a/frontend/prebuilt/assets/separator-iZHk50Ep.js b/frontend/prebuilt/assets/separator-nd9KgFnt.js similarity index 89% rename from frontend/prebuilt/assets/separator-iZHk50Ep.js rename to frontend/prebuilt/assets/separator-nd9KgFnt.js index bdb82d9a872e95c4e3fcc290be90a726fc8e067e..7dccdaab27e55e84ea986c153c66e3ec81043fd4 100644 GIT binary patch delta 36 rcmZ3(v4&%U0f$RPnNPe^RLVw+C(HtQDVE;pZh0koS;cy}i3MB$=hO^T delta 36 rcmZ3(v4&%U0f)O!N|>>$q2ES}C(HtwQ6AZ*2CfBqS;cy}i3MB$)n*H3 diff --git a/frontend/prebuilt/assets/separator-iZHk50Ep.js.map b/frontend/prebuilt/assets/separator-nd9KgFnt.js.map similarity index 98% rename from frontend/prebuilt/assets/separator-iZHk50Ep.js.map rename to frontend/prebuilt/assets/separator-nd9KgFnt.js.map index 15fe81406dc042c40ed32ba28063f5b5ac74fb06..1d8fe3dbc154a3342499adc42fac3bc7a395f5fa 100644 GIT binary patch delta 19 acmX@4en@?SJV#!NrFXhpUdcxF*}?!v;0E;o delta 19 acmX@4en@?SJV$1fN4BYfYr#hK*}?!txCWpA diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html index 5145914fdeb74c619cd90eb2ef6b0247e20abc8f..8cf690dc384314711419f62aecbcf33f44175b0f 100644 GIT binary patch delta 33 ocmX>pdQxpdQx {/* Top row: Telemetry + Radio Settings | Node Info + Neighbors */} -
+
-
+
{!data ? ( ) : sorted.length === 0 ? (

No neighbors reported

) : ( -
-
+
+
@@ -145,7 +145,7 @@ export function NeighborsPane({ {hasValidRepeaterGps && (neighborsWithCoords.length > 0 || hasValidRepeaterGps) ? ( +
Loading map...
} diff --git a/frontend/src/test/pathUtils.test.ts b/frontend/src/test/pathUtils.test.ts index 0b7f29e8..79dfdc49 100644 --- a/frontend/src/test/pathUtils.test.ts +++ b/frontend/src/test/pathUtils.test.ts @@ -7,6 +7,7 @@ import { formatRouteLabel, formatRoutingOverrideInput, getEffectiveContactRoute, + isValidLocation, resolvePath, formatDistance, formatHopCounts, @@ -665,6 +666,24 @@ describe('resolvePath', () => { }); }); +describe('isValidLocation', () => { + it('rejects null and unset coordinates', () => { + expect(isValidLocation(null, -122.3)).toBe(false); + expect(isValidLocation(47.6, null)).toBe(false); + expect(isValidLocation(0, 0)).toBe(false); + }); + + it('rejects out-of-range coordinates', () => { + expect(isValidLocation(-593.497573, -1659.939204)).toBe(false); + expect(isValidLocation(91, 0)).toBe(false); + expect(isValidLocation(0, 181)).toBe(false); + }); + + it('accepts sane coordinates', () => { + expect(isValidLocation(47.6062, -122.3321)).toBe(true); + }); +}); + describe('formatDistance', () => { it('formats distances under 1km in meters', () => { expect(formatDistance(0.5)).toBe('500m'); diff --git a/frontend/src/utils/pathUtils.ts b/frontend/src/utils/pathUtils.ts index 5d84f266..e0412526 100644 --- a/frontend/src/utils/pathUtils.ts +++ b/frontend/src/utils/pathUtils.ts @@ -273,6 +273,9 @@ export function isValidLocation(lat: number | null, lon: number | null): boolean if (lat === null || lon === null) { return false; } + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + return false; + } // (0, 0) is in the Atlantic Ocean - treat as unset if (lat === 0 && lon === 0) { return false; diff --git a/tests/test_decoder.py b/tests/test_decoder.py index a357a0b9..5d67ae34 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -448,6 +448,38 @@ class TestAdvertisementParsing: assert result.lat is None assert result.lon is None + def test_parse_advertisement_discards_out_of_range_gps(self, caplog): + """Out-of-range advert coordinates are treated as missing.""" + from app.decoder import parse_advertisement + + payload = bytearray() + payload.extend( + bytes.fromhex("f29fdc7c560f9d813d1593a8587fa46a9e7efe2f5506d38c0af41307bf9e517a") + ) + payload.extend((1718749967).to_bytes(4, byteorder="little")) + payload.extend(bytes(64)) + payload.append(0x92) + payload.extend((-593497573).to_bytes(4, byteorder="little", signed=True)) + payload.extend((-1659939204).to_bytes(4, byteorder="little", signed=True)) + payload.extend(b"Tacompton") + raw_packet = bytes.fromhex("11") + bytes(payload) + + with caplog.at_level("WARNING"): + result = parse_advertisement(bytes(payload), raw_packet=raw_packet) + + assert result is not None + assert ( + result.public_key == "f29fdc7c560f9d813d1593a8587fa46a9e7efe2f5506d38c0af41307bf9e517a" + ) + assert result.name == "Tacompton" + assert result.device_role == 2 + assert result.lat is None + assert result.lon is None + assert "Dropping location data for nonsensical packet -- packet" in caplog.text + assert raw_packet.hex().upper() in caplog.text + assert "-593.497573/-1659.939204" in caplog.text + assert "Outta this world!" in caplog.text + def test_parse_advertisement_extracts_public_key(self): """Advertisement parsing extracts the public key correctly.""" from app.decoder import parse_advertisement, parse_packet