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:
@@ -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,
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren