From 630ba67ef0ef4c804e9ab508d3d8af75ff8a3ec4 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Wed, 1 Apr 2026 16:52:25 -0700 Subject: [PATCH] Patch up radio locking and frontend contact delete behavior for bulk contact delete --- app/routers/contacts.py | 27 ++++++++++--------- frontend/src/App.tsx | 4 +++ frontend/src/components/SettingsModal.tsx | 3 +++ .../settings/BulkDeleteContactsModal.tsx | 7 ++--- .../settings/SettingsDatabaseSection.tsx | 4 ++- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/routers/contacts.py b/app/routers/contacts.py index fdf8f63..ac86a8c 100644 --- a/app/routers/contacts.py +++ b/app/routers/contacts.py @@ -357,24 +357,27 @@ async def bulk_delete_contacts(request: BulkDeleteRequest) -> dict: """Delete multiple contacts from the database (and radio if present).""" from app.websocket import broadcast_event - deleted = 0 + # Resolve all contacts first + contacts_to_delete: list[Contact] = [] for key in request.public_keys: - normalized = key.lower() - contact = await ContactRepository.get_by_key(normalized) - if not contact: - continue + contact = await ContactRepository.get_by_key(key.lower()) + if contact: + contacts_to_delete.append(contact) - if radio_manager.is_connected: - try: - async with radio_manager.radio_operation( - "bulk_delete_contact_from_radio", blocking=False - ) as mc: + # Remove from radio in a single locked operation (blocks until radio is free) + if radio_manager.is_connected and contacts_to_delete: + try: + async with radio_manager.radio_operation("bulk_delete_contacts_from_radio") as mc: + for contact in contacts_to_delete: radio_contact = mc.get_contact_by_key_prefix(contact.public_key[:12]) if radio_contact: await mc.commands.remove_contact(radio_contact) - except Exception: - pass # Best-effort radio removal during bulk delete + except Exception as e: + logger.warning("Radio removal during bulk delete failed: %s", e) + # Delete from database and broadcast events + deleted = 0 + for contact in contacts_to_delete: await ContactRepository.delete(contact.public_key) broadcast_event("contact_deleted", {"public_key": contact.public_key}) deleted += 1 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 71f83c5..c547e67 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -558,6 +558,10 @@ export function App() { onToggleBlockedKey: handleBlockKey, onToggleBlockedName: handleBlockName, contacts, + onBulkDeleteContacts: (deletedKeys: string[]) => { + const keySet = new Set(deletedKeys.map((k) => k.toLowerCase())); + setContacts((prev) => prev.filter((c) => !keySet.has(c.public_key.toLowerCase()))); + }, }; const crackerProps = { packets: rawPackets, diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 18f85af..b36d361 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -49,6 +49,7 @@ interface SettingsModalBaseProps { onToggleBlockedKey?: (key: string) => void; onToggleBlockedName?: (name: string) => void; contacts?: Contact[]; + onBulkDeleteContacts?: (deletedKeys: string[]) => void; } export type SettingsModalProps = SettingsModalBaseProps & @@ -83,6 +84,7 @@ export function SettingsModal(props: SettingsModalProps) { onToggleBlockedKey, onToggleBlockedName, contacts, + onBulkDeleteContacts, } = props; const externalSidebarNav = props.externalSidebarNav === true; const desktopSection = props.externalSidebarNav ? props.desktopSection : undefined; @@ -243,6 +245,7 @@ export function SettingsModal(props: SettingsModalProps) { onToggleBlockedKey={onToggleBlockedKey} onToggleBlockedName={onToggleBlockedName} contacts={contacts} + onBulkDeleteContacts={onBulkDeleteContacts} className={sectionContentClass} /> ) : ( diff --git a/frontend/src/components/settings/BulkDeleteContactsModal.tsx b/frontend/src/components/settings/BulkDeleteContactsModal.tsx index 46a01ff..8fd7a31 100644 --- a/frontend/src/components/settings/BulkDeleteContactsModal.tsx +++ b/frontend/src/components/settings/BulkDeleteContactsModal.tsx @@ -36,7 +36,7 @@ interface BulkDeleteContactsModalProps { open: boolean; onClose: () => void; contacts: Contact[]; - onDeleted: () => void; + onDeleted: (deletedKeys: string[]) => void; } export function BulkDeleteContactsModal({ @@ -133,9 +133,10 @@ export function BulkDeleteContactsModal({ const handleDelete = async () => { setDeleting(true); try { - const result = await api.bulkDeleteContacts([...selectedKeys]); + const keysToDelete = [...selectedKeys]; + const result = await api.bulkDeleteContacts(keysToDelete); toast.success(`Deleted ${result.deleted} contact${result.deleted === 1 ? '' : 's'}`); - onDeleted(); + onDeleted(keysToDelete); resetAndClose(); } catch (err) { console.error('Bulk delete failed:', err); diff --git a/frontend/src/components/settings/SettingsDatabaseSection.tsx b/frontend/src/components/settings/SettingsDatabaseSection.tsx index d4a7ace..dcfe209 100644 --- a/frontend/src/components/settings/SettingsDatabaseSection.tsx +++ b/frontend/src/components/settings/SettingsDatabaseSection.tsx @@ -19,6 +19,7 @@ export function SettingsDatabaseSection({ onToggleBlockedKey, onToggleBlockedName, contacts = [], + onBulkDeleteContacts, className, }: { appSettings: AppSettings; @@ -30,6 +31,7 @@ export function SettingsDatabaseSection({ onToggleBlockedKey?: (key: string) => void; onToggleBlockedName?: (name: string) => void; contacts?: Contact[]; + onBulkDeleteContacts?: (deletedKeys: string[]) => void; className?: string; }) { const [retentionDays, setRetentionDays] = useState('14'); @@ -297,7 +299,7 @@ export function SettingsDatabaseSection({ open={bulkDeleteOpen} onClose={() => setBulkDeleteOpen(false)} contacts={contacts} - onDeleted={() => {}} + onDeleted={(keys) => onBulkDeleteContacts?.(keys)} />