feat(x): X (Twitter) als Bezugsquelle pro Lage
X-Accounts werden analog zu Telegram als Quelle (source_type=x_account) konfiguriert und pro Lage ueber include_x zugeschaltet. Der Scraper (feeds/x_parser.py, twscrape) liest Account-Timelines, optional ueber einen HTTP-Proxy mit Fallback auf direkten Abruf ueber die Server-IP. - DB-Migration include_x, Pydantic-Modelle, incidents-Router - Orchestrator-X-Pipeline plus Haiku-Account-Vorselektion - sources-Router /x/validate, x_account-Typ in Stats und Frontend - Lage-Einstellungen: X-Toggle neben international und Telegram - twscrape als Abhaengigkeit Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -1831,6 +1831,7 @@ const App = {
|
||||
retention_days: parseInt(document.getElementById('inc-retention').value) || 0,
|
||||
international_sources: document.getElementById('inc-international').checked,
|
||||
include_telegram: document.getElementById('inc-telegram').checked,
|
||||
include_x: document.getElementById('inc-x').checked,
|
||||
visibility: document.getElementById('inc-visibility').checked ? 'public' : 'private',
|
||||
};
|
||||
},
|
||||
@@ -2266,6 +2267,7 @@ async handleRefresh() {
|
||||
{ const _e = document.getElementById('inc-retention'); if (_e) _e.value = incident.retention_days; }
|
||||
{ const _e = document.getElementById('inc-international'); if (_e) _e.checked = incident.international_sources !== false && incident.international_sources !== 0; }
|
||||
{ const _e = document.getElementById('inc-telegram'); if (_e) _e.checked = !!incident.include_telegram; }
|
||||
{ const _e = document.getElementById('inc-x'); if (_e) _e.checked = !!incident.include_x; }
|
||||
|
||||
{ const _e = document.getElementById('inc-visibility'); if (_e) _e.checked = incident.visibility !== 'private'; }
|
||||
updateVisibilityHint();
|
||||
@@ -2795,12 +2797,14 @@ async handleRefresh() {
|
||||
const rss = stats.by_type.rss_feed || { count: 0, articles: 0 };
|
||||
const web = stats.by_type.web_source || { count: 0, articles: 0 };
|
||||
const tg = stats.by_type.telegram_channel || { count: 0, articles: 0 };
|
||||
const x = stats.by_type.x_account || { count: 0, articles: 0 };
|
||||
const excluded = this._myExclusions.length;
|
||||
|
||||
bar.innerHTML = `
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${rss.count}</span> ${(typeof T === 'function' ? T('sources_modal.stats.rss', 'RSS-Feeds') : 'RSS-Feeds')}</span>
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${web.count}</span> ${(typeof T === 'function' ? T('sources_modal.stats.web', 'Web-Quellen') : 'Web-Quellen')}</span>
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${tg.count}</span> Telegram</span>
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${x.count}</span> X</span>
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${excluded}</span> ${(typeof T === 'function' ? T('sources_modal.stats.excluded', 'Ausgeschlossen') : 'Ausgeschlossen')}</span>
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${stats.total_articles}</span> Artikel gesamt</span>
|
||||
`;
|
||||
@@ -3246,6 +3250,31 @@ async handleRefresh() {
|
||||
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
|
||||
return;
|
||||
}
|
||||
|
||||
// X (Twitter)-URLs direkt behandeln (kein Discovery noetig)
|
||||
if (urlVal.match(/^(https?:\/\/)?(x\.com|twitter\.com)\//i)) {
|
||||
const handle = urlVal
|
||||
.replace(/^(https?:\/\/)?(x\.com|twitter\.com)\//i, '')
|
||||
.replace(/\/$/, '')
|
||||
.split(/[/?]/)[0]
|
||||
.replace(/^@/, '');
|
||||
const xUrl = 'x.com/' + handle;
|
||||
this._discoveredData = {
|
||||
name: '@' + handle,
|
||||
domain: xUrl,
|
||||
source_type: 'x_account',
|
||||
rss_url: null,
|
||||
};
|
||||
document.getElementById('src-name').value = '@' + handle;
|
||||
document.getElementById('src-type-select').value = 'x_account';
|
||||
document.getElementById('src-type-display').value = 'X (Twitter)';
|
||||
document.getElementById('src-domain').value = xUrl;
|
||||
document.getElementById('src-rss-url-group').style.display = 'none';
|
||||
document.getElementById('src-discovery-result').style.display = 'block';
|
||||
const saveBtnX = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
|
||||
if (saveBtnX) { saveBtnX.disabled = false; saveBtnX.textContent = 'Speichern'; }
|
||||
return;
|
||||
}
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) {
|
||||
UI.showToast('Bitte URL oder Domain eingeben.', 'warning');
|
||||
@@ -3365,7 +3394,7 @@ async handleRefresh() {
|
||||
document.getElementById('src-notes').value = source.notes || '';
|
||||
document.getElementById('src-domain').value = source.domain || '';
|
||||
|
||||
const typeLabel = source.source_type === 'rss_feed' ? 'RSS-Feed' : source.source_type === 'telegram_channel' ? 'Telegram' : 'Web-Quelle';
|
||||
const typeLabel = source.source_type === 'rss_feed' ? 'RSS-Feed' : source.source_type === 'telegram_channel' ? 'Telegram' : source.source_type === 'x_account' ? 'X (Twitter)' : 'Web-Quelle';
|
||||
const typeSelect = document.getElementById('src-type-select');
|
||||
if (typeSelect) typeSelect.value = source.source_type || 'web_source';
|
||||
document.getElementById('src-type-display').value = typeLabel;
|
||||
@@ -3409,7 +3438,7 @@ async handleRefresh() {
|
||||
name,
|
||||
source_type: discovered.source_type || 'web_source',
|
||||
category: document.getElementById('src-category').value,
|
||||
url: discovered.rss_url || (discovered.source_type === 'telegram_channel' ? (document.getElementById('src-domain').value || null) : null),
|
||||
url: discovered.rss_url || ((discovered.source_type === 'telegram_channel' || discovered.source_type === 'x_account') ? (document.getElementById('src-domain').value || null) : null),
|
||||
domain: document.getElementById('src-domain').value.trim() || discovered.domain || null,
|
||||
notes: document.getElementById('src-notes').value.trim() || null,
|
||||
};
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren