Dateien
AegisSight-Monitor/src/models.py
Claude Code 3f0e680446 feat(frontend): Light-i18n + Org-Sprache durch /auth/me
Backend:
- UserMeResponse um output_language (de | en) erweitert.
- /auth/me liefert die Org-Sprache aus organization_settings.

Frontend:
- Neu: static/js/i18n.js mit T(key)-Helper, I18N.load(lang) und
  applyDom() ueber data-i18n + data-i18n-attr.
- Neu: static/i18n/de.json + en.json (sichtbare Bereiche: Sidebar,
  Header, Modal-Titel, Faktencheck-Status, Refresh-Hinweise).
- dashboard.html: i18n.js Script-Tag vor api.js, data-i18n auf den
  prominenten Strings (Abmelden, + Neuer Fall, Alle/Eigene, Sidebar-
  Sektionen, Bericht exportieren, Faktencheck-Tab, Lage anlegen).
  Tutorial.init() entfernt aus DOMContentLoaded.
- components.js: factCheckLabels/Tooltips/ChipLabels als Getter ueber
  T() mit DE-Fallbacks.
- app.js: vor Setup wird I18N.load(user.output_language) aufgerufen und
  applyDom() ausgefuehrt. Tutorial.init() laeuft nur bei lang === de.

Phase 6 von 8 (eng_demo / Org-Sprache).
2026-05-13 21:14:56 +00:00

280 Zeilen
8.0 KiB
Python

