mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
feat: Add automatic scheduled contact cleanup
Add auto-cleanup feature that runs daily at 01:00 UTC using APScheduler: - New cleanup settings stored in .webui_settings.json (enabled, types, date_field, days, name_filter) - API endpoints: GET/POST /api/contacts/cleanup-settings - Scheduler functions: _cleanup_job(), schedule_cleanup(), init_cleanup_schedule() - UI toggle in Advanced Filters with validation (requires days > 0) - Debounced auto-save for filter criteria changes - Protected contacts are excluded from auto-cleanup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,9 @@ logger = logging.getLogger(__name__)
|
||||
# Global scheduler instance
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
|
||||
# Job IDs
|
||||
CLEANUP_JOB_ID = 'daily_cleanup'
|
||||
|
||||
|
||||
def get_archive_path(archive_date: str) -> Path:
|
||||
"""
|
||||
@@ -225,6 +228,180 @@ def _archive_job():
|
||||
logger.error(f"Archive job failed: {result.get('error', 'Unknown error')}")
|
||||
|
||||
|
||||
def _cleanup_job():
|
||||
"""
|
||||
Background job that runs daily at 01:00 UTC to clean up contacts.
|
||||
Uses saved cleanup settings to filter and delete contacts.
|
||||
"""
|
||||
logger.info("Running daily cleanup job...")
|
||||
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from app.routes.api import (
|
||||
get_cleanup_settings,
|
||||
get_protected_contacts,
|
||||
_filter_contacts_by_criteria
|
||||
)
|
||||
|
||||
# Get cleanup settings
|
||||
settings = get_cleanup_settings()
|
||||
|
||||
if not settings.get('enabled'):
|
||||
logger.info("Auto-cleanup is disabled, skipping")
|
||||
return
|
||||
|
||||
# Get contacts from device
|
||||
import requests
|
||||
response = requests.get(
|
||||
f"{config.BRIDGE_URL}/cli",
|
||||
json={'args': ['contacts']},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to get contacts: HTTP {response.status_code}")
|
||||
return
|
||||
|
||||
data = response.json()
|
||||
if not data.get('success'):
|
||||
logger.error(f"Failed to get contacts: {data.get('error', 'Unknown error')}")
|
||||
return
|
||||
|
||||
# Parse contacts from output
|
||||
contacts = []
|
||||
output = data.get('output', '')
|
||||
import json as json_module
|
||||
for line in output.strip().split('\n'):
|
||||
if line.strip():
|
||||
try:
|
||||
contact = json_module.loads(line)
|
||||
contacts.append(contact)
|
||||
except json_module.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if not contacts:
|
||||
logger.info("No contacts found, nothing to clean up")
|
||||
return
|
||||
|
||||
# Filter contacts using saved criteria
|
||||
criteria = {
|
||||
'types': settings.get('types', [1, 2, 3, 4]),
|
||||
'date_field': settings.get('date_field', 'last_advert'),
|
||||
'days': settings.get('days', 30),
|
||||
'name_filter': settings.get('name_filter', '')
|
||||
}
|
||||
|
||||
# Get protected contacts
|
||||
protected = get_protected_contacts()
|
||||
|
||||
# Filter contacts (this function excludes protected contacts)
|
||||
matching_contacts = _filter_contacts_by_criteria(contacts, criteria, protected)
|
||||
|
||||
if not matching_contacts:
|
||||
logger.info("No contacts match cleanup criteria")
|
||||
return
|
||||
|
||||
logger.info(f"Found {len(matching_contacts)} contacts to clean up")
|
||||
|
||||
# Delete matching contacts
|
||||
deleted_count = 0
|
||||
for contact in matching_contacts:
|
||||
name = contact.get('name', '')
|
||||
if not name:
|
||||
continue
|
||||
|
||||
try:
|
||||
delete_response = requests.post(
|
||||
f"{config.BRIDGE_URL}/cli",
|
||||
json={'args': ['contact', '-d', name]},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if delete_response.status_code == 200:
|
||||
delete_data = delete_response.json()
|
||||
if delete_data.get('success'):
|
||||
deleted_count += 1
|
||||
logger.debug(f"Deleted contact: {name}")
|
||||
else:
|
||||
logger.warning(f"Failed to delete contact {name}: {delete_data.get('error')}")
|
||||
else:
|
||||
logger.warning(f"Failed to delete contact {name}: HTTP {delete_response.status_code}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting contact {name}: {e}")
|
||||
|
||||
logger.info(f"Cleanup job completed: deleted {deleted_count}/{len(matching_contacts)} contacts")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cleanup job failed: {e}", exc_info=True)
|
||||
|
||||
|
||||
def schedule_cleanup(enabled: bool) -> bool:
|
||||
"""
|
||||
Add or remove the cleanup job from the scheduler.
|
||||
|
||||
Args:
|
||||
enabled: True to enable cleanup job, False to disable
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
global _scheduler
|
||||
|
||||
if _scheduler is None:
|
||||
logger.warning("Scheduler not initialized, cannot schedule cleanup")
|
||||
return False
|
||||
|
||||
try:
|
||||
if enabled:
|
||||
# Add cleanup job at 01:00 UTC
|
||||
trigger = CronTrigger(hour=1, minute=0)
|
||||
|
||||
_scheduler.add_job(
|
||||
func=_cleanup_job,
|
||||
trigger=trigger,
|
||||
id=CLEANUP_JOB_ID,
|
||||
name='Daily Contact Cleanup',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
logger.info("Cleanup job scheduled - will run daily at 01:00 UTC")
|
||||
else:
|
||||
# Remove cleanup job if it exists
|
||||
try:
|
||||
_scheduler.remove_job(CLEANUP_JOB_ID)
|
||||
logger.info("Cleanup job removed from scheduler")
|
||||
except Exception:
|
||||
# Job might not exist, that's OK
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scheduling cleanup: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
def init_cleanup_schedule():
|
||||
"""
|
||||
Initialize cleanup schedule from saved settings.
|
||||
Called at startup after scheduler is started.
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from app.routes.api import get_cleanup_settings
|
||||
|
||||
settings = get_cleanup_settings()
|
||||
|
||||
if settings.get('enabled'):
|
||||
schedule_cleanup(enabled=True)
|
||||
logger.info("Auto-cleanup enabled from saved settings")
|
||||
else:
|
||||
logger.info("Auto-cleanup is disabled in saved settings")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing cleanup schedule: {e}", exc_info=True)
|
||||
|
||||
|
||||
def schedule_daily_archiving():
|
||||
"""
|
||||
Initialize and start the background scheduler for daily archiving.
|
||||
@@ -261,6 +438,9 @@ def schedule_daily_archiving():
|
||||
|
||||
logger.info("Archive scheduler started - will run daily at 00:00 UTC")
|
||||
|
||||
# Initialize cleanup schedule from saved settings
|
||||
init_cleanup_schedule()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start archive scheduler: {e}", exc_info=True)
|
||||
|
||||
|
||||
@@ -177,6 +177,84 @@ def save_protected_contacts(protected_list: list) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup Settings Management
|
||||
# =============================================================================
|
||||
|
||||
def get_cleanup_settings() -> dict:
|
||||
"""
|
||||
Get auto-cleanup settings from .webui_settings.json.
|
||||
|
||||
Returns:
|
||||
Dict with cleanup settings:
|
||||
{
|
||||
'enabled': bool,
|
||||
'types': list[int],
|
||||
'date_field': str,
|
||||
'days': int,
|
||||
'name_filter': str
|
||||
}
|
||||
"""
|
||||
from pathlib import Path
|
||||
defaults = {
|
||||
'enabled': False,
|
||||
'types': [1, 2, 3, 4],
|
||||
'date_field': 'last_advert',
|
||||
'days': 30,
|
||||
'name_filter': ''
|
||||
}
|
||||
|
||||
settings_path = Path(config.MC_CONFIG_DIR) / ".webui_settings.json"
|
||||
|
||||
try:
|
||||
if not settings_path.exists():
|
||||
return defaults
|
||||
|
||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
cleanup = settings.get('cleanup_settings', {})
|
||||
# Merge with defaults to ensure all fields exist
|
||||
return {**defaults, **cleanup}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read cleanup settings: {e}")
|
||||
return defaults
|
||||
|
||||
|
||||
def save_cleanup_settings(cleanup_settings: dict) -> bool:
|
||||
"""
|
||||
Save auto-cleanup settings to .webui_settings.json (atomic write).
|
||||
|
||||
Args:
|
||||
cleanup_settings: Dict with cleanup configuration
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
from pathlib import Path
|
||||
settings_path = Path(config.MC_CONFIG_DIR) / ".webui_settings.json"
|
||||
|
||||
try:
|
||||
# Read existing settings
|
||||
settings = {}
|
||||
if settings_path.exists():
|
||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
|
||||
# Update cleanup settings
|
||||
settings['cleanup_settings'] = cleanup_settings
|
||||
|
||||
# Write back atomically
|
||||
temp_file = settings_path.with_suffix('.tmp')
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(settings, f, indent=2, ensure_ascii=False)
|
||||
temp_file.replace(settings_path)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save cleanup settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@api_bp.route('/messages', methods=['GET'])
|
||||
def get_messages():
|
||||
"""
|
||||
@@ -1895,6 +1973,128 @@ def toggle_contact_protection(public_key):
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/contacts/cleanup-settings', methods=['GET'])
|
||||
def get_cleanup_settings_api():
|
||||
"""
|
||||
Get auto-cleanup settings.
|
||||
|
||||
Returns:
|
||||
JSON with cleanup settings:
|
||||
{
|
||||
"success": true,
|
||||
"settings": {
|
||||
"enabled": false,
|
||||
"types": [1, 2, 3, 4],
|
||||
"date_field": "last_advert",
|
||||
"days": 30,
|
||||
"name_filter": ""
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
settings = get_cleanup_settings()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'settings': settings
|
||||
}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cleanup settings: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'settings': {
|
||||
'enabled': False,
|
||||
'types': [1, 2, 3, 4],
|
||||
'date_field': 'last_advert',
|
||||
'days': 30,
|
||||
'name_filter': ''
|
||||
}
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/contacts/cleanup-settings', methods=['POST'])
|
||||
def update_cleanup_settings_api():
|
||||
"""
|
||||
Update auto-cleanup settings.
|
||||
|
||||
JSON body:
|
||||
{
|
||||
"enabled": true,
|
||||
"types": [1, 2],
|
||||
"date_field": "last_advert",
|
||||
"days": 30,
|
||||
"name_filter": ""
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with update result:
|
||||
{
|
||||
"success": true,
|
||||
"message": "Cleanup settings updated",
|
||||
"settings": {...}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate fields
|
||||
if 'types' in data:
|
||||
if not isinstance(data['types'], list) or not all(t in [1, 2, 3, 4] for t in data['types']):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid types (must be list of 1, 2, 3, 4)'
|
||||
}), 400
|
||||
|
||||
if 'date_field' in data:
|
||||
if data['date_field'] not in ['last_advert', 'lastmod']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid date_field (must be "last_advert" or "lastmod")'
|
||||
}), 400
|
||||
|
||||
if 'days' in data:
|
||||
if not isinstance(data['days'], int) or data['days'] < 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid days (must be non-negative integer)'
|
||||
}), 400
|
||||
|
||||
if 'enabled' in data:
|
||||
if not isinstance(data['enabled'], bool):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Invalid enabled (must be boolean)'
|
||||
}), 400
|
||||
|
||||
# Get current settings and merge with new values
|
||||
current = get_cleanup_settings()
|
||||
updated = {**current, **data}
|
||||
|
||||
# Save settings
|
||||
if not save_cleanup_settings(updated):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Failed to save cleanup settings'
|
||||
}), 500
|
||||
|
||||
# Update scheduler based on enabled state
|
||||
from app.archiver.manager import schedule_cleanup
|
||||
schedule_cleanup(enabled=updated.get('enabled', False))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Cleanup settings updated',
|
||||
'settings': updated
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating cleanup settings: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Contact Management (Pending Contacts & Settings)
|
||||
# =============================================================================
|
||||
|
||||
@@ -51,6 +51,10 @@ let protectedContacts = []; // List of protected public_keys
|
||||
let sortBy = 'last_advert'; // 'name' or 'last_advert'
|
||||
let sortOrder = 'desc'; // 'asc' or 'desc'
|
||||
|
||||
// Auto-cleanup state
|
||||
let autoCleanupSettings = null;
|
||||
let cleanupSaveDebounceTimer = null;
|
||||
|
||||
// Map state (Leaflet)
|
||||
let leafletMap = null;
|
||||
let markersGroup = null;
|
||||
@@ -168,6 +172,9 @@ function initManagePage() {
|
||||
// Load contact counts for badges
|
||||
loadContactCounts();
|
||||
|
||||
// Load cleanup settings (populates form and auto-cleanup status)
|
||||
loadCleanupSettings();
|
||||
|
||||
// Attach event listeners for manage page
|
||||
attachManageEventListeners();
|
||||
}
|
||||
@@ -190,6 +197,33 @@ function attachManageEventListeners() {
|
||||
if (confirmCleanupBtn) {
|
||||
confirmCleanupBtn.addEventListener('click', handleCleanupConfirm);
|
||||
}
|
||||
|
||||
// Auto-cleanup toggle
|
||||
const autoCleanupSwitch = document.getElementById('autoCleanupSwitch');
|
||||
if (autoCleanupSwitch) {
|
||||
autoCleanupSwitch.addEventListener('change', handleAutoCleanupToggle);
|
||||
}
|
||||
|
||||
// Debounced auto-save for cleanup filter inputs
|
||||
const cleanupNameFilter = document.getElementById('cleanupNameFilter');
|
||||
if (cleanupNameFilter) {
|
||||
cleanupNameFilter.addEventListener('input', debouncedSaveCleanupCriteria);
|
||||
}
|
||||
|
||||
const cleanupDays = document.getElementById('cleanupDays');
|
||||
if (cleanupDays) {
|
||||
cleanupDays.addEventListener('input', debouncedSaveCleanupCriteria);
|
||||
}
|
||||
|
||||
// Type filter checkboxes
|
||||
document.querySelectorAll('.cleanup-type-filter').forEach(cb => {
|
||||
cb.addEventListener('change', debouncedSaveCleanupCriteria);
|
||||
});
|
||||
|
||||
// Date field radio buttons
|
||||
document.querySelectorAll('input[name="cleanupDateField"]').forEach(radio => {
|
||||
radio.addEventListener('change', debouncedSaveCleanupCriteria);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadContactCounts() {
|
||||
@@ -238,6 +272,221 @@ async function loadContactCounts() {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Auto-Cleanup Settings Management
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Load cleanup settings from server and apply to UI.
|
||||
*/
|
||||
async function loadCleanupSettings() {
|
||||
const statusText = document.getElementById('autoCleanupStatusText');
|
||||
if (statusText) statusText.textContent = 'Loading...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/cleanup-settings');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
autoCleanupSettings = data.settings;
|
||||
applyCleanupSettingsToUI(autoCleanupSettings);
|
||||
console.log('Loaded cleanup settings:', autoCleanupSettings);
|
||||
} else {
|
||||
console.error('Failed to load cleanup settings:', data.error);
|
||||
if (statusText) statusText.textContent = 'Error loading settings';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cleanup settings:', error);
|
||||
if (statusText) statusText.textContent = 'Network error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply cleanup settings to form inputs.
|
||||
* @param {Object} settings - Cleanup settings object
|
||||
*/
|
||||
function applyCleanupSettingsToUI(settings) {
|
||||
// Name filter
|
||||
const nameInput = document.getElementById('cleanupNameFilter');
|
||||
if (nameInput) {
|
||||
nameInput.value = settings.name_filter || '';
|
||||
}
|
||||
|
||||
// Days
|
||||
const daysInput = document.getElementById('cleanupDays');
|
||||
if (daysInput) {
|
||||
daysInput.value = settings.days || 0;
|
||||
}
|
||||
|
||||
// Date field
|
||||
const dateFieldValue = settings.date_field || 'last_advert';
|
||||
const dateRadio = document.querySelector(`input[name="cleanupDateField"][value="${dateFieldValue}"]`);
|
||||
if (dateRadio) {
|
||||
dateRadio.checked = true;
|
||||
}
|
||||
|
||||
// Contact types
|
||||
const types = settings.types || [1, 2, 3, 4];
|
||||
document.querySelectorAll('.cleanup-type-filter').forEach(cb => {
|
||||
cb.checked = types.includes(parseInt(cb.value));
|
||||
});
|
||||
|
||||
// Auto-cleanup switch and status
|
||||
const autoCleanupSwitch = document.getElementById('autoCleanupSwitch');
|
||||
const statusText = document.getElementById('autoCleanupStatusText');
|
||||
|
||||
if (autoCleanupSwitch) {
|
||||
autoCleanupSwitch.checked = settings.enabled || false;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
if (settings.enabled) {
|
||||
statusText.textContent = 'Enabled (runs daily at 01:00 UTC)';
|
||||
statusText.classList.remove('text-muted');
|
||||
statusText.classList.add('text-success');
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
statusText.classList.remove('text-success');
|
||||
statusText.classList.add('text-muted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle auto-cleanup toggle change.
|
||||
* Validates criteria before enabling.
|
||||
*/
|
||||
async function handleAutoCleanupToggle(event) {
|
||||
const enabled = event.target.checked;
|
||||
const statusText = document.getElementById('autoCleanupStatusText');
|
||||
|
||||
// Validate before enabling
|
||||
if (enabled) {
|
||||
const criteria = collectCleanupCriteria();
|
||||
|
||||
// Check if days > 0
|
||||
if (criteria.days <= 0) {
|
||||
showToast('Set "Days of Inactivity" > 0 before enabling auto-cleanup', 'warning');
|
||||
event.target.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if at least one type is selected
|
||||
if (criteria.types.length === 0) {
|
||||
showToast('Select at least one contact type before enabling auto-cleanup', 'warning');
|
||||
event.target.checked = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update status text while saving
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Saving...';
|
||||
statusText.classList.remove('text-success', 'text-muted');
|
||||
}
|
||||
|
||||
// Save settings with new enabled state
|
||||
const success = await saveCleanupSettings(enabled);
|
||||
|
||||
if (!success) {
|
||||
// Revert switch on failure
|
||||
event.target.checked = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced save for cleanup criteria changes.
|
||||
* Only saves criteria, does not change enabled state.
|
||||
*/
|
||||
function debouncedSaveCleanupCriteria() {
|
||||
// Clear existing timer
|
||||
if (cleanupSaveDebounceTimer) {
|
||||
clearTimeout(cleanupSaveDebounceTimer);
|
||||
}
|
||||
|
||||
// Set new timer (500ms debounce)
|
||||
cleanupSaveDebounceTimer = setTimeout(() => {
|
||||
// Only save if auto-cleanup settings have been loaded
|
||||
if (autoCleanupSettings !== null) {
|
||||
// Preserve current enabled state
|
||||
saveCleanupSettings(autoCleanupSettings.enabled);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cleanup settings to server.
|
||||
* @param {boolean} enabled - Whether auto-cleanup should be enabled
|
||||
* @returns {Promise<boolean>} True if save was successful
|
||||
*/
|
||||
async function saveCleanupSettings(enabled) {
|
||||
const criteria = collectCleanupCriteria();
|
||||
const statusText = document.getElementById('autoCleanupStatusText');
|
||||
|
||||
const settings = {
|
||||
enabled: enabled,
|
||||
types: criteria.types,
|
||||
date_field: criteria.date_field,
|
||||
days: criteria.days,
|
||||
name_filter: criteria.name_filter
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/cleanup-settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
autoCleanupSettings = data.settings;
|
||||
|
||||
// Update status text
|
||||
if (statusText) {
|
||||
if (data.settings.enabled) {
|
||||
statusText.textContent = 'Enabled (runs daily at 01:00 UTC)';
|
||||
statusText.classList.remove('text-muted');
|
||||
statusText.classList.add('text-success');
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
statusText.classList.remove('text-success');
|
||||
statusText.classList.add('text-muted');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Cleanup settings saved:', data.settings);
|
||||
return true;
|
||||
} else {
|
||||
console.error('Failed to save cleanup settings:', data.error);
|
||||
showToast('Failed to save settings: ' + data.error, 'danger');
|
||||
|
||||
// Restore previous status
|
||||
if (statusText && autoCleanupSettings) {
|
||||
if (autoCleanupSettings.enabled) {
|
||||
statusText.textContent = 'Enabled (runs daily at 01:00 UTC)';
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving cleanup settings:', error);
|
||||
showToast('Network error saving settings', 'danger');
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Save failed';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to store preview contacts
|
||||
let cleanupPreviewContacts = [];
|
||||
|
||||
|
||||
@@ -124,6 +124,21 @@
|
||||
<input type="number" class="form-control" id="cleanupDays" value="0" min="0">
|
||||
<small class="form-text text-muted">Contacts inactive for more than this many days will be selected</small>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Cleanup Toggle -->
|
||||
<div class="mb-3 mt-4 pt-3 border-top">
|
||||
<div class="form-check form-switch d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoCleanupSwitch" style="cursor: pointer; min-width: 3rem; min-height: 1.5rem;">
|
||||
<label class="form-check-label" for="autoCleanupSwitch" style="cursor: pointer; font-weight: 500;">
|
||||
<span id="autoCleanupLabel">Enable Auto-Cleanup</span>
|
||||
</label>
|
||||
<i class="bi bi-info-circle info-icon"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="When enabled, contacts matching the above criteria will be automatically deleted daily at 01:00 UTC"></i>
|
||||
</div>
|
||||
<small class="text-muted">Status: <span id="autoCleanupStatusText">Loading...</span></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-warning" id="cleanupPreviewBtn">
|
||||
|
||||
Reference in New Issue
Block a user