feat(x-scraper): X-Recherche-Konten im Verwaltungsportal verwalten

Neuer Sub-Tab "X-Recherche-Konten" unter Quellen: die X-Login-Konten,
mit denen der Monitor bei X scrapt (twscrape-Account-Pool), anzeigen,
hinzufuegen, Cookies erneuern, aktiv/inaktiv schalten, entfernen, plus
Sperren-Reset.

- neuer Router x_scraper.py, verwaltet den twscrape-Pool ueber dessen API
- X_ACCOUNTS_DB_PATH in config.py
- twscrape als Abhaengigkeit (git-main-Pin)
- Sub-Tab, Tabelle und zwei Modals in dashboard.html, Logik in x-scraper.js

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-05-22 11:22:35 +00:00
Ursprung 6bfff67c2f
Commit 8c75a70655
7 geänderte Dateien mit 504 neuen und 2 gelöschten Zeilen

Datei anzeigen

@@ -38,6 +38,7 @@ function setupSourceSubTabs() {
else if (subtab === "tenant-sources") loadTenantSources();
else if (subtab === "source-health") loadHealthData();
else if (subtab === "classification-review") loadClassificationQueue();
else if (subtab === "x-scraper") loadXScraperAccounts();
});
});
}

169
src/static/js/x-scraper.js Normale Datei
Datei anzeigen

@@ -0,0 +1,169 @@
/* X-Recherche-Konten: Verwaltung des twscrape-Account-Pools */
"use strict";
let xScraperCache = [];
async function loadXScraperAccounts() {
setupXScraperForms();
const tbody = document.getElementById("xScraperTable");
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Lade...</td></tr>';
try {
xScraperCache = await API.get("/api/x-scraper/accounts");
renderXScraperAccounts(xScraperCache || []);
} catch (err) {
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Fehler: ' + esc(err.message || "") + '</td></tr>';
}
}
function renderXScraperAccounts(list) {
const tbody = document.getElementById("xScraperTable");
const cnt = document.getElementById("xScraperCount");
if (cnt) cnt.textContent = list.length + (list.length === 1 ? " Konto" : " Konten");
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Keine X-Recherche-Konten. Mit „+ Konto hinzufügen" anlegen.</td></tr>';
return;
}
tbody.innerHTML = list.map((a) => {
let status;
if (!a.active) status = '<span class="text-muted">Inaktiv</span>';
else if (a.locked) status = '<span style="color:var(--warning,#b8860b);">Gesperrt</span>';
else status = '<span style="color:var(--success,#2e7d32);">Aktiv</span>';
const lastUsed = a.last_used && typeof formatDateTime === "function"
? formatDateTime(a.last_used)
: (a.last_used || "—");
const errInfo = a.error_msg
? ' <span class="info-icon" title="' + esc(a.error_msg) + '">!</span>'
: "";
const u = esc(a.username);
const toggleLabel = a.active ? "Deaktivieren" : "Aktivieren";
return '<tr>'
+ '<td><strong>' + u + '</strong>' + errInfo + '</td>'
+ '<td>' + esc(a.email || "—") + '</td>'
+ '<td>' + status + '</td>'
+ '<td>' + (a.total_requests || 0) + '</td>'
+ '<td>' + esc(lastUsed) + '</td>'
+ '<td>'
+ '<button class="btn btn-secondary btn-small" onclick="openXScraperCookiesModal(\'' + u + '\')">Cookies erneuern</button> '
+ '<button class="btn btn-secondary btn-small" onclick="toggleXScraperActive(\'' + u + '\',' + (!a.active) + ')">' + toggleLabel + '</button> '
+ '<button class="btn btn-danger btn-small" onclick="confirmDeleteXScraper(\'' + u + '\')">Entfernen</button>'
+ '</td>'
+ '</tr>';
}).join("");
}
function openXScraperAddModal() {
document.getElementById("xScraperAddError").style.display = "none";
["xsUsername", "xsPassword", "xsEmail", "xsEmailPassword", "xsCookies"].forEach((id) => {
const el = document.getElementById(id);
if (el) el.value = "";
});
openModal("modalXScraperAdd");
}
function openXScraperCookiesModal(username) {
document.getElementById("xScraperCookiesError").style.display = "none";
document.getElementById("xsCookiesUsername").value = username;
document.getElementById("xsCookiesValue").value = "";
openModal("modalXScraperCookies");
}
async function toggleXScraperActive(username, active) {
try {
await API.post("/api/x-scraper/accounts/" + encodeURIComponent(username) + "/active", { active: active });
showToast("Status geändert.", "success");
loadXScraperAccounts();
} catch (err) {
showToast(err.message || "Status konnte nicht geändert werden", "error");
}
}
function confirmDeleteXScraper(username) {
showConfirm(
"Konto entfernen",
'Soll das X-Recherche-Konto "' + username + '" entfernt werden? Der Monitor nutzt es dann nicht mehr zum Scrapen.',
async () => {
try {
await API.del("/api/x-scraper/accounts/" + encodeURIComponent(username));
showToast("Konto entfernt.", "success");
loadXScraperAccounts();
} catch (err) {
showToast(err.message || "Konto konnte nicht entfernt werden", "error");
}
}
);
}
function resetXScraperLocks() {
showConfirm(
"Sperren zurücksetzen",
"Alle temporären Sperren der X-Recherche-Konten zurücksetzen?",
async () => {
try {
await API.post("/api/x-scraper/reset-locks", {});
showToast("Sperren zurückgesetzt.", "success");
loadXScraperAccounts();
} catch (err) {
showToast(err.message || "Sperren konnten nicht zurückgesetzt werden", "error");
}
}
);
}
function setupXScraperForms() {
const addForm = document.getElementById("xScraperAddForm");
if (addForm && !addForm.dataset.wired) {
addForm.dataset.wired = "1";
addForm.addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("xScraperAddError");
errEl.style.display = "none";
const body = {
username: document.getElementById("xsUsername").value.trim().replace(/^@/, ""),
password: document.getElementById("xsPassword").value,
email: document.getElementById("xsEmail").value.trim(),
email_password: document.getElementById("xsEmailPassword").value,
cookies: document.getElementById("xsCookies").value.trim(),
};
if (!body.username || !body.cookies) {
errEl.textContent = "Benutzername und Cookies sind erforderlich.";
errEl.style.display = "block";
return;
}
try {
await API.post("/api/x-scraper/accounts", body);
closeModal("modalXScraperAdd");
showToast("Konto angelegt.", "success");
loadXScraperAccounts();
} catch (err) {
errEl.textContent = err.message || "Anlegen fehlgeschlagen";
errEl.style.display = "block";
}
});
}
const ckForm = document.getElementById("xScraperCookiesForm");
if (ckForm && !ckForm.dataset.wired) {
ckForm.dataset.wired = "1";
ckForm.addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("xScraperCookiesError");
errEl.style.display = "none";
const username = document.getElementById("xsCookiesUsername").value;
const cookies = document.getElementById("xsCookiesValue").value.trim();
if (!cookies) {
errEl.textContent = "Cookies sind erforderlich.";
errEl.style.display = "block";
return;
}
try {
await API.post("/api/x-scraper/accounts/" + encodeURIComponent(username) + "/cookies", { cookies: cookies });
closeModal("modalXScraperCookies");
showToast("Cookies erneuert.", "success");
loadXScraperAccounts();
} catch (err) {
errEl.textContent = err.message || "Cookies konnten nicht erneuert werden";
errEl.style.display = "block";
}
});
}
}