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 json
import logging import logging
import re import re
from datetime import datetime from datetime import datetime, timezone
from config import TIMEZONE from config import TIMEZONE
from typing import Optional from typing import Optional
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
@@ -206,7 +206,7 @@ async def _create_notifications_for_incident(
if not notifications: if not notifications:
return 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: if visibility == "public" and tenant_id:
cursor = await db.execute( cursor = await db.execute(
@@ -465,7 +465,7 @@ class AgentOrchestrator:
await db.execute( await db.execute(
"""UPDATE refresh_log SET status = 'cancelled', error_message = 'Vom Nutzer abgebrochen', """UPDATE refresh_log SET status = 'cancelled', error_message = 'Vom Nutzer abgebrochen',
completed_at = ? WHERE incident_id = ? AND status = 'running'""", 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() await db.commit()
except Exception as e: except Exception as e:
@@ -481,7 +481,7 @@ class AgentOrchestrator:
await db.execute( await db.execute(
"""UPDATE refresh_log SET status = 'error', error_message = ?, """UPDATE refresh_log SET status = 'error', error_message = ?,
completed_at = ? WHERE incident_id = ? AND status = 'running'""", 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() await db.commit()
except Exception as e: except Exception as e:
@@ -542,12 +542,12 @@ class AgentOrchestrator:
await db.execute( await db.execute(
"""UPDATE refresh_log SET status = 'error', error_message = 'Retry gestartet', """UPDATE refresh_log SET status = 'error', error_message = 'Retry gestartet',
completed_at = ? WHERE incident_id = ? AND status = 'running'""", 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() await db.commit()
# Refresh-Log starten # 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( cursor = await db.execute(
"INSERT INTO refresh_log (incident_id, started_at, status, trigger_type, retry_count, tenant_id) VALUES (?, ?, 'running', ?, ?, ?)", "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), (incident_id, now, trigger_type, retry_count, tenant_id),
@@ -1008,7 +1008,7 @@ class AgentOrchestrator:
cache_creation_tokens = ?, cache_read_tokens = ?, cache_creation_tokens = ?, cache_read_tokens = ?,
total_cost_usd = ?, api_calls = ? total_cost_usd = ?, api_calls = ?
WHERE id = ?""", 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.input_tokens, usage_acc.output_tokens,
usage_acc.cache_creation_tokens, usage_acc.cache_read_tokens, usage_acc.cache_creation_tokens, usage_acc.cache_read_tokens,
round(usage_acc.total_cost_usd, 7), usage_acc.call_count, log_id), 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.""" """JWT-Authentifizierung mit Magic-Link-Support und Multi-Tenancy."""
import secrets import secrets
import string import string
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError from jose import jwt, JWTError
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
@@ -23,7 +23,7 @@ def create_token(
org_slug: str = None, org_slug: str = None,
) -> str: ) -> str:
"""JWT-Token erstellen mit Tenant-Kontext.""" """JWT-Token erstellen mit Tenant-Kontext."""
now = datetime.now(TIMEZONE) now = datetime.now(timezone.utc)
expire = now + timedelta(hours=JWT_EXPIRE_HOURS) expire = now + timedelta(hours=JWT_EXPIRE_HOURS)
payload = { payload = {
"sub": str(user_id), "sub": str(user_id),

Datei anzeigen

@@ -5,7 +5,7 @@ import logging
import os import os
import sys import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime from datetime import datetime, timezone
from typing import Dict from typing import Dict
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request, Response from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request, Response
@@ -93,7 +93,7 @@ async def check_auto_refresh():
) )
incidents = await cursor.fetchall() incidents = await cursor.fetchall()
now = datetime.now(TIMEZONE) now = datetime.now(timezone.utc)
for incident in incidents: for incident in incidents:
incident_id = incident["id"] incident_id = incident["id"]
@@ -112,7 +112,7 @@ async def check_auto_refresh():
else: else:
last_time = datetime.fromisoformat(last_refresh["completed_at"]) last_time = datetime.fromisoformat(last_refresh["completed_at"])
if last_time.tzinfo is None: 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 elapsed = (now - last_time).total_seconds() / 60
if elapsed >= interval: if elapsed >= interval:
should_refresh = True should_refresh = True
@@ -143,11 +143,11 @@ async def cleanup_expired():
) )
incidents = await cursor.fetchall() incidents = await cursor.fetchall()
now = datetime.now(TIMEZONE) now = datetime.now(timezone.utc)
for incident in incidents: for incident in incidents:
created = datetime.fromisoformat(incident["created_at"]) created = datetime.fromisoformat(incident["created_at"])
if created.tzinfo is None: if created.tzinfo is None:
created = created.replace(tzinfo=TIMEZONE) created = created.replace(tzinfo=timezone.utc)
age_days = (now - created).days age_days = (now - created).days
if age_days >= incident["retention_days"]: if age_days >= incident["retention_days"]:
await db.execute( await db.execute(
@@ -164,7 +164,7 @@ async def cleanup_expired():
for orphan in orphans: for orphan in orphans:
started = datetime.fromisoformat(orphan["started_at"]) started = datetime.fromisoformat(orphan["started_at"])
if started.tzinfo is None: if started.tzinfo is None:
started = started.replace(tzinfo=TIMEZONE) started = started.replace(tzinfo=timezone.utc)
age_minutes = (now - started).total_seconds() / 60 age_minutes = (now - started).total_seconds() / 60
if age_minutes >= 15: if age_minutes >= 15:
await db.execute( await db.execute(
@@ -195,7 +195,7 @@ async def lifespan(app: FastAPI):
try: try:
result = await db.execute( result = await db.execute(
"UPDATE refresh_log SET status = 'error', completed_at = ?, error_message = 'Verwaist (Neustart, automatisch bereinigt)' WHERE status = 'running'", "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: if result.rowcount > 0:
await db.commit() await db.commit()

Datei anzeigen

@@ -1,6 +1,6 @@
"""Auth-Router: Magic-Link-Login und Nutzerverwaltung.""" """Auth-Router: Magic-Link-Login und Nutzerverwaltung."""
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from models import ( from models import (
MagicLinkRequest, MagicLinkRequest,
@@ -78,7 +78,7 @@ async def request_magic_link(
# Token + Code generieren # Token + Code generieren
token = generate_magic_token() token = generate_magic_token()
code = generate_magic_code() 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 # Alte ungenutzte Magic Links fuer diese E-Mail invalidieren
await db.execute( await db.execute(
@@ -124,10 +124,10 @@ async def verify_magic_link(
raise HTTPException(status_code=400, detail="Ungueltiger oder bereits verwendeter Link") raise HTTPException(status_code=400, detail="Ungueltiger oder bereits verwendeter Link")
# Ablauf pruefen # Ablauf pruefen
now = datetime.now(TIMEZONE) now = datetime.now(timezone.utc)
expires = datetime.fromisoformat(ml["expires_at"]) expires = datetime.fromisoformat(ml["expires_at"])
if expires.tzinfo is None: if expires.tzinfo is None:
expires = expires.replace(tzinfo=TIMEZONE) expires = expires.replace(tzinfo=timezone.utc)
if now > expires: if now > expires:
raise HTTPException(status_code=400, detail="Link abgelaufen. Bitte neuen Link anfordern.") 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") raise HTTPException(status_code=400, detail="Ungueltiger Code")
# Ablauf pruefen # Ablauf pruefen
now = datetime.now(TIMEZONE) now = datetime.now(timezone.utc)
expires = datetime.fromisoformat(ml["expires_at"]) expires = datetime.fromisoformat(ml["expires_at"])
if expires.tzinfo is None: if expires.tzinfo is None:
expires = expires.replace(tzinfo=TIMEZONE) expires = expires.replace(tzinfo=timezone.utc)
if now > expires: if now > expires:
raise HTTPException(status_code=400, detail="Code abgelaufen. Bitte neuen Code anfordern.") 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 auth import get_current_user
from middleware.license_check import require_writable_license from middleware.license_check import require_writable_license
from database import db_dependency, get_db from database import db_dependency, get_db
from datetime import datetime from datetime import datetime, timezone
from config import TIMEZONE from config import TIMEZONE
import asyncio import asyncio
import aiosqlite import aiosqlite
@@ -102,7 +102,7 @@ async def create_incident(
): ):
"""Neue Lage anlegen.""" """Neue Lage anlegen."""
tenant_id = current_user.get("tenant_id") 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( cursor = await db.execute(
"""INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval, """INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval,
retention_days, international_sources, visibility, retention_days, international_sources, visibility,
@@ -184,7 +184,7 @@ async def update_incident(
if not updates: if not updates:
return await _enrich_incident(db, row) 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) set_clause = ", ".join(f"{k} = ?" for k in updates)
values = list(updates.values()) + [incident_id] values = list(updates.values()) + [incident_id]
@@ -667,7 +667,7 @@ def _build_markdown_export(
lines.append(snap_summary) lines.append(snap_summary)
lines.append("") 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("---")
lines.append(f"*Exportiert am {now} aus AegisSight Monitor*") lines.append(f"*Exportiert am {now} aus AegisSight Monitor*")
return "\n".join(lines) return "\n".join(lines)

Datei anzeigen

@@ -1,6 +1,6 @@
"""Lizenz-Verwaltung und -Pruefung.""" """Lizenz-Verwaltung und -Pruefung."""
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from config import TIMEZONE from config import TIMEZONE
import aiosqlite 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"} return {"valid": False, "status": "no_license", "read_only": True, "message": "Keine aktive Lizenz"}
# Ablauf pruefen # Ablauf pruefen
now = datetime.now(TIMEZONE) now = datetime.now(timezone.utc)
valid_until = license_row["valid_until"] valid_until = license_row["valid_until"]
if valid_until is not None: if valid_until is not None:
try: try:
expiry = datetime.fromisoformat(valid_until) expiry = datetime.fromisoformat(valid_until)
if expiry.tzinfo is None: if expiry.tzinfo is None:
expiry = expiry.replace(tzinfo=TIMEZONE) expiry = expiry.replace(tzinfo=timezone.utc)
if now > expiry: if now > expiry:
return { return {
"valid": False, "valid": False,