feat(sources): X-Account-Verwaltung im Verwaltungsportal

Neuer Sub-Tab "X-Accounts" unter Quellen: die als Recherchequelle
eingebundenen X-Accounts anzeigen, hinzufuegen, bearbeiten und entfernen.
Schreibt source_type=x_account in die geteilte sources-Tabelle, von wo
der Monitor sie pro Lage nutzt.

- x_account im source_type-Pattern von GlobalSourceCreate/Update
- primary_language in Create/Update plus INSERT (Keyword-Matching)
- x_account-Typ und x-Kategorie in source_meta.py
- Sub-Tab, Tabelle und Modal in dashboard.html, Logik in sources.js

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-05-22 11:06:07 +00:00
Ursprung ed38d68db7
Commit bd476edb13
4 geänderte Dateien mit 235 neuen und 7 gelöschten Zeilen

Datei anzeigen

@@ -38,10 +38,152 @@ function setupSourceSubTabs() {
else if (subtab === "tenant-sources") loadTenantSources();
else if (subtab === "source-health") loadHealthData();
else if (subtab === "classification-review") loadClassificationQueue();
else if (subtab === "x-accounts") loadXAccounts();
});
});
}
// --- X-Accounts (Recherche-Accounts für den Monitor) ---
let xAccountsCache = [];
let editingXAccountId = null;
function normalizeXHandle(raw) {
let h = (raw || "").trim();
h = h.replace(/^https?:\/\//i, "").replace(/^www\./i, "");
h = h.replace(/^(x\.com\/|twitter\.com\/|nitter\.net\/)/i, "");
h = h.replace(/^@/, "").replace(/\/+$/, "");
return h.split(/[/?#]/)[0];
}
async function loadXAccounts() {
setupXAccountForm();
const tbody = document.getElementById("xAccountTable");
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Lade...</td></tr>';
try {
const all = await API.get("/api/sources/global");
xAccountsCache = (all || []).filter((s) => s.source_type === "x_account");
renderXAccounts(xAccountsCache);
} catch (err) {
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Fehler beim Laden</td></tr>';
showToast("X-Accounts konnten nicht geladen werden", "error");
}
}
function renderXAccounts(list) {
const tbody = document.getElementById("xAccountTable");
const cnt = document.getElementById("xAccountCount");
if (cnt) cnt.textContent = list.length + (list.length === 1 ? " Account" : " Accounts");
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="7" class="text-muted">Keine X-Accounts. Mit „+ X-Account hinzufügen" anlegen.</td></tr>';
return;
}
tbody.innerHTML = list.map((s) => {
const handle = normalizeXHandle(s.url || s.domain || s.name || "");
const url = "https://x.com/" + handle;
const lang = s.primary_language || s.language || "—";
const notes = s.notes ? esc(s.notes) : '<span class="text-muted">—</span>';
const status = s.status === "active"
? '<span style="color:var(--success,#2e7d32);">Aktiv</span>'
: '<span class="text-muted">Inaktiv</span>';
return '<tr>'
+ '<td><strong>' + esc(s.name || ("@" + handle)) + '</strong></td>'
+ '<td><a href="' + esc(url) + '" target="_blank" rel="noopener">' + esc(handle) + '</a></td>'
+ '<td>' + esc(lang) + '</td>'
+ '<td>' + notes + '</td>'
+ '<td>' + (s.article_count || 0) + '</td>'
+ '<td>' + status + '</td>'
+ '<td>'
+ '<button class="btn btn-secondary btn-small" onclick="openXAccountModal(' + s.id + ')">Bearbeiten</button> '
+ '<button class="btn btn-danger btn-small" onclick="confirmDeleteXAccount(' + s.id + ')">Löschen</button>'
+ '</td>'
+ '</tr>';
}).join("");
}
function filterXAccounts() {
const q = (document.getElementById("xAccountSearch").value || "").toLowerCase();
if (!q) { renderXAccounts(xAccountsCache); return; }
renderXAccounts(xAccountsCache.filter((s) =>
(s.name || "").toLowerCase().includes(q)
|| (s.url || "").toLowerCase().includes(q)
|| (s.notes || "").toLowerCase().includes(q)
));
}
function openXAccountModal(id) {
editingXAccountId = id || null;
const errEl = document.getElementById("xAccountError");
errEl.style.display = "none";
const s = editingXAccountId ? xAccountsCache.find((a) => a.id === editingXAccountId) : null;
if (editingXAccountId && !s) return;
document.getElementById("xAccountModalTitle").textContent = s ? "X-Account bearbeiten" : "X-Account hinzufügen";
document.getElementById("xAccountHandle").value = s ? normalizeXHandle(s.url || s.domain || "") : "";
document.getElementById("xAccountName").value = s ? (s.name || "") : "";
document.getElementById("xAccountLanguage").value = s ? (s.primary_language || s.language || "en") : "en";
document.getElementById("xAccountNotes").value = s ? (s.notes || "") : "";
document.getElementById("xAccountStatus").value = s ? (s.status || "active") : "active";
openModal("modalXAccount");
}
function setupXAccountForm() {
const form = document.getElementById("xAccountForm");
if (!form || form.dataset.wired) return;
form.dataset.wired = "1";
form.addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("xAccountError");
errEl.style.display = "none";
const handle = normalizeXHandle(document.getElementById("xAccountHandle").value);
if (!handle) {
errEl.textContent = "Bitte einen Handle oder eine x.com-URL eingeben.";
errEl.style.display = "block";
return;
}
const nameVal = document.getElementById("xAccountName").value.trim();
const body = {
name: nameVal || ("@" + handle),
url: "x.com/" + handle,
domain: "x.com/" + handle,
source_type: "x_account",
category: "x",
status: document.getElementById("xAccountStatus").value,
notes: document.getElementById("xAccountNotes").value.trim() || null,
primary_language: document.getElementById("xAccountLanguage").value || null,
};
try {
if (editingXAccountId) {
await API.put("/api/sources/global/" + editingXAccountId, body);
} else {
await API.post("/api/sources/global", body);
}
closeModal("modalXAccount");
loadXAccounts();
showToast("X-Account gespeichert.", "success");
} catch (err) {
errEl.textContent = err.message || "Speichern fehlgeschlagen";
errEl.style.display = "block";
}
});
}
function confirmDeleteXAccount(id) {
const s = xAccountsCache.find((a) => a.id === id);
if (!s) return;
showConfirm(
"X-Account entfernen",
'Soll der X-Account "' + (s.name || "") + '" als Recherchequelle entfernt werden?',
async () => {
try {
await API.del("/api/sources/global/" + id);
loadXAccounts();
showToast("X-Account entfernt.", "success");
} catch (err) {
showToast(err.message || "Löschen fehlgeschlagen", "error");
}
}
);
}
// --- Grundquellen ---
async function loadGlobalSources() {
try {