Fix automation timezone bug

pull/253/head
Broque Thomas 3 months ago
parent 918dbad88f
commit d97b3d1846

@ -17,11 +17,23 @@ import re
import time
import threading
import requests
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from utils.logging_config import get_logger
logger = get_logger("automation_engine")
def _utcnow():
"""Return current UTC time as timezone-aware datetime."""
return datetime.now(timezone.utc)
def _utcnow_str():
"""Return current UTC time as naive string for DB storage (consistent with SQLite CURRENT_TIMESTAMP)."""
return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
def _utc_after(seconds):
"""Return UTC time N seconds from now as naive string for DB storage."""
return (datetime.now(timezone.utc) + timedelta(seconds=seconds)).strftime('%Y-%m-%d %H:%M:%S')
SYSTEM_AUTOMATIONS = [
{
'name': 'Auto-Process Wishlist',
@ -161,8 +173,8 @@ class AutomationEngine:
if existing:
# Only reset next_run for timer-based triggers that have an initial delay
if spec.get('initial_delay') is not None:
next_run = (datetime.now() + timedelta(seconds=spec['initial_delay'])).strftime('%Y-%m-%d %H:%M:%S')
self.db.update_automation(existing['id'], next_run=next_run)
nr = _utc_after(spec['initial_delay'])
self.db.update_automation(existing['id'], next_run=nr)
logger.info(f"System automation '{spec['name']}' next_run reset to {spec['initial_delay']}s from now")
else:
logger.info(f"System automation '{spec['name']}' ready (event-based)")
@ -173,8 +185,8 @@ class AutomationEngine:
if not auto or not auto.get('enabled') or not auto.get('next_run'):
return 0
try:
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S')
remaining = (next_run - datetime.now()).total_seconds()
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
remaining = (next_run - _utcnow()).total_seconds()
return max(0, int(remaining))
except (ValueError, TypeError):
return 0
@ -561,20 +573,22 @@ class AutomationEngine:
try:
trigger_config = json.loads(auto.get('trigger_config') or '{}')
if trigger_type == 'daily_time':
# Next run is tomorrow at the configured time
# Next run is tomorrow at the configured time (compute delay from local time, store as UTC)
time_str = trigger_config.get('time', '00:00')
hour, minute = map(int, time_str.split(':'))
target = datetime.now().replace(hour=hour, minute=minute, second=0, microsecond=0) + timedelta(days=1)
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
now_local = datetime.now()
target = now_local.replace(hour=hour, minute=minute, second=0, microsecond=0) + timedelta(days=1)
next_run_str = _utc_after((target - now_local).total_seconds())
elif trigger_type == 'weekly_time':
time_str = trigger_config.get('time', '00:00')
hour, minute = map(int, time_str.split(':'))
now_local = datetime.now()
target = self._next_weekly_occurrence(hour, minute, trigger_config.get('days', []))
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
next_run_str = _utc_after((target - now_local).total_seconds())
else:
delay = self._calc_delay_seconds(trigger_config)
if delay:
next_run_str = (datetime.now() + timedelta(seconds=delay)).strftime('%Y-%m-%d %H:%M:%S')
next_run_str = _utc_after(delay)
except Exception:
pass
@ -623,14 +637,14 @@ class AutomationEngine:
auto = self.db.get_automation(automation_id)
if auto and auto.get('next_run'):
try:
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S')
remaining = (next_run - datetime.now()).total_seconds()
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
remaining = (next_run - _utcnow()).total_seconds()
if remaining > 0:
delay = remaining
except (ValueError, TypeError):
pass
next_run_str = (datetime.now() + timedelta(seconds=delay)).strftime('%Y-%m-%d %H:%M:%S')
next_run_str = _utc_after(delay)
self.db.update_automation(automation_id, next_run=next_run_str)
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
@ -650,14 +664,14 @@ class AutomationEngine:
except (ValueError, AttributeError):
hour, minute = 0, 0
now = datetime.now()
target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
if target <= now:
now_local = datetime.now()
target = now_local.replace(hour=hour, minute=minute, second=0, microsecond=0)
if target <= now_local:
target += timedelta(days=1)
delay = (target - now).total_seconds()
delay = (target - now_local).total_seconds()
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
next_run_str = _utc_after(delay)
self.db.update_automation(automation_id, next_run=next_run_str)
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
@ -680,7 +694,7 @@ class AutomationEngine:
target = self._next_weekly_occurrence(hour, minute, config.get('days', []))
delay = (target - datetime.now()).total_seconds()
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
next_run_str = _utc_after(delay)
self.db.update_automation(automation_id, next_run=next_run_str)
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))

