mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-05-01 02:52:35 +02:00
fix: contact delete by pubkey, cache contacts as pending, cache delete button
1. Delete button now sends public_key instead of name to avoid matching wrong contacts when multiple share similar names. 2. _on_advertisement adds cache-only contacts to mc.pending_contacts when manual approval is enabled, so they appear in the pending list after advertising (even if meshcore fires ADVERTISEMENT instead of NEW_CONTACT). 3. Added Delete button for cache-only contacts with dedicated /api/contacts/cached/delete endpoint and hard_delete_contact DB method. 4. approve_contact/reject_contact now handle DB-only pending contacts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -163,6 +163,15 @@ class Database:
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def hard_delete_contact(self, public_key: str) -> bool:
|
||||
"""Permanently delete a contact from the database."""
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"DELETE FROM contacts 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:
|
||||
|
||||
@@ -629,6 +629,34 @@ class DeviceManager:
|
||||
source='advert',
|
||||
)
|
||||
|
||||
# If manual mode: add cache-only contacts to pending list
|
||||
# (meshcore may fire ADVERTISEMENT instead of NEW_CONTACT for
|
||||
# contacts already in mc.pending_contacts or after restart)
|
||||
if (self._is_manual_approval_enabled()
|
||||
and pubkey not in (self.mc.contacts or {})
|
||||
and pubkey not in (self.mc.pending_contacts or {})
|
||||
and not self.db.is_contact_ignored(pubkey)
|
||||
and not self.db.is_contact_blocked(pubkey)):
|
||||
# Add to pending_contacts so it shows in pending list
|
||||
if self.mc.pending_contacts is None:
|
||||
self.mc.pending_contacts = {}
|
||||
self.mc.pending_contacts[pubkey] = {
|
||||
'public_key': pubkey,
|
||||
'adv_name': name,
|
||||
'name': name,
|
||||
'type': adv_type,
|
||||
'adv_lat': adv_lat,
|
||||
'adv_lon': adv_lon,
|
||||
'last_advert': int(time.time()),
|
||||
}
|
||||
logger.info(f"Cache contact added to pending (advert): {name} ({pubkey[:8]}...)")
|
||||
if self.socketio:
|
||||
self.socketio.emit('pending_contact', {
|
||||
'public_key': pubkey,
|
||||
'name': name,
|
||||
'type': adv_type,
|
||||
}, namespace='/chat')
|
||||
|
||||
logger.info(f"Advert from '{name}' ({pubkey[:8]}...) type={adv_type}")
|
||||
|
||||
except Exception as e:
|
||||
@@ -1164,7 +1192,7 @@ class DeviceManager:
|
||||
|
||||
try:
|
||||
self.execute(self.mc.commands.remove_contact(pubkey))
|
||||
self.db.delete_contact(pubkey) # soft-delete: sets source='deleted'
|
||||
self.db.delete_contact(pubkey) # soft-delete: sets source='advert'
|
||||
# Also remove from in-memory contacts cache
|
||||
if self.mc.contacts and pubkey in self.mc.contacts:
|
||||
del self.mc.contacts[pubkey]
|
||||
@@ -1173,6 +1201,20 @@ class DeviceManager:
|
||||
logger.error(f"Failed to delete contact: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def delete_cached_contact(self, pubkey: str) -> Dict:
|
||||
"""Hard-delete a cache-only contact from the database."""
|
||||
try:
|
||||
# Don't delete if contact is on device
|
||||
if self.mc and self.mc.contacts and pubkey in self.mc.contacts:
|
||||
return {'success': False, 'error': 'Contact is on device, use delete_contact instead'}
|
||||
deleted = self.db.hard_delete_contact(pubkey)
|
||||
if deleted:
|
||||
return {'success': True, 'message': 'Cache contact deleted'}
|
||||
return {'success': False, 'error': 'Contact not found in cache'}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete cached contact: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def reset_path(self, pubkey: str) -> Dict:
|
||||
"""Reset path to a contact."""
|
||||
if not self.is_connected:
|
||||
@@ -1322,6 +1364,19 @@ class DeviceManager:
|
||||
|
||||
try:
|
||||
contact = (self.mc.pending_contacts or {}).get(pubkey)
|
||||
# Also check DB cache for contacts not in meshcore's pending list
|
||||
if not contact:
|
||||
db_contact = self.db.get_contact(pubkey)
|
||||
if db_contact and db_contact.get('source') == 'advert':
|
||||
contact = {
|
||||
'public_key': pubkey,
|
||||
'name': db_contact.get('name', ''),
|
||||
'adv_name': db_contact.get('name', ''),
|
||||
'type': db_contact.get('type', 0),
|
||||
'adv_lat': db_contact.get('adv_lat'),
|
||||
'adv_lon': db_contact.get('adv_lon'),
|
||||
'last_advert': db_contact.get('last_advert'),
|
||||
}
|
||||
if not contact:
|
||||
return {'success': False, 'error': 'Contact not in pending list'}
|
||||
|
||||
@@ -1373,6 +1428,11 @@ class DeviceManager:
|
||||
removed = self.mc.pending_contacts.pop(pubkey, None)
|
||||
if removed:
|
||||
return {'success': True, 'message': 'Contact rejected'}
|
||||
# Also check DB cache - remove cache-only contacts on reject
|
||||
db_contact = self.db.get_contact(pubkey)
|
||||
if db_contact and db_contact.get('source') == 'advert':
|
||||
self.db.hard_delete_contact(pubkey)
|
||||
return {'success': True, 'message': 'Contact rejected'}
|
||||
return {'success': False, 'error': 'Contact not in pending list'}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reject contact: {e}")
|
||||
|
||||
@@ -224,6 +224,19 @@ def delete_contact(selector: str) -> Tuple[bool, str]:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def delete_cached_contact(public_key: str) -> Tuple[bool, str]:
|
||||
"""Hard-delete a cache-only contact from the database."""
|
||||
if not public_key or not public_key.strip():
|
||||
return False, "Public key is required"
|
||||
|
||||
try:
|
||||
dm = _get_dm()
|
||||
result = dm.delete_cached_contact(public_key.strip())
|
||||
return result['success'], result.get('message', result.get('error', ''))
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def clean_inactive_contacts(hours: int = 48) -> Tuple[bool, str]:
|
||||
"""Remove contacts inactive for specified hours. Simplified in v2."""
|
||||
# TODO: implement time-based cleanup via database query
|
||||
|
||||
@@ -2494,6 +2494,37 @@ def delete_contact_api():
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/contacts/cached/delete', methods=['POST'])
|
||||
def delete_cached_contact_api():
|
||||
"""Delete a cache-only contact from the database."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'public_key' not in data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Missing required field: public_key'
|
||||
}), 400
|
||||
|
||||
public_key = data['public_key']
|
||||
if not isinstance(public_key, str) or not public_key.strip():
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'public_key must be a non-empty string'
|
||||
}), 400
|
||||
|
||||
success, message = cli.delete_cached_contact(public_key)
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True, 'message': message}), 200
|
||||
else:
|
||||
return jsonify({'success': False, 'error': message}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting cached contact: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/contacts/protected', methods=['GET'])
|
||||
def get_protected_contacts_api():
|
||||
"""
|
||||
|
||||
@@ -2193,6 +2193,15 @@ function createExistingContactCard(contact, index) {
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
// Delete button for cache-only contacts
|
||||
if (contact.on_device === false) {
|
||||
const deleteCacheBtn = document.createElement('button');
|
||||
deleteCacheBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
deleteCacheBtn.innerHTML = '<i class="bi bi-trash"></i> <span class="btn-label">Delete</span>';
|
||||
deleteCacheBtn.onclick = () => showDeleteModal(contact);
|
||||
actionsDiv.appendChild(deleteCacheBtn);
|
||||
}
|
||||
|
||||
// Ignore/Block/Unignore/Unblock buttons
|
||||
if (contact.is_blocked) {
|
||||
const unblockBtn = document.createElement('button');
|
||||
@@ -2317,17 +2326,19 @@ async function confirmDelete() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Use contact name for deletion (meshcli remove_contact only works with name)
|
||||
const selector = contactToDelete.name;
|
||||
// Use different endpoint for cache-only vs device contacts
|
||||
const isCacheOnly = contactToDelete.on_device === false;
|
||||
const url = isCacheOnly ? '/api/contacts/cached/delete' : '/api/contacts/delete';
|
||||
const body = isCacheOnly
|
||||
? { public_key: contactToDelete.public_key }
|
||||
: { selector: contactToDelete.public_key };
|
||||
|
||||
const response = await fetch('/api/contacts/delete', {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
selector: selector
|
||||
})
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user