Quellenverwaltung: Boulevard-Kategorie, Duplikat-Prüfung, Domain-Normalisierung

- 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 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-04 23:45:01 +01:00
Ursprung 731a66ac80
Commit a6c24366a0
4 geänderte Dateien mit 35 neuen und 11 gelöschten Zeilen

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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:

Datei anzeigen

@@ -420,6 +420,7 @@
<option value="think-tank">Think Tank</option>
<option value="international">International</option>
<option value="regional">Regional</option>
<option value="boulevard">Boulevard</option>
<option value="sonstige">Sonstige</option>
</select>
<label for="sources-search" class="sr-only">Quellen durchsuchen</label>
@@ -475,6 +476,7 @@
<option value="think-tank">Think Tank</option>
<option value="international">International</option>
<option value="regional">Regional</option>
<option value="boulevard">Boulevard</option>
<option value="sonstige" selected>Sonstige</option>
</select>
</div>

Datei anzeigen

@@ -506,6 +506,7 @@ const UI = {
'think-tank': 'Think Tank',
'international': 'Intl.',
'regional': 'Regional',
'boulevard': 'Boulevard',
'sonstige': 'Sonstige',
},