From da31ab8794cf31d419f74dc63326a82a83a3cd76 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 25 Jan 2026 10:22:02 +0100 Subject: [PATCH] feat: Add configurable hour for scheduled contact cleanup Allow users to select the hour (0-23 UTC) when automatic contact cleanup runs: - Add hour selector dropdown in Advanced Filters (disabled until enabled) - Hour field saved to .webui_settings.json with cleanup_settings - API validates hour (0-23), scheduler uses CronTrigger with hour param - Status text shows configured hour (e.g., "Enabled (runs daily at 03:00 UTC)") - Documentation updated in user-guide.md Co-Authored-By: Claude Opus 4.5 --- app/archiver/manager.py | 18 ++++++++---- app/routes/api.py | 20 +++++++++---- app/static/js/contacts.js | 46 ++++++++++++++++++++++++++---- app/templates/contacts-manage.html | 33 ++++++++++++++++++++- docs/user-guide.md | 19 ++++++++++++ 5 files changed, 119 insertions(+), 17 deletions(-) diff --git a/app/archiver/manager.py b/app/archiver/manager.py index f8b57e7..f4d43e0 100644 --- a/app/archiver/manager.py +++ b/app/archiver/manager.py @@ -335,12 +335,13 @@ def _cleanup_job(): logger.error(f"Cleanup job failed: {e}", exc_info=True) -def schedule_cleanup(enabled: bool) -> bool: +def schedule_cleanup(enabled: bool, hour: int = 1) -> bool: """ Add or remove the cleanup job from the scheduler. Args: enabled: True to enable cleanup job, False to disable + hour: Hour (0-23 UTC) at which to run cleanup job Returns: True if successful, False otherwise @@ -353,8 +354,12 @@ def schedule_cleanup(enabled: bool) -> bool: try: if enabled: - # Add cleanup job at 01:00 UTC - trigger = CronTrigger(hour=1, minute=0) + # Validate hour + if not isinstance(hour, int) or hour < 0 or hour > 23: + hour = 1 + + # Add cleanup job at specified hour UTC + trigger = CronTrigger(hour=hour, minute=0) _scheduler.add_job( func=_cleanup_job, @@ -364,7 +369,7 @@ def schedule_cleanup(enabled: bool) -> bool: replace_existing=True ) - logger.info("Cleanup job scheduled - will run daily at 01:00 UTC") + logger.info(f"Cleanup job scheduled - will run daily at {hour:02d}:00 UTC") else: # Remove cleanup job if it exists try: @@ -393,8 +398,9 @@ def init_cleanup_schedule(): settings = get_cleanup_settings() if settings.get('enabled'): - schedule_cleanup(enabled=True) - logger.info("Auto-cleanup enabled from saved settings") + hour = settings.get('hour', 1) + schedule_cleanup(enabled=True, hour=hour) + logger.info(f"Auto-cleanup enabled from saved settings (hour={hour:02d}:00 UTC)") else: logger.info("Auto-cleanup is disabled in saved settings") diff --git a/app/routes/api.py b/app/routes/api.py index a0b36fd..11244f4 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -192,7 +192,8 @@ def get_cleanup_settings() -> dict: 'types': list[int], 'date_field': str, 'days': int, - 'name_filter': str + 'name_filter': str, + 'hour': int (0-23, UTC) } """ from pathlib import Path @@ -201,7 +202,8 @@ def get_cleanup_settings() -> dict: 'types': [1, 2, 3, 4], 'date_field': 'last_advert', 'days': 30, - 'name_filter': '' + 'name_filter': '', + 'hour': 1 } settings_path = Path(config.MC_CONFIG_DIR) / ".webui_settings.json" @@ -2023,7 +2025,8 @@ def update_cleanup_settings_api(): "types": [1, 2], "date_field": "last_advert", "days": 30, - "name_filter": "" + "name_filter": "", + "hour": 1 } Returns: @@ -2066,6 +2069,13 @@ def update_cleanup_settings_api(): 'error': 'Invalid enabled (must be boolean)' }), 400 + if 'hour' in data: + if not isinstance(data['hour'], int) or data['hour'] < 0 or data['hour'] > 23: + return jsonify({ + 'success': False, + 'error': 'Invalid hour (must be integer 0-23)' + }), 400 + # Get current settings and merge with new values current = get_cleanup_settings() updated = {**current, **data} @@ -2077,9 +2087,9 @@ def update_cleanup_settings_api(): 'error': 'Failed to save cleanup settings' }), 500 - # Update scheduler based on enabled state + # Update scheduler based on enabled state and hour from app.archiver.manager import schedule_cleanup - schedule_cleanup(enabled=updated.get('enabled', False)) + schedule_cleanup(enabled=updated.get('enabled', False), hour=updated.get('hour', 1)) return jsonify({ 'success': True, diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js index 3f38df9..b909331 100644 --- a/app/static/js/contacts.js +++ b/app/static/js/contacts.js @@ -224,6 +224,17 @@ function attachManageEventListeners() { document.querySelectorAll('input[name="cleanupDateField"]').forEach(radio => { radio.addEventListener('change', debouncedSaveCleanupCriteria); }); + + // Cleanup hour selector (only saves when auto-cleanup is enabled) + const cleanupHour = document.getElementById('cleanupHour'); + if (cleanupHour) { + cleanupHour.addEventListener('change', () => { + // Only save if auto-cleanup is enabled + if (autoCleanupSettings && autoCleanupSettings.enabled) { + saveCleanupSettings(true); + } + }); + } } async function loadContactCounts() { @@ -334,14 +345,23 @@ function applyCleanupSettingsToUI(settings) { // Auto-cleanup switch and status const autoCleanupSwitch = document.getElementById('autoCleanupSwitch'); const statusText = document.getElementById('autoCleanupStatusText'); + const hourSelect = document.getElementById('cleanupHour'); if (autoCleanupSwitch) { autoCleanupSwitch.checked = settings.enabled || false; } + // Hour selector + const hour = settings.hour !== undefined ? settings.hour : 1; + if (hourSelect) { + hourSelect.value = hour; + hourSelect.disabled = !settings.enabled; + } + if (statusText) { if (settings.enabled) { - statusText.textContent = 'Enabled (runs daily at 01:00 UTC)'; + const hourStr = hour.toString().padStart(2, '0'); + statusText.textContent = `Enabled (runs daily at ${hourStr}:00 UTC)`; statusText.classList.remove('text-muted'); statusText.classList.add('text-success'); } else { @@ -359,6 +379,7 @@ function applyCleanupSettingsToUI(settings) { async function handleAutoCleanupToggle(event) { const enabled = event.target.checked; const statusText = document.getElementById('autoCleanupStatusText'); + const hourSelect = document.getElementById('cleanupHour'); // Validate before enabling if (enabled) { @@ -379,6 +400,11 @@ async function handleAutoCleanupToggle(event) { } } + // Enable/disable hour selector + if (hourSelect) { + hourSelect.disabled = !enabled; + } + // Update status text while saving if (statusText) { statusText.textContent = 'Saving...'; @@ -389,8 +415,11 @@ async function handleAutoCleanupToggle(event) { const success = await saveCleanupSettings(enabled); if (!success) { - // Revert switch on failure + // Revert switch and hour selector on failure event.target.checked = !enabled; + if (hourSelect) { + hourSelect.disabled = enabled; + } } } @@ -422,13 +451,16 @@ function debouncedSaveCleanupCriteria() { async function saveCleanupSettings(enabled) { const criteria = collectCleanupCriteria(); const statusText = document.getElementById('autoCleanupStatusText'); + const hourSelect = document.getElementById('cleanupHour'); + const hour = hourSelect ? parseInt(hourSelect.value) : 1; const settings = { enabled: enabled, types: criteria.types, date_field: criteria.date_field, days: criteria.days, - name_filter: criteria.name_filter + name_filter: criteria.name_filter, + hour: hour }; try { @@ -448,7 +480,9 @@ async function saveCleanupSettings(enabled) { // Update status text if (statusText) { if (data.settings.enabled) { - statusText.textContent = 'Enabled (runs daily at 01:00 UTC)'; + const savedHour = data.settings.hour !== undefined ? data.settings.hour : 1; + const hourStr = savedHour.toString().padStart(2, '0'); + statusText.textContent = `Enabled (runs daily at ${hourStr}:00 UTC)`; statusText.classList.remove('text-muted'); statusText.classList.add('text-success'); } else { @@ -467,7 +501,9 @@ async function saveCleanupSettings(enabled) { // Restore previous status if (statusText && autoCleanupSettings) { if (autoCleanupSettings.enabled) { - statusText.textContent = 'Enabled (runs daily at 01:00 UTC)'; + const prevHour = autoCleanupSettings.hour !== undefined ? autoCleanupSettings.hour : 1; + const hourStr = prevHour.toString().padStart(2, '0'); + statusText.textContent = `Enabled (runs daily at ${hourStr}:00 UTC)`; } else { statusText.textContent = 'Disabled'; } diff --git a/app/templates/contacts-manage.html b/app/templates/contacts-manage.html index 62ea286..85e77a9 100644 --- a/app/templates/contacts-manage.html +++ b/app/templates/contacts-manage.html @@ -135,9 +135,40 @@ + title="When enabled, contacts matching the above criteria will be automatically deleted daily at the specified hour (UTC)"> Status: Loading... + + +
+ + +
diff --git a/docs/user-guide.md b/docs/user-guide.md index 3f72b8e..502f33f 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -282,6 +282,25 @@ The advanced cleanup tool allows you to filter and remove contacts based on mult - Remove all REP contacts inactive for 30+ days: Select REP, set days to 30 - Clean specific contact names: Enter partial name (e.g., "test") +### Automatic Contact Cleanup + +You can schedule automatic cleanup to run daily at a specified hour (UTC): + +1. Navigate to **Contact Management** page +2. Expand **Advanced Filters** section +3. Configure your filter criteria (types, date field, days of inactivity) +4. Toggle **Enable Auto-Cleanup** switch +5. Select the hour (UTC) when cleanup should run + +**Requirements for enabling auto-cleanup:** +- "Days of Inactivity" must be set to a value greater than 0 +- At least one contact type must be selected + +**Notes:** +- Protected contacts are never deleted by auto-cleanup +- Filter criteria changes are auto-saved when auto-cleanup is enabled +- The scheduler runs in UTC timezone + --- ## Network Commands