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:
MarekWo
2026-01-25 10:46:08 +01:00
parent da31ab8794
commit a4ef0fd497
5 changed files with 106 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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