From a6c24366a09a51fe7cbcf2f5470222940e745af0 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Wed, 4 Mar 2026 23:45:01 +0100 Subject: [PATCH] =?UTF-8?q?Quellenverwaltung:=20Boulevard-Kategorie,=20Dup?= =?UTF-8?q?likat-Pr=C3=BCfung,=20Domain-Normalisierung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Boulevard als Kategorie in HTML-Dropdowns, JS-Labels und Pydantic-Validierung - create_source: URL-Duplikat-Prüfung (409 Conflict bei existierender URL) - create_source + update_source: Domain via _DOMAIN_ALIASES normalisieren - System-Quellen (auto-entdeckt) sind jetzt von allen Nutzern editierbar Co-Authored-By: Claude Opus 4.6 --- src/models.py | 4 ++-- src/routers/sources.py | 39 ++++++++++++++++++++++++++++--------- src/static/dashboard.html | 2 ++ src/static/js/components.js | 1 + 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/models.py b/src/models.py index 0d9e389..93445e2 100644 --- a/src/models.py +++ b/src/models.py @@ -96,7 +96,7 @@ class SourceCreate(BaseModel): url: Optional[str] = None domain: Optional[str] = None source_type: str = Field(default="rss_feed", pattern="^(rss_feed|web_source|excluded)$") - category: str = Field(default="sonstige", pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|sonstige)$") + category: str = Field(default="sonstige", pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|boulevard|sonstige)$") status: str = Field(default="active", pattern="^(active|inactive)$") notes: Optional[str] = None @@ -106,7 +106,7 @@ class SourceUpdate(BaseModel): url: Optional[str] = None domain: Optional[str] = None source_type: Optional[str] = Field(default=None, pattern="^(rss_feed|web_source|excluded)$") - category: Optional[str] = Field(default=None, pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|sonstige)$") + category: Optional[str] = Field(default=None, pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|boulevard|sonstige)$") status: Optional[str] = Field(default=None, pattern="^(active|inactive)$") notes: Optional[str] = None diff --git a/src/routers/sources.py b/src/routers/sources.py index fb1b2f3..1cebb8e 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from models import SourceCreate, SourceUpdate, SourceResponse, DiscoverRequest, DiscoverResponse, DiscoverMultiResponse, DomainActionRequest from auth import get_current_user from database import db_dependency, refresh_source_counts -from source_rules import discover_source, discover_all_feeds, evaluate_feeds_with_claude, _extract_domain, _detect_category, domain_to_display_name +from source_rules import discover_source, discover_all_feeds, evaluate_feeds_with_claude, _extract_domain, _detect_category, domain_to_display_name, _DOMAIN_ALIASES import aiosqlite logger = logging.getLogger("osint.sources") @@ -16,14 +16,13 @@ SOURCE_UPDATE_COLUMNS = {"name", "url", "domain", "source_type", "category", "st def _check_source_ownership(source: dict, username: str): - """Prueft ob der Nutzer die Quelle bearbeiten/loeschen darf.""" + """Prueft ob der Nutzer die Quelle bearbeiten/loeschen darf. + + System-Quellen (auto-entdeckt) duerfen von jedem bearbeitet werden. + Nutzer-Quellen nur vom Ersteller. + """ added_by = source.get("added_by", "") - if added_by == "system": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="System-Quellen koennen nicht veraendert werden", - ) - if added_by and added_by != username: + if added_by and added_by != "system" and added_by != username: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Nur der Ersteller kann diese Quelle bearbeiten", @@ -442,13 +441,32 @@ async def create_source( ): """Neue Quelle hinzufuegen (org-spezifisch).""" tenant_id = current_user.get("tenant_id") + + # Domain normalisieren (Subdomain-Aliase auflösen) + domain = data.domain + if domain: + domain = _DOMAIN_ALIASES.get(domain.lower(), domain.lower()) + + # Duplikat-Prüfung: gleiche URL bereits vorhanden? + if data.url: + cursor = await db.execute( + "SELECT id, name FROM sources WHERE url = ? AND status = 'active'", + (data.url,), + ) + existing = await cursor.fetchone() + if existing: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Feed-URL bereits vorhanden: {existing['name']} (ID {existing['id']})", + ) + cursor = await db.execute( """INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( data.name, data.url, - data.domain, + domain, data.source_type, data.category, data.status, @@ -483,6 +501,9 @@ async def update_source( for field, value in data.model_dump(exclude_none=True).items(): if field not in SOURCE_UPDATE_COLUMNS: continue + # Domain normalisieren + if field == "domain" and value: + value = _DOMAIN_ALIASES.get(value.lower(), value.lower()) updates[field] = value if not updates: diff --git a/src/static/dashboard.html b/src/static/dashboard.html index c0169c2..cfe82d7 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -420,6 +420,7 @@ + @@ -475,6 +476,7 @@ + diff --git a/src/static/js/components.js b/src/static/js/components.js index cbd35fa..a24d132 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -506,6 +506,7 @@ const UI = { 'think-tank': 'Think Tank', 'international': 'Intl.', 'regional': 'Regional', + 'boulevard': 'Boulevard', 'sonstige': 'Sonstige', },