mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat(contacts): Redesign cleanup tool with advanced filtering
Replace hours-based cleanup with multi-criteria contact filtering system: - Add name filter (partial match, case-insensitive) - Add contact type selection (CLI, REP, ROOM, SENS) - Add date field selector (last_advert vs lastmod) - Add days of inactivity filter (0 = ignore) - Add path length filter (> X, 0 = ignore) - Implement preview modal before deletion - Support all contact types (CLI, REP, ROOM, SENS) Backend changes: - Add POST /api/contacts/preview-cleanup endpoint - Refactor POST /api/contacts/cleanup with new filter logic - Implement _filter_contacts_by_criteria helper function - Use last_advert (reliable) instead of lastmod (unreliable) Frontend changes: - Move cleanup section below Existing Contacts - Add collapsible Advanced Filters section - Replace handleCleanupInactive with handleCleanupPreview/Confirm - Add confirmation modal with contact list preview - Display deletion results (success/failure counts) Documentation updates: - Update API endpoints in CLAUDE.md - Update README.md with new cleanup instructions - Remove deprecated MC_INACTIVE_HOURS from docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
26
README.md
26
README.md
@@ -28,7 +28,7 @@ A lightweight web interface for meshcore-cli, providing browser-based access to
|
||||
- **Smart filtering:** Search by name/key, filter by contact type (CLI, REP, ROOM, SENS)
|
||||
- **Activity indicators:** Visual status icons (🟢 active, 🟡 recent, 🔴 inactive) based on last advertisement
|
||||
- **GPS location:** View contact location on Google Maps (when GPS coordinates available)
|
||||
- **Cleanup tool:** Remove inactive contacts with configurable threshold (moved from Settings)
|
||||
- **Advanced cleanup tool:** Filter and remove contacts by name, type (CLI/REP/ROOM/SENS), inactivity period, and path length with preview before deletion
|
||||
- 📦 **Message archiving** - Automatic daily archiving with browse-by-date selector
|
||||
- ⚡ **Efficient polling** - Lightweight update checks every 10s, UI refreshes only when needed
|
||||
- 📡 **Network commands** - Send advertisement (advert) or flood advertisement (floodadv) for network management
|
||||
@@ -111,7 +111,6 @@ All configuration is done via environment variables in the `.env` file.
|
||||
| `MC_SERIAL_PORT` | Serial device path (use /dev/serial/by-id/ for stability) | `/dev/ttyUSB0` |
|
||||
| `MC_DEVICE_NAME` | Device name (for .msgs and .adverts.jsonl files) | `MeshCore` |
|
||||
| `MC_CONFIG_DIR` | Configuration directory (shared between containers) | `./data/meshcore` |
|
||||
| `MC_INACTIVE_HOURS` | Inactivity threshold for contact cleanup (hours) | `48` |
|
||||
| `MC_ARCHIVE_DIR` | Archive directory path | `./data/archive` |
|
||||
| `MC_ARCHIVE_ENABLED` | Enable automatic archiving | `true` |
|
||||
| `MC_ARCHIVE_RETENTION_DAYS` | Days to show in live view | `7` |
|
||||
@@ -477,11 +476,24 @@ docker compose logs -f meshcore-bridge
|
||||
|
||||
### Managing Contacts (Cleanup)
|
||||
|
||||
Access the settings panel to clean up inactive contacts:
|
||||
1. Click the settings icon
|
||||
2. Adjust the inactivity threshold (default: 48 hours)
|
||||
3. Click "Clean Inactive Contacts"
|
||||
4. Confirm the action
|
||||
The advanced cleanup tool allows you to filter and remove contacts based on multiple criteria:
|
||||
|
||||
1. Navigate to **Contact Management** page (from slide-out menu)
|
||||
2. Scroll to **Cleanup Contacts** section (below Existing Contacts)
|
||||
3. Configure filters:
|
||||
- **Name Filter:** Enter partial contact name to search (optional)
|
||||
- **Advanced Filters** (collapsible):
|
||||
- **Contact Types:** Select which types to include (CLI, REP, ROOM, SENS)
|
||||
- **Date Field:** Choose between "Last Advert" (recommended) or "Last Modified"
|
||||
- **Days of Inactivity:** Contacts inactive for more than X days (0 = ignore)
|
||||
- **Path Length >:** Contacts with path length greater than X (0 = ignore)
|
||||
4. Click **Preview Cleanup** to see matching contacts
|
||||
5. Review the list and confirm deletion
|
||||
|
||||
**Example use cases:**
|
||||
- Remove all REP contacts inactive for 30+ days: Select REP, set days to 30
|
||||
- Clean specific contact names: Enter partial name (e.g., "test")
|
||||
- Remove distant contacts: Set path length > 5
|
||||
|
||||
### Network Commands
|
||||
|
||||
|
||||
@@ -256,42 +256,299 @@ def get_contacts():
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/contacts/cleanup', methods=['POST'])
|
||||
def cleanup_contacts():
|
||||
def _filter_contacts_by_criteria(contacts: list, criteria: dict) -> list:
|
||||
"""
|
||||
Clean up inactive contacts.
|
||||
Filter contacts based on cleanup criteria.
|
||||
|
||||
JSON body:
|
||||
hours (int): Inactivity threshold in hours (optional, default from config)
|
||||
Args:
|
||||
contacts: List of contact dicts from /api/contacts/detailed
|
||||
criteria: Filter criteria:
|
||||
- name_filter (str): Partial name match (empty = ignore)
|
||||
- types (list[int]): Contact types to include [1,2,3,4]
|
||||
- date_field (str): "last_advert" or "lastmod"
|
||||
- days (int): Days of inactivity (0 = ignore)
|
||||
- path_len (int): Minimum path length, >X (0 = ignore)
|
||||
|
||||
Returns:
|
||||
JSON with cleanup result
|
||||
List of contacts matching criteria
|
||||
"""
|
||||
name_filter = criteria.get('name_filter', '').strip().lower()
|
||||
selected_types = criteria.get('types', [1, 2, 3, 4])
|
||||
date_field = criteria.get('date_field', 'last_advert')
|
||||
days = criteria.get('days', 0)
|
||||
path_len = criteria.get('path_len', 0)
|
||||
|
||||
# Calculate timestamp threshold for days filter
|
||||
current_time = int(time.time())
|
||||
days_threshold = days * 86400 # Convert days to seconds
|
||||
|
||||
filtered = []
|
||||
for contact in contacts:
|
||||
# Filter by type
|
||||
if contact.get('type') not in selected_types:
|
||||
continue
|
||||
|
||||
# Filter by name (partial match, case-insensitive)
|
||||
if name_filter:
|
||||
contact_name = contact.get('name', '').lower()
|
||||
if name_filter not in contact_name:
|
||||
continue
|
||||
|
||||
# Filter by date (days of inactivity)
|
||||
if days > 0:
|
||||
timestamp = contact.get(date_field, 0)
|
||||
if timestamp == 0:
|
||||
# No timestamp - consider as inactive
|
||||
pass
|
||||
else:
|
||||
# Check if inactive for more than specified days
|
||||
age_seconds = current_time - timestamp
|
||||
if age_seconds <= days_threshold:
|
||||
# Still active within threshold
|
||||
continue
|
||||
|
||||
# Filter by path length (> path_len)
|
||||
if path_len > 0:
|
||||
contact_path_len = contact.get('out_path_len', -1)
|
||||
if contact_path_len <= path_len:
|
||||
continue
|
||||
|
||||
# Contact matches all criteria
|
||||
filtered.append(contact)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
@api_bp.route('/contacts/preview-cleanup', methods=['POST'])
|
||||
def preview_cleanup_contacts():
|
||||
"""
|
||||
Preview contacts that will be deleted based on filter criteria.
|
||||
|
||||
JSON body:
|
||||
{
|
||||
"name_filter": "", # Partial name match (empty = ignore)
|
||||
"types": [1, 2, 3, 4], # Contact types (1=CLI, 2=REP, 3=ROOM, 4=SENS)
|
||||
"date_field": "last_advert", # "last_advert" or "lastmod"
|
||||
"days": 2, # Days of inactivity (0 = ignore)
|
||||
"path_len": 0 # Path length > X (0 = ignore)
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with preview of contacts to be deleted:
|
||||
{
|
||||
"success": true,
|
||||
"contacts": [...],
|
||||
"count": 15
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
hours = data.get('hours', config.MC_INACTIVE_HOURS)
|
||||
|
||||
if not isinstance(hours, int) or hours < 1:
|
||||
# Validate criteria
|
||||
criteria = {
|
||||
'name_filter': data.get('name_filter', ''),
|
||||
'types': data.get('types', [1, 2, 3, 4]),
|
||||
'date_field': data.get('date_field', 'last_advert'),
|
||||
'days': data.get('days', 0),
|
||||
'path_len': data.get('path_len', 0)
|
||||
}
|
||||
|
||||
# Validate types
|
||||
if not isinstance(criteria['types'], list) or not all(t in [1, 2, 3, 4] for t in criteria['types']):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid hours value (must be positive integer)'
|
||||
'error': 'Invalid types (must be list of 1, 2, 3, 4)'
|
||||
}), 400
|
||||
|
||||
# Execute cleanup command
|
||||
success, message = cli.clean_inactive_contacts(hours)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Cleanup completed: {message}',
|
||||
'hours': hours
|
||||
}), 200
|
||||
else:
|
||||
# Validate date_field
|
||||
if criteria['date_field'] not in ['last_advert', 'lastmod']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': message
|
||||
'error': 'Invalid date_field (must be "last_advert" or "lastmod")'
|
||||
}), 400
|
||||
|
||||
# Validate numeric fields
|
||||
if not isinstance(criteria['days'], int) or criteria['days'] < 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid days (must be non-negative integer)'
|
||||
}), 400
|
||||
|
||||
if not isinstance(criteria['path_len'], int) or criteria['path_len'] < 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid path_len (must be non-negative integer)'
|
||||
}), 400
|
||||
|
||||
# Get all contacts
|
||||
success_detailed, contacts_detailed, error_detailed = cli.get_contacts_with_last_seen()
|
||||
if not success_detailed:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': error_detailed or 'Failed to get contacts'
|
||||
}), 500
|
||||
|
||||
# Convert to list format (same as /api/contacts/detailed)
|
||||
type_labels = {1: 'CLI', 2: 'REP', 3: 'ROOM', 4: 'SENS'}
|
||||
contacts = []
|
||||
for public_key, details in contacts_detailed.items():
|
||||
out_path_len = details.get('out_path_len', -1)
|
||||
contacts.append({
|
||||
'public_key': public_key,
|
||||
'name': details.get('adv_name', ''),
|
||||
'type': details.get('type'),
|
||||
'type_label': type_labels.get(details.get('type'), 'UNKNOWN'),
|
||||
'last_advert': details.get('last_advert'),
|
||||
'lastmod': details.get('lastmod'),
|
||||
'out_path_len': out_path_len,
|
||||
'out_path': details.get('out_path', ''),
|
||||
'adv_lat': details.get('adv_lat'),
|
||||
'adv_lon': details.get('adv_lon')
|
||||
})
|
||||
|
||||
# Filter contacts
|
||||
filtered_contacts = _filter_contacts_by_criteria(contacts, criteria)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'contacts': filtered_contacts,
|
||||
'count': len(filtered_contacts)
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error previewing cleanup: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/contacts/cleanup', methods=['POST'])
|
||||
def cleanup_contacts():
|
||||
"""
|
||||
Clean up contacts based on filter criteria.
|
||||
|
||||
JSON body:
|
||||
{
|
||||
"name_filter": "", # Partial name match (empty = ignore)
|
||||
"types": [1, 2, 3, 4], # Contact types (1=CLI, 2=REP, 3=ROOM, 4=SENS)
|
||||
"date_field": "last_advert", # "last_advert" or "lastmod"
|
||||
"days": 2, # Days of inactivity (0 = ignore)
|
||||
"path_len": 0 # Path length > X (0 = ignore)
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with cleanup result:
|
||||
{
|
||||
"success": true,
|
||||
"deleted_count": 15,
|
||||
"failed_count": 2,
|
||||
"failures": [
|
||||
{"name": "Contact1", "error": "..."},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate criteria (same as preview)
|
||||
criteria = {
|
||||
'name_filter': data.get('name_filter', ''),
|
||||
'types': data.get('types', [1, 2, 3, 4]),
|
||||
'date_field': data.get('date_field', 'last_advert'),
|
||||
'days': data.get('days', 0),
|
||||
'path_len': data.get('path_len', 0)
|
||||
}
|
||||
|
||||
# Validate types
|
||||
if not isinstance(criteria['types'], list) or not all(t in [1, 2, 3, 4] for t in criteria['types']):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid types (must be list of 1, 2, 3, 4)'
|
||||
}), 400
|
||||
|
||||
# Validate date_field
|
||||
if criteria['date_field'] not in ['last_advert', 'lastmod']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid date_field (must be "last_advert" or "lastmod")'
|
||||
}), 400
|
||||
|
||||
# Validate numeric fields
|
||||
if not isinstance(criteria['days'], int) or criteria['days'] < 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid days (must be non-negative integer)'
|
||||
}), 400
|
||||
|
||||
if not isinstance(criteria['path_len'], int) or criteria['path_len'] < 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid path_len (must be non-negative integer)'
|
||||
}), 400
|
||||
|
||||
# Get all contacts
|
||||
success_detailed, contacts_detailed, error_detailed = cli.get_contacts_with_last_seen()
|
||||
if not success_detailed:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': error_detailed or 'Failed to get contacts'
|
||||
}), 500
|
||||
|
||||
# Convert to list format
|
||||
type_labels = {1: 'CLI', 2: 'REP', 3: 'ROOM', 4: 'SENS'}
|
||||
contacts = []
|
||||
for public_key, details in contacts_detailed.items():
|
||||
out_path_len = details.get('out_path_len', -1)
|
||||
contacts.append({
|
||||
'public_key': public_key,
|
||||
'name': details.get('adv_name', ''),
|
||||
'type': details.get('type'),
|
||||
'type_label': type_labels.get(details.get('type'), 'UNKNOWN'),
|
||||
'last_advert': details.get('last_advert'),
|
||||
'lastmod': details.get('lastmod'),
|
||||
'out_path_len': out_path_len
|
||||
})
|
||||
|
||||
# Filter contacts to delete
|
||||
filtered_contacts = _filter_contacts_by_criteria(contacts, criteria)
|
||||
|
||||
if len(filtered_contacts) == 0:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'No contacts matched the criteria',
|
||||
'deleted_count': 0,
|
||||
'failed_count': 0,
|
||||
'failures': []
|
||||
}), 200
|
||||
|
||||
# Delete contacts one by one, track failures
|
||||
deleted_count = 0
|
||||
failed_count = 0
|
||||
failures = []
|
||||
|
||||
for contact in filtered_contacts:
|
||||
contact_name = contact['name']
|
||||
success, message = cli.delete_contact(contact_name)
|
||||
|
||||
if success:
|
||||
deleted_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
failures.append({
|
||||
'name': contact_name,
|
||||
'error': message
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Cleanup completed: {deleted_count} deleted, {failed_count} failed',
|
||||
'deleted_count': deleted_count,
|
||||
'failed_count': failed_count,
|
||||
'failures': failures
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning contacts: {e}")
|
||||
return jsonify({
|
||||
|
||||
@@ -96,10 +96,16 @@ function attachManageEventListeners() {
|
||||
approvalSwitch.addEventListener('change', handleApprovalToggle);
|
||||
}
|
||||
|
||||
// Cleanup button
|
||||
const cleanupBtn = document.getElementById('cleanupBtn');
|
||||
if (cleanupBtn) {
|
||||
cleanupBtn.addEventListener('click', handleCleanupInactive);
|
||||
// Cleanup preview button
|
||||
const cleanupPreviewBtn = document.getElementById('cleanupPreviewBtn');
|
||||
if (cleanupPreviewBtn) {
|
||||
cleanupPreviewBtn.addEventListener('click', handleCleanupPreview);
|
||||
}
|
||||
|
||||
// Cleanup confirm button (in modal)
|
||||
const confirmCleanupBtn = document.getElementById('confirmCleanupBtn');
|
||||
if (confirmCleanupBtn) {
|
||||
confirmCleanupBtn.addEventListener('click', handleCleanupConfirm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,28 +148,160 @@ async function loadContactCounts() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCleanupInactive() {
|
||||
const hoursInput = document.getElementById('inactiveHours');
|
||||
const cleanupBtn = document.getElementById('cleanupBtn');
|
||||
// Global variable to store preview contacts
|
||||
let cleanupPreviewContacts = [];
|
||||
|
||||
if (!hoursInput || !cleanupBtn) return;
|
||||
function collectCleanupCriteria() {
|
||||
/**
|
||||
* Collect cleanup filter criteria from form inputs.
|
||||
*
|
||||
* Returns:
|
||||
* Object with criteria: {name_filter, types, date_field, days, path_len}
|
||||
*/
|
||||
// Name filter
|
||||
const nameFilter = document.getElementById('cleanupNameFilter')?.value?.trim() || '';
|
||||
|
||||
const hours = parseInt(hoursInput.value);
|
||||
// Selected types (checked checkboxes)
|
||||
const typeCheckboxes = document.querySelectorAll('.cleanup-type-filter:checked');
|
||||
const types = Array.from(typeCheckboxes).map(cb => parseInt(cb.value));
|
||||
|
||||
if (isNaN(hours) || hours < 1) {
|
||||
showToast('Please enter a valid number of hours', 'warning');
|
||||
// Date field (radio button)
|
||||
const dateFieldRadio = document.querySelector('input[name="cleanupDateField"]:checked');
|
||||
const dateField = dateFieldRadio?.value || 'last_advert';
|
||||
|
||||
// Days of inactivity
|
||||
const days = parseInt(document.getElementById('cleanupDays')?.value) || 0;
|
||||
|
||||
// Path length
|
||||
const pathLen = parseInt(document.getElementById('cleanupPathLen')?.value) || 0;
|
||||
|
||||
return {
|
||||
name_filter: nameFilter,
|
||||
types: types,
|
||||
date_field: dateField,
|
||||
days: days,
|
||||
path_len: pathLen
|
||||
};
|
||||
}
|
||||
|
||||
async function handleCleanupPreview() {
|
||||
const previewBtn = document.getElementById('cleanupPreviewBtn');
|
||||
if (!previewBtn) return;
|
||||
|
||||
// Collect filter criteria
|
||||
const criteria = collectCleanupCriteria();
|
||||
|
||||
// Validate: at least one type must be selected
|
||||
if (criteria.types.length === 0) {
|
||||
showToast('Please select at least one contact type', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm action
|
||||
if (!confirm(`This will remove all contacts inactive for more than ${hours} hours. Continue?`)) {
|
||||
return;
|
||||
// Disable button during preview
|
||||
const originalHTML = previewBtn.innerHTML;
|
||||
previewBtn.disabled = true;
|
||||
previewBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Loading...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/preview-cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(criteria)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
cleanupPreviewContacts = data.contacts || [];
|
||||
|
||||
if (cleanupPreviewContacts.length === 0) {
|
||||
showToast('No contacts match the selected criteria', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate modal with preview
|
||||
populateCleanupModal(cleanupPreviewContacts);
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('cleanupConfirmModal'));
|
||||
modal.show();
|
||||
} else {
|
||||
showToast('Preview failed: ' + (data.error || 'Unknown error'), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup preview:', error);
|
||||
showToast('Network error during preview', 'danger');
|
||||
} finally {
|
||||
// Re-enable button
|
||||
previewBtn.disabled = false;
|
||||
previewBtn.innerHTML = originalHTML;
|
||||
}
|
||||
}
|
||||
|
||||
function populateCleanupModal(contacts) {
|
||||
/**
|
||||
* Populate cleanup confirmation modal with list of contacts.
|
||||
*/
|
||||
const countEl = document.getElementById('cleanupContactCount');
|
||||
const listEl = document.getElementById('cleanupContactList');
|
||||
|
||||
if (countEl) {
|
||||
countEl.textContent = contacts.length;
|
||||
}
|
||||
|
||||
// Disable button during operation
|
||||
const originalHTML = cleanupBtn.innerHTML;
|
||||
cleanupBtn.disabled = true;
|
||||
cleanupBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Cleaning...';
|
||||
if (listEl) {
|
||||
listEl.innerHTML = '';
|
||||
|
||||
contacts.forEach(contact => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item';
|
||||
|
||||
// Format last seen time
|
||||
const lastSeenTimestamp = contact.last_advert || 0;
|
||||
const lastSeenText = lastSeenTimestamp > 0 ? formatRelativeTime(lastSeenTimestamp) : 'Never';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>${escapeHtml(contact.name)}</strong>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
Type: <span class="badge bg-secondary">${contact.type_label}</span>
|
||||
| Last advert: ${lastSeenText}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
/**
|
||||
* Escape HTML special characters to prevent XSS.
|
||||
*/
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function handleCleanupConfirm() {
|
||||
const confirmBtn = document.getElementById('confirmCleanupBtn');
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('cleanupConfirmModal'));
|
||||
|
||||
if (!confirmBtn) return;
|
||||
|
||||
// Collect criteria again (in case user changed filters)
|
||||
const criteria = collectCleanupCriteria();
|
||||
|
||||
// Disable button during cleanup
|
||||
const originalHTML = confirmBtn.innerHTML;
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/cleanup', {
|
||||
@@ -171,17 +309,34 @@ async function handleCleanupInactive() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
inactive_hours: hours
|
||||
})
|
||||
body: JSON.stringify(criteria)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(data.message || 'Cleanup completed successfully', 'success');
|
||||
// Hide modal first
|
||||
if (modal) modal.hide();
|
||||
|
||||
// Show success message
|
||||
let message = `Cleanup completed: ${data.deleted_count} deleted`;
|
||||
if (data.failed_count > 0) {
|
||||
message += `, ${data.failed_count} failed`;
|
||||
}
|
||||
|
||||
showToast(message, data.failed_count > 0 ? 'warning' : 'success');
|
||||
|
||||
// Show failures if any
|
||||
if (data.failures && data.failures.length > 0) {
|
||||
console.error('Cleanup failures:', data.failures);
|
||||
// Optionally show detailed failure list to user
|
||||
}
|
||||
|
||||
// Reload contact counts
|
||||
loadContactCounts();
|
||||
|
||||
// Clear preview
|
||||
cleanupPreviewContacts = [];
|
||||
} else {
|
||||
showToast('Cleanup failed: ' + (data.error || 'Unknown error'), 'danger');
|
||||
}
|
||||
@@ -190,8 +345,8 @@ async function handleCleanupInactive() {
|
||||
showToast('Network error during cleanup', 'danger');
|
||||
} finally {
|
||||
// Re-enable button
|
||||
cleanupBtn.disabled = false;
|
||||
cleanupBtn.innerHTML = originalHTML;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerHTML = originalHTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,28 +37,6 @@
|
||||
title="When enabled, new contacts must be manually approved before they can communicate with your node"></i>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup Inactive Contacts Section -->
|
||||
<div class="cleanup-section">
|
||||
<h6>
|
||||
<i class="bi bi-trash"></i>
|
||||
<span>Cleanup Inactive Contacts</span>
|
||||
<i class="bi bi-info-circle info-icon"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Remove contacts that haven't sent an advertisement in a specified time period"></i>
|
||||
</h6>
|
||||
<div class="mb-3">
|
||||
<label for="inactiveHours" class="form-label">Remove contacts inactive for:</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inactiveHours" value="48" min="1">
|
||||
<span class="input-group-text">hours</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger" id="cleanupBtn">
|
||||
<i class="bi bi-trash"></i> Clean Inactive Contacts
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">
|
||||
@@ -87,5 +65,118 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup Contacts Section -->
|
||||
<div class="cleanup-section">
|
||||
<h6>
|
||||
<i class="bi bi-trash"></i>
|
||||
<span>Cleanup Contacts</span>
|
||||
<i class="bi bi-info-circle info-icon"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Remove contacts based on various filter criteria"></i>
|
||||
</h6>
|
||||
|
||||
<!-- Basic Filters (Always visible) -->
|
||||
<div class="mb-3">
|
||||
<label for="cleanupNameFilter" class="form-label">Name Filter (optional):</label>
|
||||
<input type="text" class="form-control" id="cleanupNameFilter" placeholder="Enter partial name to search...">
|
||||
</div>
|
||||
|
||||
<!-- Advanced Filters (Collapsible) -->
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-sm btn-outline-secondary w-100" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilters" aria-expanded="false" aria-controls="advancedFilters">
|
||||
<i class="bi bi-sliders"></i> Advanced Filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse" id="advancedFilters">
|
||||
<!-- Contact Types -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contact Types:</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input cleanup-type-filter" type="checkbox" value="1" id="cleanupTypeCLI" checked>
|
||||
<label class="form-check-label" for="cleanupTypeCLI">CLI</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input cleanup-type-filter" type="checkbox" value="2" id="cleanupTypeREP" checked>
|
||||
<label class="form-check-label" for="cleanupTypeREP">REP</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input cleanup-type-filter" type="checkbox" value="3" id="cleanupTypeROOM" checked>
|
||||
<label class="form-check-label" for="cleanupTypeROOM">ROOM</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input cleanup-type-filter" type="checkbox" value="4" id="cleanupTypeSENS" checked>
|
||||
<label class="form-check-label" for="cleanupTypeSENS">SENS</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Field -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Date Field:</label>
|
||||
<div class="d-flex gap-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="cleanupDateField" id="cleanupDateLastAdvert" value="last_advert" checked>
|
||||
<label class="form-check-label" for="cleanupDateLastAdvert">Last Advert</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="cleanupDateField" id="cleanupDateLastMod" value="lastmod">
|
||||
<label class="form-check-label" for="cleanupDateLastMod">Last Modified</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Days of Inactivity -->
|
||||
<div class="mb-3">
|
||||
<label for="cleanupDays" class="form-label">Days of Inactivity (0 = ignore):</label>
|
||||
<input type="number" class="form-control" id="cleanupDays" value="2" min="0">
|
||||
<small class="form-text text-muted">Contacts inactive for more than this many days will be selected</small>
|
||||
</div>
|
||||
|
||||
<!-- Path Length -->
|
||||
<div class="mb-3">
|
||||
<label for="cleanupPathLen" class="form-label">Path Length > (0 = ignore):</label>
|
||||
<input type="number" class="form-control" id="cleanupPathLen" value="0" min="0">
|
||||
<small class="form-text text-muted">Select contacts with path length greater than this value</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-warning" id="cleanupPreviewBtn">
|
||||
<i class="bi bi-eye"></i> Preview Cleanup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup Confirmation Modal -->
|
||||
<div class="modal fade" id="cleanupConfirmModal" tabindex="-1" aria-labelledby="cleanupConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title" id="cleanupConfirmModalLabel">
|
||||
<i class="bi bi-exclamation-triangle"></i> Confirm Contact Cleanup
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="fw-bold">The following <span id="cleanupContactCount">0</span> contact(s) will be permanently deleted:</p>
|
||||
<div id="cleanupContactList" class="list-group mb-3" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Contact list will be populated here -->
|
||||
</div>
|
||||
<div class="alert alert-warning mb-0">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone!
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmCleanupBtn">
|
||||
<i class="bi bi-trash"></i> Delete Contacts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user