Phase 3c: Kundenquellen-Tab mit Filter + Sort + Bulk-Promote

Backend
- routers/sources.py: POST /api/sources/tenant/bulk-promote NEU
  Nimmt Liste von source_ids, promotet jede einzeln zur Grundquelle.
  Returns {promoted, skipped[{id,name,reason}], failed[{id,error}]}.
  Ueberspringt Quellen die schon Grundquellen sind oder deren URL bereits
  als Grundquelle existiert.

Frontend
- dashboard.html sub-tenant-sources: action-bar erweitert um
  3 Filter-Selects (Typ, Kategorie, Org), Bulk-Promote-Button.
  Tabelle bekommt Checkbox-Spalte + sortable Spalten (Sort-Icons).
- sources.js: tenant-Tab komplett refactored
  - State: tenantFilters, tenantSort, tenantSelected (Set)
  - applyTenantFilterAndSort: zentraler Render-Pfad mit allen Filtern + Sort
  - populateTenantFilters: Org-Liste aus Daten, Typ/Kategorie aus META
  - toggleTenantSelect / toggleTenantSelectAll: Selection-Logik
  - bulkPromoteSelected: showConfirm -> POST -> Toast mit Ergebnis
  - renderTenantSources: Checkbox-Spalte, dynamische typeLabel/categoryLabel
  - Counter zeigt jetzt N gefiltert / Gesamt
Dieser Commit ist enthalten in:
claude-dev
2026-05-09 03:07:55 +00:00
Ursprung eda60f9299
Commit 9350e4538a
3 geänderte Dateien mit 220 neuen und 35 gelöschten Zeilen

Datei anzeigen

@@ -231,6 +231,72 @@ async def promote_to_global(
class BulkPromoteRequest(BaseModel):
source_ids: list[int]
@router.post("/tenant/bulk-promote")
async def bulk_promote_tenant_sources(
data: BulkPromoteRequest,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Mehrere Tenant-Quellen auf einen Schlag zur Grundquelle befoerdern.
Returns:
promoted: int - Anzahl erfolgreich promoter Quellen
skipped: list - {id, name, reason} fuer uebersprungene
failed: list - {id, error} fuer Fehler
"""
promoted = 0
skipped = []
failed = []
for sid in data.source_ids:
try:
cur = await db.execute("SELECT * FROM sources WHERE id = ?", (sid,))
row = await cur.fetchone()
if not row:
failed.append({"id": sid, "error": "nicht gefunden"})
continue
if row["tenant_id"] is None:
skipped.append({"id": sid, "name": row["name"], "reason": "bereits Grundquelle"})
continue
before = dict(row)
if row["url"]:
cur = await db.execute(
"SELECT id FROM sources WHERE url = ? AND tenant_id IS NULL",
(row["url"],),
)
if await cur.fetchone():
skipped.append({"id": sid, "name": row["name"],
"reason": "URL bereits als Grundquelle vorhanden"})
continue
await db.execute(
"UPDATE sources SET tenant_id = NULL, added_by = 'system' WHERE id = ?",
(sid,),
)
cur = await db.execute("SELECT * FROM sources WHERE id = ?", (sid,))
after = dict(await cur.fetchone())
await log_action(
db, admin, get_client_ip(request),
action="update", resource_type="source", resource_id=sid,
before=before, after=after,
)
promoted += 1
except Exception as e:
failed.append({"id": sid, "error": str(e)})
await db.commit()
return {"promoted": promoted, "skipped": skipped, "failed": failed}
@router.post("/discover")
async def discover_source_endpoint(
url: str,