feat(sources): Sammel-Bearbeitung fuer Vorschlaege im Aufgaben-Reiter
Die Vorschlags-Tabelle hat jetzt eine Checkbox-Spalte mit Alle-auswaehlen (wirkt auf die gefilterten Zeilen) und zwei Knoepfe Ausgewaehlte annehmen / Ausgewaehlte ablehnen. Noetig geworden, weil ein Pruefaluf hunderte Vorschlaege erzeugen kann und Einzelklicks mit Dialog dann nicht praktikabel sind. Neuer Endpoint PUT /suggestions/bulk nutzt exakt dieselbe Entscheidungslogik wie der Einzel-Endpoint (gemeinsamer Helper _apply_suggestion_decision, inkl. Auto-Reject und Duplikat-Schutz), bereits erledigte IDs werden uebersprungen statt als Fehler gewertet. Ein Audit-Eintrag pro Sammelaktion mit Zaehlern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -972,7 +972,7 @@
|
||||
<script src="/static/js/a11y.js?v=20260725a"></script>
|
||||
<script src="/static/js/app.js?v=20260725v"></script>
|
||||
<script src="/static/js/sources.js?v=20260725f"></script>
|
||||
<script src="/static/js/aufgaben.js?v=20260725c"></script>
|
||||
<script src="/static/js/aufgaben.js?v=20260725d"></script>
|
||||
<script src="/static/js/x-scraper.js?v=20260725a"></script>
|
||||
<script src="/static/js/audit.js?v=20260725a"></script>
|
||||
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
let suggestionsCache = [];
|
||||
let healthHistoryCache = [];
|
||||
let classificationQueueCache = [];
|
||||
// Auswahl für die Sammel-Bearbeitung der Vorschläge
|
||||
let aufgabenSelected = new Set();
|
||||
|
||||
// Checkbox-Filter des Aufgaben-Reiters. Leeres Set = keine Einschränkung.
|
||||
const aufgabenFilters = { types: new Set(), prio: new Set(), conf: new Set() };
|
||||
@@ -61,6 +63,7 @@ async function loadAufgaben() {
|
||||
]);
|
||||
suggestionsCache = suggestions || [];
|
||||
healthHistoryCache = history || [];
|
||||
aufgabenSelected.clear();
|
||||
renderAufgabenSuggestions();
|
||||
renderAufgabenVerlauf();
|
||||
} catch (err) {
|
||||
@@ -115,6 +118,11 @@ function renderAufgabenSuggestions() {
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
// Auswahl auf die sichtbaren (gefilterten) Zeilen begrenzen, damit nie
|
||||
// etwas Unsichtbares mitbearbeitet wird
|
||||
const visibleIds = new Set(pending.map((s) => s.id));
|
||||
aufgabenSelected.forEach((id) => { if (!visibleIds.has(id)) aufgabenSelected.delete(id); });
|
||||
|
||||
const headCount = pending.length === allPending.length
|
||||
? `${allPending.length} offen`
|
||||
: `${pending.length} von ${allPending.length}`;
|
||||
@@ -122,11 +130,16 @@ function renderAufgabenSuggestions() {
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<div class="card-header">
|
||||
<h2>Vorschläge (${headCount})</h2>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="btn btn-primary btn-small" id="suggBulkAcceptBtn" onclick="bulkSuggestions(true)" disabled>Ausgewählte annehmen (0)</button>
|
||||
<button class="btn btn-secondary btn-small" id="suggBulkRejectBtn" onclick="bulkSuggestions(false)" disabled>Ausgewählte ablehnen (0)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:34px;"><input type="checkbox" id="aufgabenSelectAll" onchange="toggleSuggSelectAll(this.checked)" title="Alle angezeigten auswählen"></th>
|
||||
<th>Typ</th>
|
||||
<th>Titel</th>
|
||||
<th>Beschreibung</th>
|
||||
@@ -140,6 +153,7 @@ function renderAufgabenSuggestions() {
|
||||
.map(
|
||||
(s) => `
|
||||
<tr>
|
||||
<td><input type="checkbox" class="sugg-select" data-id="${s.id}" ${aufgabenSelected.has(s.id) ? "checked" : ""} onchange="toggleSuggSelect(${s.id}, this.checked)"></td>
|
||||
<td><span class="badge badge-suggestion-${s.suggestion_type}">${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}</span></td>
|
||||
<td>${esc(s.title)}</td>
|
||||
<td class="text-secondary" style="max-width:420px; white-space:normal; overflow-wrap:anywhere;">${esc(s.description || "")}</td>
|
||||
@@ -157,6 +171,57 @@ function renderAufgabenSuggestions() {
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
updateSuggBulkButtons();
|
||||
}
|
||||
|
||||
// --- Sammel-Bearbeitung der Vorschläge ---
|
||||
function toggleSuggSelectAll(checked) {
|
||||
document.querySelectorAll("#aufgabenSuggestions input.sugg-select").forEach((cb) => {
|
||||
cb.checked = checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if (checked) aufgabenSelected.add(id); else aufgabenSelected.delete(id);
|
||||
});
|
||||
updateSuggBulkButtons();
|
||||
}
|
||||
|
||||
function toggleSuggSelect(id, checked) {
|
||||
id = parseInt(id);
|
||||
if (checked) aufgabenSelected.add(id); else aufgabenSelected.delete(id);
|
||||
const visible = document.querySelectorAll("#aufgabenSuggestions input.sugg-select").length;
|
||||
const checkedVisible = document.querySelectorAll("#aufgabenSuggestions input.sugg-select:checked").length;
|
||||
const all = document.getElementById("aufgabenSelectAll");
|
||||
if (all) all.checked = visible > 0 && visible === checkedVisible;
|
||||
updateSuggBulkButtons();
|
||||
}
|
||||
|
||||
function updateSuggBulkButtons() {
|
||||
const n = aufgabenSelected.size;
|
||||
const a = document.getElementById("suggBulkAcceptBtn");
|
||||
const r = document.getElementById("suggBulkRejectBtn");
|
||||
if (a) { a.disabled = n === 0; a.textContent = `Ausgewählte annehmen (${n})`; }
|
||||
if (r) { r.disabled = n === 0; r.textContent = `Ausgewählte ablehnen (${n})`; }
|
||||
}
|
||||
|
||||
async function bulkSuggestions(accept) {
|
||||
if (aufgabenSelected.size === 0) return;
|
||||
const ids = Array.from(aufgabenSelected);
|
||||
const text = accept
|
||||
? `Sollen ${ids.length} Vorschläge angenommen werden? Die Aktionen werden sofort ausgeführt, z.B. Quellen deaktiviert, angelegt oder URLs korrigiert.`
|
||||
: `Sollen ${ids.length} Vorschläge abgelehnt werden? Sie wandern in den Verlauf. Beim nächsten Prüflauf können sie erneut vorgeschlagen werden.`;
|
||||
const ok = await showConfirm(accept ? "Ausgewählte annehmen" : "Ausgewählte ablehnen", text);
|
||||
if (!ok) return;
|
||||
try {
|
||||
const r = await API.put("/api/sources/suggestions/bulk", { suggestion_ids: ids, accept: accept });
|
||||
let msg = accept ? `${r.accepted} angenommen` : `${r.rejected} abgelehnt`;
|
||||
if (accept && r.rejected) msg += `, ${r.rejected} nicht ausführbar (abgelehnt)`;
|
||||
if (r.skipped) msg += `, ${r.skipped} bereits erledigt`;
|
||||
showToast(msg + ".", "success");
|
||||
aufgabenSelected.clear();
|
||||
loadAufgaben();
|
||||
if (typeof loadUnifiedSources === "function") loadUnifiedSources();
|
||||
} catch (err) {
|
||||
showToast("Sammel-Aktion fehlgeschlagen. " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function renderAufgabenVerlauf() {
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren