Promote develop → main (2026-07-25 22:22 UTC) #16

Zusammengeführt
IntelSight_Admin hat 41 Commits von develop nach main 2026-07-26 00:22:10 +02:00 zusammengeführt
7 geänderte Dateien mit 263 neuen und 138 gelöschten Zeilen
Nur Änderungen aus Commit 2df2fa1cfa werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -2,7 +2,7 @@
import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from auth import get_current_admin
from database import db_dependency
import aiosqlite
@@ -25,7 +25,7 @@ async def list_audit(
action: Optional[str] = None,
resource_type: Optional[str] = None,
resource_id: Optional[int] = None,
admin_id: Optional[int] = None,
admin_id: Optional[str] = None,
from_ts: Optional[str] = None,
to_ts: Optional[str] = None,
limit: int = 200,
@@ -33,7 +33,11 @@ async def list_audit(
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Audit-Log-Eintraege auflisten mit Filter + Pagination."""
"""Audit-Log-Eintraege auflisten mit Filter + Pagination.
action, resource_type und admin_id akzeptieren kommagetrennte Mehrfachwerte
(Checkbox-Filter im Frontend), ein Einzelwert funktioniert weiterhin.
"""
if limit < 1 or limit > 1000:
limit = 200
if offset < 0:
@@ -42,17 +46,26 @@ async def list_audit(
where = []
params = []
if action:
where.append("action = ?")
params.append(action)
vals = [v.strip() for v in action.split(",") if v.strip()]
if vals:
where.append(f"action IN ({','.join('?' for _ in vals)})")
params.extend(vals)
if resource_type:
where.append("resource_type = ?")
params.append(resource_type)
vals = [v.strip() for v in resource_type.split(",") if v.strip()]
if vals:
where.append(f"resource_type IN ({','.join('?' for _ in vals)})")
params.extend(vals)
if resource_id is not None:
where.append("resource_id = ?")
params.append(resource_id)
if admin_id is not None:
where.append("admin_id = ?")
params.append(admin_id)
if admin_id:
try:
ids = [int(v) for v in admin_id.split(",") if v.strip()]
except ValueError:
raise HTTPException(status_code=422, detail="admin_id muss eine Zahl oder kommagetrennte Zahlen sein")
if ids:
where.append(f"admin_id IN ({','.join('?' for _ in ids)})")
params.extend(ids)
if from_ts:
where.append("ts >= ?")
params.append(from_ts)

Datei anzeigen

@@ -928,6 +928,49 @@ input[type="date"].filter-select { padding: 6px 10px; }
.health-run-item { display: inline-flex; align-items: center; gap: 8px; }
.health-run-item #healthRunInfo { font-size: 12px; }
/* Checkbox-Filterleisten (Standard fuer alle Filter im Portal) */
.filter-bar {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 14px;
margin-bottom: 14px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
}
.filter-group {
display: flex;
align-items: baseline;
gap: 4px 10px;
flex-wrap: wrap;
}
.filter-group-label {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.4px;
min-width: 92px;
cursor: default;
}
.filter-check {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
cursor: pointer;
padding: 2px 7px;
border-radius: 6px;
user-select: none;
}
.filter-check:hover { background: var(--bg-tertiary); }
.filter-check input {
accent-color: var(--accent);
cursor: pointer;
margin: 0;
}
/* Checkbox-Spalte: nur sichtbar bei Herkunfts-Filter "Kundenquellen" */
.select-col { display: none; }
table.show-select .select-col { display: table-cell; }

Datei anzeigen

@@ -6,7 +6,7 @@
<title>AegisSight Monitor-Verwaltung</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="apple-touch-icon" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/css/style.css?v=20260725j">
<link rel="stylesheet" href="/static/css/style.css?v=20260725k">
<script>(function(){var t=localStorage.getItem('portal_theme');if(t==='light')document.documentElement.setAttribute('data-theme','light');try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
<style>
@@ -296,25 +296,6 @@
<div class="action-bar">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<input type="text" class="search-input" id="globalSourceSearch" placeholder="Quelle suchen...">
<select class="filter-select" id="filterOrigin" onchange="filterUnifiedSources()">
<option value="">Alle Quellen</option>
<option value="global">Nur Grundquellen</option>
<option value="tenant">Nur Kundenquellen</option>
</select>
<select class="filter-select" id="globalFilterType" onchange="filterUnifiedSources()">
<option value="">Alle Typen</option>
</select>
<select class="filter-select" id="globalFilterCategory" onchange="filterUnifiedSources()">
<option value="">Alle Kategorien</option>
</select>
<select class="filter-select" id="globalFilterStatus" onchange="filterUnifiedSources()">
<option value="">Alle Status</option>
<option value="active">Aktiv</option>
<option value="inactive">Inaktiv</option>
</select>
<select class="filter-select" id="globalFilterLanguage" onchange="filterUnifiedSources()">
<option value="">Alle Sprachen</option>
</select>
<span class="text-secondary" id="globalSourceCount"></span>
</div>
<button class="btn btn-primary" id="tenantBulkPromoteBtn" onclick="bulkPromoteSelected()" disabled style="display:none;margin-right:8px;">Ausgewählte übernehmen (0)</button>
@@ -322,6 +303,7 @@
<button class="btn btn-secondary" id="newPdfSourceBtn" style="margin-right:8px;">+ PDF hochladen</button>
<button class="btn btn-primary" id="newGlobalSourceBtn">+ Neue Grundquelle</button>
</div>
<div class="filter-bar" id="unifiedFilterBar"></div>
<div class="card">
<div class="table-wrap">
<table id="unifiedTable">
@@ -355,16 +337,6 @@
<div class="review-toolbar-info">
<span><strong id="aufgabenSuggestionCount">0</strong> Vorschläge offen</span>
<span><strong id="reviewPendingCount">0</strong> Klassifikationen ausstehend</span>
<label class="review-conf-filter">
Mindest-Konfidenz
<select class="filter-select" id="reviewMinConfidence" onchange="loadClassificationQueue()">
<option value="0">alle</option>
<option value="0.5">0.5+</option>
<option value="0.7">0.7+</option>
<option value="0.85">0.85+</option>
<option value="0.9">0.9+</option>
</select>
</label>
</div>
<div class="review-toolbar-actions">
<button class="btn btn-secondary btn-small" onclick="triggerExternalReputationSync()" title="IFCN-Faktenchecker und EUvsDisinfo neu syncen">Externe Daten syncen</button>
@@ -372,6 +344,7 @@
<button class="btn btn-primary btn-small" onclick="bulkApproveHighConfidence()" title="Alle Vorschläge ab 0.85 Konfidenz übernehmen">Alle ≥ 0.85 genehmigen</button>
</div>
</div>
<div class="filter-bar" id="aufgabenFilterBar"></div>
<div id="aufgabenSuggestions"></div>
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Klassifikations-Review</h2></div>
@@ -419,16 +392,7 @@
<div class="section" id="sec-audit">
<div class="action-bar" style="flex-wrap:wrap;gap:8px;">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<select class="filter-select" id="auditFilterAction">
<option value="">Alle Aktionen</option>
</select>
<select class="filter-select" id="auditFilterResource">
<option value="">Alle Ressourcen</option>
</select>
<input type="number" class="filter-select" id="auditFilterResourceId" placeholder="Ressourcen-ID" min="1" style="width:130px;">
<select class="filter-select" id="auditFilterAdmin">
<option value="">Alle Admins</option>
</select>
<input type="date" class="filter-select" id="auditFilterFrom" title="Von (Datum)">
<input type="date" class="filter-select" id="auditFilterTo" title="Bis (Datum)">
<button class="btn btn-secondary btn-small" id="auditFilterReset">Filter zuruecksetzen</button>
@@ -439,6 +403,7 @@
<button class="btn btn-secondary btn-small" id="auditNextBtn" disabled>Weiter &rarr;</button>
</div>
</div>
<div class="filter-bar" id="auditFilterBar"></div>
<div class="card">
<div class="table-wrap">
<table>
@@ -1005,11 +970,11 @@
</div>
<script src="/static/js/a11y.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725s"></script>
<script src="/static/js/sources.js?v=20260725d"></script>
<script src="/static/js/aufgaben.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725t"></script>
<script src="/static/js/sources.js?v=20260725e"></script>
<script src="/static/js/aufgaben.js?v=20260725b"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/audit.js?v=20260509d"></script>
<script src="/static/js/audit.js?v=20260725a"></script>
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
</body>
</html>

Datei anzeigen

@@ -1375,6 +1375,36 @@ function populateSelect(el, items, allLabel) {
if (current && items.some(it => it.key === current)) el.value = current;
}
// Checkbox-Filtergruppe (Standard für alle Filter im Portal, statt Dropdowns).
// selected ist ein Set mit den angehakten Keys. Nichts angehakt = keine
// Einschränkung. onChange wird nach jedem Toggle aufgerufen.
function renderFilterGroup(container, title, items, selected, onChange) {
if (!container) return;
const wrap = document.createElement("div");
wrap.className = "filter-group";
const lbl = document.createElement("span");
lbl.className = "filter-group-label";
lbl.textContent = title;
lbl.title = "Nichts angehakt = alle";
wrap.appendChild(lbl);
(items || []).forEach((it) => {
const label = document.createElement("label");
label.className = "filter-check";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = selected.has(it.key);
cb.addEventListener("change", () => {
if (cb.checked) selected.add(it.key);
else selected.delete(it.key);
onChange();
});
label.appendChild(cb);
label.appendChild(document.createTextNode(it.label));
wrap.appendChild(label);
});
container.appendChild(wrap);
}
document.addEventListener("DOMContentLoaded", () => {
// Beim Page-Load Meta einmalig laden (asynchron, blockiert nicht)
if (window.API && (localStorage.getItem("token") || window.location.pathname === "/dashboard")) {

Datei anzeigen

@@ -5,6 +5,9 @@ let auditCache = { items: [], total: 0, offset: 0, limit: 200 };
let auditDistinct = { actions: [], resource_types: [], admins: [] };
let expandedRows = new Set();
// Checkbox-Filter (Mehrfachauswahl, leeres Set = keine Einschränkung)
const auditFilters = { actions: new Set(), resources: new Set(), admins: new Set() };
const ACTION_LABELS = {
create: "Erstellt",
update: "Geändert",
@@ -37,20 +40,22 @@ document.addEventListener("DOMContentLoaded", () => {
tab.addEventListener("click", () => loadAudit());
});
// Filter-Inputs verdrahten
["auditFilterAction", "auditFilterResource", "auditFilterResourceId", "auditFilterAdmin",
"auditFilterFrom", "auditFilterTo"].forEach((id) => {
// Filter-Inputs verdrahten (Checkbox-Gruppen laufen über renderAuditFilterBar)
["auditFilterResourceId", "auditFilterFrom", "auditFilterTo"].forEach((id) => {
const el = document.getElementById(id);
if (el) el.addEventListener("change", () => { auditCache.offset = 0; loadAudit(); });
});
const reset = document.getElementById("auditFilterReset");
if (reset) reset.addEventListener("click", () => {
["auditFilterAction", "auditFilterResource", "auditFilterAdmin",
"auditFilterFrom", "auditFilterTo"].forEach((id) => {
auditFilters.actions.clear();
auditFilters.resources.clear();
auditFilters.admins.clear();
["auditFilterResourceId", "auditFilterFrom", "auditFilterTo"].forEach((id) => {
const el = document.getElementById(id);
if (el) el.value = "";
});
renderAuditFilterBar();
auditCache.offset = 0;
loadAudit();
});
@@ -70,57 +75,49 @@ document.addEventListener("DOMContentLoaded", () => {
});
async function loadAuditDistinct() {
// Distinct-Werte nur einmal laden, die Leiste nur bei Bedarf neu aufbauen
// (sonst wuerde jeder Checkbox-Klick die Leiste flackern lassen).
if (auditDistinct.actions && auditDistinct.actions.length) {
const bar = document.getElementById("auditFilterBar");
if (bar && !bar.children.length) renderAuditFilterBar();
return;
}
try {
auditDistinct = await API.get("/api/audit-log/distinct");
const actSel = document.getElementById("auditFilterAction");
if (actSel && actSel.options.length <= 1) {
auditDistinct.actions.forEach((a) => {
const opt = document.createElement("option");
opt.value = a;
opt.textContent = ACTION_LABELS[a] || a;
actSel.appendChild(opt);
});
}
const resSel = document.getElementById("auditFilterResource");
if (resSel && resSel.options.length <= 1) {
auditDistinct.resource_types.forEach((r) => {
const opt = document.createElement("option");
opt.value = r;
opt.textContent = RESOURCE_LABELS[r] || r;
resSel.appendChild(opt);
});
}
const admSel = document.getElementById("auditFilterAdmin");
if (admSel && admSel.options.length <= 1) {
auditDistinct.admins.forEach((a) => {
const opt = document.createElement("option");
opt.value = a.id;
opt.textContent = a.username;
admSel.appendChild(opt);
});
}
renderAuditFilterBar();
} catch (err) {
console.error("Audit-Filter laden fehlgeschlagen:", err);
}
}
// Checkbox-Filterleiste (Aktion, Ressource, Admin) aus den Distinct-Werten
function renderAuditFilterBar() {
const bar = document.getElementById("auditFilterBar");
if (!bar) return;
bar.innerHTML = "";
const onChange = () => { auditCache.offset = 0; loadAudit(); };
renderFilterGroup(bar, "Aktion",
(auditDistinct.actions || []).map((a) => ({ key: a, label: ACTION_LABELS[a] || a })),
auditFilters.actions, onChange);
renderFilterGroup(bar, "Ressource",
(auditDistinct.resource_types || []).map((r) => ({ key: r, label: RESOURCE_LABELS[r] || r })),
auditFilters.resources, onChange);
renderFilterGroup(bar, "Admin",
(auditDistinct.admins || []).map((a) => ({ key: String(a.id), label: a.username })),
auditFilters.admins, onChange);
}
async function loadAudit() {
await loadAuditDistinct();
const params = new URLSearchParams();
const action = document.getElementById("auditFilterAction")?.value;
const resource = document.getElementById("auditFilterResource")?.value;
const resourceId = document.getElementById("auditFilterResourceId")?.value;
const adminId = document.getElementById("auditFilterAdmin")?.value;
const from = document.getElementById("auditFilterFrom")?.value;
const to = document.getElementById("auditFilterTo")?.value;
if (action) params.append("action", action);
if (resource) params.append("resource_type", resource);
if (auditFilters.actions.size) params.append("action", [...auditFilters.actions].join(","));
if (auditFilters.resources.size) params.append("resource_type", [...auditFilters.resources].join(","));
if (auditFilters.admins.size) params.append("admin_id", [...auditFilters.admins].join(","));
if (resourceId) params.append("resource_id", resourceId);
if (adminId) params.append("admin_id", adminId);
if (from) params.append("from_ts", from);
if (to) params.append("to_ts", to);
params.append("limit", auditCache.limit);

Datei anzeigen

@@ -3,6 +3,18 @@
let suggestionsCache = [];
let healthHistoryCache = [];
let classificationQueueCache = [];
// Checkbox-Filter des Aufgaben-Reiters. Leeres Set = keine Einschränkung.
const aufgabenFilters = { types: new Set(), prio: new Set(), conf: new Set() };
// Konfidenz-Stufen für den Klassifikations-Filter (Mehrfachauswahl)
const CONF_BUCKETS = [
{ key: "b85", label: "ab 85 %", test: (c) => c >= 0.85 },
{ key: "b70", label: "70 bis 85 %", test: (c) => c >= 0.7 && c < 0.85 },
{ key: "b50", label: "50 bis 70 %", test: (c) => c >= 0.5 && c < 0.7 },
{ key: "b0", label: "unter 50 %", test: (c) => c < 0.5 },
];
const SUGGESTION_TYPE_LABELS = {
add_source: "Neue Quelle",
@@ -36,6 +48,7 @@ async function refreshTasksBadge() {
// --- Aufgaben laden ---
async function loadAufgaben() {
renderAufgabenFilters();
try {
const [suggestions, history] = await Promise.all([
API.get("/api/sources/suggestions"),
@@ -52,14 +65,36 @@ async function loadAufgaben() {
refreshTasksBadge();
}
// Checkbox-Filterleiste des Aufgaben-Reiters
function renderAufgabenFilters() {
const bar = document.getElementById("aufgabenFilterBar");
if (!bar) return;
bar.innerHTML = "";
renderFilterGroup(bar, "Vorschlagstyp",
Object.entries(SUGGESTION_TYPE_LABELS).map(([key, label]) => ({ key, label })),
aufgabenFilters.types, renderAufgabenSuggestions);
renderFilterGroup(bar, "Priorität",
Object.entries(PRIORITY_LABELS).map(([key, label]) => ({ key, label })),
aufgabenFilters.prio, renderAufgabenSuggestions);
renderFilterGroup(bar, "Konfidenz", CONF_BUCKETS.map(b => ({ key: b.key, label: b.label })),
aufgabenFilters.conf, renderClassificationList);
}
function renderAufgabenSuggestions() {
const pane = document.getElementById("aufgabenSuggestions");
if (!pane) return;
const pending = suggestionsCache.filter((s) => s.status === "pending");
const allPending = suggestionsCache.filter((s) => s.status === "pending");
const countEl = document.getElementById("aufgabenSuggestionCount");
if (countEl) countEl.textContent = String(pending.length);
if (countEl) countEl.textContent = String(allPending.length);
if (pending.length === 0) {
const f = aufgabenFilters;
const pending = allPending.filter((s) => {
if (f.types.size && !f.types.has(s.suggestion_type)) return false;
if (f.prio.size && !f.prio.has(s.priority)) return false;
return true;
});
if (allPending.length === 0) {
pane.innerHTML = `
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Vorschläge</h2></div>
@@ -67,10 +102,21 @@ function renderAufgabenSuggestions() {
</div>`;
return;
}
if (pending.length === 0) {
pane.innerHTML = `
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Vorschläge (0 von ${allPending.length})</h2></div>
<div class="card-body text-muted">Keine Vorschläge mit diesen Filtern.</div>
</div>`;
return;
}
const headCount = pending.length === allPending.length
? `${allPending.length} offen`
: `${pending.length} von ${allPending.length}`;
pane.innerHTML = `
<div class="card" style="margin-bottom:16px;">
<div class="card-header">
<h2>Vorschläge (${pending.length} offen)</h2>
<h2>Vorschläge (${headCount})</h2>
</div>
<div class="table-wrap">
<table>
@@ -271,22 +317,40 @@ const MEDIA_TYPE_LABELS = {
async function loadClassificationQueue() {
const list = document.getElementById("classificationReviewList");
if (!list) return;
const minConf = parseFloat(document.getElementById("reviewMinConfidence")?.value || "0");
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Lade…</div>';
try {
const items = await API.get(`/api/sources/classification/queue?limit=200&min_confidence=${minConf}`);
const countEl = document.getElementById("reviewPendingCount");
if (countEl) countEl.textContent = String(items.length);
if (items.length === 0) {
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Keine ausstehenden Klassifikationen.</div>';
return;
}
list.innerHTML = items.map((it) => renderClassificationQueueItem(it)).join("");
classificationQueueCache = await API.get("/api/sources/classification/queue?limit=200&min_confidence=0");
renderClassificationList();
} catch (err) {
list.innerHTML = `<div class="text-danger" style="padding:24px;text-align:center;">Fehler. ${esc(err.message)}</div>`;
}
}
// Wendet den Konfidenz-Checkbox-Filter clientseitig auf die geladene Queue an
function renderClassificationList() {
const list = document.getElementById("classificationReviewList");
if (!list) return;
const all = classificationQueueCache || [];
const f = aufgabenFilters.conf;
const items = f.size
? all.filter((it) => {
const c = (it.proposed && it.proposed.confidence) || 0;
return CONF_BUCKETS.some((b) => f.has(b.key) && b.test(c));
})
: all;
const countEl = document.getElementById("reviewPendingCount");
if (countEl) countEl.textContent = String(all.length);
if (all.length === 0) {
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Keine ausstehenden Klassifikationen.</div>';
return;
}
if (items.length === 0) {
list.innerHTML = '<div class="text-muted" style="padding:24px;text-align:center;">Keine Klassifikationen mit diesen Filtern.</div>';
return;
}
list.innerHTML = items.map((it) => renderClassificationQueueItem(it)).join("");
}
function renderClassificationQueueItem(item) {
const cur = item.current || {};
const prop = item.proposed || {};

Datei anzeigen

@@ -5,8 +5,10 @@
let globalSourcesCache = [];
// Auswahl für Bulk-Promote (nur bei Herkunfts-Filter Kundenquellen sichtbar)
let tenantSelected = new Set();
// Aktiver Herkunfts-Filter ("" = alle, "global", "tenant")
let originFilter = "";
// Checkbox-Filter der Quellenliste. Leeres Set = keine Einschränkung.
const unifiedFilters = { origin: new Set(), types: new Set(), cats: new Set(), status: new Set(), langs: new Set() };
let unifiedLangOptions = [];
let lastShowSelect = false;
// Lazy-Cache für die Health-Ausklapp-Details je Quelle
const sourceHealthDetailCache = new Map();
@@ -85,11 +87,6 @@ function setupSourceSubTabs() {
async function loadUnifiedSources() {
try {
await ensureMeta();
// Kategorien/Typen-Dropdowns aus META befüllen (idempotent)
if (window.META && window.META.categories && window.META.categories.length) {
populateSelect(document.getElementById("globalFilterCategory"), window.META.categories, "Alle Kategorien");
populateSelect(document.getElementById("globalFilterType"), window.META.types || [], "Alle Typen");
}
const [list, stats, languages] = await Promise.all([
API.get("/api/sources?scope=all"),
API.get("/api/sources/stats"),
@@ -97,11 +94,7 @@ async function loadUnifiedSources() {
]);
globalSourcesCache = list;
sourceHealthDetailCache.clear();
populateSelect(
document.getElementById("globalFilterLanguage"),
(languages || []).map(l => ({ key: l, label: l })),
"Alle Sprachen",
);
unifiedLangOptions = (languages || []).map(l => ({ key: l, label: l }));
// datalist fuer Edit-Modal
const dl = document.getElementById("languageSuggestions");
if (dl) {
@@ -112,6 +105,7 @@ async function loadUnifiedSources() {
dl.appendChild(o);
});
}
renderUnifiedFilters();
renderUnifiedStats(stats);
filterUnifiedSources();
checkHealthRunOnLoad();
@@ -120,6 +114,24 @@ async function loadUnifiedSources() {
}
}
// Checkbox-Filterleiste der Quellenliste (Herkunft, Typ, Status, Kategorie, Sprache)
function renderUnifiedFilters() {
const bar = document.getElementById("unifiedFilterBar");
if (!bar) return;
bar.innerHTML = "";
renderFilterGroup(bar, "Herkunft", [
{ key: "global", label: "Grundquellen" },
{ key: "tenant", label: "Kundenquellen" },
], unifiedFilters.origin, filterUnifiedSources);
renderFilterGroup(bar, "Typ", (window.META && window.META.types) || [], unifiedFilters.types, filterUnifiedSources);
renderFilterGroup(bar, "Status", [
{ key: "active", label: "Aktiv" },
{ key: "inactive", label: "Inaktiv" },
], unifiedFilters.status, filterUnifiedSources);
renderFilterGroup(bar, "Kategorie", (window.META && window.META.categories) || [], unifiedFilters.cats, filterUnifiedSources);
renderFilterGroup(bar, "Sprache", unifiedLangOptions, unifiedFilters.langs, filterUnifiedSources);
}
async function showSourceAudit(sourceId, sourceName) {
@@ -338,7 +350,9 @@ function renderUnifiedSources(sources) {
});
tbody.innerHTML = html;
const originLabel = originFilter === "global" ? "Grundquellen" : originFilter === "tenant" ? "Kundenquellen" : "Quellen";
const o = unifiedFilters.origin;
const originLabel = (o.has("global") && !o.has("tenant")) ? "Grundquellen"
: (o.has("tenant") && !o.has("global")) ? "Kundenquellen" : "Quellen";
document.getElementById("globalSourceCount").textContent = `${sources.length} ${originLabel}`;
updateBulkButton();
@@ -358,26 +372,19 @@ document.addEventListener("DOMContentLoaded", () => {
function filterUnifiedSources() {
const q = (document.getElementById("globalSourceSearch")?.value || "").toLowerCase();
const typeFilter = document.getElementById("globalFilterType")?.value || "";
const catFilter = document.getElementById("globalFilterCategory")?.value || "";
const statusFilter = document.getElementById("globalFilterStatus")?.value || "";
const langFilter = document.getElementById("globalFilterLanguage")?.value || "";
const newOrigin = document.getElementById("filterOrigin")?.value || "";
if (newOrigin !== originFilter) {
originFilter = newOrigin;
tenantSelected.clear();
}
const f = unifiedFilters;
updateOriginControls();
let filtered = globalSourcesCache.filter((s) => {
if (originFilter === "global" && s.tenant_id != null) return false;
if (originFilter === "tenant" && s.tenant_id == null) return false;
if (f.origin.size) {
const isTenant = s.tenant_id != null;
if (!((isTenant && f.origin.has("tenant")) || (!isTenant && f.origin.has("global")))) return false;
}
if (q && !(s.name.toLowerCase().includes(q) || (s.domain || "").toLowerCase().includes(q) || (s.url || "").toLowerCase().includes(q) || (s.bias || "").toLowerCase().includes(q) || (s.org_name || "").toLowerCase().includes(q))) return false;
if (typeFilter && s.source_type !== typeFilter) return false;
if (catFilter && s.category !== catFilter) return false;
if (statusFilter && s.status !== statusFilter) return false;
if (langFilter && s.language !== langFilter) return false;
if (f.types.size && !f.types.has(s.source_type)) return false;
if (f.cats.size && !f.cats.has(s.category)) return false;
if (f.status.size && !f.status.has(s.status)) return false;
if (f.langs.size && !f.langs.has(s.language)) return false;
return true;
});
@@ -410,9 +417,15 @@ function sortUnifiedSources(field) {
filterUnifiedSources();
}
// Checkbox-Spalte + Bulk-Knopf nur bei Herkunfts-Filter "Kundenquellen"
// Checkbox-Spalte + Bulk-Knopf nur, wenn der Herkunfts-Filter ausschließlich
// auf Kundenquellen steht. Beim Verlassen wird die Auswahl geleert.
function updateOriginControls() {
const showSelect = originFilter === "tenant";
const o = unifiedFilters.origin;
const showSelect = o.has("tenant") && !o.has("global");
if (!showSelect && lastShowSelect) {
tenantSelected.clear();
}
lastShowSelect = showSelect;
const table = document.getElementById("unifiedTable");
if (table) table.classList.toggle("show-select", showSelect);
const btn = document.getElementById("tenantBulkPromoteBtn");