Beim Anlegen einer neuen Lage ist der Schalter "Internationale Quellen einbeziehen" ab jetzt standardmaessig DEAKTIVIERT. Hintergrund: Bei lokalen DACH-Ereignissen (Tier-/Personenstoryen wie "Buckelwal timmy") hat der "international=True"-Default zu schlechteren Treffern gefuehrt, weil Claude in Deutsch UND Englisch suchte und die englische Berichterstattung haeufig fehlt. Excluded-Sources- und Boulevard-Filter haben das Problem zusaetzlich verschaerft. Aenderungen: - src/models.py IncidentCreate.international_sources: bool=True -> False (nur das Pydantic-Default beim Create-Endpoint - IncidentResponse/Incident bleiben True, weil das die DB-Werte bestehender Lagen reflektiert) - src/static/dashboard.html: <input id="inc-international" checked> -> ohne checked (UI-Default ist jetzt unchecked, User muss bewusst aktivieren fuer internationale Lagen) - Tooltip-Text ergaenzt: "Deaktiviert (Standard): ... empfohlen fuer DACH-Lagen." Bestandslagen sind nicht betroffen - DB-Schema-Default INTEGER DEFAULT 1 bleibt unveraendert, fuer alle existierenden Lagen behaelt international seinen aktuellen Wert. Damit ist die Buckelwal-Diagnose komplett geloest: - Bug 1 (rss_parser min_matches adaptiv) seita08df3dauf main - Bug 2 (Eigennamen-Pflicht-Keywords) seite83f80dauf main - Bug 3 (international-Default) jetzt auf develop, gleich Cherry-pick auf main
299 Zeilen
9.5 KiB
Python
299 Zeilen
9.5 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
|
|
|
|
|
|
# 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)$"
|
|
POLITICAL_ORIENTATION_PATTERN = "^(links_extrem|links|mitte_links|liberal|mitte|konservativ|mitte_rechts|rechts|rechts_extrem|na)$"
|
|
MEDIA_TYPE_PATTERN = "^(tageszeitung|wochenzeitung|magazin|tv_sender|radio|oeffentlich_rechtlich|nachrichtenagentur|online_only|blog|telegram_kanal|telegram_bot|podcast|social_media|imageboard|think_tank|ngo|behoerde|staatsmedium|fachmedium|sonstige)$"
|
|
RELIABILITY_PATTERN = "^(sehr_hoch|hoch|gemischt|niedrig|sehr_niedrig|na)$"
|
|
ALIGNMENT_PATTERN = "^(prorussisch|proiranisch|prowestlich|proukrainisch|prochinesisch|projapanisch|proisraelisch|propalaestinensisch|protuerkisch|panarabisch|neutral|sonstige)$"
|
|
COUNTRY_CODE_PATTERN = "^[A-Z]{2}$"
|
|
CLASSIFICATION_SOURCE_PATTERN = "^(manual|llm_approved|llm_pending|legacy)$"
|
|
|
|
|
|
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
|
|
political_orientation: Optional[str] = Field(default=None, pattern=POLITICAL_ORIENTATION_PATTERN)
|
|
media_type: Optional[str] = Field(default=None, pattern=MEDIA_TYPE_PATTERN)
|
|
reliability: Optional[str] = Field(default=None, pattern=RELIABILITY_PATTERN)
|
|
state_affiliated: Optional[bool] = None
|
|
country_code: Optional[str] = Field(default=None, pattern=COUNTRY_CODE_PATTERN)
|
|
alignments: Optional[list[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
|
|
political_orientation: Optional[str] = Field(default=None, pattern=POLITICAL_ORIENTATION_PATTERN)
|
|
media_type: Optional[str] = Field(default=None, pattern=MEDIA_TYPE_PATTERN)
|
|
reliability: Optional[str] = Field(default=None, pattern=RELIABILITY_PATTERN)
|
|
state_affiliated: Optional[bool] = None
|
|
country_code: Optional[str] = Field(default=None, pattern=COUNTRY_CODE_PATTERN)
|
|
alignments: Optional[list[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
|