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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-25 10:22:02 +01:00
parent a23eb2a5f4
commit da31ab8794
5 changed files with 119 additions and 17 deletions

View File

@@ -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")

View File

@@ -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,

View File

@@ -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';
}

View File

@@ -135,9 +135,40 @@
<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>
title="When enabled, contacts matching the above criteria will be automatically deleted daily at the specified hour (UTC)"></i>
</div>
<small class="text-muted">Status: <span id="autoCleanupStatusText">Loading...</span></small>
<!-- Cleanup Hour Selector -->
<div class="mt-2 d-flex align-items-center gap-2">
<label for="cleanupHour" class="form-label mb-0 small">Run at:</label>
<select class="form-select form-select-sm" id="cleanupHour" style="width: auto;" disabled>
<option value="0">00:00 UTC</option>
<option value="1" selected>01:00 UTC</option>
<option value="2">02:00 UTC</option>
<option value="3">03:00 UTC</option>
<option value="4">04:00 UTC</option>
<option value="5">05:00 UTC</option>
<option value="6">06:00 UTC</option>
<option value="7">07:00 UTC</option>
<option value="8">08:00 UTC</option>
<option value="9">09:00 UTC</option>
<option value="10">10:00 UTC</option>
<option value="11">11:00 UTC</option>
<option value="12">12:00 UTC</option>
<option value="13">13:00 UTC</option>
<option value="14">14:00 UTC</option>
<option value="15">15:00 UTC</option>
<option value="16">16:00 UTC</option>
<option value="17">17:00 UTC</option>
<option value="18">18:00 UTC</option>
<option value="19">19:00 UTC</option>
<option value="20">20:00 UTC</option>
<option value="21">21:00 UTC</option>
<option value="22">22:00 UTC</option>
<option value="23">23:00 UTC</option>
</select>
</div>
</div>
</div>

View File

@@ -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