Fix: UTC fuer interne Timer, Berlin nur fuer Anzeige

Korrektur: Alle DB-Timestamps (refresh_log, created_at, updated_at,
auth, notifications) bleiben UTC fuer korrekte Timer-Vergleiche.
Europe/Berlin nur fuer angezeigte Werte (Exporte, Prompts, API).
Verhindert zu fruehes Ausloesen des Auto-Refresh-Timers.

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

Datei anzeigen

@@ -3,7 +3,7 @@ import asyncio
import json
import logging
import re
from datetime import datetime
from datetime import datetime, timezone
from config import TIMEZONE
from typing import Optional
from urllib.parse import urlparse, urlunparse
@@ -206,7 +206,7 @@ async def _create_notifications_for_incident(
if not notifications:
return
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
if visibility == "public" and tenant_id:
cursor = await db.execute(
@@ -465,7 +465,7 @@ class AgentOrchestrator:
await db.execute(
"""UPDATE refresh_log SET status = 'cancelled', error_message = 'Vom Nutzer abgebrochen',
completed_at = ? WHERE incident_id = ? AND status = 'running'""",
(datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'), incident_id),
(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'), incident_id),
)
await db.commit()
except Exception as e:
@@ -481,7 +481,7 @@ class AgentOrchestrator:
await db.execute(
"""UPDATE refresh_log SET status = 'error', error_message = ?,
completed_at = ? WHERE incident_id = ? AND status = 'running'""",
(error[:500], datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'), incident_id),
(error[:500], datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'), incident_id),
)
await db.commit()
except Exception as e:
@@ -542,12 +542,12 @@ class AgentOrchestrator:
await db.execute(
"""UPDATE refresh_log SET status = 'error', error_message = 'Retry gestartet',
completed_at = ? WHERE incident_id = ? AND status = 'running'""",
(datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'), incident_id),
(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'), incident_id),
)
await db.commit()
# Refresh-Log starten
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
cursor = await db.execute(
"INSERT INTO refresh_log (incident_id, started_at, status, trigger_type, retry_count, tenant_id) VALUES (?, ?, 'running', ?, ?, ?)",
(incident_id, now, trigger_type, retry_count, tenant_id),
@@ -1008,7 +1008,7 @@ class AgentOrchestrator:
cache_creation_tokens = ?, cache_read_tokens = ?,
total_cost_usd = ?, api_calls = ?
WHERE id = ?""",
(datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'), new_count,
(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'), new_count,
usage_acc.input_tokens, usage_acc.output_tokens,
usage_acc.cache_creation_tokens, usage_acc.cache_read_tokens,
round(usage_acc.total_cost_usd, 7), usage_acc.call_count, log_id),

Datei anzeigen

@@ -1,7 +1,7 @@
"""JWT-Authentifizierung mit Magic-Link-Support und Multi-Tenancy."""
import secrets
import string
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
@@ -23,7 +23,7 @@ def create_token(
org_slug: str = None,
) -> str:
"""JWT-Token erstellen mit Tenant-Kontext."""
now = datetime.now(TIMEZONE)
now = datetime.now(timezone.utc)
expire = now + timedelta(hours=JWT_EXPIRE_HOURS)
payload = {
"sub": str(user_id),

Datei anzeigen

@@ -5,7 +5,7 @@ import logging
import os
import sys
from contextlib import asynccontextmanager
from datetime import datetime
from datetime import datetime, timezone
from typing import Dict
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request, Response
@@ -93,7 +93,7 @@ async def check_auto_refresh():
)
incidents = await cursor.fetchall()
now = datetime.now(TIMEZONE)
now = datetime.now(timezone.utc)
for incident in incidents:
incident_id = incident["id"]
@@ -112,7 +112,7 @@ async def check_auto_refresh():
else:
last_time = datetime.fromisoformat(last_refresh["completed_at"])
if last_time.tzinfo is None:
last_time = last_time.replace(tzinfo=TIMEZONE)
last_time = last_time.replace(tzinfo=timezone.utc)
elapsed = (now - last_time).total_seconds() / 60
if elapsed >= interval:
should_refresh = True
@@ -143,11 +143,11 @@ async def cleanup_expired():
)
incidents = await cursor.fetchall()
now = datetime.now(TIMEZONE)
now = datetime.now(timezone.utc)
for incident in incidents:
created = datetime.fromisoformat(incident["created_at"])
if created.tzinfo is None:
created = created.replace(tzinfo=TIMEZONE)
created = created.replace(tzinfo=timezone.utc)
age_days = (now - created).days
if age_days >= incident["retention_days"]:
await db.execute(
@@ -164,7 +164,7 @@ async def cleanup_expired():
for orphan in orphans:
started = datetime.fromisoformat(orphan["started_at"])
if started.tzinfo is None:
started = started.replace(tzinfo=TIMEZONE)
started = started.replace(tzinfo=timezone.utc)
age_minutes = (now - started).total_seconds() / 60
if age_minutes >= 15:
await db.execute(
@@ -195,7 +195,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).isoformat(),),
(datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'),),
)
if result.rowcount > 0:
await db.commit()

Datei anzeigen

@@ -1,6 +1,6 @@
"""Auth-Router: Magic-Link-Login und Nutzerverwaltung."""
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, Request, status
from models import (
MagicLinkRequest,
@@ -78,7 +78,7 @@ async def request_magic_link(
# Token + Code generieren
token = generate_magic_token()
code = generate_magic_code()
expires_at = (datetime.now(TIMEZONE) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)).strftime('%Y-%m-%d %H:%M:%S')
expires_at = (datetime.now(timezone.utc) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)).strftime('%Y-%m-%d %H:%M:%S')
# Alte ungenutzte Magic Links fuer diese E-Mail invalidieren
await db.execute(
@@ -124,10 +124,10 @@ async def verify_magic_link(
raise HTTPException(status_code=400, detail="Ungueltiger oder bereits verwendeter Link")
# Ablauf pruefen
now = datetime.now(TIMEZONE)
now = datetime.now(timezone.utc)
expires = datetime.fromisoformat(ml["expires_at"])
if expires.tzinfo is None:
expires = expires.replace(tzinfo=TIMEZONE)
expires = expires.replace(tzinfo=timezone.utc)
if now > expires:
raise HTTPException(status_code=400, detail="Link abgelaufen. Bitte neuen Link anfordern.")
@@ -200,10 +200,10 @@ async def verify_magic_code(
raise HTTPException(status_code=400, detail="Ungueltiger Code")
# Ablauf pruefen
now = datetime.now(TIMEZONE)
now = datetime.now(timezone.utc)
expires = datetime.fromisoformat(ml["expires_at"])
if expires.tzinfo is None:
expires = expires.replace(tzinfo=TIMEZONE)
expires = expires.replace(tzinfo=timezone.utc)
if now > expires:
raise HTTPException(status_code=400, detail="Code abgelaufen. Bitte neuen Code anfordern.")

Datei anzeigen

@@ -5,7 +5,7 @@ from models import IncidentCreate, IncidentUpdate, IncidentResponse, Subscriptio
from auth import get_current_user
from middleware.license_check import require_writable_license
from database import db_dependency, get_db
from datetime import datetime
from datetime import datetime, timezone
from config import TIMEZONE
import asyncio
import aiosqlite
@@ -102,7 +102,7 @@ async def create_incident(
):
"""Neue Lage anlegen."""
tenant_id = current_user.get("tenant_id")
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
cursor = await db.execute(
"""INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval,
retention_days, international_sources, visibility,
@@ -184,7 +184,7 @@ async def update_incident(
if not updates:
return await _enrich_incident(db, row)
updates["updated_at"] = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
updates["updated_at"] = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [incident_id]
@@ -667,7 +667,7 @@ def _build_markdown_export(
lines.append(snap_summary)
lines.append("")
now = datetime.now(TIMEZONE).strftime("%Y-%m-%d %H:%M UTC")
now = datetime.now(TIMEZONE).strftime("%Y-%m-%d %H:%M Uhr")
lines.append("---")
lines.append(f"*Exportiert am {now} aus AegisSight Monitor*")
return "\n".join(lines)

Datei anzeigen

@@ -1,6 +1,6 @@
"""Lizenz-Verwaltung und -Pruefung."""
import logging
from datetime import datetime
from datetime import datetime, timezone
from config import TIMEZONE
import aiosqlite
@@ -38,14 +38,14 @@ async def check_license(db: aiosqlite.Connection, organization_id: int) -> dict:
return {"valid": False, "status": "no_license", "read_only": True, "message": "Keine aktive Lizenz"}
# Ablauf pruefen
now = datetime.now(TIMEZONE)
now = datetime.now(timezone.utc)
valid_until = license_row["valid_until"]
if valid_until is not None:
try:
expiry = datetime.fromisoformat(valid_until)
if expiry.tzinfo is None:
expiry = expiry.replace(tzinfo=TIMEZONE)
expiry = expiry.replace(tzinfo=timezone.utc)
if now > expiry:
return {
"valid": False,