Fix: Komplett auf Europe/Berlin + DB-Migration + Timer-Fix

- ALLE Timestamps einheitlich Europe/Berlin (kein UTC mehr)
- DB-Migration: 1704 bestehende Timestamps von UTC nach Berlin konvertiert
- Auto-Refresh Timer Fix: ORDER BY id DESC statt completed_at DESC
  (verhindert falsche Sortierung bei gemischten Timestamp-Formaten)
- started_at statt completed_at fuer Timer-Vergleich (konsistenter)
- Manuelle Refreshes werden bei Intervall-Pruefung beruecksichtigt
- Debug-Logging fuer Auto-Refresh Entscheidungen
- astimezone() fuer Timestamps mit Offset-Info

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-07 02:56:51 +01:00
Ursprung a8e9f34ff8
Commit a69352575d
6 geänderte Dateien mit 45 neuen und 35 gelöschten Zeilen

Datei anzeigen

@@ -5,7 +5,7 @@ import logging
import os
import sys
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from datetime import datetime
from typing import Dict
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request, Response
@@ -93,15 +93,15 @@ async def check_auto_refresh():
)
incidents = await cursor.fetchall()
now = datetime.now(timezone.utc)
now = datetime.now(TIMEZONE)
for incident in incidents:
incident_id = incident["id"]
interval = incident["refresh_interval"]
# Nur letzten AUTO-Refresh prüfen (manuelle Refreshs ignorieren)
# Letzten abgeschlossenen Refresh prüfen (egal ob auto oder manual)
cursor = await db.execute(
"SELECT completed_at FROM refresh_log WHERE incident_id = ? AND trigger_type = 'auto' AND status = 'completed' ORDER BY completed_at DESC LIMIT 1",
"SELECT started_at FROM refresh_log WHERE incident_id = ? AND status = 'completed' ORDER BY id DESC LIMIT 1",
(incident_id,),
)
last_refresh = await cursor.fetchone()
@@ -110,21 +110,27 @@ async def check_auto_refresh():
if not last_refresh:
should_refresh = True
else:
last_time = datetime.fromisoformat(last_refresh["completed_at"])
last_time = datetime.fromisoformat(last_refresh["started_at"])
if last_time.tzinfo is None:
last_time = last_time.replace(tzinfo=timezone.utc)
last_time = last_time.replace(tzinfo=TIMEZONE)
else:
last_time = last_time.astimezone(TIMEZONE)
elapsed = (now - last_time).total_seconds() / 60
if elapsed >= interval:
should_refresh = True
logger.info(f"Auto-Refresh Lage {incident_id}: {elapsed:.1f} Min seit letztem Refresh (Intervall: {interval} Min)")
else:
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
cursor = await db.execute(
running_cursor = await db.execute(
"SELECT id FROM refresh_log WHERE incident_id = ? AND status = 'running' LIMIT 1",
(incident_id,),
)
if await cursor.fetchone():
continue # Laufender Refresh — überspringen
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")
@@ -143,11 +149,13 @@ async def cleanup_expired():
)
incidents = await cursor.fetchall()
now = datetime.now(timezone.utc)
now = datetime.now(TIMEZONE)
for incident in incidents:
created = datetime.fromisoformat(incident["created_at"])
if created.tzinfo is None:
created = created.replace(tzinfo=timezone.utc)
created = created.replace(tzinfo=TIMEZONE)
else:
created = created.astimezone(TIMEZONE)
age_days = (now - created).days
if age_days >= incident["retention_days"]:
await db.execute(
@@ -164,12 +172,14 @@ async def cleanup_expired():
for orphan in orphans:
started = datetime.fromisoformat(orphan["started_at"])
if started.tzinfo is None:
started = started.replace(tzinfo=timezone.utc)
started = started.replace(tzinfo=TIMEZONE)
else:
started = started.astimezone(TIMEZONE)
age_minutes = (now - started).total_seconds() / 60
if age_minutes >= 15:
await db.execute(
"UPDATE refresh_log SET status = 'error', completed_at = ?, error_message = ? WHERE id = ?",
(now.isoformat(), f"Verwaist (>{int(age_minutes)} Min ohne Abschluss, automatisch bereinigt)", orphan["id"]),
(now.strftime('%Y-%m-%d %H:%M:%S'), f"Verwaist (>{int(age_minutes)} Min ohne Abschluss, automatisch bereinigt)", orphan["id"]),
)
logger.warning(f"Verwaisten Refresh #{orphan['id']} für Lage {orphan['incident_id']} bereinigt ({int(age_minutes)} Min)")
@@ -195,7 +205,7 @@ async def lifespan(app: FastAPI):
try:
result = await db.execute(
"UPDATE refresh_log SET status = 'error', completed_at = ?, error_message = 'Verwaist (Neustart, automatisch bereinigt)' WHERE status = 'running'",
(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'),),
(datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'),),
)
if result.rowcount > 0:
await db.commit()