fix(brain): Trigger-Zeit UTC-konsistent, project_summary liest Hauptchat, Privatkopie-Regel

- Trigger-Zeit (Issue 4): fires_at wird beim Anlegen IMMER auf UTC normalisiert
  (naiv = lokale Wanduhrzeit Europe/Berlin → UTC, aware → astimezone). Passt zur
  UTC-Ausfuehrung in background.py → feuert zur gemeinten Ortszeit, zeitzonen-
  portabel. Tool-Desc + Prompt: fires_at = lokale Wanduhrzeit ohne Zone schreiben,
  Server rechnet um. Behebt den 2h-Versatz.
- project_summary (Issue 2): liest jetzt auch den Hauptchat (name='Hauptchat')
  und die Beschreibung sagt ARIA, es fuer 'schau mal in Projekt X / Hauptchat
  rein' / 'hol dir die Infos aus ...' zu nutzen.
- Privatkopie (Issue 3): IDENTITY_ANCHOR-Regel — YouTube-Downloads fuer Stefans
  PRIVATgebrauch sind eine Privatkopie (§53 UrhG), nicht reflexartig ablehnen/
  moralisieren; Urteil bleibt fuer Weiterverbreitung/kommerziell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 23:12:02 +02:00
co-authored by Claude Opus 4.8
parent 761217cb5a
commit 55938c3173
3 changed files with 76 additions and 26 deletions
+27 -3
View File
@@ -24,7 +24,7 @@ import os
import re
import shutil
import time
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional
@@ -40,6 +40,29 @@ def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _local_offset_hours(dt: datetime) -> int:
"""Grobe Europe/Berlin-Naeherung (CEST=+2 Maerz-Okt, sonst CET=+1) — dieselbe
Logik wie build_time_section im Prompt, ohne zoneinfo/tzdata im Brain-Image."""
return 2 if 3 <= dt.month <= 10 else 1
def normalize_fires_at_utc(iso: str) -> str:
"""Bringt einen fires_at-ISO IMMER auf UTC (+00:00).
- Aware (endet auf Z oder hat einen Offset) → in UTC umgerechnet.
- Naiv (keine Zone) → als LOKALE Wanduhrzeit (Europe/Berlin) interpretiert
und nach UTC umgerechnet.
So speichern wir stets den absoluten Instant. Die Ausfuehrung (background.py,
UTC) trifft damit exakt die vom Nutzer gemeinte Ortszeit — und bleibt
zeitzonen-portabel (feuert am selben Moment, egal wo Stefan gerade ist)."""
dt = datetime.fromisoformat((iso or "").strip().replace("Z", "+00:00"))
if dt.tzinfo is None:
# Naiv = lokale Wanduhrzeit → UTC = lokal - Offset.
dt = (dt - timedelta(hours=_local_offset_hours(dt))).replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc).isoformat(timespec="seconds")
def _safe_name(name: str) -> str:
if not isinstance(name, str) or not NAME_RE.match(name):
raise ValueError(f"Ungueltiger Trigger-Name: {name!r}")
@@ -127,9 +150,10 @@ def create_timer(
_safe_name(name)
if _path(name).exists():
raise ValueError(f"Trigger '{name}' existiert schon")
# ISO validieren
# ISO validieren UND auf UTC normalisieren (naiv = lokale Wanduhrzeit →
# UTC). So passt das Anlegen zur UTC-Ausfuehrung in background.py.
try:
datetime.fromisoformat(fires_at_iso.replace("Z", "+00:00"))
fires_at_iso = normalize_fires_at_utc(fires_at_iso)
except Exception:
raise ValueError(f"fires_at_iso ungueltig: {fires_at_iso}")
data = {