@ -65,7 +65,7 @@ from core.web_scan_manager import WebScanManager
from core.lyrics_client import lyrics_client
from database.music_database import get_database
from services.sync_service import PlaylistSyncService
from datetime import datetime
from datetime import datetime, timezone
import yt_dlp
from core.matching_engine import MusicMatchingEngine
from beatport_unified_scraper import BeatportUnifiedScraper
@ -968,11 +968,11 @@ def _register_automation_handlers():
state = automation_progress_states.get(aid)
if state:
started_at = state.get('started_at')
finished_at = state.get('finished_at') or datetime.now().isoformat()
finished_at = state.get('finished_at') or datetime.now(timezone.utc).isoformat()
log_entries = list(state.get('log', []))
else:
started_at = datetime.now().isoformat()
finished_at = datetime.now().isoformat()
started_at = datetime.now(timezone.utc).isoformat()
finished_at = datetime.now(timezone.utc).isoformat()
log_entries = []
# Compute duration
@ -1181,7 +1181,7 @@ def _init_automation_progress(automation_id, automation_name, action_type):
'progress': 0, 'phase': 'Starting...', 'current_item': '',
'processed': 0, 'total': 0,
'log': [{'type': 'info', 'text': f'Starting {automation_name}'}],
'started_at': datetime.now().isoformat(),
'started_at': datetime.now(timezone.utc).isoformat(),
'finished_at': None,
}
@ -1202,7 +1202,7 @@ def _update_automation_progress(automation_id, **kwargs):
state[k] = v
# Immediate emit on finish so frontend gets final state without waiting for loop
if kwargs.get('status') in ('finished', 'error'):
state['finished_at'] = datetime.now().isoformat()
state['finished_at'] = datetime.now(timezone.utc).isoformat()
try:
socketio.emit('automation:progress', {str(automation_id): dict(state)})
except Exception:

@ -43515,15 +43515,20 @@ function _autoFormatNotify(type) {
if (type === 'fire_signal') return '\u26A1 Signal';
return type || '';
}
function _autoParseUTC(ts) {
// If timestamp already has timezone info (+00:00 or Z), parse as-is; otherwise append Z to treat as UTC
if (/[Zz]$/.test(ts) || /[+-]\d{2}:\d{2}$/.test(ts)) return new Date(ts).getTime();
return new Date(ts + 'Z').getTime();
}
function _autoTimeAgo(ts) {
if (!ts) return 'Never';
const d = (Date.now() - new Date(ts + 'Z').getTime()) / 1000;
const d = (Date.now() - _autoParseUTC(ts)) / 1000;
if (d < 60) return 'just now'; if (d < 3600) return Math.floor(d/60) + 'm ago';
if (d < 86400) return Math.floor(d/3600) + 'h ago'; return Math.floor(d/86400) + 'd ago';
}
function _autoTimeUntil(ts) {
if (!ts) return '';
const d = (new Date(ts + 'Z').getTime() - Date.now()) / 1000;
const d = (_autoParseUTC(ts) - Date.now()) / 1000;
if (d <= 0) return 'soon'; if (d < 60) return 'in ' + Math.ceil(d) + 's';
if (d < 3600) return 'in ' + Math.ceil(d/60) + 'm'; if (d < 86400) return 'in ' + Math.round(d/3600) + 'h';
return 'in ' + Math.round(d/86400) + 'd';

Loading…
Cancel
Save