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',
},