feat(contacts): complete cache functionality, fix display bugs

- _on_new_contact() in manual mode: upsert to DB as cache (source='advert')
  so contacts appear in @mentions and Cache filter before approval
- _on_advertisement(): check mc.pending_contacts for name/metadata fallback
- get_pending_contacts(): include last_advert in response
- /api/contacts/cached: return numeric last_advert timestamp
- contacts.js: fix adv_lat/adv_lon field names (was c.lat/c.lon),
  use last_advert timestamp instead of last_seen datetime string
- upsert_contact: source priority — never downgrade 'device' to 'advert'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-09 08:51:25 +01:00
parent 34b6e9b1ec
commit 09fbc56956
4 changed files with 45 additions and 8 deletions
+5 -1
View File
@@ -107,7 +107,11 @@ class Database:
adv_lat = COALESCE(excluded.adv_lat, contacts.adv_lat),
adv_lon = COALESCE(excluded.adv_lon, contacts.adv_lon),
last_seen = datetime('now'),
source = excluded.source,
source = CASE
WHEN excluded.source = 'device' THEN 'device'
WHEN contacts.source = 'device' THEN contacts.source
ELSE excluded.source
END,
is_protected = CASE WHEN contacts.is_protected = 1 THEN 1 ELSE excluded.is_protected END,
lastmod = datetime('now')""",
fields
+29 -4
View File
@@ -531,8 +531,15 @@ class DeviceManager:
contact = (self.mc.contacts or {}).get(pubkey, {})
name = contact.get('adv_name', contact.get('name', ''))
# If contact is unknown or has no name, firmware may have just auto-added it.
# Refresh contacts from device to pick up the new entry.
# Also check pending contacts (manual approval mode)
if not name:
pending = (self.mc.pending_contacts or {}).get(pubkey, {})
if pending:
name = pending.get('adv_name', pending.get('name', ''))
if not contact:
contact = pending
# If contact is still unknown, firmware may have just auto-added it.
if not name and pubkey not in (self.mc.contacts or {}):
logger.info(f"Unknown advert from {pubkey[:8]}..., refreshing contacts")
await self.mc.ensure_contacts(follow=True)
@@ -749,9 +756,26 @@ class DeviceManager:
return
if self._is_manual_approval_enabled():
# Manual mode: don't add to DB, just notify frontend
# meshcore library already puts it in mc.pending_contacts
# Manual mode: meshcore puts it in mc.pending_contacts for approval
logger.info(f"Pending contact (manual mode): {name} ({pubkey[:8]}...)")
# Also add to DB cache for @mentions and Cache filter
last_adv = data.get('last_advert')
last_advert_val = (
str(int(last_adv))
if last_adv and isinstance(last_adv, (int, float)) and last_adv > 0
else str(int(time.time()))
)
self.db.upsert_contact(
public_key=pubkey,
name=name,
type=data.get('type', data.get('adv_type', 0)),
adv_lat=data.get('adv_lat'),
adv_lon=data.get('adv_lon'),
last_advert=last_advert_val,
source='advert', # cache-only until approved
)
if self.socketio:
self.socketio.emit('pending_contact', {
'public_key': pubkey,
@@ -1158,6 +1182,7 @@ class DeviceManager:
'type': c.get('type', c.get('adv_type', 0)),
'adv_lat': c.get('adv_lat'),
'adv_lon': c.get('adv_lon'),
'last_advert': c.get('last_advert'),
}
for pk, c in pending.items()
]
+8
View File
@@ -675,12 +675,20 @@ def get_cached_contacts():
contacts = []
for c in db_contacts:
pk = c.get('public_key', '')
# Parse last_advert to numeric timestamp
la = c.get('last_advert')
try:
last_advert_ts = int(la) if la else 0
except (ValueError, TypeError):
last_advert_ts = 0
contacts.append({
'public_key': pk,
'public_key_prefix': pk[:12],
'name': c.get('name', ''),
'first_seen': c.get('first_seen', ''),
'last_seen': c.get('last_seen', ''),
'last_advert': last_advert_ts,
'source': c.get('source', ''),
'adv_lat': c.get('adv_lat'),
'adv_lon': c.get('adv_lon'),
+3 -3
View File
@@ -1652,9 +1652,9 @@ async function loadExistingContacts() {
public_key: c.public_key,
public_key_prefix: c.public_key_prefix || c.public_key.substring(0, 12),
type_label: c.type_label || '',
adv_lat: c.lat || 0,
adv_lon: c.lon || 0,
last_seen: c.last_seen || 0,
adv_lat: c.adv_lat || 0,
adv_lon: c.adv_lon || 0,
last_seen: c.last_advert || 0,
on_device: false,
source: c.source || 'cache'
}));