mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-05-05 13:02:34 +02:00
feat: Use local timezone for scheduled cleanup instead of UTC
The scheduler now uses the timezone configured in .env (TZ variable) instead of hardcoded UTC: - Add get_local_timezone_name() helper to manager.py - BackgroundScheduler uses system local timezone - API returns timezone field in cleanup-settings response - Frontend displays timezone next to hour selector and in status text - Updated documentation to reflect timezone behavior This makes the cleanup hour more intuitive for users in non-UTC timezones. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,37 @@ _scheduler: Optional[BackgroundScheduler] = None
|
||||
CLEANUP_JOB_ID = 'daily_cleanup'
|
||||
|
||||
|
||||
def get_local_timezone_name() -> str:
|
||||
"""
|
||||
Get the local timezone name for display purposes.
|
||||
Uses TZ environment variable if set, otherwise detects from system.
|
||||
|
||||
Returns:
|
||||
Timezone name (e.g., 'Europe/Warsaw', 'UTC', 'CET')
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# First check TZ environment variable
|
||||
tz_env = os.environ.get('TZ')
|
||||
if tz_env:
|
||||
return tz_env
|
||||
|
||||
# Fall back to system timezone detection
|
||||
try:
|
||||
# Try to get timezone name from datetime
|
||||
local_tz = datetime.now().astimezone().tzinfo
|
||||
if local_tz:
|
||||
tz_name = str(local_tz)
|
||||
# Clean up timezone name if needed
|
||||
if tz_name and tz_name != 'None':
|
||||
return tz_name
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 'local'
|
||||
|
||||
|
||||
def get_archive_path(archive_date: str) -> Path:
|
||||
"""
|
||||
Get the path to an archive file for a specific date.
|
||||
@@ -341,7 +372,7 @@ def schedule_cleanup(enabled: bool, hour: int = 1) -> bool:
|
||||
|
||||
Args:
|
||||
enabled: True to enable cleanup job, False to disable
|
||||
hour: Hour (0-23 UTC) at which to run cleanup job
|
||||
hour: Hour (0-23, local time) at which to run cleanup job
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
@@ -358,7 +389,7 @@ def schedule_cleanup(enabled: bool, hour: int = 1) -> bool:
|
||||
if not isinstance(hour, int) or hour < 0 or hour > 23:
|
||||
hour = 1
|
||||
|
||||
# Add cleanup job at specified hour UTC
|
||||
# Add cleanup job at specified hour (local time)
|
||||
trigger = CronTrigger(hour=hour, minute=0)
|
||||
|
||||
_scheduler.add_job(
|
||||
@@ -369,7 +400,8 @@ def schedule_cleanup(enabled: bool, hour: int = 1) -> bool:
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
logger.info(f"Cleanup job scheduled - will run daily at {hour:02d}:00 UTC")
|
||||
tz_name = get_local_timezone_name()
|
||||
logger.info(f"Cleanup job scheduled - will run daily at {hour:02d}:00 ({tz_name})")
|
||||
else:
|
||||
# Remove cleanup job if it exists
|
||||
try:
|
||||
@@ -400,7 +432,8 @@ def init_cleanup_schedule():
|
||||
if settings.get('enabled'):
|
||||
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)")
|
||||
tz_name = get_local_timezone_name()
|
||||
logger.info(f"Auto-cleanup enabled from saved settings (hour={hour:02d}:00 {tz_name})")
|
||||
else:
|
||||
logger.info("Auto-cleanup is disabled in saved settings")
|
||||
|
||||
@@ -424,12 +457,15 @@ def schedule_daily_archiving():
|
||||
return
|
||||
|
||||
try:
|
||||
# Use local timezone (from TZ env variable or system default)
|
||||
tz_name = get_local_timezone_name()
|
||||
|
||||
_scheduler = BackgroundScheduler(
|
||||
daemon=True,
|
||||
timezone='UTC' # Use UTC for consistency
|
||||
daemon=True
|
||||
# No timezone specified = uses system local timezone
|
||||
)
|
||||
|
||||
# Schedule job for midnight every day
|
||||
# Schedule job for midnight every day (local time)
|
||||
trigger = CronTrigger(hour=0, minute=0)
|
||||
|
||||
_scheduler.add_job(
|
||||
@@ -442,7 +478,7 @@ def schedule_daily_archiving():
|
||||
|
||||
_scheduler.start()
|
||||
|
||||
logger.info("Archive scheduler started - will run daily at 00:00 UTC")
|
||||
logger.info(f"Archive scheduler started - will run daily at 00:00 ({tz_name})")
|
||||
|
||||
# Initialize cleanup schedule from saved settings
|
||||
init_cleanup_schedule()
|
||||
|
||||
@@ -1989,15 +1989,22 @@ def get_cleanup_settings_api():
|
||||
"types": [1, 2, 3, 4],
|
||||
"date_field": "last_advert",
|
||||
"days": 30,
|
||||
"name_filter": ""
|
||||
}
|
||||
"name_filter": "",
|
||||
"hour": 1
|
||||
},
|
||||
"timezone": "Europe/Warsaw"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from app.archiver.manager import get_local_timezone_name
|
||||
|
||||
settings = get_cleanup_settings()
|
||||
timezone = get_local_timezone_name()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'settings': settings
|
||||
'settings': settings,
|
||||
'timezone': timezone
|
||||
}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cleanup settings: {e}")
|
||||
@@ -2009,8 +2016,10 @@ def get_cleanup_settings_api():
|
||||
'types': [1, 2, 3, 4],
|
||||
'date_field': 'last_advert',
|
||||
'days': 30,
|
||||
'name_filter': ''
|
||||
}
|
||||
'name_filter': '',
|
||||
'hour': 1
|
||||
},
|
||||
'timezone': 'local'
|
||||
}), 500
|
||||
|
||||
|
||||
@@ -2088,13 +2097,14 @@ def update_cleanup_settings_api():
|
||||
}), 500
|
||||
|
||||
# Update scheduler based on enabled state and hour
|
||||
from app.archiver.manager import schedule_cleanup
|
||||
from app.archiver.manager import schedule_cleanup, get_local_timezone_name
|
||||
schedule_cleanup(enabled=updated.get('enabled', False), hour=updated.get('hour', 1))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Cleanup settings updated',
|
||||
'settings': updated
|
||||
'settings': updated,
|
||||
'timezone': get_local_timezone_name()
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -54,6 +54,7 @@ let sortOrder = 'desc'; // 'asc' or 'desc'
|
||||
// Auto-cleanup state
|
||||
let autoCleanupSettings = null;
|
||||
let cleanupSaveDebounceTimer = null;
|
||||
let cleanupTimezone = 'local'; // Timezone from server (e.g., 'Europe/Warsaw')
|
||||
|
||||
// Map state (Leaflet)
|
||||
let leafletMap = null;
|
||||
@@ -300,8 +301,9 @@ async function loadCleanupSettings() {
|
||||
|
||||
if (data.success) {
|
||||
autoCleanupSettings = data.settings;
|
||||
cleanupTimezone = data.timezone || 'local';
|
||||
applyCleanupSettingsToUI(autoCleanupSettings);
|
||||
console.log('Loaded cleanup settings:', autoCleanupSettings);
|
||||
console.log('Loaded cleanup settings:', autoCleanupSettings, 'timezone:', cleanupTimezone);
|
||||
} else {
|
||||
console.error('Failed to load cleanup settings:', data.error);
|
||||
if (statusText) statusText.textContent = 'Error loading settings';
|
||||
@@ -346,6 +348,7 @@ function applyCleanupSettingsToUI(settings) {
|
||||
const autoCleanupSwitch = document.getElementById('autoCleanupSwitch');
|
||||
const statusText = document.getElementById('autoCleanupStatusText');
|
||||
const hourSelect = document.getElementById('cleanupHour');
|
||||
const timezoneLabel = document.getElementById('cleanupTimezoneLabel');
|
||||
|
||||
if (autoCleanupSwitch) {
|
||||
autoCleanupSwitch.checked = settings.enabled || false;
|
||||
@@ -358,10 +361,15 @@ function applyCleanupSettingsToUI(settings) {
|
||||
hourSelect.disabled = !settings.enabled;
|
||||
}
|
||||
|
||||
// Display timezone next to hour selector
|
||||
if (timezoneLabel) {
|
||||
timezoneLabel.textContent = `(${cleanupTimezone})`;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
if (settings.enabled) {
|
||||
const hourStr = hour.toString().padStart(2, '0');
|
||||
statusText.textContent = `Enabled (runs daily at ${hourStr}:00 UTC)`;
|
||||
statusText.textContent = `Enabled (runs daily at ${hourStr}:00 ${cleanupTimezone})`;
|
||||
statusText.classList.remove('text-muted');
|
||||
statusText.classList.add('text-success');
|
||||
} else {
|
||||
@@ -476,13 +484,17 @@ async function saveCleanupSettings(enabled) {
|
||||
|
||||
if (data.success) {
|
||||
autoCleanupSettings = data.settings;
|
||||
// Update timezone if provided in response
|
||||
if (data.timezone) {
|
||||
cleanupTimezone = data.timezone;
|
||||
}
|
||||
|
||||
// Update status text
|
||||
if (statusText) {
|
||||
if (data.settings.enabled) {
|
||||
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.textContent = `Enabled (runs daily at ${hourStr}:00 ${cleanupTimezone})`;
|
||||
statusText.classList.remove('text-muted');
|
||||
statusText.classList.add('text-success');
|
||||
} else {
|
||||
@@ -503,7 +515,7 @@ async function saveCleanupSettings(enabled) {
|
||||
if (autoCleanupSettings.enabled) {
|
||||
const prevHour = autoCleanupSettings.hour !== undefined ? autoCleanupSettings.hour : 1;
|
||||
const hourStr = prevHour.toString().padStart(2, '0');
|
||||
statusText.textContent = `Enabled (runs daily at ${hourStr}:00 UTC)`;
|
||||
statusText.textContent = `Enabled (runs daily at ${hourStr}:00 ${cleanupTimezone})`;
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
<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 the specified hour (UTC)"></i>
|
||||
title="When enabled, contacts matching the above criteria will be automatically deleted daily at the specified hour"></i>
|
||||
</div>
|
||||
<small class="text-muted">Status: <span id="autoCleanupStatusText">Loading...</span></small>
|
||||
|
||||
@@ -143,31 +143,32 @@
|
||||
<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>
|
||||
<option value="0">00:00</option>
|
||||
<option value="1" selected>01:00</option>
|
||||
<option value="2">02:00</option>
|
||||
<option value="3">03:00</option>
|
||||
<option value="4">04:00</option>
|
||||
<option value="5">05:00</option>
|
||||
<option value="6">06:00</option>
|
||||
<option value="7">07:00</option>
|
||||
<option value="8">08:00</option>
|
||||
<option value="9">09:00</option>
|
||||
<option value="10">10:00</option>
|
||||
<option value="11">11:00</option>
|
||||
<option value="12">12:00</option>
|
||||
<option value="13">13:00</option>
|
||||
<option value="14">14:00</option>
|
||||
<option value="15">15:00</option>
|
||||
<option value="16">16:00</option>
|
||||
<option value="17">17:00</option>
|
||||
<option value="18">18:00</option>
|
||||
<option value="19">19:00</option>
|
||||
<option value="20">20:00</option>
|
||||
<option value="21">21:00</option>
|
||||
<option value="22">22:00</option>
|
||||
<option value="23">23:00</option>
|
||||
</select>
|
||||
<span class="small text-muted" id="cleanupTimezoneLabel"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,13 +284,13 @@ The advanced cleanup tool allows you to filter and remove contacts based on mult
|
||||
|
||||
### Automatic Contact Cleanup
|
||||
|
||||
You can schedule automatic cleanup to run daily at a specified hour (UTC):
|
||||
You can schedule automatic cleanup to run daily at a specified hour:
|
||||
|
||||
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
|
||||
5. Select the hour when cleanup should run
|
||||
|
||||
**Requirements for enabling auto-cleanup:**
|
||||
- "Days of Inactivity" must be set to a value greater than 0
|
||||
@@ -299,7 +299,7 @@ You can schedule automatic cleanup to run daily at a specified hour (UTC):
|
||||
**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
|
||||
- The scheduler uses the timezone configured in `.env` file (`TZ` variable, e.g., `TZ=Europe/Warsaw`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user