feat: Slot-basierter Auto-Refresh mit konfigurierbarer Startzeit
Auto-Refresh nutzt jetzt eine feste Anker-Uhrzeit (refresh_start_time) statt reinem Intervall-basiertem Driften. Verpasste Slots werden max. 1x aufgeholt. Bestehendes Intervall-Verhalten bleibt als Fallback erhalten (ohne Startzeit). Migration: Bestehende Auto-Lagen erhalten 07:00 als Startzeit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
73
src/main.py
73
src/main.py
@@ -5,7 +5,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request, Response
|
||||
@@ -107,11 +107,11 @@ scheduler = AsyncIOScheduler()
|
||||
|
||||
|
||||
async def check_auto_refresh():
|
||||
"""Prüft welche Lagen einen Auto-Refresh brauchen."""
|
||||
"""Prüft welche Lagen einen Auto-Refresh brauchen (Slot-basiert)."""
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"SELECT id, refresh_interval FROM incidents WHERE status = 'active' AND refresh_mode = 'auto'"
|
||||
"SELECT id, refresh_interval, refresh_start_time FROM incidents WHERE status = 'active' AND refresh_mode = 'auto'"
|
||||
)
|
||||
incidents = await cursor.fetchall()
|
||||
|
||||
@@ -120,18 +120,72 @@ async def check_auto_refresh():
|
||||
for incident in incidents:
|
||||
incident_id = incident["id"]
|
||||
interval = incident["refresh_interval"]
|
||||
start_time_str = incident["refresh_start_time"]
|
||||
|
||||
# Letzten abgeschlossenen Refresh prüfen (egal ob auto oder manual)
|
||||
# Letzten abgeschlossenen oder laufenden Refresh pruefen
|
||||
cursor = await db.execute(
|
||||
"SELECT started_at FROM refresh_log WHERE incident_id = ? AND status = 'completed' ORDER BY id DESC LIMIT 1",
|
||||
"SELECT started_at, status FROM refresh_log WHERE incident_id = ? AND status IN ('completed', 'running') ORDER BY id DESC LIMIT 1",
|
||||
(incident_id,),
|
||||
)
|
||||
last_refresh = await cursor.fetchone()
|
||||
|
||||
# Laufenden Refresh ueberspringen
|
||||
if last_refresh and last_refresh["status"] == "running":
|
||||
logger.debug(f"Auto-Refresh Lage {incident_id}: uebersprungen (laeuft bereits)")
|
||||
continue
|
||||
|
||||
should_refresh = False
|
||||
|
||||
if not last_refresh:
|
||||
# Noch nie gelaufen -> sofort starten
|
||||
should_refresh = True
|
||||
logger.info(f"Auto-Refresh Lage {incident_id}: erster Refresh")
|
||||
elif start_time_str:
|
||||
# Slot-basierte Logik: Naechsten faelligen Slot berechnen
|
||||
try:
|
||||
start_h, start_m = map(int, start_time_str.split(":"))
|
||||
except (ValueError, AttributeError):
|
||||
logger.warning(f"Auto-Refresh Lage {incident_id}: ungueltiges Startzeit-Format '{start_time_str}'")
|
||||
continue
|
||||
|
||||
last_time = datetime.fromisoformat(last_refresh["started_at"])
|
||||
if last_time.tzinfo is None:
|
||||
last_time = last_time.replace(tzinfo=TIMEZONE)
|
||||
else:
|
||||
last_time = last_time.astimezone(TIMEZONE)
|
||||
|
||||
# Anker: heute um start_time
|
||||
anchor_today = now.replace(hour=start_h, minute=start_m, second=0, microsecond=0)
|
||||
interval_td = timedelta(minutes=interval)
|
||||
|
||||
if interval >= 1440:
|
||||
# Taeglicher oder laengerer Rhythmus
|
||||
days_interval = interval // 1440
|
||||
# Letzter Slot der <= now ist
|
||||
current_slot = anchor_today
|
||||
if current_slot > now:
|
||||
current_slot -= timedelta(days=days_interval)
|
||||
# Sicherheitsschleife: weiter zurueck falls noetig
|
||||
while current_slot > now:
|
||||
current_slot -= timedelta(days=days_interval)
|
||||
else:
|
||||
# Untertaegig: Slots ab Anker im Intervall-Takt
|
||||
# Anker zurueck bis vor last_refresh
|
||||
ref_anchor = anchor_today
|
||||
while ref_anchor > last_time:
|
||||
ref_anchor -= interval_td
|
||||
# Von dort vorwaerts bis zum letzten Slot <= now
|
||||
current_slot = ref_anchor
|
||||
while current_slot + interval_td <= now:
|
||||
current_slot += interval_td
|
||||
|
||||
if current_slot > last_time:
|
||||
should_refresh = True
|
||||
logger.info(f"Auto-Refresh Lage {incident_id}: Slot {current_slot.strftime('%H:%M')} faellig (letzter Refresh: {last_time.strftime('%Y-%m-%d %H:%M')})")
|
||||
else:
|
||||
logger.debug(f"Auto-Refresh Lage {incident_id}: kein faelliger Slot (letzter: {current_slot.strftime('%H:%M')})")
|
||||
else:
|
||||
# Fallback: altes Intervall-Verhalten (kein start_time gesetzt)
|
||||
last_time = datetime.fromisoformat(last_refresh["started_at"])
|
||||
if last_time.tzinfo is None:
|
||||
last_time = last_time.replace(tzinfo=TIMEZONE)
|
||||
@@ -145,15 +199,6 @@ async def check_auto_refresh():
|
||||
logger.debug(f"Auto-Refresh Lage {incident_id}: {elapsed:.1f}/{interval} Min — noch nicht faellig")
|
||||
|
||||
if should_refresh:
|
||||
# Prüfen ob bereits ein laufender Refresh existiert
|
||||
running_cursor = await db.execute(
|
||||
"SELECT id FROM refresh_log WHERE incident_id = ? AND status = 'running' LIMIT 1",
|
||||
(incident_id,),
|
||||
)
|
||||
if await running_cursor.fetchone():
|
||||
logger.debug(f"Auto-Refresh Lage {incident_id}: uebersprungen (laeuft bereits)")
|
||||
continue
|
||||
|
||||
await orchestrator.enqueue_refresh(incident_id, trigger_type="auto")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren