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:
@@ -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
169
src/static/js/x-scraper.js
Normale Datei
@@ -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";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren