mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
fix(contacts): sync device↔DB contacts, restore contact cache
- get_contacts_with_last_seen() reads from mc.contacts (device firmware) instead of DB, so /api/contacts/detailed returns only device contacts - _sync_contacts_to_db() now bidirectional: downgrades stale 'device' contacts to 'advert' (cache-only) when not on device anymore - delete_contact() sets source='advert' (cache) instead of 'deleted', keeping contacts visible in @mentions and cache filter - get_contacts() returns all contacts (no 'deleted' filter needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,7 @@ class Database:
|
||||
def get_contacts(self) -> List[Dict]:
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM contacts WHERE source != 'deleted' ORDER BY last_seen DESC"
|
||||
"SELECT * FROM contacts ORDER BY last_seen DESC"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@@ -147,18 +147,33 @@ class Database:
|
||||
return dict(row) if row else None
|
||||
|
||||
def delete_contact(self, public_key: str) -> bool:
|
||||
"""Soft-delete: mark as 'deleted' instead of removing.
|
||||
"""Move contact to cache (source='advert') instead of deleting.
|
||||
|
||||
Keeps the row so FK references in direct_messages stay intact.
|
||||
upsert_contact() overwrites source on re-add, auto-undeleting.
|
||||
Contact stays visible in cache and @mentions but not in device list.
|
||||
upsert_contact() overwrites source on re-add (back to 'device').
|
||||
"""
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"UPDATE contacts SET source = 'deleted', lastmod = datetime('now') WHERE public_key = ?",
|
||||
"UPDATE contacts SET source = 'advert', lastmod = datetime('now') WHERE public_key = ?",
|
||||
(public_key.lower(),)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def downgrade_stale_device_contacts(self, active_device_keys: set) -> int:
|
||||
"""Downgrade contacts marked 'device' that are no longer on the device."""
|
||||
with self._connect() as conn:
|
||||
all_device = conn.execute(
|
||||
"SELECT public_key FROM contacts WHERE source = 'device'"
|
||||
).fetchall()
|
||||
stale_keys = [r['public_key'] for r in all_device
|
||||
if r['public_key'] not in active_device_keys]
|
||||
if stale_keys:
|
||||
conn.executemany(
|
||||
"UPDATE contacts SET source = 'advert', lastmod = datetime('now') WHERE public_key = ?",
|
||||
[(k,) for k in stale_keys]
|
||||
)
|
||||
return len(stale_keys)
|
||||
|
||||
def set_contact_protected(self, public_key: str, protected: bool) -> bool:
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
|
||||
@@ -268,11 +268,15 @@ class DeviceManager:
|
||||
logger.debug(f"Subscribed to {event_type.value}")
|
||||
|
||||
def _sync_contacts_to_db(self):
|
||||
"""Sync device contacts to database."""
|
||||
"""Sync device contacts to database (bidirectional).
|
||||
|
||||
- Upserts device contacts with source='device'
|
||||
- Downgrades DB contacts marked 'device' that are no longer on device to 'advert'
|
||||
"""
|
||||
if not self.mc or not self.mc.contacts:
|
||||
return
|
||||
|
||||
count = 0
|
||||
device_keys = set()
|
||||
for pubkey, contact in self.mc.contacts.items():
|
||||
# last_advert from meshcore is Unix timestamp (int) or None
|
||||
last_adv = contact.get('last_advert')
|
||||
@@ -290,8 +294,13 @@ class DeviceManager:
|
||||
adv_lon=contact.get('adv_lon'),
|
||||
source='device',
|
||||
)
|
||||
count += 1
|
||||
logger.info(f"Synced {count} contacts from device to database")
|
||||
device_keys.add(pubkey.lower())
|
||||
|
||||
# Downgrade stale 'device' contacts to 'advert' (cache-only)
|
||||
stale = self.db.downgrade_stale_device_contacts(device_keys)
|
||||
if stale:
|
||||
logger.info(f"Downgraded {stale} stale device contacts to cache")
|
||||
logger.info(f"Synced {len(device_keys)} contacts from device to database")
|
||||
|
||||
def execute(self, coro, timeout: float = 30) -> Any:
|
||||
"""
|
||||
|
||||
@@ -147,24 +147,25 @@ def _parse_last_advert(value) -> int:
|
||||
|
||||
|
||||
def get_contacts_with_last_seen() -> Tuple[bool, Dict[str, Dict], str]:
|
||||
"""Get contacts with last_advert timestamps from DB."""
|
||||
"""Get contacts actually on the device firmware (from mc.contacts)."""
|
||||
try:
|
||||
dm = _get_dm()
|
||||
contacts = dm.db.get_contacts()
|
||||
if not dm.mc or not dm.mc.contacts:
|
||||
return True, {}, ""
|
||||
contacts_dict = {}
|
||||
for c in contacts:
|
||||
pk = c.get('public_key', '')
|
||||
for pk, contact in dm.mc.contacts.items():
|
||||
last_adv = contact.get('last_advert')
|
||||
contacts_dict[pk] = {
|
||||
'public_key': pk,
|
||||
'type': c.get('type', 1),
|
||||
'flags': c.get('flags', 0),
|
||||
'out_path_len': c.get('out_path_len', -1),
|
||||
'out_path': c.get('out_path', ''),
|
||||
'adv_name': c.get('name', ''),
|
||||
'last_advert': _parse_last_advert(c.get('last_advert')),
|
||||
'adv_lat': c.get('adv_lat', 0.0),
|
||||
'adv_lon': c.get('adv_lon', 0.0),
|
||||
'lastmod': c.get('lastmod', ''),
|
||||
'type': contact.get('type', 1),
|
||||
'flags': contact.get('flags', 0),
|
||||
'out_path_len': contact.get('out_path_len', -1),
|
||||
'out_path': contact.get('out_path', ''),
|
||||
'adv_name': contact.get('adv_name', contact.get('name', '')),
|
||||
'last_advert': int(last_adv) if last_adv and isinstance(last_adv, (int, float)) and last_adv > 0 else 0,
|
||||
'adv_lat': contact.get('adv_lat', 0.0),
|
||||
'adv_lon': contact.get('adv_lon', 0.0),
|
||||
'lastmod': '',
|
||||
}
|
||||
return True, contacts_dict, ""
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user