mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-10 07:15:09 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b7e2737ee | |||
| 01158ac69f | |||
| 485df05372 | |||
| e5e9eab935 | |||
| 33b2d3c260 | |||
| eccbd0bac5 |
@@ -1,3 +1,13 @@
|
|||||||
|
## [3.11.3] - 2026-04-12
|
||||||
|
|
||||||
|
* Bugfix: Add icons and screenshots for webmanifest
|
||||||
|
* Bugfix: Use incoming DMs, not just outgoing, for recency ranking for preferential radio contact load
|
||||||
|
|
||||||
|
## [3.11.2] - 2026-04-12
|
||||||
|
|
||||||
|
* Feature: Unread DMs are always at the top of the DM list no matter what
|
||||||
|
* Bugfix: Webmanifest needs withCredentials
|
||||||
|
|
||||||
## [3.11.1] - 2026-04-12
|
## [3.11.1] - 2026-04-12
|
||||||
|
|
||||||
* Feature: Home Assistant MQTT fanout
|
* Feature: Home Assistant MQTT fanout
|
||||||
|
|||||||
@@ -148,6 +148,39 @@ def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool:
|
|||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable",
|
"purpose": "maskable",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"src": f"{base}favicon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": f"{base}favicon-256x256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": f"{base}screenshot-wide.png",
|
||||||
|
"sizes": "1367x909",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "wide",
|
||||||
|
"label": "RemoteTerm desktop view",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": f"{base}screenshot-mobile.png",
|
||||||
|
"sizes": "1170x2532",
|
||||||
|
"type": "image/png",
|
||||||
|
"label": "RemoteTerm mobile view",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": f"{base}screenshot-mobile-2.png",
|
||||||
|
"sizes": "750x1334",
|
||||||
|
"type": "image/png",
|
||||||
|
"label": "RemoteTerm mobile conversation",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
+12
-6
@@ -1295,7 +1295,13 @@ async def stop_background_contact_reconciliation() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def get_contacts_selected_for_radio_sync() -> list[Contact]:
|
async def get_contacts_selected_for_radio_sync() -> list[Contact]:
|
||||||
"""Return the contacts that would be loaded onto the radio right now."""
|
"""Return the contacts that would be loaded onto the radio right now.
|
||||||
|
|
||||||
|
Fill order:
|
||||||
|
1. Favorites (up to full capacity)
|
||||||
|
2. Most recently DM-active non-repeaters (sent or received, up to 80% refill target)
|
||||||
|
3. Most recently advertised non-repeaters (up to 80% refill target)
|
||||||
|
"""
|
||||||
app_settings = await AppSettingsRepository.get()
|
app_settings = await AppSettingsRepository.get()
|
||||||
max_contacts = _effective_radio_capacity(app_settings.max_radio_contacts)
|
max_contacts = _effective_radio_capacity(app_settings.max_radio_contacts)
|
||||||
refill_target, _full_sync_trigger = _compute_radio_contact_limits(max_contacts)
|
refill_target, _full_sync_trigger = _compute_radio_contact_limits(max_contacts)
|
||||||
@@ -1315,7 +1321,7 @@ async def get_contacts_selected_for_radio_sync() -> list[Contact]:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if len(selected_contacts) < refill_target:
|
if len(selected_contacts) < refill_target:
|
||||||
for contact in await ContactRepository.get_recently_contacted_non_repeaters(
|
for contact in await ContactRepository.get_recently_dm_active_non_repeaters(
|
||||||
limit=max_contacts
|
limit=max_contacts
|
||||||
):
|
):
|
||||||
key = contact.public_key.lower()
|
key = contact.public_key.lower()
|
||||||
@@ -1354,8 +1360,8 @@ async def _sync_contacts_to_radio_inner(mc: MeshCore) -> dict:
|
|||||||
|
|
||||||
Fill order is:
|
Fill order is:
|
||||||
1. Favorite contacts
|
1. Favorite contacts
|
||||||
2. Most recently interacted-with non-repeaters
|
2. Most recently DM-active non-repeaters (sent or received)
|
||||||
3. Most recently advert-heard non-repeaters without interaction history
|
3. Most recently advert-heard non-repeaters
|
||||||
|
|
||||||
Favorite contacts are always reloaded first, up to the configured capacity.
|
Favorite contacts are always reloaded first, up to the configured capacity.
|
||||||
Additional non-favorite fill stops at the refill target (80% of capacity).
|
Additional non-favorite fill stops at the refill target (80% of capacity).
|
||||||
@@ -1489,8 +1495,8 @@ async def sync_recent_contacts_to_radio(force: bool = False, mc: MeshCore | None
|
|||||||
"""
|
"""
|
||||||
Load contacts to the radio for DM ACK support.
|
Load contacts to the radio for DM ACK support.
|
||||||
|
|
||||||
Fill order is favorites, then recently contacted non-repeaters,
|
Fill order is favorites, then recently DM-active non-repeaters (sent or
|
||||||
then recently advert-heard non-repeaters. Favorites are always reloaded
|
received), then recently advert-heard non-repeaters. Favorites are always reloaded
|
||||||
up to the configured capacity; additional non-favorite fill stops at the
|
up to the configured capacity; additional non-favorite fill stops at the
|
||||||
80% refill target.
|
80% refill target.
|
||||||
Only runs at most once every CONTACT_SYNC_THROTTLE_SECONDS unless forced.
|
Only runs at most once every CONTACT_SYNC_THROTTLE_SECONDS unless forced.
|
||||||
|
|||||||
@@ -294,6 +294,28 @@ class ContactRepository:
|
|||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
return [ContactRepository._row_to_contact(row) for row in rows]
|
return [ContactRepository._row_to_contact(row) for row in rows]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_recently_dm_active_non_repeaters(limit: int = 200) -> list[Contact]:
|
||||||
|
"""Get non-repeater contacts with the most recent DM activity (sent or received)."""
|
||||||
|
cursor = await db.conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT c.*
|
||||||
|
FROM contacts c
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT conversation_key, MAX(received_at) AS last_dm
|
||||||
|
FROM messages
|
||||||
|
WHERE type = 'PRIV'
|
||||||
|
GROUP BY conversation_key
|
||||||
|
) m ON c.public_key = m.conversation_key
|
||||||
|
WHERE c.type != 2 AND length(c.public_key) = 64
|
||||||
|
ORDER BY m.last_dm DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(limit,),
|
||||||
|
)
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [ContactRepository._row_to_contact(row) for row in rows]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_recently_advertised_non_repeaters(limit: int = 200) -> list[Contact]:
|
async def get_recently_advertised_non_repeaters(limit: int = 200) -> list[Contact]:
|
||||||
"""Get recently advert-heard non-repeater contacts."""
|
"""Get recently advert-heard non-repeater contacts."""
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
<link rel="icon" type="image/png" href="./favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="./favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="shortcut icon" href="./favicon.ico" />
|
<link rel="shortcut icon" href="./favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="./site.webmanifest" />
|
<link rel="manifest" href="./site.webmanifest" crossorigin="use-credentials" />
|
||||||
<script>
|
<script>
|
||||||
// Register minimal service worker for PWA installability.
|
// Register minimal service worker for PWA installability.
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "remoteterm-meshcore-frontend",
|
"name": "remoteterm-meshcore-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.11.1",
|
"version": "3.11.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -265,6 +265,12 @@ export function Sidebar({
|
|||||||
const sortContactsByOrder = useCallback(
|
const sortContactsByOrder = useCallback(
|
||||||
(items: Contact[], order: SortOrder) =>
|
(items: Contact[], order: SortOrder) =>
|
||||||
[...items].sort((a, b) => {
|
[...items].sort((a, b) => {
|
||||||
|
// Unread DM contacts always float to the top
|
||||||
|
const unreadA = unreadCounts[getStateKey('contact', a.public_key)] || 0;
|
||||||
|
const unreadB = unreadCounts[getStateKey('contact', b.public_key)] || 0;
|
||||||
|
if (unreadA > 0 && unreadB === 0) return -1;
|
||||||
|
if (unreadA === 0 && unreadB > 0) return 1;
|
||||||
|
|
||||||
if (order === 'recent') {
|
if (order === 'recent') {
|
||||||
const timeA = getContactRecentTime(a);
|
const timeA = getContactRecentTime(a);
|
||||||
const timeB = getContactRecentTime(b);
|
const timeB = getContactRecentTime(b);
|
||||||
@@ -274,7 +280,7 @@ export function Sidebar({
|
|||||||
}
|
}
|
||||||
return (a.name || a.public_key).localeCompare(b.name || b.public_key);
|
return (a.name || a.public_key).localeCompare(b.name || b.public_key);
|
||||||
}),
|
}),
|
||||||
[getContactRecentTime]
|
[getContactRecentTime, unreadCounts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortRepeatersByOrder = useCallback(
|
const sortRepeatersByOrder = useCallback(
|
||||||
|
|||||||
@@ -513,6 +513,42 @@ describe('Sidebar section summaries', () => {
|
|||||||
expect(contactRows).toEqual(['DM Recent', 'Advert Only', 'No Recency']);
|
expect(contactRows).toEqual(['DM Recent', 'Advert Only', 'No Recency']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('floats contacts with unread DMs above read contacts regardless of recency', () => {
|
||||||
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
||||||
|
const readRecent = makeContact('11'.repeat(32), 'Read Recent', 1, { last_advert: 500 });
|
||||||
|
const unreadOld = makeContact('22'.repeat(32), 'Unread Old', 1, { last_advert: 100 });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Sidebar
|
||||||
|
contacts={[readRecent, unreadOld]}
|
||||||
|
channels={[publicChannel]}
|
||||||
|
activeConversation={null}
|
||||||
|
onSelectConversation={vi.fn()}
|
||||||
|
onNewMessage={vi.fn()}
|
||||||
|
lastMessageTimes={{
|
||||||
|
[getStateKey('contact', readRecent.public_key)]: 500,
|
||||||
|
[getStateKey('contact', unreadOld.public_key)]: 200,
|
||||||
|
}}
|
||||||
|
unreadCounts={{
|
||||||
|
[getStateKey('contact', unreadOld.public_key)]: 3,
|
||||||
|
}}
|
||||||
|
mentions={{}}
|
||||||
|
showCracker={false}
|
||||||
|
crackerRunning={false}
|
||||||
|
onToggleCracker={vi.fn()}
|
||||||
|
onMarkAllRead={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contactRows = screen
|
||||||
|
.getAllByText(/^(Read Recent|Unread Old)$/)
|
||||||
|
.map((node) => node.textContent)
|
||||||
|
.filter((text): text is string => Boolean(text));
|
||||||
|
|
||||||
|
// Unread Old has unread DMs so it floats above Read Recent despite older recency
|
||||||
|
expect(contactRows).toEqual(['Unread Old', 'Read Recent']);
|
||||||
|
});
|
||||||
|
|
||||||
it('sorts repeaters by heard recency even when message times disagree', () => {
|
it('sorts repeaters by heard recency even when message times disagree', () => {
|
||||||
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
||||||
const staleMessageRelay = makeContact(
|
const staleMessageRelay = makeContact(
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "remoteterm-meshcore"
|
name = "remoteterm-meshcore"
|
||||||
version = "3.11.1"
|
version = "3.11.3"
|
||||||
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
|
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+122
-7
@@ -377,14 +377,22 @@ class TestSyncRecentContactsToRadio:
|
|||||||
assert result["loaded"] == 2
|
assert result["loaded"] == 2
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_fills_remaining_slots_with_recently_contacted_then_advertised(self, test_db):
|
async def test_fills_remaining_slots_with_dm_active_then_advertised(self, test_db):
|
||||||
"""Fill order is favorites, then recent contacts, then recent adverts."""
|
"""Fill order is favorites, then DM-active contacts, then recent adverts."""
|
||||||
await _insert_contact(KEY_A, "Alice", last_contacted=100)
|
await _insert_contact(KEY_A, "Alice")
|
||||||
await _insert_contact(KEY_B, "Bob", last_contacted=2000)
|
await _insert_contact(KEY_B, "Bob")
|
||||||
await _insert_contact("cc" * 32, "Carol", last_contacted=1000)
|
await _insert_contact("cc" * 32, "Carol")
|
||||||
await _insert_contact("dd" * 32, "Dave", last_advert=3000)
|
await _insert_contact("dd" * 32, "Dave", last_advert=3000)
|
||||||
await _insert_contact("ee" * 32, "Eve", last_advert=2500)
|
await _insert_contact("ee" * 32, "Eve", last_advert=2500)
|
||||||
|
|
||||||
|
# Create DM activity for Alice (oldest), Bob (most recent), Carol (middle)
|
||||||
|
for key, ts in [(KEY_A, 100), (KEY_B, 2000), ("cc" * 32, 1000)]:
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hi', ?)",
|
||||||
|
(key, ts),
|
||||||
|
)
|
||||||
|
await test_db.conn.commit()
|
||||||
|
|
||||||
await AppSettingsRepository.update(max_radio_contacts=5)
|
await AppSettingsRepository.update(max_radio_contacts=5)
|
||||||
await ContactRepository.set_favorite(KEY_A, True)
|
await ContactRepository.set_favorite(KEY_A, True)
|
||||||
|
|
||||||
@@ -401,6 +409,7 @@ class TestSyncRecentContactsToRadio:
|
|||||||
loaded_keys = [
|
loaded_keys = [
|
||||||
call.args[0]["public_key"] for call in mock_mc.commands.add_contact.call_args_list
|
call.args[0]["public_key"] for call in mock_mc.commands.add_contact.call_args_list
|
||||||
]
|
]
|
||||||
|
# Alice (favorite), then Bob & Carol (DM-active, most recent first), then Dave (advert)
|
||||||
assert loaded_keys == [KEY_A, KEY_B, "cc" * 32, "dd" * 32]
|
assert loaded_keys == [KEY_A, KEY_B, "cc" * 32, "dd" * 32]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -509,8 +518,15 @@ class TestSyncAndOffloadAll:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_duplicate_favorite_not_loaded_twice(self, test_db):
|
async def test_duplicate_favorite_not_loaded_twice(self, test_db):
|
||||||
"""Duplicate favorite entries still load the contact only once."""
|
"""Duplicate favorite entries still load the contact only once."""
|
||||||
await _insert_contact(KEY_A, "Alice", last_contacted=2000)
|
await _insert_contact(KEY_A, "Alice")
|
||||||
await _insert_contact(KEY_B, "Bob", last_contacted=1000)
|
await _insert_contact(KEY_B, "Bob")
|
||||||
|
|
||||||
|
# Bob has DM activity so he appears in tier 2
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hi', 1000)",
|
||||||
|
(KEY_B,),
|
||||||
|
)
|
||||||
|
await test_db.conn.commit()
|
||||||
|
|
||||||
await AppSettingsRepository.update(max_radio_contacts=2)
|
await AppSettingsRepository.update(max_radio_contacts=2)
|
||||||
await ContactRepository.set_favorite(KEY_A, True)
|
await ContactRepository.set_favorite(KEY_A, True)
|
||||||
@@ -1862,3 +1878,102 @@ class TestCollectRepeaterTelemetryLpp:
|
|||||||
await _collect_repeater_telemetry(mc, contact)
|
await _collect_repeater_telemetry(mc, contact)
|
||||||
|
|
||||||
assert "lpp_sensors" not in recorded_data
|
assert "lpp_sensors" not in recorded_data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# get_contacts_selected_for_radio_sync — DM-active prioritization
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestContactSelectionDmActive:
|
||||||
|
"""Verify that tier 2 prioritizes contacts with recent DM activity."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_incoming_dm_contact_selected_over_advert_only(self, test_db):
|
||||||
|
"""A contact who sent us a DM should be prioritized over one who only advertised."""
|
||||||
|
from app.radio_sync import get_contacts_selected_for_radio_sync
|
||||||
|
|
||||||
|
# Create two non-repeater contacts
|
||||||
|
dm_sender_key = "aa" * 32
|
||||||
|
advert_only_key = "bb" * 32
|
||||||
|
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO contacts (public_key, name, type, last_seen, last_advert) VALUES (?, ?, 1, 100, 100)",
|
||||||
|
(dm_sender_key, "DM Sender"),
|
||||||
|
)
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO contacts (public_key, name, type, last_seen, last_advert) VALUES (?, ?, 1, 200, 200)",
|
||||||
|
(advert_only_key, "Advert Only"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# DM Sender sent us a message (incoming DM)
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hello', 300)",
|
||||||
|
(dm_sender_key,),
|
||||||
|
)
|
||||||
|
await test_db.conn.commit()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.radio_sync.AppSettingsRepository.get",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
|
||||||
|
):
|
||||||
|
selected = await get_contacts_selected_for_radio_sync()
|
||||||
|
|
||||||
|
keys = [c.public_key for c in selected]
|
||||||
|
assert dm_sender_key in keys
|
||||||
|
assert advert_only_key in keys
|
||||||
|
# DM Sender should come before Advert Only (tier 2 before tier 3)
|
||||||
|
assert keys.index(dm_sender_key) < keys.index(advert_only_key)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_outgoing_dm_contact_also_selected(self, test_db):
|
||||||
|
"""A contact we sent a DM to should also appear via DM-active tier."""
|
||||||
|
from app.radio_sync import get_contacts_selected_for_radio_sync
|
||||||
|
|
||||||
|
contact_key = "cc" * 32
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, 1)",
|
||||||
|
(contact_key, "Outgoing Target"),
|
||||||
|
)
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO messages (type, conversation_key, text, received_at, outgoing) VALUES ('PRIV', ?, 'hey', 300, 1)",
|
||||||
|
(contact_key,),
|
||||||
|
)
|
||||||
|
await test_db.conn.commit()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.radio_sync.AppSettingsRepository.get",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
|
||||||
|
):
|
||||||
|
selected = await get_contacts_selected_for_radio_sync()
|
||||||
|
|
||||||
|
keys = [c.public_key for c in selected]
|
||||||
|
assert contact_key in keys
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_repeaters_excluded_from_dm_active_tier(self, test_db):
|
||||||
|
"""Repeater contacts should not appear in tier 2 even with DM activity."""
|
||||||
|
from app.radio_sync import get_contacts_selected_for_radio_sync
|
||||||
|
|
||||||
|
repeater_key = "dd" * 32
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, 2)",
|
||||||
|
(repeater_key, "Repeater"),
|
||||||
|
)
|
||||||
|
await test_db.conn.execute(
|
||||||
|
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'cmd', 300)",
|
||||||
|
(repeater_key,),
|
||||||
|
)
|
||||||
|
await test_db.conn.commit()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.radio_sync.AppSettingsRepository.get",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
|
||||||
|
):
|
||||||
|
selected = await get_contacts_selected_for_radio_sync()
|
||||||
|
|
||||||
|
keys = [c.public_key for c in selected]
|
||||||
|
assert repeater_key not in keys
|
||||||
|
|||||||
@@ -983,7 +983,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remoteterm-meshcore"
|
name = "remoteterm-meshcore"
|
||||||
version = "3.11.1"
|
version = "3.11.3"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiomqtt" },
|
{ name = "aiomqtt" },
|
||||||
|
|||||||
Reference in New Issue
Block a user