"""Pydantic Models für Request/Response Schemas."""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# Auth (Magic Link)
class MagicLinkRequest(BaseModel):
email: str = Field(min_length=1, max_length=254)
class MagicLinkResponse(BaseModel):
message: str
class VerifyTokenRequest(BaseModel):
token: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
username: str
class UserMeResponse(BaseModel):
id: int
username: str
email: str = ""
role: str = "member"
org_name: str = ""
org_slug: str = ""
tenant_id: Optional[int] = None
license_status: str = "unknown"
license_type: str = ""
read_only: bool = False
read_only_reason: Optional[str] = None
unlimited_budget: bool = False
credits_total: Optional[int] = None
credits_remaining: Optional[int] = None
credits_percent_used: Optional[float] = None
is_global_admin: bool = False
output_language: str = "de"
# Incidents (Lagen)
class IncidentCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
description: Optional[str] = None
type: str = Field(default="adhoc", pattern="^(adhoc|research)$")
refresh_mode: str = Field(default="manual", pattern="^(manual|auto)$")
refresh_interval: int = Field(default=15, ge=10, le=10080)
refresh_start_time: Optional[str] = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$")
retention_days: int = Field(default=0, ge=0, le=999)
international_sources: bool = False
include_telegram: bool = False
visibility: str = Field(default="public", pattern="^(public|private)$")
class IncidentUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
type: Optional[str] = Field(default=None, pattern="^(adhoc|research)$")
status: Optional[str] = Field(default=None, pattern="^(active|archived)$")
refresh_mode: Optional[str] = Field(default=None, pattern="^(manual|auto)$")
refresh_interval: Optional[int] = Field(default=None, ge=10, le=10080)
refresh_start_time: Optional[str] = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$")
retention_days: Optional[int] = Field(default=None, ge=0, le=999)
international_sources: Optional[bool] = None
include_telegram: Optional[bool] = None
visibility: Optional[str] = Field(default=None, pattern="^(public|private)$")
class DescriptionEnhanceRequest(BaseModel):
title: str = Field(min_length=3)
description: str | None = None
type: str = Field(default="adhoc", pattern="^(adhoc|research)$")
class IncidentResponse(BaseModel):
"""Vollstaendige Lage-Details (fuer GET /incidents/{id}).
Enthaelt summary + latest_developments, aber NICHT mehr sources_json —
das wird separat per GET /incidents/{id}/sources geladen (Lazy-Load).
"""
id: int
title: str
description: Optional[str]
type: str
status: str
refresh_mode: str
refresh_interval: int
refresh_start_time: Optional[str] = None
retention_days: int
visibility: str = "public"
summary: Optional[str]
latest_developments: Optional[str] = None
international_sources: bool = True
include_telegram: bool = False
created_by: int
created_by_username: str = ""
created_at: str
updated_at: str
article_count: int = 0
source_count: int = 0
class IncidentListItem(BaseModel):
"""Schlankes Sidebar-Item (fuer GET /incidents).
Enthaelt, was Sidebar und Edit-Dialog brauchen — kein summary,
kein sources_json. Statt summary-Volltext ein ``has_summary``-Bit,
damit das Frontend "erster Refresh"-Zustand erkennen kann.
description bleibt drin (kurz, vom Edit-Modal direkt genutzt).
"""
id: int
title: str
description: Optional[str] = None
type: str
status: str
refresh_mode: str
refresh_interval: int
refresh_start_time: Optional[str] = None
retention_days: int
visibility: str = "public"
international_sources: bool = True
include_telegram: bool = False
created_by: int
created_by_username: str = ""
created_at: str
updated_at: str
article_count: int = 0
source_count: int = 0
has_summary: bool = False
# Sources (Quellenverwaltung)
SOURCE_TYPE_PATTERN = "^(rss_feed|web_source|excluded|telegram_channel|podcast_feed)$"
SOURCE_CATEGORY_PATTERN = "^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|boulevard|sonstige)$"
SOURCE_STATUS_PATTERN = "^(active|inactive)$"
class SourceCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
url: Optional[str] = None
domain: Optional[str] = None
source_type: str = Field(default="rss_feed", pattern=SOURCE_TYPE_PATTERN)
category: str = Field(default="sonstige", pattern=SOURCE_CATEGORY_PATTERN)
status: str = Field(default="active", pattern=SOURCE_STATUS_PATTERN)
notes: Optional[str] = None
language: Optional[str] = None
bias: Optional[str] = None
class SourceUpdate(BaseModel):
name: Optional[str] = Field(default=None, max_length=200)
url: Optional[str] = None
domain: Optional[str] = None
source_type: Optional[str] = Field(default=None, pattern=SOURCE_TYPE_PATTERN)
category: Optional[str] = Field(default=None, pattern=SOURCE_CATEGORY_PATTERN)
status: Optional[str] = Field(default=None, pattern=SOURCE_STATUS_PATTERN)
notes: Optional[str] = None
language: Optional[str] = None
bias: Optional[str] = None
class SourceResponse(BaseModel):
id: int
name: str
url: Optional[str]
domain: Optional[str]
source_type: str
category: str
status: str
notes: Optional[str]
added_by: Optional[str]
article_count: int = 0
last_seen_at: Optional[str] = None
created_at: str
language: Optional[str] = None
bias: Optional[str] = None
political_orientation: Optional[str] = None
media_type: Optional[str] = None
reliability: Optional[str] = None
state_affiliated: bool = False
country_code: Optional[str] = None
classification_source: Optional[str] = None
classified_at: Optional[str] = None
alignments: list[str] = []
is_global: bool = False
ifcn_signatory: bool = False
eu_disinfo_listed: bool = False
eu_disinfo_case_count: int = 0
eu_disinfo_last_seen: Optional[str] = None
external_data_synced_at: Optional[str] = None
# Source Discovery
class DiscoverRequest(BaseModel):
url: str = Field(min_length=1, max_length=500)
class DiscoverResponse(BaseModel):
name: str
domain: str
rss_url: Optional[str] = None
category: str
source_type: str
# Multi-Discovery
class DiscoverMultiResponse(BaseModel):
domain: str
category: str
added_count: int
skipped_count: int
total_found: int
sources: list[SourceResponse]
fallback_single: bool = False
# Domain-Aktionen (Ausschließen/Ausschluss aufheben)
class DomainActionRequest(BaseModel):
domain: str = Field(min_length=1, max_length=200)
notes: Optional[str] = None
# Notifications
class NotificationResponse(BaseModel):
id: int
incident_id: Optional[int]
type: str
title: str
text: str
icon: str
is_read: bool
created_at: str
class NotificationMarkReadRequest(BaseModel):
notification_ids: Optional[list[int]] = None # None = alle
class SubscriptionUpdate(BaseModel):
notify_email_summary: bool = False
notify_email_new_articles: bool = False
notify_email_status_change: bool = False
class SubscriptionResponse(BaseModel):
notify_email_summary: bool = False
notify_email_new_articles: bool = False
notify_email_status_change: bool = False
class FeedbackRequest(BaseModel):
category: str = Field(pattern="^(bug|feature|question|other)$")
message: str = Field(min_length=10, max_length=5000)
# --- Global Admin: Org-Wechsel (herausnehmbar) ---
class SwitchOrgRequest(BaseModel):
organization_id: int
class OrgListItem(BaseModel):
id: int
name: str
slug: str
is_active: bool