mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-07-07 02:10:58 +02: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
|
# Global scheduler instance
|
||||||
_scheduler: Optional[BackgroundScheduler] = None
|
_scheduler: Optional[BackgroundScheduler] = None
|
||||||
|
|
||||||
|
# Job IDs
|
||||||
|
CLEANUP_JOB_ID = 'daily_cleanup'
|
||||||
|
|
||||||
|
|
||||||
def get_archive_path(archive_date: str) -> Path:
|
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')}")
|
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():
|
def schedule_daily_archiving():
|
||||||
"""
|
"""
|
||||||
Initialize and start the background scheduler for 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")
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Failed to start archive scheduler: {e}", exc_info=True)
|
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
|
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'])
|
@api_bp.route('/messages', methods=['GET'])
|
||||||
def get_messages():
|
def get_messages():
|
||||||
"""
|
"""
|
||||||
@@ -1895,6 +1973,128 @@ def toggle_contact_protection(public_key):
|
|||||||
}), 500
|
}), 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)
|
# 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 sortBy = 'last_advert'; // 'name' or 'last_advert'
|
||||||
let sortOrder = 'desc'; // 'asc' or 'desc'
|
let sortOrder = 'desc'; // 'asc' or 'desc'
|
||||||
|
|
||||||
|
// Auto-cleanup state
|
||||||
|
let autoCleanupSettings = null;
|
||||||
|
let cleanupSaveDebounceTimer = null;
|
||||||
|
|
||||||
// Map state (Leaflet)
|
// Map state (Leaflet)
|
||||||
let leafletMap = null;
|
let leafletMap = null;
|
||||||
let markersGroup = null;
|
let markersGroup = null;
|
||||||
@@ -168,6 +172,9 @@ function initManagePage() {
|
|||||||
// Load contact counts for badges
|
// Load contact counts for badges
|
||||||
loadContactCounts();
|
loadContactCounts();
|
||||||
|
|
||||||
|
// Load cleanup settings (populates form and auto-cleanup status)
|
||||||
|
loadCleanupSettings();
|
||||||
|
|
||||||
// Attach event listeners for manage page
|
// Attach event listeners for manage page
|
||||||
attachManageEventListeners();
|
attachManageEventListeners();
|
||||||
}
|
}
|
||||||
@@ -190,6 +197,33 @@ function attachManageEventListeners() {
|
|||||||
if (confirmCleanupBtn) {
|
if (confirmCleanupBtn) {
|
||||||
confirmCleanupBtn.addEventListener('click', handleCleanupConfirm);
|
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() {
|
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
|
// Global variable to store preview contacts
|
||||||
let cleanupPreviewContacts = [];
|
let cleanupPreviewContacts = [];
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,21 @@
|
|||||||
<input type="number" class="form-control" id="cleanupDays" value="0" min="0">
|
<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>
|
<small class="form-text text-muted">Contacts inactive for more than this many days will be selected</small>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-warning" id="cleanupPreviewBtn">
|
<button class="btn btn-warning" id="cleanupPreviewBtn">
|
||||||
|
|||||||
Reference in New Issue
Block a user