diff --git a/src/main.py b/src/main.py
index 4d69710..c4d7450 100644
--- a/src/main.py
+++ b/src/main.py
@@ -10,7 +10,7 @@ from config import STATIC_DIR, PORT
from database import db_dependency
from auth import verify_password, create_token
from models import LoginRequest, TokenResponse
-from routers import organizations, licenses, users, dashboard
+from routers import organizations, licenses, users, dashboard, sources
import aiosqlite
@@ -39,6 +39,7 @@ app.include_router(organizations.router)
app.include_router(licenses.router)
app.include_router(users.router)
app.include_router(dashboard.router)
+app.include_router(sources.router)
# --- Login ---
diff --git a/src/routers/sources.py b/src/routers/sources.py
new file mode 100644
index 0000000..047dc2b
--- /dev/null
+++ b/src/routers/sources.py
@@ -0,0 +1,167 @@
+"""Grundquellen-Verwaltung und Kundenquellen-Übersicht."""
+from fastapi import APIRouter, Depends, HTTPException, status
+from pydantic import BaseModel, Field
+from typing import Optional
+from auth import get_current_admin
+from database import db_dependency
+import aiosqlite
+
+router = APIRouter(prefix="/api/sources", tags=["sources"])
+
+
+class GlobalSourceCreate(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="^(rss_feed|web_source|excluded)$")
+ category: str = Field(default="sonstige")
+ status: str = Field(default="active", pattern="^(active|inactive)$")
+ notes: Optional[str] = None
+
+
+class GlobalSourceUpdate(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="^(rss_feed|web_source|excluded)$")
+ category: Optional[str] = None
+ status: Optional[str] = Field(default=None, pattern="^(active|inactive)$")
+ notes: Optional[str] = None
+
+
+@router.get("/global")
+async def list_global_sources(
+ admin: dict = Depends(get_current_admin),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Alle Grundquellen auflisten (tenant_id IS NULL)."""
+ cursor = await db.execute(
+ "SELECT * FROM sources WHERE tenant_id IS NULL ORDER BY category, source_type, name"
+ )
+ return [dict(row) for row in await cursor.fetchall()]
+
+
+@router.post("/global", status_code=201)
+async def create_global_source(
+ data: GlobalSourceCreate,
+ admin: dict = Depends(get_current_admin),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Neue Grundquelle anlegen."""
+ if data.url:
+ cursor = await db.execute(
+ "SELECT id, name FROM sources WHERE url = ? AND tenant_id IS NULL",
+ (data.url,),
+ )
+ existing = await cursor.fetchone()
+ if existing:
+ raise HTTPException(
+ status_code=409,
+ detail=f"URL bereits vorhanden: {existing['name']}",
+ )
+
+ cursor = await db.execute(
+ """INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'system', NULL)""",
+ (data.name, data.url, data.domain, data.source_type, data.category, data.status, data.notes),
+ )
+ await db.commit()
+
+ cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (cursor.lastrowid,))
+ return dict(await cursor.fetchone())
+
+
+@router.put("/global/{source_id}")
+async def update_global_source(
+ source_id: int,
+ data: GlobalSourceUpdate,
+ admin: dict = Depends(get_current_admin),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Grundquelle bearbeiten."""
+ cursor = await db.execute(
+ "SELECT * FROM sources WHERE id = ? AND tenant_id IS NULL", (source_id,)
+ )
+ row = await cursor.fetchone()
+ if not row:
+ raise HTTPException(status_code=404, detail="Grundquelle nicht gefunden")
+
+ updates = {}
+ for field, value in data.model_dump(exclude_none=True).items():
+ updates[field] = value
+
+ if not updates:
+ return dict(row)
+
+ set_clause = ", ".join(f"{k} = ?" for k in updates)
+ values = list(updates.values()) + [source_id]
+ await db.execute(f"UPDATE sources SET {set_clause} WHERE id = ?", values)
+ await db.commit()
+
+ cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,))
+ return dict(await cursor.fetchone())
+
+
+@router.delete("/global/{source_id}", status_code=204)
+async def delete_global_source(
+ source_id: int,
+ admin: dict = Depends(get_current_admin),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Grundquelle loeschen."""
+ cursor = await db.execute(
+ "SELECT id FROM sources WHERE id = ? AND tenant_id IS NULL", (source_id,)
+ )
+ if not await cursor.fetchone():
+ raise HTTPException(status_code=404, detail="Grundquelle nicht gefunden")
+
+ await db.execute("DELETE FROM sources WHERE id = ?", (source_id,))
+ await db.commit()
+
+
+@router.get("/tenant")
+async def list_tenant_sources(
+ admin: dict = Depends(get_current_admin),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Alle tenant-spezifischen Quellen mit Org-Name auflisten."""
+ cursor = await db.execute("""
+ SELECT s.*, o.name as org_name
+ FROM sources s
+ LEFT JOIN organizations o ON o.id = s.tenant_id
+ WHERE s.tenant_id IS NOT NULL
+ ORDER BY o.name, s.category, s.name
+ """)
+ return [dict(row) for row in await cursor.fetchall()]
+
+
+@router.post("/tenant/{source_id}/promote")
+async def promote_to_global(
+ source_id: int,
+ admin: dict = Depends(get_current_admin),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Tenant-Quelle zur Grundquelle befoerdern."""
+ cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,))
+ row = await cursor.fetchone()
+ if not row:
+ raise HTTPException(status_code=404, detail="Quelle nicht gefunden")
+ if row["tenant_id"] is None:
+ raise HTTPException(status_code=400, detail="Bereits eine Grundquelle")
+
+ if row["url"]:
+ cursor = await db.execute(
+ "SELECT id FROM sources WHERE url = ? AND tenant_id IS NULL",
+ (row["url"],),
+ )
+ if await cursor.fetchone():
+ raise HTTPException(status_code=409, detail="URL bereits als Grundquelle vorhanden")
+
+ await db.execute(
+ "UPDATE sources SET tenant_id = NULL, added_by = 'system' WHERE id = ?",
+ (source_id,),
+ )
+ await db.commit()
+
+ cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,))
+ return dict(await cursor.fetchone())
diff --git a/src/static/dashboard.html b/src/static/dashboard.html
index 4f585a1..a792cc0 100644
--- a/src/static/dashboard.html
+++ b/src/static/dashboard.html
@@ -22,6 +22,7 @@
+
@@ -193,6 +194,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ URL |
+ Domain |
+ Typ |
+ Kategorie |
+ Status |
+ Aktionen |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Domain |
+ Typ |
+ Kategorie |
+ Organisation |
+ Hinzugefuegt von |
+ Aktionen |
+
+
+
+
+
+
+
+
+
+
+
+
+