From 370ff115b48a8e7e592e5ac90d9f1cccd4891c62 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Mon, 16 Mar 2026 14:53:48 -0700 Subject: [PATCH] Fix DM collapse on same second send --- app/AGENTS.md | 2 +- app/database.py | 10 +- app/migrations.py | 33 ++++ app/repository/raw_packets.py | 12 ++ app/services/messages.py | 154 ++++++++++++------ ...-B-rKDkVs.js => BotCodeEditor-CAcMF_eo.js} | Bin 467083 -> 467083 bytes ...s.js.map => BotCodeEditor-CAcMF_eo.js.map} | Bin 1956915 -> 1956915 bytes ...l-DzEfUHYA.js => CrackerPanel-oiYTLODx.js} | Bin 129410 -> 129410 bytes ...YA.js.map => CrackerPanel-oiYTLODx.js.map} | Bin 396562 -> 396562 bytes ...apView-CCM06mdk.js => MapView-D-l2CjgS.js} | Bin 4103 -> 4103 bytes ...CM06mdk.js.map => MapView-D-l2CjgS.js.map} | Bin 14925 -> 14925 bytes ...RN4lfs.js => NeighborsMiniMap-CstRT55H.js} | Bin 1670 -> 1670 bytes ...s.map => NeighborsMiniMap-CstRT55H.js.map} | Bin 5886 -> 5886 bytes ...p-BEJ9Rkwd.js => PathRouteMap-Bi6ttl0X.js} | Bin 3868 -> 3868 bytes ...wd.js.map => PathRouteMap-Bi6ttl0X.js.map} | Bin 14070 -> 14070 bytes .../{Popup-s69FReak.js => Popup-DqhwkenL.js} | Bin 920 -> 920 bytes ...-s69FReak.js.map => Popup-DqhwkenL.js.map} | Bin 4181 -> 4181 bytes ...EPkkr.js => RepeaterDashboard-DHzYsK_s.js} | Bin 29193 -> 29193 bytes ....map => RepeaterDashboard-DHzYsK_s.js.map} | Bin 92004 -> 92004 bytes ...iew-4aOlPoPL.js => SearchView-CwhbvaNz.js} | Bin 5106 -> 5106 bytes ...PoPL.js.map => SearchView-CwhbvaNz.js.map} | Bin 16886 -> 16886 bytes ...-Bje6NQB0.js => SettingsModal-BYAS3H3e.js} | Bin 77263 -> 77263 bytes ...0.js.map => SettingsModal-BYAS3H3e.js.map} | Bin 196690 -> 196690 bytes ...DClWBHTO.js => VisualizerView-CgyTsQ_S.js} | Bin 590321 -> 590321 bytes ....js.map => VisualizerView-CgyTsQ_S.js.map} | Bin 3112795 -> 3112795 bytes frontend/prebuilt/assets/hooks-DIGY9mWZ.js | Bin 0 -> 157 bytes ...-DtoQuSJA.js.map => hooks-DIGY9mWZ.js.map} | Bin 1128 -> 1128 bytes frontend/prebuilt/assets/hooks-DtoQuSJA.js | Bin 157 -> 0 bytes .../{index-CM9d1p2a.js => index-BMtXbNEw.js} | Bin 583385 -> 583385 bytes ...-CM9d1p2a.js.map => index-BMtXbNEw.js.map} | Bin 2117527 -> 2117527 bytes ...eaflet-CetcwTHt.js => leaflet-DgeNdCAZ.js} | Bin 153764 -> 153764 bytes ...etcwTHt.js.map => leaflet-DgeNdCAZ.js.map} | Bin 700833 -> 700833 bytes ...ator-DiyBVm1x.js => separator-C-sbEem-.js} | Bin 1068 -> 1068 bytes ...BVm1x.js.map => separator-C-sbEem-.js.map} | Bin 5058 -> 5058 bytes frontend/prebuilt/index.html | Bin 2761 -> 2761 bytes tests/test_echo_dedup.py | 30 ++-- tests/test_key_normalization.py | 7 +- tests/test_migrations.py | 38 +++-- tests/test_packet_pipeline.py | 68 +++++++- 39 files changed, 261 insertions(+), 93 deletions(-) rename frontend/prebuilt/assets/{BotCodeEditor-B-rKDkVs.js => BotCodeEditor-CAcMF_eo.js} (99%) rename frontend/prebuilt/assets/{BotCodeEditor-B-rKDkVs.js.map => BotCodeEditor-CAcMF_eo.js.map} (99%) rename frontend/prebuilt/assets/{CrackerPanel-DzEfUHYA.js => CrackerPanel-oiYTLODx.js} (99%) rename frontend/prebuilt/assets/{CrackerPanel-DzEfUHYA.js.map => CrackerPanel-oiYTLODx.js.map} (99%) rename frontend/prebuilt/assets/{MapView-CCM06mdk.js => MapView-D-l2CjgS.js} (92%) rename frontend/prebuilt/assets/{MapView-CCM06mdk.js.map => MapView-D-l2CjgS.js.map} (99%) rename frontend/prebuilt/assets/{NeighborsMiniMap-9qRN4lfs.js => NeighborsMiniMap-CstRT55H.js} (85%) rename frontend/prebuilt/assets/{NeighborsMiniMap-9qRN4lfs.js.map => NeighborsMiniMap-CstRT55H.js.map} (97%) rename frontend/prebuilt/assets/{PathRouteMap-BEJ9Rkwd.js => PathRouteMap-Bi6ttl0X.js} (95%) rename frontend/prebuilt/assets/{PathRouteMap-BEJ9Rkwd.js.map => PathRouteMap-Bi6ttl0X.js.map} (99%) rename frontend/prebuilt/assets/{Popup-s69FReak.js => Popup-DqhwkenL.js} (81%) rename frontend/prebuilt/assets/{Popup-s69FReak.js.map => Popup-DqhwkenL.js.map} (97%) rename frontend/prebuilt/assets/{RepeaterDashboard-4YMEPkkr.js => RepeaterDashboard-DHzYsK_s.js} (98%) rename frontend/prebuilt/assets/{RepeaterDashboard-4YMEPkkr.js.map => RepeaterDashboard-DHzYsK_s.js.map} (99%) rename frontend/prebuilt/assets/{SearchView-4aOlPoPL.js => SearchView-CwhbvaNz.js} (97%) rename frontend/prebuilt/assets/{SearchView-4aOlPoPL.js.map => SearchView-CwhbvaNz.js.map} (99%) rename frontend/prebuilt/assets/{SettingsModal-Bje6NQB0.js => SettingsModal-BYAS3H3e.js} (99%) rename frontend/prebuilt/assets/{SettingsModal-Bje6NQB0.js.map => SettingsModal-BYAS3H3e.js.map} (99%) rename frontend/prebuilt/assets/{VisualizerView-DClWBHTO.js => VisualizerView-CgyTsQ_S.js} (99%) rename frontend/prebuilt/assets/{VisualizerView-DClWBHTO.js.map => VisualizerView-CgyTsQ_S.js.map} (99%) create mode 100644 frontend/prebuilt/assets/hooks-DIGY9mWZ.js rename frontend/prebuilt/assets/{hooks-DtoQuSJA.js.map => hooks-DIGY9mWZ.js.map} (90%) delete mode 100644 frontend/prebuilt/assets/hooks-DtoQuSJA.js rename frontend/prebuilt/assets/{index-CM9d1p2a.js => index-BMtXbNEw.js} (99%) rename frontend/prebuilt/assets/{index-CM9d1p2a.js.map => index-BMtXbNEw.js.map} (99%) rename frontend/prebuilt/assets/{leaflet-CetcwTHt.js => leaflet-DgeNdCAZ.js} (99%) rename frontend/prebuilt/assets/{leaflet-CetcwTHt.js.map => leaflet-DgeNdCAZ.js.map} (99%) rename frontend/prebuilt/assets/{separator-DiyBVm1x.js => separator-C-sbEem-.js} (89%) rename frontend/prebuilt/assets/{separator-DiyBVm1x.js.map => separator-C-sbEem-.js.map} (98%) diff --git a/app/AGENTS.md b/app/AGENTS.md index 77ce4dbe..a5f00d0c 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -353,7 +353,7 @@ tests/ The MeshCore radio protocol encodes `sender_timestamp` as a 4-byte little-endian integer (Unix seconds). This is a firmware-level wire format — the radio, the Python library (`commands/messaging.py`), and the decoder (`decoder.py`) all read/write exactly 4 bytes. Millisecond Unix timestamps would overflow 4 bytes, so higher resolution is not possible without a firmware change. -**Consequence:** The dedup index `(type, conversation_key, text, COALESCE(sender_timestamp, 0))` operates at 1-second granularity. Sending identical text to the same conversation twice within one second will hit the UNIQUE constraint on the second insert, returning HTTP 500 *after* the radio has already transmitted. The message is sent over the air but not stored in the database. Do not attempt to fix this by switching to millisecond timestamps — it will break echo dedup (the echo's 4-byte timestamp won't match the stored value) and overflow `to_bytes(4, "little")`. +**Consequence:** Channel-message dedup still operates at 1-second granularity because the radio protocol only provides second-resolution `sender_timestamp`. Do not attempt to fix this by switching to millisecond timestamps — it will break echo dedup (the echo's 4-byte timestamp won't match the stored value) and overflow `to_bytes(4, "little")`. Direct messages no longer share that channel dedup index; they are deduplicated by raw-packet identity instead so legitimate same-text same-second DMs can coexist. ### Outgoing DM echoes remain undecrypted diff --git a/app/database.py b/app/database.py index 5e902156..838ba670 100644 --- a/app/database.py +++ b/app/database.py @@ -50,9 +50,10 @@ CREATE TABLE IF NOT EXISTS messages ( acked INTEGER DEFAULT 0, sender_name TEXT, sender_key TEXT - -- Deduplication: identical text + timestamp in the same conversation is treated as a - -- mesh echo/repeat. Outgoing sends allocate a collision-free sender_timestamp before - -- transmit so legitimate repeat sends do not collide with this index. + -- Deduplication: channel echoes/repeats use a channel-only unique index on + -- identical conversation/text/timestamp. Direct messages are deduplicated + -- separately via raw-packet linkage so legitimate same-text same-second DMs + -- can coexist. -- Enforced via idx_messages_dedup_null_safe (unique index) rather than a table constraint -- to avoid the storage overhead of SQLite's autoindex duplicating every message text. ); @@ -90,7 +91,8 @@ CREATE TABLE IF NOT EXISTS contact_name_history ( CREATE INDEX IF NOT EXISTS idx_messages_received ON messages(received_at); CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_dedup_null_safe - ON messages(type, conversation_key, text, COALESCE(sender_timestamp, 0)); + ON messages(type, conversation_key, text, COALESCE(sender_timestamp, 0)) + WHERE type = 'CHAN'; CREATE INDEX IF NOT EXISTS idx_raw_packets_message_id ON raw_packets(message_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_packets_payload_hash ON raw_packets(payload_hash); CREATE INDEX IF NOT EXISTS idx_contacts_on_radio ON contacts(on_radio); diff --git a/app/migrations.py b/app/migrations.py index 8ef18593..31611a29 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -331,6 +331,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int: await set_version(conn, 42) applied += 1 + # Migration 43: Limit message dedup index to channel messages only + if version < 43: + logger.info("Applying migration 43: narrow message dedup index to channels") + await _migrate_043_split_message_dedup_by_type(conn) + await set_version(conn, 43) + applied += 1 + if applied > 0: logger.info( "Applied %d migration(s), schema now at version %d", applied, await get_version(conn) @@ -2443,3 +2450,29 @@ async def _migrate_042_add_channel_flood_scope_override(conn: aiosqlite.Connecti raise await conn.commit() + + +async def _migrate_043_split_message_dedup_by_type(conn: aiosqlite.Connection) -> None: + """Restrict the message dedup index to channel messages.""" + cursor = await conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='messages'" + ) + if await cursor.fetchone() is None: + await conn.commit() + return + + cursor = await conn.execute("PRAGMA table_info(messages)") + columns = {row[1] for row in await cursor.fetchall()} + required_columns = {"type", "conversation_key", "text", "sender_timestamp"} + if not required_columns.issubset(columns): + logger.debug("messages table missing dedup-index columns, skipping migration 43") + await conn.commit() + return + + await conn.execute("DROP INDEX IF EXISTS idx_messages_dedup_null_safe") + await conn.execute( + """CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_dedup_null_safe + ON messages(type, conversation_key, text, COALESCE(sender_timestamp, 0)) + WHERE type = 'CHAN'""" + ) + await conn.commit() diff --git a/app/repository/raw_packets.py b/app/repository/raw_packets.py index f661bc3e..3a31e233 100644 --- a/app/repository/raw_packets.py +++ b/app/repository/raw_packets.py @@ -109,6 +109,18 @@ class RawPacketRepository: ) await db.conn.commit() + @staticmethod + async def get_linked_message_id(packet_id: int) -> int | None: + """Return the linked message ID for a raw packet, if any.""" + cursor = await db.conn.execute( + "SELECT message_id FROM raw_packets WHERE id = ?", + (packet_id,), + ) + row = await cursor.fetchone() + if not row: + return None + return row["message_id"] + @staticmethod async def prune_old_undecrypted(max_age_days: int) -> int: """Delete undecrypted packets older than max_age_days. Returns count deleted.""" diff --git a/app/services/messages.py b/app/services/messages.py index d23169ec..980db94c 100644 --- a/app/services/messages.py +++ b/app/services/messages.py @@ -1,3 +1,4 @@ +import asyncio import logging import time from collections.abc import Callable @@ -13,6 +14,7 @@ logger = logging.getLogger(__name__) BroadcastFn = Callable[..., Any] LOG_MESSAGE_PREVIEW_LEN = 32 +_decrypted_dm_store_lock = asyncio.Lock() def _truncate_for_log(text: str, max_chars: int = LOG_MESSAGE_PREVIEW_LEN) -> str: @@ -125,6 +127,45 @@ async def increment_ack_and_broadcast( return ack_count +async def _reconcile_duplicate_message( + *, + existing_msg: Message, + packet_id: int | None, + path: str | None, + received_at: int, + path_len: int | None, + broadcast_fn: BroadcastFn, +) -> None: + logger.debug( + "Duplicate %s for %s (msg_id=%d, outgoing=%s) - adding path", + existing_msg.type, + existing_msg.conversation_key[:12], + existing_msg.id, + existing_msg.outgoing, + ) + + if path is not None: + paths = await MessageRepository.add_path(existing_msg.id, path, received_at, path_len) + else: + paths = existing_msg.paths or [] + + if existing_msg.outgoing and existing_msg.type == "CHAN": + ack_count = await MessageRepository.increment_ack_count(existing_msg.id) + else: + ack_count = existing_msg.acked + + if existing_msg.outgoing or path is not None: + broadcast_message_acked( + message_id=existing_msg.id, + ack_count=ack_count, + paths=paths, + broadcast_fn=broadcast_fn, + ) + + if packet_id is not None: + await RawPacketRepository.mark_decrypted(packet_id, existing_msg.id) + + async def handle_duplicate_message( *, packet_id: int | None, @@ -153,35 +194,15 @@ async def handle_duplicate_message( ) return - logger.debug( - "Duplicate %s for %s (msg_id=%d, outgoing=%s) - adding path", - msg_type, - conversation_key[:12], - existing_msg.id, - existing_msg.outgoing, + await _reconcile_duplicate_message( + existing_msg=existing_msg, + packet_id=packet_id, + path=path, + received_at=received_at, + path_len=path_len, + broadcast_fn=broadcast_fn, ) - if path is not None: - paths = await MessageRepository.add_path(existing_msg.id, path, received_at, path_len) - else: - paths = existing_msg.paths or [] - - if existing_msg.outgoing and existing_msg.type == "CHAN": - ack_count = await MessageRepository.increment_ack_count(existing_msg.id) - else: - ack_count = existing_msg.acked - - if existing_msg.outgoing or path is not None: - broadcast_message_acked( - message_id=existing_msg.id, - ack_count=ack_count, - paths=paths, - broadcast_fn=broadcast_fn, - ) - - if packet_id is not None: - await RawPacketRepository.mark_decrypted(packet_id, existing_msg.id) - async def create_message_from_decrypted( *, @@ -290,32 +311,64 @@ async def create_dm_message_from_decrypted( conversation_key = their_public_key.lower() sender_name = contact.name if contact and not outgoing else None - msg_id = await MessageRepository.create( - msg_type="PRIV", - text=decrypted.message, - conversation_key=conversation_key, - sender_timestamp=decrypted.timestamp, - received_at=received, - path=path, - path_len=path_len, - outgoing=outgoing, - sender_key=conversation_key if not outgoing else None, - sender_name=sender_name, - ) + async with _decrypted_dm_store_lock: + linked_message_id = await RawPacketRepository.get_linked_message_id(packet_id) + if linked_message_id is not None: + existing_msg = await MessageRepository.get_by_id(linked_message_id) + if existing_msg is not None: + await _reconcile_duplicate_message( + existing_msg=existing_msg, + packet_id=packet_id, + path=path, + received_at=received, + path_len=path_len, + broadcast_fn=broadcast_fn, + ) + return None - if msg_id is None: - await handle_duplicate_message( - packet_id=packet_id, + if outgoing: + existing_msg = await MessageRepository.get_by_content( + msg_type="PRIV", + conversation_key=conversation_key, + text=decrypted.message, + sender_timestamp=decrypted.timestamp, + ) + if existing_msg is not None: + await _reconcile_duplicate_message( + existing_msg=existing_msg, + packet_id=packet_id, + path=path, + received_at=received, + path_len=path_len, + broadcast_fn=broadcast_fn, + ) + return None + + msg_id = await MessageRepository.create( msg_type="PRIV", - conversation_key=conversation_key, text=decrypted.message, + conversation_key=conversation_key, sender_timestamp=decrypted.timestamp, - path=path, received_at=received, + path=path, path_len=path_len, - broadcast_fn=broadcast_fn, + outgoing=outgoing, + sender_key=conversation_key if not outgoing else None, + sender_name=sender_name, ) - return None + if msg_id is None: + await handle_duplicate_message( + packet_id=packet_id, + msg_type="PRIV", + conversation_key=conversation_key, + text=decrypted.message, + sender_timestamp=decrypted.timestamp, + path=path, + received_at=received, + path_len=path_len, + broadcast_fn=broadcast_fn, + ) + return None logger.info( 'Stored direct message "%s" for %r (msg ID %d in contact ID %s, outgoing=%s)', @@ -364,6 +417,15 @@ async def create_fallback_direct_message( message_repository=MessageRepository, ) -> Message | None: """Store and broadcast a CONTACT_MSG_RECV fallback direct message.""" + existing = await message_repository.get_by_content( + msg_type="PRIV", + conversation_key=conversation_key, + text=text, + sender_timestamp=sender_timestamp, + ) + if existing is not None: + return None + msg_id = await message_repository.create( msg_type="PRIV", text=text, diff --git a/frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js b/frontend/prebuilt/assets/BotCodeEditor-CAcMF_eo.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js rename to frontend/prebuilt/assets/BotCodeEditor-CAcMF_eo.js index 6fe75b68d496488bd2fe9f2368ee4f6d2217ac73..5cf59fe33088def78f159a314951e79b94bbbc82 100644 GIT binary patch delta 59 zcmeA^DARpVW`YWblW$2xlAmjNqfV<1W2+8Rs}6Ik4oj;JYpV|1RvmVK1p#NrWM8-V O)O@|HV!hnN0xkeF@)6Mh delta 59 zcmeA^DARpVW`YWbv#(`}VS!O%qfV<1W2+8Rs}6Ik4oj;JYpV|1RvmVK1pz1BB5#-M OuwuQeV!hnN0xkd{^bukJ diff --git a/frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js.map b/frontend/prebuilt/assets/BotCodeEditor-CAcMF_eo.js.map similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-B-rKDkVs.js.map rename to frontend/prebuilt/assets/BotCodeEditor-CAcMF_eo.js.map index 2b3bfa80f5e66a231c152fcf635dc41b92bdd435..9b10c82940aa0ad0f12b8f7df8d63ad5088a915f 100644 GIT binary patch delta 108 zcmV~$yA6U+06@|BL7spj_(4%C%R7Z3v2z9rV{J)oaeH?mCJf^+p3@IqKXjiIej^Y-yiQK9*_V4 delta 108 zcmV~$yA6U+06@|BL7spj_(4%C%Nv&Czb5wAhjIj^yM6Wf9tRwhX4Qo diff --git a/frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js b/frontend/prebuilt/assets/CrackerPanel-oiYTLODx.js similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-DzEfUHYA.js rename to frontend/prebuilt/assets/CrackerPanel-oiYTLODx.js index 51e0cacfbeabefc1735896c8cce6d52dc3a8c2f5..9a73fba411fa490bd299840b1179838f0c35c907 100644 GIT binary patch delta 41 xcmZqr%--~weL@$9lW$2xlAmjNz delta 41 xcmZqr%--~weL@$9v#(`}VS!O%F7M2#)Eo`j3y!n}tAwK>t6?$34O0@vb`3nmG delta 35 qcmbOF7M2#)Eo`j3ye?I)X`vpGj(Sp3lVjitL{5k(RmP bQ5%mN@C&%;<`_9=r3dR}73<|D7H|OoM2#0J delta 71 zcmZoyXjhnE&f)B9nPOOAlsM5|lfyZ+B)L4qqhw-%I!CdYrCU&HV)n%OitHu%fu+G- bjvJ2~@C!IQ`x=<#rey1773<|D7H|OoHc}S- diff --git a/frontend/prebuilt/assets/MapView-CCM06mdk.js.map b/frontend/prebuilt/assets/MapView-D-l2CjgS.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-CCM06mdk.js.map rename to frontend/prebuilt/assets/MapView-D-l2CjgS.js.map index b01df0c98fdf25f1d94e116ba63db9e7d874615b..3718397262190027b9d4da4138fb0ef02d7ad2b6 100644 GIT binary patch delta 19 acmX?Ga<*iGEQgD3j*)X#dhkY74J!anng-?o delta 19 acmX?Ga<*iGEQhnRuYp-^O7=!o4J!anO9tft diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js b/frontend/prebuilt/assets/NeighborsMiniMap-CstRT55H.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-9qRN4lfs.js rename to frontend/prebuilt/assets/NeighborsMiniMap-CstRT55H.js index 1693c641a0f02ed5f8ee85717403fa8e069c0258..0fb0bcb49a4cbc8d3489f2f039ed95f40d28f813 100644 GIT binary patch delta 60 zcmZqUZR4FF&Ee!*5|QNRT0T+PmBS@H)i1@_F=}FgI)_VPMtOE>p3lbl*I5Oei%Wt+ POiexXvWoR`6AQQiQ6>}5 delta 60 zcmZqUZR4FF&Ef28nPOOAlsHk@mBTr;B)L4qqhw-%I!CdYrCU&HV)n-Q*I5ND3xoVj Pa?**XdEZ~*``T@w}n delta 58 zcmbOuH%D%QCWo`HWr|^eQQ}0yU=HWhlH~FbkCKT!itHu%fu+G-jvKc`^6|TRSq5d7 Or|4xB>*XdEZ~*``trJ23 diff --git a/frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js.map b/frontend/prebuilt/assets/PathRouteMap-Bi6ttl0X.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap-BEJ9Rkwd.js.map rename to frontend/prebuilt/assets/PathRouteMap-Bi6ttl0X.js.map index bce486ca7a682fa0b7e8efb9822acd3563b95040..8cedc4c364ffbeb0beace7f9f786b0ba9751c564 100644 GIT binary patch delta 18 ZcmeyC`z?2ZGJB?3NlA`D#73<%W&lx$2iX7s delta 18 ZcmeyC`z?2ZGP|pnWl(l`%0{g-W&lrs2fhFR diff --git a/frontend/prebuilt/assets/Popup-s69FReak.js b/frontend/prebuilt/assets/Popup-DqhwkenL.js similarity index 81% rename from frontend/prebuilt/assets/Popup-s69FReak.js rename to frontend/prebuilt/assets/Popup-DqhwkenL.js index 2266132836edef2d05d6e50d92c20db486ca5fdf..4e4c8d57d349c2364142268c05166a45b65fdb35 100644 GIT binary patch delta 48 zcmbQiK7)ON35QF1s$Yt;W7I?&1r8_Ql87Wf*Yb@iD$D{dg&F19sd+wnS;cy}i3MB$ Da!wCY delta 48 zcmbQiK7)ON35RoPNpg9JN6ADR1rBFl%M`-`qr{CVD$D}KW|nS2sfpQoS;cy}i3MB$ DaEA`x diff --git a/frontend/prebuilt/assets/Popup-s69FReak.js.map b/frontend/prebuilt/assets/Popup-DqhwkenL.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-s69FReak.js.map rename to frontend/prebuilt/assets/Popup-DqhwkenL.js.map index d9449f75618ee7e2f4e9b71ffa518a393e17a8e4..bae74710a0b39b1ae0424e9f95b92e234654fdd5 100644 GIT binary patch delta 19 acmcbra8+S~G>1!JMtOE>p3g>QBLM(PuLjZp delta 19 acmcbra8+S~G)J+SrCU&HV)jO5BLM(LPzGNB diff --git a/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js b/frontend/prebuilt/assets/RepeaterDashboard-DHzYsK_s.js similarity index 98% rename from frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js rename to frontend/prebuilt/assets/RepeaterDashboard-DHzYsK_s.js index 6639056ce267c7e0b074450d20c81323ce587b6a..33dd7df7bb6a3f532370c9d450427454937b347b 100644 GIT binary patch delta 129 zcmeBt!r1wQaYB%?b8$&fh^eWEURJS^j#6TAacW7ier8@uYK5+oZ%IUwpKJNVQbP`x z^i;nTXUC|C>wxUSjPmT%JfDfr4{^ZMOy0<-!r`o2oaCCCtGoFw<2ii}n8}+r8@f3N UxOh}W7JJ7R1MShvO)TI70Lzdq+5i9m delta 129 zcmeBt!r1wQaYB%?Wnqw?Nlsd^URJS^j#6TAacW7ier8@uYK5+|uVsp1fl=bbQbP{s z)RN@#5Ra0H>kK)H%`Dx5QWLW$K0m|(Q!{xZqY8&hW~EbDu3^RIyNu`bIbbGl-fZaR VAYc;d>l%=qT?Dj8FE_D(3jn;eEtUWP diff --git a/frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js.map b/frontend/prebuilt/assets/RepeaterDashboard-DHzYsK_s.js.map similarity index 99% rename from frontend/prebuilt/assets/RepeaterDashboard-4YMEPkkr.js.map rename to frontend/prebuilt/assets/RepeaterDashboard-DHzYsK_s.js.map index 456290e3855e49e5dfc3367b9321a89c081de9eb..1cede9d78d8c1f6815cb81f837c0aa655cdfe5b7 100644 GIT binary patch delta 25 gcmaEIj`hho)`l&N;px0C9#xUW-tonHS;b1V0Fg`z2mk;8 delta 25 gcmaEIj`hho)`l&N;px04k-n}0+1W*US;b1V0FI;y&;S4c diff --git a/frontend/prebuilt/assets/SearchView-4aOlPoPL.js b/frontend/prebuilt/assets/SearchView-CwhbvaNz.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-4aOlPoPL.js rename to frontend/prebuilt/assets/SearchView-CwhbvaNz.js index a6fd9a698fa9aaae6402e81544612ec05cc9d108..3f5b89f5aafc23153964b459c4ee6e9bcb12f4dd 100644 GIT binary patch delta 36 rcmeyQ{z-j;JBO2RNko#LYxze1$-)B8xv_DQ*>nK~M(N4^dqpPmoA7S` YEx~wHgg??T*x1836=;=SZejr!0Mn8yQ2+n{ delta 132 zcmX?qo8|m%mI?mKPP#?jF4_E&h Z-ItMbihy%^Wk_*gd@xX>UT$Im7XUd=7&QO@ delta 82 zcmey^tn#s0WkMg1v#(`}VS!PiURJTvWJadV&AyE7zKkHu1jNih%mT!$K+Fcj>_E&h Z-ItMbihzrAPPmgth(A!HUT$Im7XTO47g+!R diff --git a/frontend/prebuilt/assets/VisualizerView-DClWBHTO.js.map b/frontend/prebuilt/assets/VisualizerView-CgyTsQ_S.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-DClWBHTO.js.map rename to frontend/prebuilt/assets/VisualizerView-CgyTsQ_S.js.map index d0d7dfab23ed463b77b48ae383a549df5edd4121..56713bce5af8c5e2c941c5f6199c5c4123fe2162 100644 GIT binary patch delta 164 zcmWO0w-JE>0D#exe>vxz(}y8U;OLbGP^p~a>n)=+iCrAU>)+@54t@TflVE?jovw%T z@qvUSED?!HOyZJ|K$4P@v}7bJImt^wic*rYRHQ03sY^qe(vr4xq$@q?%Rq)QlCex= VDl?hOLYA_Uwfqko*}mBA?mq*rFG>Ia delta 164 zcmWO0w-JE>0D#exe>vxz(}x{U=rryuVF#!9ddnzHVi!m8`uF+1L!ZCrBsd;!*Zt{o zejp(UOGKg)lei=#kffv}Eg8v5PV!QaqLid86{$*1>e7&=w4^N^=}J%fGLWH+WGoYz V%1q|6kfp3-E&sztwl8+O`wzaIF4O=3 diff --git a/frontend/prebuilt/assets/hooks-DIGY9mWZ.js b/frontend/prebuilt/assets/hooks-DIGY9mWZ.js new file mode 100644 index 0000000000000000000000000000000000000000..69b245732c87790bb1304337c8fcc6ec2c8d0a82 GIT binary patch literal 157 zcmW-ayAFat5Jh{wVuKA7Ky8SH3Na=^VPbrAD-5{GJ~BHSNeKV0OMTBhM@XZQ!ZZ`A zg)!MxbbHq2H+|bM7t1y|#x0|{LoNT@0K6N3lA?#Y-ESWY**Vpa(28LcPYC2tF^b|; sax=r|3ln;aoAV&cR?KRL4OHA1q58}Dw0UX$u}N_}+}HBzo&Fm90419?^Z)<= literal 0 HcmV?d00001 diff --git a/frontend/prebuilt/assets/hooks-DtoQuSJA.js.map b/frontend/prebuilt/assets/hooks-DIGY9mWZ.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-DtoQuSJA.js.map rename to frontend/prebuilt/assets/hooks-DIGY9mWZ.js.map index 573bb41cfd8199c44c729b874b7bc9aff22a9b20..b6fc5512ef54d61f10c01793e2cff923f4bc90f4 100644 GIT binary patch delta 18 ZcmaFC@q%N547;a$q-Ab+)JA1j763lh1@Qm? delta 18 ZcmaFC@q%N540}m_U}>o! diff --git a/frontend/prebuilt/assets/hooks-DtoQuSJA.js b/frontend/prebuilt/assets/hooks-DtoQuSJA.js deleted file mode 100644 index e3594f31edc42ee2d43e2ca5be99889f49d758ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmW-ay$*sf6h?QRqQM0QXmm5g!Qfzw4E{Yp0~f49Z*zMaNl4#aVE)c`jvg!$$GOy| zfiWwqeY3SYm494Z=yjXCP)=}Y35S{MLA5<_Rsz?*=b2Zs^`>TxMo^MY1nkKOPSSi( rHo&+PCR!{$6p@r0;m}hJm9kcw?sdN&-iTk9ofvQaaXHS;x4TMbHk%1u91&-@u-R{_Kq(`(NmmSkXV#hl3%3jtXrJqnwqPNBJG=45SE!*uIr+k zW8|Eb9*iOxkY7+*pzBhYQJ$Te=QCN0QHDLzG1%C{IJMcAvE7#ugqeVt8Hic7`!cdR zm9azJ-9D|H6^PlkPb+8lYvO=8ZTg#^?D-rp%cdv)VlU!wPOl6p4vY`pe)ku<`8Jph z)1^0Z6maBcMuzzKyHrdE%E?XNxRJw}!?`>osVvd23M{l;XcNbF69FgRl87Wf*K(li J^>PynxB#=ha-IMH delta 326 zcmcb4R{7>xM@(A(Ye)ku<`8Jph z)1^0Z6mYmyxu%7BL^@6f%E?XNxRJw}!z9r^Cm=t-2Q0K*XcNbF69H#m%M`-`qeP(V J^>PynxB#*^aTfpp diff --git a/frontend/prebuilt/assets/index-CM9d1p2a.js.map b/frontend/prebuilt/assets/index-BMtXbNEw.js.map similarity index 99% rename from frontend/prebuilt/assets/index-CM9d1p2a.js.map rename to frontend/prebuilt/assets/index-BMtXbNEw.js.map index 25ed43d269e44008cb9c1484dbf21d8ed2b5285c..6708e37925058a3b5aa9a099b6f42bc1a5bd395a 100644 GIT binary patch delta 89 zcmbO}zmah|5I3|iwlK9Yx3ILZwy?FZw{Wy@ws5s@xA3&^w(zy^w+OTdwg|Ndw}`Ze rwurTew@9=|wn()|x5%`}w#c=}Z&B#-=XLTeiAeHuE!WE`R;mR6_Wc|P delta 89 zcmbO}zmah|5I3|iwlK9Yx3ILZwy?FZw{Wy@ws5s@xA3&^w(zy^w+OTdwg|Ndw}`Ze rwurTew@9=|wn()|x5%`}w#c=}Z&B#-=XLhAOff7lO4Q3LR;mR6^G6$^ diff --git a/frontend/prebuilt/assets/leaflet-CetcwTHt.js b/frontend/prebuilt/assets/leaflet-DgeNdCAZ.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-CetcwTHt.js rename to frontend/prebuilt/assets/leaflet-DgeNdCAZ.js index 9517f74d557b5a96709e08efdf9b44dc290b1732..d639aa1b117951e3eaa6cf3bb006c1f2afcb0c08 100644 GIT binary patch delta 44 zcmZ3ok#osL&IzU*PQE1(Nq(;7jkc|}j9YD)!XpG+(o_9XoE@X|vWoR`6AQQiUEL0* delta 44 zcmZ3ok#osL&IzU*&c2o@h6P56jkc|}j9YD)!XpHnQ%jP|Lp)0KvWoR`6AQQiRp$<- diff --git a/frontend/prebuilt/assets/leaflet-CetcwTHt.js.map b/frontend/prebuilt/assets/leaflet-DgeNdCAZ.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-CetcwTHt.js.map rename to frontend/prebuilt/assets/leaflet-DgeNdCAZ.js.map index 01f7bc050a48929cc77a7fe7704783bdb61bf213..6549b6f8f907b1ca32b906660571ab0090edab82 100644 GIT binary patch delta 51 zcmZ4ZSZm>9tqHOmF6pU$Db9{jjjFAxjIF9nt*Xqesw}OltgWhSt*Y#;svNDVoLg17 HYBmA@y@(Hx delta 51 zcmZ4ZSZm>9tqHOm&Z#BIpdQxpdQx