Großes Cleanup: Bugs fixen, Features fertigstellen, toten Code entfernen
Bugs behoben: - handleEdit() async keyword hinzugefügt (E-Mail-Checkboxen funktionieren jetzt) - parseUTC() Funktion definiert (Fortschritts-Timer nutzt Server-Startzeit) - Status cancelling wird im Frontend korrekt angezeigt Features fertiggestellt: - Sidebar: Lagen nach Typ getrennt (adhoc/research) mit Zählern - Quellen-Bearbeiten: Edit-Button pro Quelle, Formular vorausfüllen - Lizenz-Info: Org-Name und Lizenzstatus im Header angezeigt Toter Code entfernt: - 5 verwaiste Dateien gelöscht (alte rss_parser, style.css, components.js, layout.js, setup_users) - 6 ungenutzte Pydantic Models entfernt - Ungenutzte Funktionen/Imports in auth.py, routers, agents, config - Tote API-Methoden, Legacy-UI-Methoden, verwaiste WS-Handler - Abgeschlossene DB-Migrationen aufgeräumt Sonstiges: - requirements.txt: passlib[bcrypt] durch bcrypt ersetzt - Umlaute korrigiert (index.html) - CSS: incident-type-label → incident-type-badge, .login-success hinzugefügt - Schließen statt Schliessen im Feedback-Modal
Dieser Commit ist enthalten in:
@@ -136,22 +136,10 @@ const API = {
|
||||
return this._request('GET', '/sources/stats');
|
||||
},
|
||||
|
||||
refreshSourceCounts() {
|
||||
return this._request('POST', '/sources/refresh-counts');
|
||||
},
|
||||
|
||||
discoverSource(url) {
|
||||
return this._request('POST', '/sources/discover', { url });
|
||||
},
|
||||
|
||||
discoverMulti(url) {
|
||||
return this._request('POST', '/sources/discover-multi', { url });
|
||||
},
|
||||
|
||||
rediscoverExisting() {
|
||||
return this._request('POST', '/sources/rediscover-existing');
|
||||
},
|
||||
|
||||
blockDomain(domain, notes) {
|
||||
return this._request('POST', '/sources/block-domain', { domain, notes });
|
||||
},
|
||||
@@ -173,10 +161,6 @@ const API = {
|
||||
return this._request('GET', `/notifications?limit=${limit}`);
|
||||
},
|
||||
|
||||
getUnreadCount() {
|
||||
return this._request('GET', '/notifications/unread-count');
|
||||
},
|
||||
|
||||
markNotificationsRead(ids = null) {
|
||||
return this._request('PUT', '/notifications/mark-read', { notification_ids: ids });
|
||||
},
|
||||
|
||||
@@ -370,7 +370,6 @@ const App = {
|
||||
currentIncidentId: null,
|
||||
incidents: [],
|
||||
_originalTitle: document.title,
|
||||
_notificationCount: 0,
|
||||
_refreshingIncidents: new Set(),
|
||||
_editingIncidentId: null,
|
||||
_currentArticles: [],
|
||||
@@ -403,6 +402,42 @@ const App = {
|
||||
const user = await API.getMe();
|
||||
this._currentUsername = user.username;
|
||||
document.getElementById('header-user').textContent = user.username;
|
||||
|
||||
// Org-Name anzeigen
|
||||
const orgNameEl = document.getElementById('header-org-name');
|
||||
if (orgNameEl && user.org_name) {
|
||||
orgNameEl.textContent = user.org_name;
|
||||
orgNameEl.title = user.org_name;
|
||||
}
|
||||
|
||||
// Lizenz-Badge anzeigen
|
||||
const badgeEl = document.getElementById('header-license-badge');
|
||||
if (badgeEl) {
|
||||
const licenseLabels = {
|
||||
trial: 'Trial',
|
||||
annual: 'Annual',
|
||||
permanent: 'Permanent',
|
||||
expired: 'Abgelaufen',
|
||||
unknown: 'Unbekannt'
|
||||
};
|
||||
const status = user.read_only ? 'expired' : (user.license_status || 'unknown');
|
||||
const cssClass = user.read_only ? 'license-expired'
|
||||
: user.license_type === 'trial' ? 'license-trial'
|
||||
: user.license_type === 'annual' ? 'license-annual'
|
||||
: user.license_type === 'permanent' ? 'license-permanent'
|
||||
: 'license-unknown';
|
||||
const label = user.read_only ? 'Abgelaufen'
|
||||
: licenseLabels[user.license_type] || licenseLabels[user.license_status] || 'Unbekannt';
|
||||
badgeEl.textContent = label;
|
||||
badgeEl.className = 'header-license-badge ' + cssClass;
|
||||
}
|
||||
|
||||
// Warnung bei abgelaufener Lizenz
|
||||
const warningEl = document.getElementById('header-license-warning');
|
||||
if (warningEl && user.read_only) {
|
||||
warningEl.textContent = 'Lizenz abgelaufen – nur Lesezugriff';
|
||||
warningEl.classList.add('visible');
|
||||
}
|
||||
} catch {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
@@ -432,7 +467,6 @@ const App = {
|
||||
WS.connect();
|
||||
WS.on('status_update', (msg) => this.handleStatusUpdate(msg));
|
||||
WS.on('refresh_complete', (msg) => this.handleRefreshComplete(msg));
|
||||
WS.on('notification', (msg) => this.handleNotification(msg));
|
||||
WS.on('refresh_summary', (msg) => this.handleRefreshSummary(msg));
|
||||
WS.on('refresh_error', (msg) => this.handleRefreshError(msg));
|
||||
WS.on('refresh_cancelled', (msg) => this.handleRefreshCancelled(msg));
|
||||
@@ -476,6 +510,7 @@ const App = {
|
||||
|
||||
renderSidebar() {
|
||||
const activeContainer = document.getElementById('active-incidents');
|
||||
const researchContainer = document.getElementById('active-research');
|
||||
const archivedContainer = document.getElementById('archived-incidents');
|
||||
|
||||
// Filter-Buttons aktualisieren
|
||||
@@ -491,19 +526,34 @@ const App = {
|
||||
filtered = filtered.filter(i => i.created_by_username === this._currentUsername);
|
||||
}
|
||||
|
||||
const active = filtered.filter(i => i.status === 'active');
|
||||
// Aktive Lagen nach Typ aufteilen
|
||||
const activeAdhoc = filtered.filter(i => i.status === 'active' && (!i.type || i.type === 'adhoc'));
|
||||
const activeResearch = filtered.filter(i => i.status === 'active' && i.type === 'research');
|
||||
const archived = filtered.filter(i => i.status === 'archived');
|
||||
|
||||
const emptyLabel = this._sidebarFilter === 'mine' ? 'Keine eigenen Lagen' : 'Keine aktiven Lagen';
|
||||
const emptyLabelAdhoc = this._sidebarFilter === 'mine' ? 'Keine eigenen Ad-hoc-Lagen' : 'Keine Ad-hoc-Lagen';
|
||||
const emptyLabelResearch = this._sidebarFilter === 'mine' ? 'Keine eigenen Recherchen' : 'Keine Recherchen';
|
||||
|
||||
activeContainer.innerHTML = active.length
|
||||
? active.map(i => UI.renderIncidentItem(i, i.id === this.currentIncidentId)).join('')
|
||||
: `<div style="padding:8px 12px;font-size:12px;color:var(--text-tertiary);">${emptyLabel}</div>`;
|
||||
activeContainer.innerHTML = activeAdhoc.length
|
||||
? activeAdhoc.map(i => UI.renderIncidentItem(i, i.id === this.currentIncidentId)).join('')
|
||||
: `<div style="padding:8px 12px;font-size:12px;color:var(--text-tertiary);">${emptyLabelAdhoc}</div>`;
|
||||
|
||||
researchContainer.innerHTML = activeResearch.length
|
||||
? activeResearch.map(i => UI.renderIncidentItem(i, i.id === this.currentIncidentId)).join('')
|
||||
: `<div style="padding:8px 12px;font-size:12px;color:var(--text-tertiary);">${emptyLabelResearch}</div>`;
|
||||
|
||||
archivedContainer.innerHTML = archived.length
|
||||
? archived.map(i => UI.renderIncidentItem(i, i.id === this.currentIncidentId)).join('')
|
||||
: '<div style="padding:8px 12px;font-size:12px;color:var(--text-tertiary);">Kein Archiv</div>';
|
||||
|
||||
// Zähler aktualisieren
|
||||
const countAdhoc = document.getElementById('count-active-incidents');
|
||||
const countResearch = document.getElementById('count-active-research');
|
||||
const countArchived = document.getElementById('count-archived-incidents');
|
||||
if (countAdhoc) countAdhoc.textContent = `(${activeAdhoc.length})`;
|
||||
if (countResearch) countResearch.textContent = `(${activeResearch.length})`;
|
||||
if (countArchived) countArchived.textContent = `(${archived.length})`;
|
||||
|
||||
// Sidebar-Stats aktualisieren
|
||||
this.updateSidebarStats();
|
||||
},
|
||||
@@ -1547,7 +1597,7 @@ const App = {
|
||||
}
|
||||
},
|
||||
|
||||
handleEdit() {
|
||||
async handleEdit() {
|
||||
if (!this.currentIncidentId) return;
|
||||
const incident = this.incidents.find(i => i.id === this.currentIncidentId);
|
||||
if (!incident) return;
|
||||
@@ -1677,16 +1727,7 @@ const App = {
|
||||
await this.loadIncidents();
|
||||
},
|
||||
|
||||
handleNotification(msg) {
|
||||
// Legacy-Fallback: Einzelne Notifications ans NotificationCenter weiterleiten
|
||||
const incident = this.incidents.find(i => i.id === msg.incident_id);
|
||||
NotificationCenter.add({
|
||||
incident_id: msg.incident_id,
|
||||
title: incident ? incident.title : 'Lage #' + msg.incident_id,
|
||||
text: msg.data.message || 'Neue Entwicklung',
|
||||
icon: 'warning',
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
handleRefreshSummary(msg) {
|
||||
const d = msg.data;
|
||||
@@ -2322,9 +2363,17 @@ const App = {
|
||||
document.getElementById('src-discovery-result').style.display = 'none';
|
||||
document.getElementById('src-discover-btn').disabled = false;
|
||||
document.getElementById('src-discover-btn').textContent = 'Erkennen';
|
||||
// Save-Button Text zurücksetzen
|
||||
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
|
||||
if (saveBtn) saveBtn.textContent = 'Speichern';
|
||||
// Block-Form ausblenden
|
||||
const blockForm = document.getElementById('sources-block-form');
|
||||
if (blockForm) blockForm.style.display = 'none';
|
||||
} else {
|
||||
// Beim Schließen: Bearbeitungsmodus zurücksetzen
|
||||
this._editingSourceId = null;
|
||||
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
|
||||
if (saveBtn) saveBtn.textContent = 'Speichern';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2417,6 +2466,66 @@ const App = {
|
||||
}
|
||||
},
|
||||
|
||||
editSource(id) {
|
||||
const source = this._sourcesOnly.find(s => s.id === id);
|
||||
if (!source) {
|
||||
UI.showToast('Quelle nicht gefunden.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this._editingSourceId = id;
|
||||
|
||||
// Formular öffnen falls geschlossen (direkt, ohne toggleSourceForm das _editingSourceId zurücksetzt)
|
||||
const form = document.getElementById('sources-add-form');
|
||||
if (form) {
|
||||
form.style.display = 'block';
|
||||
const blockForm = document.getElementById('sources-block-form');
|
||||
if (blockForm) blockForm.style.display = 'none';
|
||||
}
|
||||
|
||||
// Discovery-URL mit vorhandener URL/Domain befüllen
|
||||
const discoverUrlInput = document.getElementById('src-discover-url');
|
||||
if (discoverUrlInput) {
|
||||
discoverUrlInput.value = source.url || source.domain || '';
|
||||
}
|
||||
|
||||
// Discovery-Ergebnis anzeigen und Felder befüllen
|
||||
document.getElementById('src-discovery-result').style.display = 'block';
|
||||
document.getElementById('src-name').value = source.name || '';
|
||||
document.getElementById('src-category').value = source.category || 'sonstige';
|
||||
document.getElementById('src-notes').value = source.notes || '';
|
||||
document.getElementById('src-domain').value = source.domain || '';
|
||||
|
||||
const typeLabel = source.source_type === 'rss_feed' ? 'RSS-Feed' : 'Web-Quelle';
|
||||
document.getElementById('src-type-display').value = typeLabel;
|
||||
|
||||
const rssGroup = document.getElementById('src-rss-url-group');
|
||||
const rssInput = document.getElementById('src-rss-url');
|
||||
if (source.url) {
|
||||
rssInput.value = source.url;
|
||||
rssGroup.style.display = 'block';
|
||||
} else {
|
||||
rssInput.value = '';
|
||||
rssGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
// _discoveredData setzen damit saveSource() die richtigen Werte nutzt
|
||||
this._discoveredData = {
|
||||
name: source.name,
|
||||
domain: source.domain,
|
||||
category: source.category,
|
||||
source_type: source.source_type,
|
||||
rss_url: source.url,
|
||||
};
|
||||
|
||||
// Submit-Button-Text ändern
|
||||
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
|
||||
if (saveBtn) saveBtn.textContent = 'Quelle speichern';
|
||||
|
||||
// Zum Formular scrollen
|
||||
if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
},
|
||||
|
||||
async saveSource() {
|
||||
const name = document.getElementById('src-name').value.trim();
|
||||
if (!name) {
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/**
|
||||
* Parst einen UTC-Zeitstring vom Server in ein Date-Objekt.
|
||||
*/
|
||||
function parseUTC(dateStr) {
|
||||
if (!dateStr) return null;
|
||||
try {
|
||||
const d = new Date(dateStr.endsWith('Z') ? dateStr : dateStr + 'Z');
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI-Komponenten für das Dashboard.
|
||||
*/
|
||||
@@ -149,19 +162,6 @@ const UI = {
|
||||
return `${explanationHtml}<div class="evidence-chips">${chips}</div>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifizierungs-Badge.
|
||||
*/
|
||||
verificationBadge(status) {
|
||||
const map = {
|
||||
verified: { class: 'badge-verified', text: 'Verifiziert' },
|
||||
unverified: { class: 'badge-unverified', text: 'Offen' },
|
||||
contradicted: { class: 'badge-contradicted', text: 'Widerlegt' },
|
||||
};
|
||||
const badge = map[status] || map.unverified;
|
||||
return `<span class="badge ${badge.class}">${badge.text}</span>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toast-Benachrichtigung anzeigen.
|
||||
*/
|
||||
@@ -228,6 +228,7 @@ const UI = {
|
||||
deep_researching: { active: 1, label: 'Tiefenrecherche läuft...' },
|
||||
analyzing: { active: 2, label: 'Analysiert Meldungen...' },
|
||||
factchecking: { active: 3, label: 'Faktencheck läuft...' },
|
||||
cancelling: { active: 0, label: 'Wird abgebrochen...' },
|
||||
};
|
||||
|
||||
const step = steps[status] || steps.queued;
|
||||
@@ -553,6 +554,7 @@ const UI = {
|
||||
<span class="source-feed-name">${this.escape(feed.name)}</span>
|
||||
<span class="source-type-badge type-${feed.source_type}">${typeLabel}</span>
|
||||
<span class="source-feed-url" title="${this.escape(feed.url || '')}">${this.escape(urlDisplay)}</span>
|
||||
<button class="source-edit-btn" onclick="App.editSource(${feed.id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>
|
||||
<button class="source-delete-btn" onclick="App.deleteSingleFeed(${feed.id})" title="Löschen" aria-label="Löschen">×</button>
|
||||
</div>`;
|
||||
});
|
||||
@@ -572,6 +574,7 @@ const UI = {
|
||||
<span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span>
|
||||
${feedCountBadge}
|
||||
<div class="source-group-actions" onclick="event.stopPropagation()">
|
||||
${!hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>` : ''}
|
||||
<button class="btn btn-small btn-secondary" onclick="App.blockDomainDirect('${escapedDomain}')">Sperren</button>
|
||||
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="Löschen" aria-label="Löschen">×</button>
|
||||
</div>
|
||||
@@ -593,26 +596,6 @@ const UI = {
|
||||
return url.length > 50 ? url.substring(0, 47) + '...' : url;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* URLs in Evidence-Text als kompakte Hostname-Chips rendern (Legacy-Fallback).
|
||||
*/
|
||||
renderEvidenceChips(text) {
|
||||
return this.renderEvidence(text);
|
||||
},
|
||||
|
||||
/**
|
||||
* URLs in Evidence-Text als klickbare Links rendern (Legacy).
|
||||
*/
|
||||
linkifyEvidence(text) {
|
||||
if (!text) return '';
|
||||
const escaped = this.escape(text);
|
||||
return escaped.replace(
|
||||
/(https?:\/\/[^\s,)]+)/g,
|
||||
'<a href="$1" target="_blank" rel="noopener">$1</a>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* HTML escapen.
|
||||
*/
|
||||
|
||||
@@ -261,16 +261,6 @@ const LayoutManager = {
|
||||
this._debouncedSave();
|
||||
},
|
||||
|
||||
resetAllTilesToDefault() {
|
||||
if (!this._grid) return;
|
||||
this.DEFAULT_LAYOUT.forEach(cfg => {
|
||||
const node = this._grid.engine.nodes.find(
|
||||
n => n.el && n.el.getAttribute('gs-id') === cfg.id
|
||||
);
|
||||
if (node) this._grid.update(node.el, { h: cfg.h });
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this._grid) {
|
||||
this._grid.destroy(false);
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren