/**
 * OSINT Lagemonitor - Hauptanwendungslogik.
 */

/** Feste Zeitzone fuer alle Anzeigen — NIEMALS aendern. */
const TIMEZONE = 'Europe/Berlin';

/** Gibt Jahr/Monat(0-basiert)/Tag/Stunde/Minute in Berliner Zeit zurueck. */
function _tz(d) {
    const s = d.toLocaleString('en-CA', {
        timeZone: TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit',
        hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
    });
    const m = s.match(/(\d{4})-(\d{2})-(\d{2}),?\s*(\d{2}):(\d{2}):(\d{2})/);
    if (!m) return { year: d.getFullYear(), month: d.getMonth(), date: d.getDate(), hours: d.getHours(), minutes: d.getMinutes() };
    return { year: +m[1], month: +m[2] - 1, date: +m[3], hours: +m[4], minutes: +m[5] };
}

/**
 * Theme Manager: Dark/Light Theme Toggle mit localStorage-Persistenz.
 */
const ThemeManager = {
    _key: 'osint_theme',
    init() {
        const saved = localStorage.getItem(this._key);
        const theme = saved || 'dark';
        document.documentElement.setAttribute('data-theme', theme);
        this._updateIcon(theme);
    },
    toggle() {
        const current = document.documentElement.getAttribute('data-theme') || 'dark';
        const next = current === 'dark' ? 'light' : 'dark';
        document.documentElement.setAttribute('data-theme', next);
        localStorage.setItem(this._key, next);
        this._updateIcon(next);
        UI.updateMapTheme();
    },
    _updateIcon(theme) {
        const el = document.getElementById('theme-toggle');
        if (!el) return;
        el.classList.remove('dark', 'light');
        el.classList.add(theme);
        el.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
    }
};

/*
 * Der A11yManager (Barrierefreiheits-Panel) liegt seit 2026-07-25 in
 * js/a11y.js, weil ihn die Studio-Ansicht ebenfalls einbindet.
 */

/**
 * Notification-Center: Glocke mit Badge + History-Panel.
 */
const NotificationCenter = {
    _notifications: [],
    _unreadCount: 0,
    _isOpen: false,
    _maxItems: 50,
    _syncTimer: null,

    async init() {
        // Glocken-Container dynamisch in .header-right vor #header-user einfügen
        const headerRight = document.querySelector('.header-right');
        const headerUser = document.getElementById('header-user');
        if (!headerRight || !headerUser) return;

        const container = document.createElement('div');
        container.className = 'notification-center';
        container.innerHTML = `
            <button class="notification-bell" id="notification-bell" title="Benachrichtigungen" aria-label="Benachrichtigungen" aria-expanded="false" aria-haspopup="true">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
                    <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
                </svg>
                <span class="notification-badge" id="notification-badge" style="display:none;">0</span>
            </button>
            <div class="notification-panel" id="notification-panel" style="display:none;">
                <div class="notification-panel-header">
                    <span class="notification-panel-title">${(typeof T === 'function' ? T('notifications.title', 'Benachrichtigungen') : 'Benachrichtigungen')}</span>
                    <button class="notification-mark-read" id="notification-mark-read">${(typeof T === 'function' ? T('notifications.mark_all_read', 'Alle gelesen') : 'Alle gelesen')}</button>
                </div>
                <div class="notification-panel-list" id="notification-panel-list">
                    <div class="notification-empty">Keine Benachrichtigungen</div>
                </div>
            </div>
        `;
        headerRight.insertBefore(container, headerUser);

        // Event-Listener
        document.getElementById('notification-bell').addEventListener('click', (e) => {
            e.stopPropagation();
            this.toggle();
        });
        document.getElementById('notification-mark-read').addEventListener('click', (e) => {
            e.stopPropagation();
            this.markAllRead();
        });
        // Klick außerhalb schließt Panel
        document.addEventListener('click', (e) => {
            if (this._isOpen && !container.contains(e.target)) {
                this.close();
            }
        });

        // Notifications aus DB laden
        await this._loadFromDB();
    },

    add(notification) {
        // Optimistisches UI: sofort anzeigen
        notification.read = false;
        notification.timestamp = notification.timestamp || new Date().toISOString();
        this._notifications.unshift(notification);
        if (this._notifications.length > this._maxItems) {
            this._notifications.pop();
        }
        this._unreadCount++;
        this._updateBadge();
        this._renderList();

        // DB-Sync mit Debounce (Orchestrator schreibt parallel in DB)
        clearTimeout(this._syncTimer);
        this._syncTimer = setTimeout(() => this._syncFromDB(), 500);
    },

    toggle() {
        this._isOpen ? this.close() : this.open();
    },

    open() {
        this._isOpen = true;
        const panel = document.getElementById('notification-panel');
        if (panel) panel.style.display = 'flex';
        const bell = document.getElementById('notification-bell');
        if (bell) bell.setAttribute('aria-expanded', 'true');
    },

    close() {
        this._isOpen = false;
        const panel = document.getElementById('notification-panel');
        if (panel) panel.style.display = 'none';
        const bell = document.getElementById('notification-bell');
        if (bell) bell.setAttribute('aria-expanded', 'false');
    },

    async markAllRead() {
        this._notifications.forEach(n => n.read = true);
        this._unreadCount = 0;
        this._updateBadge();
        this._renderList();

        // In DB als gelesen markieren (fire-and-forget)
        try {
            await API.markNotificationsRead(null);
        } catch (e) {
            console.warn('Notifications als gelesen markieren fehlgeschlagen:', e);
        }
    },

    _updateBadge() {
        const badge = document.getElementById('notification-badge');
        if (!badge) return;
        if (this._unreadCount > 0) {
            badge.style.display = 'flex';
            badge.textContent = this._unreadCount > 99 ? '99+' : this._unreadCount;
            document.title = `(${this._unreadCount}) ${App._originalTitle}`;
        } else {
            badge.style.display = 'none';
            document.title = App._originalTitle;
        }
    },

    _renderList() {
        const list = document.getElementById('notification-panel-list');
        if (!list) return;

        if (this._notifications.length === 0) {
            list.innerHTML = ('<div class="notification-empty">' + (typeof T === 'function' ? T('notifications.empty', 'Keine Benachrichtigungen') : 'Keine Benachrichtigungen') + '</div>');
            return;
        }

        list.innerHTML = this._notifications.map(n => {
            const time = new Date(n.timestamp);
            const timeStr = time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
            const unreadClass = n.read ? '' : ' unread';
            const icon = n.icon || 'info';
            return `<div class="notification-item${unreadClass}" onclick="NotificationCenter._handleClick(${n.incident_id})" data-id="${n.incident_id}" role="button" tabindex="0">
                <div class="notification-item-icon ${icon}">${this._iconSymbol(icon)}</div>
                <div class="notification-item-body">
                    <div class="notification-item-title">${this._escapeHtml(n.title)}</div>
                    <div class="notification-item-text">${this._escapeHtml(n.text)}</div>
                </div>
                <div class="notification-item-time">${timeStr}</div>
            </div>`;
        }).join('');
    },

    _handleClick(incidentId) {
        this.close();
        if (incidentId) {
            App.selectIncident(incidentId);
        }
    },

    _iconSymbol(type) {
        switch (type) {
            case 'success': return '\u2713';
            case 'warning': return '!';
            case 'error': return '\u2717';
            default: return 'i';
        }
    },

    _escapeHtml(text) {
        const d = document.createElement('div');
        d.textContent = text || '';
        return d.innerHTML;
    },

    async _loadFromDB() {
        try {
            const items = await API.listNotifications(50);
            this._notifications = items.map(n => ({
                id: n.id,
                incident_id: n.incident_id,
                title: n.title,
                text: n.text,
                icon: n.icon || 'info',
                type: n.type,
                read: !!n.is_read,
                timestamp: n.created_at,
            }));
            this._unreadCount = this._notifications.filter(n => !n.read).length;
            this._updateBadge();
            this._renderList();
        } catch (e) {
            console.warn('Notifications laden fehlgeschlagen:', e);
        }
    },

    async _syncFromDB() {
        try {
            const items = await API.listNotifications(50);
            this._notifications = items.map(n => ({
                id: n.id,
                incident_id: n.incident_id,
                title: n.title,
                text: n.text,
                icon: n.icon || 'info',
                type: n.type,
                read: !!n.is_read,
                timestamp: n.created_at,
            }));
            this._unreadCount = this._notifications.filter(n => !n.read).length;
            this._updateBadge();
            this._renderList();
        } catch (e) {
            console.warn('Notifications sync fehlgeschlagen:', e);
        }
    },
};

const App = {
    currentIncidentId: null,
    incidents: [],
    _originalTitle: document.title,
    _refreshingIncidents: new Set(),
    _editingIncidentId: null,
    _currentArticles: [],
    _currentSnapshots: [],
    _snapshotFullCache: new Map(),
    _currentSources: [],
    _currentIncidentType: 'adhoc',
    _sidebarFilter: 'all',
    _currentUsername: '',
    _allSources: [],
    _sourcesOnly: [],
    _myExclusions: [],  // [{domain, notes, created_at}]
    _expandedGroups: new Set(),
    _editingSourceId: null,
    _timelineFilter: 'all',
    _timelineRange: 'all',
    _activeStripWindow: null,
    _timelineSearchTimer: null,
    _pendingComplete: null,
    _pendingCompleteTimer: null,

    async init() {
        ThemeManager.init();
        A11yManager.init();
        // Auth prüfen
        const token = localStorage.getItem('osint_token');
        if (!token) {
            window.location.href = '/';
            return;
        }

        try {
            const user = await API.getMe();
            this.user = user;
            this._currentUsername = user.email;

            // Studio-Button vorerst nur fuer info@aegis-sight.de sichtbar (experimentelle Ansicht)
            if (user.email === 'info@aegis-sight.de') {
                const studioLink = document.getElementById('studio-link');
                if (studioLink) studioLink.style.display = '';
            }

            // i18n: Sprache anhand der Org laden (default 'de') und DOM uebersetzen
            if (window.I18N) {
                const targetLang = user.output_language || 'de';
                await window.I18N.load(targetLang);
                window.I18N.applyDom();
            }

            document.getElementById('header-user').textContent = user.email;

            // Dropdown-Daten befuellen
            const orgNameEl = document.getElementById('header-org-name');
            if (orgNameEl) orgNameEl.textContent = user.org_name || '-';

            const licInfoEl = document.getElementById('header-license-info');
            if (licInfoEl) {
                const licenseLabels = {
                    trial: 'Trial',
                    annual: 'Jahreslizenz',
                    permanent: 'Permanent',
                };
                const label = user.read_only ? 'Abgelaufen'
                    : licenseLabels[user.license_type] || user.license_status || '-';
                licInfoEl.textContent = label;
            }

            // Credits-Anzeige im Dropdown
            const creditsSection = document.getElementById('credits-section');
            if (creditsSection && user.credits_total) {
                creditsSection.style.display = 'block';
                const bar = document.getElementById('credits-bar');
                const remainingEl = document.getElementById('credits-remaining');
                const totalEl = document.getElementById('credits-total');
                
                const remaining = user.credits_remaining || 0;
                const total = user.credits_total || 1;
                const percentUsed = user.credits_percent_used || 0;
                const percentRemaining = Math.max(0, 100 - percentUsed);
                
                remainingEl.textContent = remaining.toLocaleString('de-DE');
                totalEl.textContent = total.toLocaleString('de-DE');
                bar.style.width = percentRemaining + '%';
                
                // Farbwechsel je nach Verbrauch
                bar.classList.remove('warning', 'critical');
                if (percentUsed > 80) {
                    bar.classList.add('critical');
                } else if (percentUsed > 50) {
                    bar.classList.add('warning');
                }
                const percentEl = document.getElementById("credits-percent");
                if (percentEl) percentEl.textContent = percentRemaining.toFixed(0) + "% verbleibend";

                // Bezugszeitraum benennen. Ohne den Zusatz liest sich ein
                // monatliches Kontingent wie ein Gesamtvorrat, der nie wiederkommt.
                const labelEl = document.getElementById('credits-label');
                if (labelEl) {
                    const _tt = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
                    labelEl.textContent = user.credits_period === 'monthly'
                        ? _tt('credits.label_monthly', 'Credits diesen Monat')
                        : _tt('credits.label', 'Credits');
                }
            }

            // Dropdown Toggle
            const userBtn = document.getElementById('header-user-btn');
            const userDropdown = document.getElementById('header-user-dropdown');
            if (userBtn && userDropdown) {
                userBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const isOpen = userDropdown.classList.toggle('open');
                    userBtn.setAttribute('aria-expanded', isOpen);
                });
                userDropdown.addEventListener('click', (e) => {
                    e.stopPropagation();
                });
                document.addEventListener('click', () => {
                    userDropdown.classList.remove('open');
                    userBtn.setAttribute('aria-expanded', 'false');
                });
            }

            // Warnung bei Read-Only (Lizenz abgelaufen oder Token-Budget aufgebraucht)
            const warningEl = document.getElementById('header-license-warning');
            if (warningEl) {
                if (user.read_only) {
                    let text = 'Nur Lesezugriff';
                    const reason = user.read_only_reason;
                    if (reason === 'budget_exceeded') {
                        text = 'Token-Budget aufgebraucht – nur Lesezugriff. Für Aufstockung oder Upgrade bitte info@aegis-sight.de kontaktieren.';
                    } else if (reason === 'expired') {
                        text = (typeof T === 'function' ? T('license.expired_readonly', 'Lizenz abgelaufen – nur Lesezugriff') : 'Lizenz abgelaufen – nur Lesezugriff');
                    } else if (reason === 'no_license') {
                        text = (typeof T === 'function' ? T('license.none_readonly', 'Keine aktive Lizenz – nur Lesezugriff') : 'Keine aktive Lizenz – nur Lesezugriff');
                    } else if (reason === 'org_disabled') {
                        text = (typeof T === 'function' ? T('license.org_disabled_readonly', 'Organisation deaktiviert – nur Lesezugriff') : 'Organisation deaktiviert – nur Lesezugriff');
                    }
                    warningEl.textContent = text;
                    warningEl.classList.add('visible');
                } else {
                    warningEl.textContent = '';
                    warningEl.classList.remove('visible');
                }
            }

            // --- Global Admin: Org-Switcher (herausnehmbar) ---
            if (user.is_global_admin) {
                this._initOrgSwitcher(user.tenant_id);
            }

            // Tutorial nur bei deutscher Org starten -- englische Demo-Mandanten
            // sollen direkt im Dashboard landen.
            try {
                const lang = (window.I18N && window.I18N.lang) || 'de';
                if (lang === 'de' && typeof Tutorial !== 'undefined' && Tutorial.init) {
                    Tutorial.init();
                }
            } catch (e) { /* Tutorial optional */ }
        } catch {
            window.location.href = '/';
            return;
        }

        // Event-Listener
        document.getElementById('logout-btn').addEventListener('click', () => this.logout());
        document.getElementById('new-incident-btn').addEventListener('click', () => openModal('modal-new'));
        document.getElementById('new-incident-form').addEventListener('submit', (e) => this.handleFormSubmit(e));
        document.getElementById('refresh-btn').addEventListener('click', () => this.handleRefresh());
        document.getElementById('delete-incident-btn').addEventListener('click', () => this.handleDelete());
        document.getElementById('edit-incident-btn').addEventListener('click', () => this.handleEdit());
        document.getElementById('archive-incident-btn').addEventListener('click', () => this.handleArchive());
        document.getElementById('inc-international').addEventListener('change', () => updateSourcesHint());
        document.getElementById('inc-visibility').addEventListener('change', () => updateVisibilityHint());
    // Telegram-Kategorien Toggle
    const tgCheckbox = document.getElementById('inc-telegram');
    if (tgCheckbox) {
        tgCheckbox.addEventListener('change', () => updateIntervalMin());
    }
    { const xCheckbox = document.getElementById('inc-x');
      if (xCheckbox) xCheckbox.addEventListener('change', () => updateIntervalMin()); }
    { const ivInput = document.getElementById('inc-refresh-value');
      if (ivInput) ivInput.addEventListener('change', () => {
        const u = parseInt(document.getElementById('inc-refresh-unit').value);
        const m = (u === 1) ? _getMinIntervalMinutes() : 1;
        if (isNaN(parseInt(ivInput.value)) || parseInt(ivInput.value) < m) ivInput.value = m;
      }); }


        // Feedback
        document.getElementById('feedback-form').addEventListener('submit', (e) => this.submitFeedback(e));
        document.getElementById('fb-message').addEventListener('input', (e) => {
            document.getElementById('fb-char-count').textContent = e.target.value.length.toLocaleString('de-DE');
        });

        // Sidebar-Chevrons initial auf offen setzen (Archiv geschlossen)
        document.querySelectorAll('.sidebar-chevron').forEach(c => c.classList.add('open'));
        document.getElementById('chevron-archived-incidents').classList.remove('open');

        // Lagen laden (frueh, damit Sidebar sofort sichtbar)
        await this.loadIncidents();

        // Netzwerkanalysen laden

        // Notification-Center initialisieren
        try { await NotificationCenter.init(); } catch (e) { console.warn('NotificationCenter:', e); }

        // WebSocket
        WS.connect();
        WS.on('status_update', (msg) => this.handleStatusUpdate(msg));
        WS.on('refresh_complete', (msg) => this.handleRefreshComplete(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));

        // Laufende Refreshes wiederherstellen
        try {
            const data = await API.getRefreshingIncidents();
            const details = data.details || {};
            const currentTask = data.current;
            const queuedIds = data.queued || [];

            // Restore running refreshes
            if (data.refreshing && data.refreshing.length > 0) {
                data.refreshing.forEach(id => {
                    this._refreshingIncidents.add(id);
                    const d = details[String(id)] || {};
                    const inc = this.incidents.find(i => i.id === id);
                    const isFirst = inc && !inc.has_summary;
                    const isCurrent = (id === currentTask);
                    // Use 'researching' as default step for the actively running task
                    UI.showProgress(isCurrent ? 'researching' : 'queued', { started_at: d.started_at }, id, isFirst);
                });
            }

            // Restore queued incidents
            if (queuedIds.length > 0) {
                queuedIds.forEach((id, idx) => {
                    this._refreshingIncidents.add(id);
                    const inc = this.incidents.find(i => i.id === id);
                    const isFirst = inc && !inc.has_summary;
                    UI.showProgress('queued', { queue_position: idx + 1 }, id, isFirst);
                    // Pipeline-Reset auch nach F5: aktive Lage in Queue -> Icons grau
                    if (id === this.currentIncidentId && typeof Pipeline !== 'undefined' && Pipeline.beginQueue) {
                        Pipeline.beginQueue(id);
                    }
                });
            }

            if (data.refreshing.length > 0 || queuedIds.length > 0) {
                this.renderSidebar();
            }
        } catch (e) { /* Kein kritischer Fehler */ }

        // Heartbeat: periodischer Status-Abgleich als Sicherheitsnetz
        this._statusSyncInterval = setInterval(() => this.syncRefreshStatus(), 60000);

        // Zuletzt ausgewählte Lage wiederherstellen
        const savedId = localStorage.getItem('selectedIncidentId');
        if (savedId) {
            const id = parseInt(savedId, 10);
            if (this.incidents.some(inc => inc.id === id)) {
                await this.selectIncident(id);
            }
        }

        // Leaflet-Karte nachladen falls CDN langsam war
        setTimeout(() => UI.retryPendingMap(), 2000);
    },

    async loadIncidents() {
        try {
            this.incidents = await API.listIncidents();
            this.renderSidebar();
        } catch (err) {
            UI.showToast('Fehler beim Laden der Lagen: ' + err.message, 'error');
        }
    },


    // === Sammelaktionen in der Seitenleiste ===
    // Die Haken sind normalerweise weg. Erst ein Klick auf Archivieren oder
    // Loeschen unten in der Seitenleiste schaltet den Auswahlmodus ein. Das
    // entspricht dem Verhalten im Studio, damit beide Ansichten sich gleich
    // bedienen lassen.
    _bulkMode: null,
    _bulkSel: new Set(),

    startBulkMode(mode) {
        this._bulkMode = mode;
        this._bulkSel.clear();
        this.renderSidebar();
    },

    endBulkMode() {
        this._bulkMode = null;
        this._bulkSel.clear();
        const alle = document.getElementById('sidebar-bulk-all');
        if (alle) alle.checked = false;
        this.renderSidebar();
    },

    toggleBulkSel(id, on) {
        if (on) this._bulkSel.add(id); else this._bulkSel.delete(id);
        this.renderSidebar();
    },

    // Sichtbar heisst: was der aktive Filter der Seitenleiste gerade zeigt.
    _visibleIncidents() {
        let sichtbar = this.incidents || [];
        if (this._sidebarFilter === 'mine') {
            sichtbar = sichtbar.filter(i => i.created_by_username === this._currentUsername);
        }
        return sichtbar;
    },

    selectAllVisible(on) {
        const sichtbar = this._visibleIncidents();
        if (on) sichtbar.forEach(i => this._bulkSel.add(i.id));
        else sichtbar.forEach(i => this._bulkSel.delete(i.id));
        this.renderSidebar();
    },

    _renderBulkBar() {
        const mode = this._bulkMode;
        const tools = document.getElementById('sidebar-bulk-tools');
        const bar = document.getElementById('sidebar-bulk-bar');
        if (tools) tools.hidden = mode !== null;
        if (bar) bar.hidden = mode === null;
        if (mode === null) return;

        const gewaehlt = (this.incidents || []).filter(i => this._bulkSel.has(i.id));
        const n = gewaehlt.length;
        const aktive = gewaehlt.filter(i => i.status === 'active').length;
        const archivierte = n - aktive;

        const zaehler = document.getElementById('sidebar-bulk-count');
        if (zaehler) {
            zaehler.textContent = n === 0
                ? (mode === 'delete' ? 'Lagen zum Löschen anhaken' : 'Lagen zum Archivieren anhaken')
                : (n === 1 ? '1 Lage ausgewählt' : n + ' Lagen ausgewählt');
        }

        // Nur anbieten, was zum Modus und zur Auswahl passt.
        const knopfArchiv = document.getElementById('sidebar-bulk-archive');
        const knopfAktiv = document.getElementById('sidebar-bulk-activate');
        const knopfLoesch = document.getElementById('sidebar-bulk-delete');
        if (knopfArchiv) {
            knopfArchiv.hidden = mode !== 'archive' || aktive === 0;
            const t = knopfArchiv.querySelector('span');
            if (t) t.textContent = 'Archivieren (' + aktive + ')';
        }
        if (knopfAktiv) {
            knopfAktiv.hidden = mode !== 'archive' || archivierte === 0;
            const t = knopfAktiv.querySelector('span');
            if (t) t.textContent = 'Aktivieren (' + archivierte + ')';
        }
        if (knopfLoesch) {
            knopfLoesch.hidden = mode !== 'delete' || n === 0;
            const t = knopfLoesch.querySelector('span');
            if (t) t.textContent = 'Löschen (' + n + ')';
        }

        const sichtbar = this._visibleIncidents();
        const alle = document.getElementById('sidebar-bulk-all');
        if (alle) alle.checked = sichtbar.length > 0 && sichtbar.every(i => this._bulkSel.has(i.id));
    },

    // Die Sammelaktionen laufen nacheinander ueber die normalen Endpunkte, damit
    // deren Rechtepruefung greift. Fehlschlaege werden gezaehlt statt still
    // verschluckt.
    async _bulkRun(ids, fn, verb) {
        let ok = 0;
        const fehlgeschlagen = [];
        for (const id of ids) {
            try { await fn(id); ok++; }
            catch (e) { fehlgeschlagen.push(id); }
        }
        this._bulkSel.clear();
        this._bulkMode = null;
        await this.loadIncidents();
        if (fehlgeschlagen.length) {
            UI.showToast(ok + ' ' + verb + ', ' + fehlgeschlagen.length
                + ' fehlgeschlagen (IDs ' + fehlgeschlagen.join(', ') + ')', 'error');
        } else {
            UI.showToast(ok + ' ' + (ok === 1 ? 'Lage' : 'Lagen') + ' ' + verb, 'success');
        }
    },

    async bulkStatus(status) {
        const ids = (this.incidents || [])
            .filter(i => this._bulkSel.has(i.id)
                && (status === 'archived' ? i.status === 'active' : i.status !== 'active'))
            .map(i => i.id);
        if (!ids.length) return;
        const verb = status === 'archived' ? 'archiviert' : 'wieder aktiviert';
        await this._bulkRun(ids, (id) => API.updateIncident(id, { status }), verb);
    },

    async bulkDelete() {
        const gewaehlt = (this.incidents || []).filter(i => this._bulkSel.has(i.id));
        if (!gewaehlt.length) return;
        const artikel = gewaehlt.reduce((n, i) => n + (i.article_count || 0), 0);
        const namen = gewaehlt.slice(0, 8).map(i => '\n\u2022 ' + i.title).join('')
            + (gewaehlt.length > 8 ? '\n\u2022 … und ' + (gewaehlt.length - 8) + ' weitere' : '');
        const frage = gewaehlt.length + ' ' + (gewaehlt.length === 1 ? 'Lage' : 'Lagen')
            + ' endgültig löschen?' + namen
            + '\n\nDamit gehen ' + artikel + ' Artikel samt Faktenchecks, Lageberichten'
            + ' und Karten verloren. Das lässt sich nicht rückgängig machen.'
            + '\n\nWenn du sie nur aus der Liste nehmen willst, nutze stattdessen Archivieren.';
        if (!confirm(frage)) return;

        const ids = gewaehlt.map(i => i.id);
        const offeneDabei = this.currentIncidentId && ids.includes(this.currentIncidentId);
        await this._bulkRun(ids, (id) => API.deleteIncident(id), 'gelöscht');
        if (offeneDabei) {
            this.currentIncidentId = null;
            const leer = document.getElementById('empty-state');
            const inhalt = document.getElementById('incident-view');
            if (leer) leer.style.display = 'flex';
            if (inhalt) inhalt.style.display = 'none';
        }
    },
    renderSidebar() {
        const activeContainer = document.getElementById('active-incidents');
        const researchContainer = document.getElementById('active-research');
        const archivedContainer = document.getElementById('archived-incidents');

        // Filter-Buttons aktualisieren
        document.querySelectorAll('.sidebar-filter-btn').forEach(btn => {
            const isActive = btn.dataset.filter === this._sidebarFilter;
            btn.classList.toggle('active', isActive);
            btn.setAttribute('aria-pressed', String(isActive));
        });

        // Lagen nach Filter einschränken
        let filtered = this.incidents;
        if (this._sidebarFilter === 'mine') {
            filtered = filtered.filter(i => i.created_by_username === this._currentUsername);
        }

        // 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 _tEmpty = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        const emptyLabelAdhoc = this._sidebarFilter === 'mine'
            ? _tEmpty('sidebar.empty_adhoc_mine', 'Kein eigenes Live-Monitoring')
            : _tEmpty('sidebar.empty_adhoc', 'Kein Live-Monitoring');
        const emptyLabelResearch = this._sidebarFilter === 'mine'
            ? _tEmpty('sidebar.empty_research_mine', 'Keine eigenen Deep-Research')
            : _tEmpty('sidebar.empty_research', 'Keine Deep-Research');

        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();

        // Zustand der Sammelaktionen nachziehen
        this._renderBulkBar();
    },

    setSidebarFilter(filter) {
        this._sidebarFilter = filter;
        this.renderSidebar();
    },

    _announceForSR(text) {
        let el = document.getElementById('sr-announcement');
        if (!el) {
            el = document.createElement('div');
            el.id = 'sr-announcement';
            el.setAttribute('role', 'status');
            el.setAttribute('aria-live', 'polite');
            el.className = 'sr-only';
            document.body.appendChild(el);
        }
        el.textContent = '';
        requestAnimationFrame(() => { el.textContent = text; });
    },

    async selectIncident(id) {
        this.closeRefreshHistory();
        this.currentIncidentId = id;
        localStorage.setItem('selectedIncidentId', id);
        const inc = this.incidents.find(i => i.id === id);
        if (inc) this._announceForSR('Lage ausgewählt: ' + inc.title);
        this.renderSidebar();

        var mc = document.getElementById("main-content");
        mc.scrollTop = 0;

        document.getElementById('empty-state').style.display = 'none';
        document.getElementById('incident-view').style.display = 'flex';

        // GridStack-Animation deaktivieren und Scroll komplett sperren
        // bis alle Tile-Resize-Operationen (doppeltes rAF) abgeschlossen sind
        var gridEl = document.querySelector('.tab-panels');
        if (gridEl) gridEl.classList.remove('grid-stack-animate');
        var scrollLock = function() { mc.scrollTop = 0; };
        mc.addEventListener('scroll', scrollLock);

        // gridstack-Layout initialisieren (einmalig)
        if (typeof LayoutManager !== 'undefined') LayoutManager.init();

        // Refresh-Status fuer diese Lage wiederherstellen
        const isRefreshing = this._refreshingIncidents.has(id);
        this._updateRefreshButton(isRefreshing);
        // Hide any popup/mini from previous incident
        const prevOverlay = document.getElementById('progress-overlay');
        if (prevOverlay) prevOverlay.style.display = 'none';
        if (typeof UI !== 'undefined' && UI._syncHeaderAccess) UI._syncHeaderAccess();
        const prevMini = document.getElementById('progress-mini');
        if (prevMini) prevMini.style.display = 'none';
        const blurTarget = document.getElementById('incident-view');
        // Wenn gerade ein erster Refresh laeuft, Blur stehen lassen statt
        // remove+add im selben Tick — CSS filter:blur greift sonst nicht.
        const _restState = isRefreshing ? UI._progressState[id] : null;
        const _willReBlur = _restState && _restState.isFirst && !_restState.minimized;
        if (blurTarget && !_willReBlur) blurTarget.classList.remove('refresh-blurred');

        if (isRefreshing) {
            const state = UI._progressState[id];
            if (state) {
                // Restore exactly as it was: popup open or minimized
                if (state.minimized) {
                    UI._showMiniProgress(state.step, state);
                } else {
                    UI._showPopupProgress(state.step, {}, state);
                }
                UI._lockActionsIfFirst(state.isFirst);
            } else {
                // No state yet — show popup (first status update will refine)
                UI.showProgress('researching', {}, id, false);
            }
        } else {
            UI._lockActionsIfFirst(false);
        }

// Alte Inhalte sofort leeren um Flackern beim Wechsel zu vermeiden
        var el;
        el = document.getElementById("incident-title"); if (el) el.textContent = "";
        el = document.getElementById("summary-content"); if (el) el.scrollTop = 0;
        el = document.getElementById("summary-text"); if (el) el.innerHTML = "";
        el = document.getElementById("zusammenfassung-text"); if (el) el.innerHTML = "";
        el = document.getElementById("factcheck-filters"); if (el) el.innerHTML = "";
        el = document.querySelector(".factcheck-list"); if (el) el.scrollTop = 0;
        el = document.getElementById("factcheck-list"); if (el) el.innerHTML = "";
        el = document.getElementById("source-overview-content"); if (el) el.innerHTML = "";
        el = document.getElementById("source-overview-header-stats"); if (el) el.textContent = "";
        el = document.getElementById("timeline-entries"); if (el) el.innerHTML = "";
        await this.loadIncidentDetail(id);

        // Scroll-Sperre nach 3 Frames aufheben (nach allen doppelten rAF-Callbacks)
        mc.scrollTop = 0;
        requestAnimationFrame(() => {
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    mc.scrollTop = 0;
                    mc.removeEventListener('scroll', scrollLock);
                    if (gridEl) gridEl.classList.add('grid-stack-animate');
                });
            });
        });



    },

    async loadIncidentDetail(id) {
        try {
            const [incident, articlesResponse, factchecks, snapshots, locationsResponse, sourcesResponse] = await Promise.all([
                API.getIncident(id),
                API.getArticles(id, { limit: 500, offset: 0 }),
                API.getFactChecks(id),
                API.getSnapshots(id),
                API.getLocations(id).catch(() => []),
                API.getIncidentSources(id).catch(() => ({ sources: [] })),
            ]);

            // Sources-Array (ersetzt frueheres incident.sources_json — lazy via /sources-Endpunkt)
            this._currentSources = (sourcesResponse && sourcesResponse.sources) || [];

            // Articles: neue Shape {total, articles} oder alter nackter Array (Rueckwaertskompatibel)
            let articles, articlesTotal;
            if (Array.isArray(articlesResponse)) {
                articles = articlesResponse;
                articlesTotal = articlesResponse.length;
            } else {
                articles = articlesResponse.articles || [];
                articlesTotal = articlesResponse.total || articles.length;
            }

            // Locations-API gibt jetzt {category_labels, locations} oder Array (Rueckwaertskompatibel)
            let locations, categoryLabels;
            if (Array.isArray(locationsResponse)) {
                locations = locationsResponse;
                categoryLabels = null;
            } else if (locationsResponse && locationsResponse.locations) {
                locations = locationsResponse.locations;
                categoryLabels = locationsResponse.category_labels || null;
            } else {
                locations = [];
                categoryLabels = null;
            }

            this._currentArticlesTotal = articlesTotal;
            this._currentArticlesLoaded = articles.length;
            this._currentIncidentIdForLoad = id;

            this.renderIncidentDetail(incident, articles, factchecks, snapshots, locations, categoryLabels);

            // Pipeline an die geladene Lage binden (laedt /api/incidents/{id}/pipeline)
            if (typeof Pipeline !== 'undefined' && Pipeline.bindToIncident) {
                Pipeline.bindToIncident(id).catch(err => console.warn('pipeline-bind:', err));
            }

            // Quellenuebersicht aus Aggregat-Endpunkt (alle Quellen, nicht nur erste Seite)
            this._loadSourcesSummary(id).catch(err => console.warn('sources-summary:', err));

            // Wenn mehr Artikel existieren als initial geladen: progressiver Hintergrund-Load
            if (articlesTotal > articles.length) {
                this._loadRemainingArticlesInBackground(id).catch(err => console.warn('bg-articles:', err));
            }
        } catch (err) {
            console.error('loadIncidentDetail Fehler:', err);
            UI.showToast('Fehler beim Laden: ' + err.message, 'error');
        }
    },

    /** Quellenuebersicht aus Aggregat-Endpunkt nachladen (ersetzt Client-Zaehlung). */
    async _loadSourcesSummary(incidentId) {
        const data = await API.getArticlesSourcesSummary(incidentId);
        if (this.currentIncidentId !== incidentId) return; // User hat gewechselt
        this._currentSourcesSummary = data;
        const soEl = document.getElementById('source-overview-content');
        const statsEl = document.getElementById('source-overview-header-stats');
        if (soEl && typeof UI.renderSourceOverviewFromSummary === 'function') {
            soEl.innerHTML = UI.renderSourceOverviewFromSummary(data);
        }
        if (statsEl && data) {
            statsEl.textContent = `${data.total} Artikel aus ${data.sources.length} Quellen`;
        }
    },

    /** Quellenuebersicht der Lage nach Quellentyp filtern (Web/Telegram/X). */
    filterSourceOverview(type, chipEl) {
        const content = document.getElementById('source-overview-content');
        if (!content) return;
        content.querySelectorAll('.source-type-filter-chip').forEach(c => c.classList.remove('active'));
        if (chipEl) chipEl.classList.add('active');
        // ein offenes Detail-Panel schliessen
        const det = content.querySelector('.source-overview-detail');
        if (det) det.remove();
        content.querySelectorAll('.source-overview-item.active').forEach(it => {
            it.classList.remove('active');
            it.setAttribute('aria-expanded', 'false');
        });
        // Quellen-Boxen nach Typ ein-/ausblenden
        content.querySelectorAll('.source-overview-item').forEach(it => {
            const t = it.dataset.type || 'web';
            it.style.display = (!type || t === type) ? '' : 'none';
        });
    },

    /** Klick auf eine Quellen-Box: Liste der Artikel inline aufklappen (mutual-exclusive). */
    toggleSourceOverviewDetail(el) {
        if (!el) return;
        const grid = el.parentElement;
        if (!grid) return;
        const sourceName = el.dataset.source || '';
        const wasActive = el.classList.contains('active');

        // Alle anderen schliessen + bestehendes Detail entfernen
        grid.querySelectorAll('.source-overview-item.active').forEach(it => {
            it.classList.remove('active');
            it.setAttribute('aria-expanded', 'false');
        });
        const existingDetail = grid.querySelector('.source-overview-detail');
        if (existingDetail) existingDetail.remove();

        // Wenn das geklickte Item bereits aktiv war: nur schliessen
        if (wasActive) return;

        // Neues Detail einfuegen direkt nach dem geklickten Item
        el.classList.add('active');
        el.setAttribute('aria-expanded', 'true');

        const type = this._currentIncidentType;
        const getDate = (a) => (type === 'research' && a.published_at) ? a.published_at : (a.collected_at || a.published_at);
        const articles = (this._currentArticles || [])
            .filter(a => (a.source || 'Unbekannt') === sourceName)
            .sort((a, b) => {
                const ta = new Date(getDate(a) || 0).getTime();
                const tb = new Date(getDate(b) || 0).getTime();
                return tb - ta;
            });

        // Lagebild-Quellennummer pro Artikel ermitteln (matcht Artikel zu sources_json)
        const normalize = (s) => (s || '').toLowerCase().replace(/^(der|die|das)\s+/, '').replace(/\s+/g, ' ').trim();
        const sourcesList = this._currentSources || [];
        const urlToNr = new Map();
        sourcesList.forEach(s => {
            if (s.url && s.nr != null) urlToNr.set(String(s.url).trim(), s.nr);
        });
        const findNr = (a) => {
            // 1) Exakter URL-Match
            if (a.source_url) {
                const exact = urlToNr.get(String(a.source_url).trim());
                if (exact != null) return exact;
            }
            // 2) Fallback: Match via Quellen-Namen (kann mehrfach treffen, nimm erstes)
            if (a.source) {
                const target = normalize(a.source);
                const hit = sourcesList.find(s => s.nr != null && normalize(s.name) === target);
                if (hit) return hit.nr;
            }
            return null;
        };

        const detail = document.createElement('div');
        detail.className = 'source-overview-detail';
        if (articles.length === 0) {
            detail.innerHTML = '<div class="source-overview-detail-empty">Keine Artikel gefunden.</div>';
        } else {
            const fmtDate = (ts) => {
                if (!ts) return '—';
                try {
                    const d = new Date(ts);
                    if (isNaN(d.getTime())) return '—';
                    return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', timeZone: TIMEZONE })
                        + ' '
                        + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
                } catch (e) { return '—'; }
            };
            const items = articles.map(a => {
                const nr = findNr(a);
                const numHtml = nr != null
                    ? `<span class="source-overview-detail-num">[${UI.escape(String(nr))}]</span>`
                    : `<span class="source-overview-detail-num source-overview-detail-num--none" title="Nicht im Lagebild zitiert">—</span>`;
                const dateStr = fmtDate(getDate(a));
                const headline = UI.escape(a.headline_de || a.headline || '(ohne Titel)');
                const inner = a.source_url
                    ? `<a href="${UI.escape(a.source_url)}" target="_blank" rel="noopener">${headline}</a>`
                    : headline;
                return `<li>
                    ${numHtml}
                    <span class="source-overview-detail-date">${UI.escape(dateStr)}</span>
                    <span class="source-overview-detail-headline">${inner}</span>
                </li>`;
            }).join('');
            detail.innerHTML = `<ul class="source-overview-detail-list">${items}</ul>`;
        }
        el.insertAdjacentElement('afterend', detail);
    },

    /** Restliche Artikel seitenweise im Hintergrund nachladen und in _currentArticles mergen. */
    async _loadRemainingArticlesInBackground(incidentId) {
        const BATCH = 500;
        while (this.currentIncidentId === incidentId
               && this._currentArticlesLoaded < this._currentArticlesTotal) {
            let resp;
            try {
                resp = await API.getArticles(incidentId, { limit: BATCH, offset: this._currentArticlesLoaded });
            } catch (err) {
                console.warn('Hintergrund-Load Artikel fehlgeschlagen:', err);
                return;
            }
            if (this.currentIncidentId !== incidentId) return;
            const batch = (resp && resp.articles) ? resp.articles : (Array.isArray(resp) ? resp : []);
            if (!batch.length) break;
            this._currentArticles = (this._currentArticles || []).concat(batch);
            this._currentArticlesLoaded += batch.length;
            this.rerenderTimeline();
            // Kleiner Yield, damit das UI reaktiv bleibt
            await new Promise(r => setTimeout(r, 30));
        }
    },

    renderIncidentDetail(incident, articles, factchecks, snapshots, locations, categoryLabels) {
        // Header Strip
        { const _e = document.getElementById('incident-title'); if (_e) _e.textContent = incident.title; }
        { const _e = document.getElementById('incident-description'); if (_e) _e.textContent = incident.description || ''; }

        // Typ-Badge
        const typeBadge = document.getElementById('incident-type-badge');
        typeBadge.className = 'incident-type-badge ' + (incident.type === 'research' ? 'type-research' : 'type-adhoc');
        typeBadge.textContent = incident.type === 'research' ? 'Analyse' : 'Live';

        // KI-Weg-Kennzeichen. Wird nur angezeigt, wenn die Lage ausdruecklich
        // auf einen Weg gestellt ist. Ohne Angabe gilt die Vorgabe der
        // Organisation, dann bleibt die Kopfzeile bewusst unbeschriftet.
        { const _ai = document.getElementById('incident-ai-badge');
          if (_ai) {
            const _b = incident.ai_backend;
            if (_b === 'bedrock') {
              _ai.hidden = false;
              _ai.className = 'incident-ai-badge ai-eu';
              _ai.textContent = 'EU';
              _ai.title = 'KI-Verarbeitung in der EU (AWS Bedrock, Frankfurt), Recherche ueber den europaeischen Suchindex staan';
            } else if (_b === 'cli') {
              _ai.hidden = false;
              _ai.className = 'incident-ai-badge ai-anthropic';
              _ai.textContent = 'Anthropic';
              _ai.title = 'KI-Verarbeitung ueber Anthropic mit eingebauter Websuche (heutiger Weg)';
            } else {
              _ai.hidden = true;
            }
          } }

        // Kachel-Label: 'Recherchebericht' fuer Recherche-Lagen, 'Lagebild' fuer Live-Monitoring
        const _tI18n = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        const _lbLabel = incident.type === 'research'
            ? _tI18n('tab.summary_report', 'Recherchebericht')
            : _tI18n('card.summary', 'Lagebild');
        const _cardTitle = document.querySelector('#panel-lagebild .card-title');
        if (_cardTitle) _cardTitle.textContent = _lbLabel;
        if (typeof LayoutManager !== 'undefined' && typeof LayoutManager.applyTypeLabels === 'function') {
            LayoutManager.applyTypeLabels(incident.type);
        }
        {
            const _nt = document.querySelector("#inc-notify-summary");
            if (_nt) {
                const _ns = _nt.closest("label")?.querySelector(".toggle-text");
                if (_ns) {
                    _ns.textContent = incident.type === 'research'
                        ? _tI18n('modal.notify.summary_research', 'Neuer Recherchebericht')
                        : _tI18n('modal.notify.summary', 'Neues Lagebild');
                }
            }
        }

        // Archiv-Button Text
        this._updateArchiveButton(incident.status);

        // Ersteller anzeigen
        const creatorEl = document.getElementById('incident-creator');
        if (creatorEl) {
            creatorEl.textContent = (incident.created_by_username || '').split('@')[0];
        }

        // Delete-Button: nur Ersteller darf löschen
        const deleteBtn = document.getElementById('delete-incident-btn');
        const isCreator = incident.created_by_username === this._currentUsername;
        deleteBtn.disabled = !isCreator;
        deleteBtn.title = isCreator ? '' : `Nur ${(incident.created_by_username || '').split('@')[0]} kann diese Lage löschen`;

        // Zusammenfassung-Kachel + Lagebild-Kachel aufteilen
        const zusammenfassungText = document.getElementById('zusammenfassung-text');
        const summaryText = document.getElementById('summary-text');
        const zusammenfassungCard = document.getElementById('zusammenfassung-card');
        const zusammenfassungTitle = zusammenfassungCard ? zusammenfassungCard.querySelector('.card-title') : null;

        if (incident.type === 'research') {
            // Recherche: ZUSAMMENFASSUNG-Sektion aus Briefing extrahieren
            if (zusammenfassungTitle) zusammenfassungTitle.textContent = (typeof T === 'function') ? T('tab.summary_short', 'Zusammenfassung') : 'Zusammenfassung';
            if (incident.summary) {
                const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
                if (zusammenfassung) {
                    if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, this._currentSources);
                    if (zusammenfassungCard) zusammenfassungCard.style.display = '';
                    summaryText.innerHTML = UI.renderSummary(remaining, this._currentSources, incident.type);
                } else {
                    if (zusammenfassungText) zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Zusammenfassung wird beim n\u00e4chsten Refresh generiert.</span>';
                    if (zusammenfassungCard) zusammenfassungCard.style.display = '';
                    summaryText.innerHTML = UI.renderSummary(incident.summary, this._currentSources, incident.type);
                }
            } else {
                if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
                summaryText.innerHTML = '<span style="color:var(--text-disabled);">Noch keine Zusammenfassung. Klicke auf "Aktualisieren" um die Recherche zu starten.</span>';
            }
        } else {
            // Live-Monitoring (adhoc): Kachel zeigt "Neueste Entwicklungen" (max 8 Bullets mit Zeitstempel)
            if (zusammenfassungTitle) zusammenfassungTitle.textContent = (typeof T === 'function') ? T('tab.latest_developments', 'Neueste Entwicklungen') : 'Neueste Entwicklungen';
            if (zusammenfassungCard) zusammenfassungCard.style.display = '';
            const devText = (incident.latest_developments || '').trim();
            if (devText) {
                if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderLatestDevelopments(devText, this._currentSources);
            } else if (zusammenfassungText) {
                zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.</span>';
            }
            if (incident.summary) {
                summaryText.innerHTML = UI.renderSummary(incident.summary, this._currentSources, incident.type);
            } else {
                summaryText.innerHTML = '<span style="color:var(--text-disabled);">Noch kein Lagebild. Klicke auf "Aktualisieren" um die Recherche zu starten.</span>';
            }
        }

        // Meta (im Header-Strip) — relative Zeitangabe mit vollem Datum als Tooltip
        const updated = incident.updated_at ? parseUTC(incident.updated_at) : null;
        const metaUpdated = document.getElementById('meta-updated');
        if (updated) {
            const fullDate = `${updated.toLocaleDateString('de-DE', { timeZone: TIMEZONE })} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })}`;
            metaUpdated.textContent = `Stand: ${App._timeAgo(updated)}`;
            metaUpdated.title = fullDate;
        } else {
            metaUpdated.textContent = '';
            metaUpdated.title = '';
        }

        // Zeitstempel direkt im Lagebild-Card-Header
        const lagebildTs = document.getElementById('lagebild-timestamp');
        if (lagebildTs) {
            lagebildTs.textContent = updated
                ? `Stand: ${updated.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE })} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })} Uhr`
                : '';
        }

        // Öffentliche Stimmung (Foren-Kachel): Tab + Inhalt nur einblenden,
        // wenn fuer diese Lage tatsaechlich Stimmungs-Text vorhanden ist.
        const stimmungTabBtn = document.getElementById('tab-btn-stimmung');
        const stimmungText = document.getElementById('stimmung-text');
        const stimmungTs = document.getElementById('stimmung-timestamp');
        const moodText = (incident.public_mood || '').trim();
        if (moodText && stimmungTabBtn) {
            stimmungTabBtn.style.display = '';
            if (stimmungText) stimmungText.innerHTML = UI.renderPublicMood(moodText);
            if (stimmungTs && incident.public_mood_updated_at) {
                const mUpd = parseUTC(incident.public_mood_updated_at);
                if (mUpd) {
                    stimmungTs.textContent = `Stand: ${mUpd.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE })} ${mUpd.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })} Uhr`;
                }
            }
        } else if (stimmungTabBtn) {
            stimmungTabBtn.style.display = 'none';
            if (stimmungText) stimmungText.innerHTML = '';
        }

        { const _e = document.getElementById('meta-refresh-mode'); if (_e) {
            if (incident.refresh_mode === 'auto' && incident.refresh_start_time) {
                const intervalText = App._formatInterval(incident.refresh_interval);
                _e.textContent = `Auto alle ${intervalText} ab ${incident.refresh_start_time} Uhr`;
            } else if (incident.refresh_mode === 'auto') {
                _e.textContent = `Auto alle ${App._formatInterval(incident.refresh_interval)}`;
            } else {
                _e.textContent = 'Manuell';
            }
        } }

        // International-Badge
        const intlBadge = document.getElementById('intl-badge');
        if (intlBadge) {
            const isIntl = incident.international_sources !== false && incident.international_sources !== 0;
            intlBadge.className = 'intl-badge ' + (isIntl ? 'intl-yes' : 'intl-no');
            intlBadge.textContent = isIntl ? 'International' : 'Nur DE';
        }

        // Faktencheck
        const fcFilters = document.getElementById('fc-filters');
        const factcheckList = document.getElementById('factcheck-list');
        if (factchecks.length > 0) {
            fcFilters.innerHTML = UI.renderFactCheckFilters(factchecks);
            factcheckList.innerHTML = factchecks.map(fc => UI.renderFactCheck(fc)).join('');
        } else {
            fcFilters.innerHTML = '';
            factcheckList.innerHTML = '<div style="padding:12px;font-size:13px;color:var(--text-tertiary);">Noch keine Fakten geprüft</div>';
        }

        // Quellenuebersicht wird aus dem Aggregat-Endpunkt (_loadSourcesSummary) gefuellt,
        // damit sie immer alle Artikel der Lage zeigt — unabhaengig von Paginierung.
        const sourceOverview = document.getElementById('source-overview-content');
        if (sourceOverview) {
            sourceOverview.innerHTML = '<div style="padding:12px;font-size:13px;color:var(--text-tertiary);">Quellenübersicht wird geladen…</div>';
        }
        const _soStats = document.getElementById("source-overview-header-stats");
        if (_soStats) {
            const total = (this._currentArticlesTotal != null) ? this._currentArticlesTotal : articles.length;
            _soStats.textContent = total + " Artikel";
        }

        // Timeline - Artikel + Snapshots zwischenspeichern und rendern
        this._currentArticles = articles;
        this._currentSnapshots = snapshots || [];
        this._snapshotFullCache = new Map();
        this._currentIncidentType = incident.type;

        // Tab-Auswahl: gemerkt pro Lage (localStorage), Default = erster Tab
        if (typeof LayoutManager !== 'undefined' && typeof LayoutManager.restoreTabFor === 'function') {
            LayoutManager.restoreTabFor(incident.id);
        }
        this._timelineFilter = 'all';
        this._timelineRange = 'all';
        this._activeStripWindow = null;
        const _tsEl = document.getElementById('timeline-search'); if (_tsEl) _tsEl.value = '';
        document.querySelectorAll('.ht-filter-btn').forEach(btn => {
            const isActive = btn.dataset.filter === 'all';
            btn.classList.toggle('active', isActive);
            btn.setAttribute('aria-pressed', String(isActive));
        });
        document.querySelectorAll('.ht-range-btn').forEach(btn => {
            const isActive = btn.dataset.range === 'all';
            btn.classList.toggle('active', isActive);
            btn.setAttribute('aria-pressed', String(isActive));
        });
        this.rerenderTimeline();
        this._resizeTimelineTile();

        // Karte rendern
        UI.renderMap(locations || [], categoryLabels);
    },

    _collectEntries(filterType, searchTerm, range) {
        const type = this._currentIncidentType;
        const getArticleDate = (a) => (type === 'research' && a.published_at) ? a.published_at : a.collected_at;

        let entries = [];

        if (filterType === 'all' || filterType === 'articles') {
            let articles = this._currentArticles || [];
            if (searchTerm) {
                articles = articles.filter(a => {
                    const text = `${a.headline || ''} ${a.headline_de || ''} ${a.source || ''} ${a.content_de || ''} ${a.content_original || ''}`.toLowerCase();
                    return text.includes(searchTerm);
                });
            }
            articles.forEach(a => entries.push({ kind: 'article', data: a, timestamp: getArticleDate(a) || '' }));
        }

        if (filterType === 'all' || filterType === 'snapshots') {
            let snapshots = this._currentSnapshots || [];
            if (searchTerm) {
                // Suche erfolgt clientseitig ueber Preview (Snapshots-Liste enthaelt keinen Volltext mehr).
                // Die asynchrone Volltext-Server-Suche wird separat ausgeloest (rerenderTimeline).
                snapshots = snapshots.filter(s => (s.summary_preview || s.summary || '').toLowerCase().includes(searchTerm));
            }
            snapshots.forEach(s => entries.push({ kind: 'snapshot', data: s, timestamp: s.created_at || '' }));
        }

        if (range && range !== 'all') {
            const now = Date.now();
            const cutoff = range === '24h' ? now - 24 * 60 * 60 * 1000 : now - 7 * 24 * 60 * 60 * 1000;
            entries = entries.filter(e => new Date(e.timestamp || 0).getTime() >= cutoff);
        }

        return entries;
    },

    _updateTimelineCount(entries) {
        const articleCount = entries.filter(e => e.kind === 'article').length;
        const snapshotCount = entries.filter(e => e.kind === 'snapshot').length;
        const countEl = document.getElementById('article-count');
        if (!countEl) return;
        if (articleCount > 0 && snapshotCount > 0) {
            countEl.innerHTML = `<span class="ht-legend-dot"></span> ${articleCount} Meldung${articleCount !== 1 ? 'en' : ''} + <span class="ht-legend-dot ht-legend-gold"></span> ${snapshotCount} Lagebericht${snapshotCount !== 1 ? 'e' : ''}`;
        } else if (articleCount > 0) {
            countEl.innerHTML = `<span class="ht-legend-dot"></span> ${articleCount} Meldung${articleCount !== 1 ? 'en' : ''}`;
        } else if (snapshotCount > 0) {
            countEl.innerHTML = `<span class="ht-legend-dot ht-legend-gold"></span> ${snapshotCount} Lagebericht${snapshotCount !== 1 ? 'e' : ''}`;
        } else {
            countEl.textContent = '0 Meldungen';
        }
    },

    debouncedRerenderTimeline() {
        clearTimeout(this._timelineSearchTimer);
        this._timelineSearchTimer = setTimeout(() => this.rerenderTimeline(), 250);
    },

    /** Heatmap-Strip oben + vertikaler Newsfeed-Stream darunter.
     *  Klick auf Heatmap-Balken: Stream filtert auf das Zeitfenster (aktive Balken hervorgehoben).
     */
    rerenderTimeline() {
        const container = document.getElementById('timeline');
        if (!container) return;
        const searchTerm = (document.getElementById('timeline-search')?.value || '').toLowerCase();
        const filterType = this._timelineFilter;
        const range = this._timelineRange;

        let entries = this._collectEntries(filterType, searchTerm, range);
        this._updateTimelineCount(entries);

        // Strip nutzt IMMER alle Eintraege im Range (unabhaengig von Filter/Search/Strip-Window)
        const stripEntries = this._collectEntries('all', '', range);
        stripEntries.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));

        // Wenn ein Heatmap-Balken aktiv ist: Stream zusaetzlich auf dieses Zeitfenster filtern
        const win = this._activeStripWindow;
        if (win && entries.length > 0) {
            entries = entries.filter(e => {
                const ts = new Date(e.timestamp || 0).getTime();
                return ts >= win.start && ts < win.end;
            });
        }

        let html = '<div class="ht-tl">';
        if (stripEntries.length > 0) {
            html += this._renderTimelineStrip(stripEntries);
        }

        // Banner mit aktivem Filter
        if (win) {
            html += `<div class="ht-strip-banner">
                <span class="ht-strip-banner-icon" aria-hidden="true">▼</span>
                <span class="ht-strip-banner-text">Gefiltert auf <strong>${UI.escape(win.label)}</strong> · ${entries.length} Eintr${entries.length === 1 ? 'ag' : 'äge'}</span>
                <button class="ht-strip-banner-close" onclick="App.clearStripWindow()" aria-label="Filter aufheben">Filter aufheben</button>
            </div>`;
        }

        html += '<div class="ht-stream">';
        if (entries.length === 0) {
            html += win
                ? '<div class="ht-empty">Keine Einträge in diesem Zeitfenster.</div>'
                : (searchTerm || range !== 'all')
                    ? '<div class="ht-empty">Keine Einträge im gewählten Zeitraum.</div>'
                    : '<div class="ht-empty">Noch keine Meldungen. Starte eine Recherche mit "Aktualisieren".</div>';
        } else {
            html += this._renderVerticalStream(entries);
        }
        html += '</div>';
        html += '</div>';
        container.innerHTML = html;
    },

    /** Granularitaets-Heuristik fuer den Newsfeed: Stunden bei kurzen Spannen, sonst Tage. */
    _calcGranularity(entries) {
        if (!entries || entries.length < 2) return 'day';
        const ts = entries.map(e => new Date(e.timestamp || 0).getTime()).filter(t => t > 0);
        if (ts.length < 2) return 'day';
        const span = Math.max(...ts) - Math.min(...ts);
        if (span <= 48 * 60 * 60 * 1000) return 'hour';
        return 'day';
    },

    /** Vertikaler Stream: Datums-Trennzeilen + Lagebericht-Sektionen + Meldungen. */
    _renderVerticalStream(entries) {
        if (!entries || entries.length === 0) {
            return '<div class="ht-empty">Keine Einträge.</div>';
        }
        // Neueste oben
        const sorted = [...entries].sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0));
        const granularity = this._calcGranularity(sorted);
        const groups = this._groupByTimePeriod(sorted, granularity);

        let html = '<div class="vt-timeline">';
        groups.forEach(g => {
            const groupId = 'vt-grp-' + g.key.replace(/[^a-z0-9]/gi, '-');
            html += `<div class="vt-time-group" id="${groupId}" data-time-key="${UI.escape(g.key)}">`;
            html += `<div class="vt-time-label"><span class="vt-time-label-text">${UI.escape(g.label)}</span></div>`;
            html += this._renderTimeGroupEntries(g.entries, this._currentIncidentType);
            html += `</div>`;
        });
        html += '</div>';
        return html;
    },

    /* ======= Quanti-Strip ======= */
    _stripGranularity(stripEntries) {
        if (stripEntries.length < 2) return 'day';
        const ts = stripEntries.map(e => new Date(e.timestamp || 0).getTime()).filter(t => t > 0);
        if (ts.length < 2) return 'day';
        const span = Math.max(...ts) - Math.min(...ts);
        const DAY = 86400000;
        if (span <= 2 * DAY) return 'hour';
        if (span <= 60 * DAY) return 'day';
        if (span <= 365 * DAY) return 'week';
        return 'month';
    },

    _buildStripBuckets(stripEntries, granularity) {
        if (stripEntries.length === 0) return [];
        const ts = stripEntries.map(e => new Date(e.timestamp || 0).getTime()).filter(t => t > 0);
        if (ts.length === 0) return [];
        const minTs = Math.min(...ts);
        const maxTs = Math.max(...ts);

        // Bucket-Start fuer minTs ermitteln
        const minDate = new Date(minTs);
        const tzMin = _tz(minDate);
        let firstStart;
        let stepMs;
        if (granularity === 'hour') {
            firstStart = new Date(tzMin.year, tzMin.month, tzMin.date, tzMin.hours).getTime();
            stepMs = 3600000;
        } else if (granularity === 'day') {
            firstStart = new Date(tzMin.year, tzMin.month, tzMin.date).getTime();
            stepMs = 86400000;
        } else if (granularity === 'week') {
            const dow = (minDate.getDay() + 6) % 7; // 0=Mo
            firstStart = new Date(tzMin.year, tzMin.month, tzMin.date - dow).getTime();
            stepMs = 7 * 86400000;
        } else {
            firstStart = new Date(tzMin.year, tzMin.month, 1).getTime();
            stepMs = null; // dynamisch (Monatsgrenzen)
        }

        const buckets = [];
        const fmt = (t) => {
            const d = new Date(t);
            if (granularity === 'hour') return d.toLocaleString('de-DE', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
            if (granularity === 'day') return d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short', timeZone: TIMEZONE });
            if (granularity === 'week') return 'Woche ab ' + d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE });
            return d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric', timeZone: TIMEZONE });
        };

        if (granularity === 'month') {
            let d = new Date(firstStart);
            while (d.getTime() <= maxTs && buckets.length < 240) {
                const start = d.getTime();
                const next = new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime();
                buckets.push({ start, end: next, label: fmt(start), articles: 0, snapshots: 0 });
                d = new Date(next);
            }
        } else {
            for (let t = firstStart; t <= maxTs && buckets.length < 240; t += stepMs) {
                buckets.push({ start: t, end: t + stepMs, label: fmt(t), articles: 0, snapshots: 0 });
            }
        }

        // Eintraege zaehlen
        stripEntries.forEach(e => {
            const ets = new Date(e.timestamp || 0).getTime();
            // Linear-Suche, da Buckets sortiert; bei vielen Buckets ggf. Binary
            for (let i = 0; i < buckets.length; i++) {
                if (ets >= buckets[i].start && ets < buckets[i].end) {
                    if (e.kind === 'article') buckets[i].articles++;
                    else if (e.kind === 'snapshot') buckets[i].snapshots++;
                    break;
                }
            }
        });

        return buckets;
    },

    _renderTimelineStrip(stripEntries) {
        const granularity = this._stripGranularity(stripEntries);
        const buckets = this._buildStripBuckets(stripEntries, granularity);
        if (buckets.length === 0) return '';

        const maxCount = Math.max(1, ...buckets.map(b => b.articles));
        const win = this._activeStripWindow;

        let html = '<div class="ht-strip">';
        html += '<div class="ht-strip-cells">';
        buckets.forEach(b => {
            const intensity = b.articles > 0 ? Math.min(1, b.articles / maxCount) : 0;
            const cls = ['ht-strip-cell'];
            if (b.snapshots > 0) cls.push('has-snapshot');
            if (b.articles === 0 && b.snapshots === 0) cls.push('empty');
            if (win && win.start === b.start && win.end === b.end) cls.push('active');
            const tip = `${b.label}: ${b.articles} Meldung${b.articles === 1 ? '' : 'en'}` +
                (b.snapshots > 0 ? ` + ${b.snapshots} Lagebericht${b.snapshots === 1 ? '' : 'e'}` : '');
            // data-Attribute statt JSON-String im onclick-Inline (vermeidet Quote-Konflikte bei Labels mit Komma/Anführungszeichen)
            html += `<div class="${cls.join(' ')}" style="--intensity:${intensity.toFixed(3)};" title="${UI.escape(tip)}" data-start="${b.start}" data-end="${b.end}" data-label="${UI.escape(b.label || '')}" onclick="App.handleStripClick(this)"></div>`;
        });
        html += '</div>';

        // Wenige Datums-Labels unter dem Strip
        const labelCount = Math.min(buckets.length, 6);
        const stride = Math.max(1, Math.floor(buckets.length / labelCount));
        const labelTexts = [];
        for (let i = 0; i < buckets.length; i += stride) {
            const b = buckets[i];
            const d = new Date(b.start);
            let txt;
            if (granularity === 'hour') txt = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
            else if (granularity === 'day') txt = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE });
            else if (granularity === 'week') txt = 'KW ' + d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE });
            else txt = d.toLocaleDateString('de-DE', { month: 'short', year: '2-digit', timeZone: TIMEZONE });
            labelTexts.push({ text: txt, idx: i });
        }
        if (labelTexts.length) {
            html += '<div class="ht-strip-labels" style="grid-template-columns: repeat(' + buckets.length + ', 1fr);">';
            const seen = new Set(labelTexts.map(l => l.idx));
            for (let i = 0; i < buckets.length; i++) {
                if (seen.has(i)) {
                    const t = labelTexts.find(l => l.idx === i).text;
                    html += `<div class="ht-strip-label">${UI.escape(t)}</div>`;
                } else {
                    html += '<div></div>';
                }
            }
            html += '</div>';
        }
        html += '</div>';
        return html;
    },

    setTimelineFilter(filter) {
        this._timelineFilter = filter;
        this._activeStripWindow = null;
        document.querySelectorAll('.ht-filter-btn').forEach(btn => {
            const isActive = btn.dataset.filter === filter;
            btn.classList.toggle('active', isActive);
            btn.setAttribute('aria-pressed', String(isActive));
        });
        this.rerenderTimeline();
    },

    setTimelineRange(range) {
        this._timelineRange = range;
        this._activeStripWindow = null;
        document.querySelectorAll('.ht-range-btn').forEach(btn => {
            const isActive = btn.dataset.range === range;
            btn.classList.toggle('active', isActive);
            btn.setAttribute('aria-pressed', String(isActive));
        });
        this.rerenderTimeline();
    },

    /** Robuster Click-Handler fuer Heatmap-Cells (vermeidet Quote-Konflikte). */
    handleStripClick(el) {
        if (!el) return;
        const start = parseInt(el.dataset.start, 10);
        const end = parseInt(el.dataset.end, 10);
        const label = el.dataset.label || '';
        if (!isNaN(start) && !isNaN(end)) {
            this.openTimelineWindow(start, end, label);
        }
    },

    /** Klick auf Heatmap-Balken: Stream auf dieses Zeitfenster filtern.
     *  Zweiter Klick auf denselben Balken hebt den Filter auf.
     */
    openTimelineWindow(startMs, endMs, label) {
        const win = this._activeStripWindow;
        if (win && win.start === startMs && win.end === endMs) {
            this._activeStripWindow = null;
        } else {
            this._activeStripWindow = { start: startMs, end: endMs, label: label || '' };
        }
        this.rerenderTimeline();
    },

    /** Strip-Filter aufheben (z.B. via Banner-Button). */
    clearStripWindow() {
        this._activeStripWindow = null;
        this.rerenderTimeline();
    },

    _resizeTimelineTile() {
        // Tab-Modus: Kein internes Resize noetig, Panel waechst mit Inhalt.
        // Wir scrollen lediglich ein offenes Detail in den sichtbaren Bereich.
        requestAnimationFrame(() => { requestAnimationFrame(() => {
            const card = document.querySelector('.timeline-card');
            if (!card) return;
            const cardBottom = card.getBoundingClientRect().bottom;
            const viewBottom = window.innerHeight;
            if (cardBottom > viewBottom) {
                window.scrollBy({ top: cardBottom - viewBottom + 16, behavior: document.documentElement.dataset.a11yMotion ? 'instant' : 'smooth' });
            }
        }); });
    },

    _buildFullVerticalTimeline(filterType, searchTerm) {
        let entries = this._collectEntries(filterType, searchTerm);
        if (entries.length === 0) {
            return '<div class="ht-empty">Keine Einträge.</div>';
        }

        entries.sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0));
        const granularity = this._calcGranularity(entries);
        const groups = this._groupByTimePeriod(entries, granularity);

        let html = '<div class="vt-timeline">';
        groups.forEach(g => {
            html += `<div class="vt-time-group">`;
            html += `<div class="vt-time-label"><span class="vt-time-label-text">${UI.escape(g.label)}</span></div>`;
            html += this._renderTimeGroupEntries(g.entries, this._currentIncidentType);
            html += `</div>`;
        });
        html += '</div>';
        return html;
    },

    /**
     * Einträge nach Zeitperiode gruppieren.
     */
    _groupByTimePeriod(entries, granularity) {
        const np = _tz(new Date());
        const todayKey = `${np.year}-${np.month}-${np.date}`;
        const yp = _tz(new Date(Date.now() - 86400000));
        const yesterdayKey = `${yp.year}-${yp.month}-${yp.date}`;

        const groups = [];
        let currentGroup = null;

        entries.forEach(entry => {
            const d = entry.timestamp ? new Date(entry.timestamp) : null;
            let key, label;

            if (!d || isNaN(d.getTime())) {
                key = 'unknown';
                label = 'Unbekannt';
            } else if (granularity === 'hour') {
                const ep = _tz(d);
                key = `${ep.year}-${ep.month}-${ep.date}-${ep.hours}`;
                label = `${ep.hours.toString().padStart(2, '0')}:00 Uhr`;
            } else {
                const ep = _tz(d);
                key = `${ep.year}-${ep.month}-${ep.date}`;
                if (key === todayKey) {
                    label = 'Heute';
                } else if (key === yesterdayKey) {
                    label = 'Gestern';
                } else {
                    label = d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short', timeZone: TIMEZONE });
                }
            }

            if (!currentGroup || currentGroup.key !== key) {
                currentGroup = { key, label, entries: [] };
                groups.push(currentGroup);
            }
            currentGroup.entries.push(entry);
        });

        return groups;
    },

    /**
     * Entries einer Zeitgruppe rendern, mit Cluster-Erkennung.
     */
    _renderTimeGroupEntries(entries, type) {
        // Cluster-Erkennung: ≥4 Artikel pro Minute
        const minuteCounts = {};
        entries.forEach(e => {
            if (e.kind === 'article') {
                const mk = this._getMinuteKey(e.timestamp);
                minuteCounts[mk] = (minuteCounts[mk] || 0) + 1;
            }
        });

        const minuteRendered = {};
        let html = '';

        entries.forEach(e => {
            if (e.kind === 'snapshot') {
                html += this._renderSnapshotEntry(e.data);
            } else {
                const mk = this._getMinuteKey(e.timestamp);
                const isCluster = minuteCounts[mk] >= 4;
                const isFirstInCluster = isCluster && !minuteRendered[mk];
                if (isFirstInCluster) minuteRendered[mk] = true;
                html += this._renderArticleEntry(e.data, type, isFirstInCluster ? minuteCounts[mk] : 0);
            }
        });

        return html;
    },

    /**
     * Artikel-Eintrag für den Zeitstrahl rendern.
     */
    _renderArticleEntry(article, type, clusterCount) {
        const dateField = (type === 'research' && article.published_at)
            ? article.published_at : article.collected_at;
        const time = dateField
            ? (parseUTC(dateField) || new Date(dateField)).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })
            : '--:--';

        const headline = article.headline_de || article.headline;
        const sourceUrl = article.source_url
            ? `<a href="${UI.escape(article.source_url)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">${UI.escape(article.source)}</a>`
            : UI.escape(article.source);

        const langBadge = article.language && article.language !== 'de'
            ? `<span class="lang-badge">${article.language.toUpperCase()}</span>` : '';

        const clusterBadge = clusterCount > 0
            ? `<span class="vt-cluster-count">${clusterCount}</span>` : '';

        const content = article.content_de || article.content_original || '';
        const hasContent = content.length > 0;

        let detailHtml = '';
        if (hasContent) {
            const truncated = content.length > 400 ? content.substring(0, 400) + '...' : content;
            detailHtml = `<div class="vt-article-detail">
                <div class="vt-article-detail-content">${UI.escape(truncated)}</div>
                ${article.source_url ? `<a href="${UI.escape(article.source_url)}" target="_blank" rel="noopener" class="vt-article-detail-link" onclick="event.stopPropagation()">Artikel öffnen &rarr;</a>` : ''}
            </div>`;
        }

        return `<div class="vt-entry ${hasContent ? 'expandable' : ''}" ${hasContent ? 'onclick="App.toggleTimelineEntry(this)"' : ''}>
            <div class="vt-article-header">
                <span class="vt-article-time">${time}</span>
                <span class="vt-article-source">${sourceUrl}</span>
                ${langBadge}${clusterBadge}
            </div>
            <div class="vt-article-headline">${UI.escape(headline)}</div>
            ${detailHtml}
        </div>`;
    },

    /**
     * Snapshot/Lagebericht-Eintrag für den Zeitstrahl rendern.
     * Volltext + sources_json werden erst beim Aufklappen lazy nachgeladen.
     */
    _renderSnapshotEntry(snapshot) {
        const time = snapshot.created_at
            ? (parseUTC(snapshot.created_at) || new Date(snapshot.created_at)).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })
            : '--:--';

        const stats = [];
        if (snapshot.article_count) stats.push(`${snapshot.article_count} Artikel`);
        if (snapshot.fact_check_count) stats.push(`${snapshot.fact_check_count} Fakten`);
        const statsText = stats.join(', ');

        // Vorschau: erste 200 Zeichen aus summary_preview (vom Server gekuerzt) oder Fallback summary
        const previewText = snapshot.summary_preview || snapshot.summary || '';
        const preview = previewText.length > 200 ? previewText.substring(0, 200) + '...' : previewText;

        // Volltext aus Cache (falls bereits geladen), sonst Platzhalter fuer Lazy-Load
        const cached = this._snapshotFullCache && this._snapshotFullCache.get(snapshot.id);
        const detailHtml = cached
            ? UI.renderSummary(cached.summary, cached.sources_json, this._currentIncidentType)
            : '<div class="vt-snapshot-loading">Lagebericht wird geladen…</div>';
        const loadedAttr = cached ? ' data-loaded="yes"' : '';

        return `<div class="vt-entry vt-snapshot expandable" data-snapshot-id="${snapshot.id}"${loadedAttr} onclick="App.toggleTimelineEntry(this)">
            <div class="vt-snapshot-header">
                <span class="vt-snapshot-badge">Lagebericht</span>
                <span class="vt-snapshot-time">${time}</span>
                <span class="vt-snapshot-stats">${UI.escape(statsText)}</span>
            </div>
            <div class="vt-snapshot-preview">${UI.escape(preview)}</div>
            <div class="vt-snapshot-detail">${detailHtml}</div>
        </div>`;
    },

    /**
     * Volltext eines Snapshots bei Bedarf nachladen und in das DOM einsetzen.
     * Ergebnis wird in _snapshotFullCache gecacht.
     */
    async lazyLoadSnapshotDetail(el) {
        if (!el || el.dataset.loaded === 'yes' || el.dataset.loaded === 'loading') return;
        const snapId = parseInt(el.dataset.snapshotId || '0', 10);
        if (!snapId || !this.currentIncidentId) return;
        el.dataset.loaded = 'loading';
        try {
            let snap = this._snapshotFullCache.get(snapId);
            if (!snap) {
                snap = await API.getSnapshot(this.currentIncidentId, snapId);
                this._snapshotFullCache.set(snapId, snap);
            }
            const detailEl = el.querySelector('.vt-snapshot-detail');
            if (detailEl) {
                detailEl.innerHTML = UI.renderSummary(snap.summary, snap.sources_json, this._currentIncidentType);
            }
            el.dataset.loaded = 'yes';
            // Nach dem Laden die Timeline-Kachel an neue Hoehe anpassen
            if (el.classList.contains('expanded')) this._resizeTimelineTile();
        } catch (err) {
            console.error('Snapshot-Volltext laden fehlgeschlagen:', err);
            el.dataset.loaded = '';
            const detailEl = el.querySelector('.vt-snapshot-detail');
            if (detailEl) detailEl.innerHTML = '<div class="vt-snapshot-error">Fehler beim Laden des Lageberichts.</div>';
        }
    },

    /**
     * Timeline-Eintrag auf-/zuklappen (mutual-exclusive pro Zeitgruppe).
     */
    toggleTimelineEntry(el) {
        const container = el.closest('.ht-detail-content') || el.closest('.vt-time-group');
        if (container) {
            container.querySelectorAll('.vt-entry.expanded').forEach(item => {
                if (item !== el) item.classList.remove('expanded');
            });
        }
        el.classList.toggle('expanded');
        if (el.classList.contains('expanded')) {
            // Snapshots: Volltext lazy nachladen (nur wenn noch nicht geladen)
            if (el.classList.contains('vt-snapshot') && el.dataset.snapshotId) {
                this.lazyLoadSnapshotDetail(el);
            }
            requestAnimationFrame(() => {
                var scrollParent = el.closest('.ht-detail-content');
                if (scrollParent && el.classList.contains('vt-snapshot')) {
                    scrollParent.scrollTo({ top: 0, behavior: document.documentElement.dataset.a11yMotion ? 'instant' : 'smooth' });
                } else {
                    el.scrollIntoView({ behavior: document.documentElement.dataset.a11yMotion ? 'instant' : 'smooth', block: 'nearest' });
                }
            });
        }
        // Timeline-Kachel an Inhalt anpassen
        this._resizeTimelineTile();
    },

    /**
     * Minutenschlüssel für Cluster-Erkennung.
     */
    _getMinuteKey(timestamp) {
        if (!timestamp) return 'none';
        const d = new Date(timestamp);
        const p = _tz(d);
        return `${p.year}-${p.month}-${p.date}-${p.hours}-${p.minutes}`;
    },

    // === Event Handlers ===

    _getFormData() {
        const value = parseInt(document.getElementById('inc-refresh-value').value) || 30;
        const unit = parseInt(document.getElementById('inc-refresh-unit').value) || 1;
        const interval = Math.max(_getMinIntervalMinutes(), Math.min(10080, value * unit));
        return {
            title: document.getElementById('inc-title').value.trim(),
            description: document.getElementById('inc-description').value.trim() || null,
            type: document.getElementById('inc-type').value,
            refresh_mode: document.getElementById('inc-refresh-mode').value,
            refresh_interval: interval,
            refresh_start_time: document.getElementById('inc-refresh-mode').value === 'auto'
                ? document.getElementById('inc-refresh-starttime').value || null
                : null,
            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',
            ai_backend: (document.getElementById('inc-ai-backend') || {}).value || '',
        };
    },

    _clearFormErrors(formEl) {
        formEl.querySelectorAll('.form-error').forEach(el => el.remove());
        formEl.querySelectorAll('[aria-invalid]').forEach(el => {
            el.removeAttribute('aria-invalid');
            el.removeAttribute('aria-describedby');
        });
    },

    _showFieldError(field, message) {
        field.setAttribute('aria-invalid', 'true');
        const errorId = field.id + '-error';
        field.setAttribute('aria-describedby', errorId);
        const errorEl = document.createElement('div');
        errorEl.className = 'form-error';
        errorEl.id = errorId;
        errorEl.setAttribute('role', 'alert');
        errorEl.textContent = message;
        field.parentNode.appendChild(errorEl);
    },

    async handleFormSubmit(e) {
        e.preventDefault();
        const submitBtn = document.getElementById('modal-new-submit');
        const form = document.getElementById('new-incident-form');
        this._clearFormErrors(form);

        // Validierung
        const titleField = document.getElementById('inc-title');
        if (!titleField.value.trim()) {
            this._showFieldError(titleField, 'Bitte einen Titel eingeben.');
            titleField.focus();
            return;
        }

        submitBtn.disabled = true;

        try {
            const data = this._getFormData();

            if (this._editingIncidentId) {
                // Edit-Modus: ID sichern bevor closeModal sie löscht
                const editId = this._editingIncidentId;
                await API.updateIncident(editId, data);

                // E-Mail-Subscription speichern
                await API.updateSubscription(editId, {
                    notify_email_summary: document.getElementById('inc-notify-summary').checked,
                    notify_email_new_articles: document.getElementById('inc-notify-new-articles').checked,
                    notify_email_status_change: document.getElementById('inc-notify-status-change').checked,
                });

                closeModal('modal-new');
                await this.loadIncidents();
                await this.loadIncidentDetail(editId);
                UI.showToast((typeof T === 'function' ? T('toast.incident_updated', 'Lage aktualisiert.') : 'Lage aktualisiert.'), 'success');
            } else {
                // Create-Modus
                const incident = await API.createIncident(data);

                // E-Mail-Subscription speichern
                await API.updateSubscription(incident.id, {
                    notify_email_summary: document.getElementById('inc-notify-summary').checked,
                    notify_email_new_articles: document.getElementById('inc-notify-new-articles').checked,
                    notify_email_status_change: document.getElementById('inc-notify-status-change').checked,
                });

                closeModal('modal-new');

                await this.loadIncidents();

                // Refresh-Status VOR selectIncident setzen, damit selectIncident
                // beim Oeffnen sofort Blur + Aktions-Lock setzt (statt sie erst
                // per WebSocket-Nachricht spaeter wieder zu aktivieren — dazwischen
                // war der Fallinhalt kurzzeitig unblurred und klickbar).
                this._refreshingIncidents.add(incident.id);
                UI._progressState[incident.id] = {
                    step: 'queued', isFirst: true, startTime: null, minimized: false,
                };

                await this.selectIncident(incident.id);

                this._updateRefreshButton(true);
                await API.refreshIncident(incident.id);
                UI.showToast(`Lage "${incident.title}" angelegt. Recherche gestartet.`, 'success');
            }
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        } finally {
            submitBtn.disabled = false;
            this._editingIncidentId = null;
        }
    },

async generateDescription() {
        const title = document.getElementById('inc-title').value.trim();
        const description = document.getElementById('inc-description').value.trim();
        const type = document.getElementById('inc-type').value;
        const btn = document.getElementById('btn-enhance-description');
        const btnText = document.getElementById('enhance-btn-text');
        const spinner = document.getElementById('enhance-spinner');
        const textarea = document.getElementById('inc-description');

        if (title.length < 3 || !btn) return;

        // Vorherigen Request abbrechen falls noch aktiv
        if (this._enhanceController) this._enhanceController.abort();
        this._enhanceController = new AbortController();

        btn.disabled = true;
        btnText.textContent = (typeof T === 'function') ? T('modal.new_incident.enhance_loading', 'Wird generiert...') : 'Wird generiert...';
        spinner.style.display = '';
        textarea.readOnly = true;
        textarea.classList.add('textarea--loading');

        try {
            const result = await API.enhanceDescription(title, description || null, type, this._enhanceController.signal);
            textarea.value = result.description;
            _autoResizeTextarea(textarea);
        } catch (err) {
            if (err.name === 'AbortError') {
                // still
            } else {
                let msg = (typeof T === 'function') ? T('enhance.error_default', 'Beschreibung konnte nicht generiert werden') : 'Beschreibung konnte nicht generiert werden';
                if (err.status === 503) msg = (typeof T === 'function') ? T('enhance.error_unavailable', 'KI-Zugang aktuell nicht verfügbar. Bitte Administrator kontaktieren.') : 'KI-Zugang aktuell nicht verfügbar. Bitte Administrator kontaktieren.';
                else if (err.status === 429) msg = (typeof T === 'function') ? T('enhance.error_busy', 'KI ist gerade ausgelastet. Bitte kurz warten und erneut versuchen.') : 'KI ist gerade ausgelastet. Bitte kurz warten und erneut versuchen.';
                else if (err.status === 504) msg = (typeof T === 'function') ? T('enhance.error_timeout', 'KI antwortet gerade nicht. Bitte erneut versuchen.') : 'KI antwortet gerade nicht. Bitte erneut versuchen.';
                else if (err.status === 403) msg = err.detail || 'Zugriff verweigert.';
                UI.showToast(msg, 'error');
            }
        } finally {
            btnText.textContent = (typeof T === 'function') ? T('modal.new_incident.enhance', 'Beschreibung generieren') : 'Beschreibung generieren';
            spinner.style.display = 'none';
            btn.disabled = title.length < 3;
            textarea.readOnly = false;
            textarea.classList.remove('textarea--loading');
            this._enhanceController = null;
        }
    },

async handleRefresh() {
        if (!this.currentIncidentId) return;
        if (this._refreshingIncidents.has(this.currentIncidentId)) {
            UI.showToast('Aktualisierung wurde bereits gestartet und ist in Bearbeitung.', 'info');
            return;
        }
        try {
            this._refreshingIncidents.add(this.currentIncidentId);
            this._updateRefreshButton(true);
            // showProgress called via handleStatusUpdate
            const result = await API.refreshIncident(this.currentIncidentId);
            // Pipeline auf "pending" setzen, damit alte gruene Haekchen nicht
            // faelschlich "schon fertig" suggerieren waehrend die Lage in der Queue steht
            if (typeof Pipeline !== 'undefined' && Pipeline.beginQueue) {
                Pipeline.beginQueue(this.currentIncidentId);
            }
            if (result && result.status === 'skipped') {
                UI.showToast('Aktualisierung ist in der Warteschlange und wird ausgefuehrt, sobald die aktuelle Recherche abgeschlossen ist.', 'info');
            } else {
                UI.showToast((typeof T === 'function' ? T('toast.refresh_started', 'Aktualisierung gestartet.') : 'Aktualisierung gestartet.'), 'success');
                var _inc2 = this.incidents.find(function(i) { return i.id === this.currentIncidentId; }.bind(this));
                UI.showProgress('queued', {}, this.currentIncidentId, _inc2 && !_inc2.has_summary);
            }
        } catch (err) {
            this._refreshingIncidents.delete(this.currentIncidentId);
            this._updateRefreshButton(false);
            UI.hideProgress();
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    _geoparsePolling: null,

    async triggerGeoparse() {
        if (!this.currentIncidentId) return;
        const btn = document.getElementById('geoparse-btn');
        if (btn) { btn.disabled = true; btn.textContent = (typeof T === 'function' ? T('action.starting', 'Wird gestartet...') : 'Wird gestartet...'); }
        try {
            const result = await API.triggerGeoparse(this.currentIncidentId);
            if (result.status === 'done') {
                UI.showToast(result.message, 'info');
                if (btn) { btn.disabled = false; btn.textContent = 'Orte erkennen'; }
                return;
            }
            UI.showToast(result.message, 'info');
            this._pollGeoparse(this.currentIncidentId);
        } catch (err) {
            UI.showToast('Geoparsing fehlgeschlagen: ' + err.message, 'error');
            if (btn) { btn.disabled = false; btn.textContent = 'Orte erkennen'; }
        }
    },

    _pollGeoparse(incidentId) {
        if (this._geoparsePolling) clearInterval(this._geoparsePolling);
        const btn = document.getElementById('geoparse-btn');
        this._geoparsePolling = setInterval(async () => {
            try {
                const st = await API.getGeoparseStatus(incidentId);
                if (st.status === 'running') {
                    if (btn) btn.textContent = `${st.processed}/${st.total} Artikel...`;
                } else {
                    clearInterval(this._geoparsePolling);
                    this._geoparsePolling = null;
                    if (btn) { btn.disabled = false; btn.textContent = 'Orte erkennen'; }
                    if (st.status === 'done' && st.locations > 0) {
                        UI.showToast(`${st.locations} Orte aus ${st.processed} Artikeln erkannt`, 'success');
                        const locResp = await API.getLocations(incidentId).catch(() => []);
                        let locs, catLabels;
                        if (Array.isArray(locResp)) { locs = locResp; catLabels = null; }
                        else if (locResp && locResp.locations) { locs = locResp.locations; catLabels = locResp.category_labels || null; }
                        else { locs = []; catLabels = null; }
                        UI.renderMap(locs, catLabels);
                    } else if (st.status === 'done') {
                        UI.showToast('Keine neuen Orte gefunden', 'info');
                    } else if (st.status === 'error') {
                        UI.showToast('Geoparsing fehlgeschlagen: ' + (st.error || ''), 'error');
                    }
                }
            } catch {
                clearInterval(this._geoparsePolling);
                this._geoparsePolling = null;
                if (btn) { btn.disabled = false; btn.textContent = 'Orte erkennen'; }
            }
        }, 3000);
    },

    _formatInterval(minutes) {
        if (minutes >= 10080 && minutes % 10080 === 0) {
            const w = minutes / 10080;
            return w === 1 ? '1 Woche' : `${w} Wochen`;
        }
        if (minutes >= 1440 && minutes % 1440 === 0) {
            const d = minutes / 1440;
            return d === 1 ? '1 Tag' : `${d} Tage`;
        }
        if (minutes >= 60 && minutes % 60 === 0) {
            const h = minutes / 60;
            return h === 1 ? '1 Stunde' : `${h} Stunden`;
        }
        return `${minutes} Min.`;
    },

    _setIntervalFields(minutes) {
        let value, unit;
        if (minutes >= 10080 && minutes % 10080 === 0) {
            value = minutes / 10080; unit = '10080';
        } else if (minutes >= 1440 && minutes % 1440 === 0) {
            value = minutes / 1440; unit = '1440';
        } else if (minutes >= 60 && minutes % 60 === 0) {
            value = minutes / 60; unit = '60';
        } else {
            value = minutes; unit = '1';
        }
        const input = document.getElementById('inc-refresh-value');
        input.value = value;
        input.min = unit === '1' ? 10 : 1;
        { const _e = document.getElementById('inc-refresh-unit'); if (_e) _e.value = unit; }
    },

    _refreshHistoryOpen: false,

    toggleRefreshHistory() {
        if (this._refreshHistoryOpen) {
            this.closeRefreshHistory();
        } else {
            this._openRefreshHistory();
        }
    },

    async _openRefreshHistory() {
        if (!this.currentIncidentId) return;
        const popover = document.getElementById('refresh-history-popover');
        if (!popover) return;

        this._refreshHistoryOpen = true;
        popover.style.display = 'flex';

        // Lade Refresh-Log
        const list = document.getElementById('refresh-history-list');
        list.innerHTML = '<div style="padding:12px;color:var(--text-disabled);font-size:12px;">Lade...</div>';

        try {
            const logs = await API.getRefreshLog(this.currentIncidentId, 20);
            this._renderRefreshHistory(logs);
        } catch (e) {
            list.innerHTML = '<div style="padding:12px;color:var(--error);font-size:12px;">Fehler beim Laden</div>';
        }

        // Outside-Click Listener
        setTimeout(() => {
            const handler = (e) => {
                if (!popover.contains(e.target) && !e.target.closest('.meta-updated-link')) {
                    this.closeRefreshHistory();
                    document.removeEventListener('click', handler);
                }
            };
            document.addEventListener('click', handler);
            popover._outsideHandler = handler;
        }, 0);
    },

    closeRefreshHistory() {
        this._refreshHistoryOpen = false;
        const popover = document.getElementById('refresh-history-popover');
        if (popover) {
            popover.style.display = 'none';
            if (popover._outsideHandler) {
                document.removeEventListener('click', popover._outsideHandler);
                delete popover._outsideHandler;
            }
        }
    },

    _renderRefreshHistory(logs) {
        const list = document.getElementById('refresh-history-list');
        if (!list) return;

        if (!logs || logs.length === 0) {
            list.innerHTML = '<div style="padding:12px;color:var(--text-disabled);font-size:12px;">Noch keine Refreshes durchgeführt</div>';
            return;
        }

        list.innerHTML = logs.map(log => {
            const started = parseUTC(log.started_at) || new Date(log.started_at);
            const timeStr = started.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', timeZone: TIMEZONE }) + ' ' +
                started.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });

            let detail = '';
            if (log.status === 'completed') {
                detail = `${log.articles_found} Artikel`;
                if (log.duration_seconds != null) {
                    detail += ` in ${this._formatDuration(log.duration_seconds)}`;
                }
            } else if (log.status === 'running') {
                detail = 'Läuft...';
            } else if (log.status === 'error') {
                detail = '';
            }

            const retryInfo = log.retry_count > 0 ? ` (Versuch ${log.retry_count + 1})` : '';
            const errorHtml = log.error_message
                ? `<div class="rh-info-error" title="${log.error_message.replace(/"/g, '&quot;')}">${log.error_message}</div>`
                : '';

            return `<div class="refresh-history-entry">
                <div class="rh-status-dot ${log.status}"></div>
                <div class="rh-info">
                    <div class="rh-info-time">${timeStr}${retryInfo}</div>
                    ${detail ? `<div class="rh-info-detail">${detail}</div>` : ''}
                    ${errorHtml}
                </div>
                <span class="rh-trigger-badge ${log.trigger_type}">${log.trigger_type === 'auto' ? 'Auto' : 'Manuell'}</span>
            </div>`;
        }).join('');
    },

    _formatDuration(seconds) {
        if (seconds == null) return '';
        if (seconds < 60) return `${Math.round(seconds)}s`;
        const m = Math.floor(seconds / 60);
        const s = Math.round(seconds % 60);
        return s > 0 ? `${m}m ${s}s` : `${m}m`;
    },

    _timeAgo(date) {
        if (!date) return '';
        const now = new Date();
        const diff = Math.floor((now - date) / 1000);
        if (diff < 60) return 'gerade eben';
        if (diff < 3600) return `vor ${Math.floor(diff / 60)}m`;
        if (diff < 86400) return `vor ${Math.floor(diff / 3600)}h`;
        return `vor ${Math.floor(diff / 86400)}d`;
    },

    _updateRefreshButton(disabled) {
        const btn = document.getElementById('refresh-btn');
        if (!btn) return;
        const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        // Hard-Stop: Lese-Modus (Budget aufgebraucht / Lizenz abgelaufen) -> immer disabled
        if (this.user && this.user.read_only) {
            btn.disabled = true;
            const reason = this.user.read_only_reason;
            btn.textContent = reason === 'budget_exceeded'
                ? _t('action.budget_exceeded', 'Budget aufgebraucht')
                : _t('action.read_only', 'Nur Lesezugriff');
            btn.title = reason === 'budget_exceeded'
                ? _t('action.budget_exceeded_title', 'Token-Budget aufgebraucht. Bitte Verwaltung kontaktieren.')
                : _t('action.read_only_title', 'Lizenz erlaubt keinen Schreibzugriff');
            return;
        }
        btn.disabled = disabled;
        btn.textContent = disabled
            ? _t('action.refreshing', 'Läuft...')
            : _t('action.refresh', 'Aktualisieren');
        btn.title = '';
    },

    async handleDelete() {
        if (!this.currentIncidentId) return;
        if (!await confirmDialog('Lage wirklich löschen? Alle gesammelten Daten gehen verloren.')) return;

        try {
            await API.deleteIncident(this.currentIncidentId);
            this.currentIncidentId = null;
            if (typeof LayoutManager !== 'undefined') LayoutManager.destroy();
            document.getElementById('incident-view').style.display = 'none';
            document.getElementById('empty-state').style.display = 'flex';
            await this.loadIncidents();
            UI.showToast((typeof T === 'function' ? T('toast.incident_deleted', 'Lage gelöscht.') : 'Lage gelöscht.'), 'success');
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    async handleEdit() {
        if (!this.currentIncidentId) return;
        const incident = this.incidents.find(i => i.id === this.currentIncidentId);
        if (!incident) return;

        this._editingIncidentId = this.currentIncidentId;

        // Formular mit aktuellen Werten füllen
        { const _e = document.getElementById('inc-title'); if (_e) _e.value = incident.title; }
        { const _e = document.getElementById('inc-description'); if (_e) { _e.value = incident.description || ''; _autoResizeTextarea(_e); } }
        { const _b = document.getElementById('btn-enhance-description'); if (_b) _b.disabled = (incident.title || '').trim().length < 3; }
        { const _e = document.getElementById('inc-type'); if (_e) _e.value = incident.type || 'adhoc'; }
        { const _e = document.getElementById('inc-refresh-mode'); if (_e) _e.value = incident.refresh_mode; }
        App._setIntervalFields(incident.refresh_interval);
        { const _e = document.getElementById('inc-refresh-starttime'); if (_e) _e.value = incident.refresh_start_time || '07:00'; }
        { 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'; }
        { const _e = document.getElementById('inc-ai-backend'); if (_e) _e.value = incident.ai_backend || ''; }
        updateVisibilityHint();
        updateSourcesHint();
        toggleTypeDefaults(true);
        toggleRefreshInterval();
        updateIntervalMin();

        // Modal-Titel und Submit ändern
        { const _e = document.getElementById('modal-new-title'); if (_e) _e.textContent = (typeof T === 'function') ? T('modal.new_incident.edit_title', 'Lage bearbeiten') : 'Lage bearbeiten'; }
        { const _e = document.getElementById('modal-new-submit'); if (_e) _e.textContent = (typeof T === 'function') ? T('common.save', 'Speichern') : 'Speichern'; }

        // E-Mail-Subscription laden
        try {
            const sub = await API.getSubscription(this.currentIncidentId);
            { const _e = document.getElementById('inc-notify-summary'); if (_e) _e.checked = !!sub.notify_email_summary; }
            { const _e = document.getElementById('inc-notify-new-articles'); if (_e) _e.checked = !!sub.notify_email_new_articles; }
            { const _e = document.getElementById('inc-notify-status-change'); if (_e) _e.checked = !!sub.notify_email_status_change; }
        } catch (e) {
            { const _e = document.getElementById('inc-notify-summary'); if (_e) _e.checked = false; }
            { const _e = document.getElementById('inc-notify-new-articles'); if (_e) _e.checked = false; }
            { const _e = document.getElementById('inc-notify-status-change'); if (_e) _e.checked = false; }
        }

        openModal('modal-new');
    },

    async handleArchive() {
        if (!this.currentIncidentId) return;
        const incident = this.incidents.find(i => i.id === this.currentIncidentId);
        if (!incident) return;

        const isArchived = incident.status === 'archived';
        const action = isArchived ? 'wiederherstellen' : 'archivieren';

        if (!await confirmDialog(`Lage wirklich ${action}?`)) return;

        try {
            const newStatus = isArchived ? 'active' : 'archived';
            await API.updateIncident(this.currentIncidentId, { status: newStatus });
            await this.loadIncidents();
            await this.loadIncidentDetail(this.currentIncidentId);
            this._updateArchiveButton(newStatus);
            UI.showToast(isArchived ? (typeof T === 'function' ? T('toast.incident_restored', 'Lage wiederhergestellt.') : 'Lage wiederhergestellt.') : (typeof T === 'function' ? T('toast.incident_archived', 'Lage archiviert.') : 'Lage archiviert.'), 'success');
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    _updateSidebarDot(incidentId, mode) {
        const dot = document.getElementById(`dot-${incidentId}`);
        if (!dot) return;
        const incident = this.incidents.find(i => i.id === incidentId);
        const baseClass = incident ? (incident.status === 'active' ? 'active' : 'archived') : 'active';

        if (mode === 'error') {
            dot.className = `incident-dot refresh-error`;
            setTimeout(() => {
                dot.className = `incident-dot ${baseClass}`;
            }, 3000);
        } else if (this._refreshingIncidents.has(incidentId)) {
            dot.className = `incident-dot refreshing`;
        } else {
            dot.className = `incident-dot ${baseClass}`;
        }
    },

    _updateArchiveButton(status) {
        const btn = document.getElementById('archive-incident-btn');
        if (!btn) return;
        const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        btn.textContent = status === 'archived'
            ? _t('action.restore', 'Wiederherstellen')
            : _t('action.archive', 'Archivieren');
    },

    // === WebSocket Handlers ===

    handleStatusUpdate(msg) {
        const status = msg.data.status;
        if (status === 'retrying') {
            if (msg.incident_id === this.currentIncidentId) {
                UI.showProgressError('', true, msg.data.delay || 120, msg.incident_id);
            }
            return;
        }
        if (status !== 'idle') {
            this._refreshingIncidents.add(msg.incident_id);
        }
        this._updateSidebarDot(msg.incident_id);
        // Detect first refresh: no summary means first run
        const inc = this.incidents.find(i => i.id === msg.incident_id);
        const isFirst = inc && !inc.has_summary;
        // Update progress state for ALL incidents (sidebar + popup if current)
        UI.showProgress(status, msg.data, msg.incident_id, isFirst);
        // Re-render sidebar so status is baked into HTML (survives future re-renders)
        this.renderSidebar();
        if (msg.incident_id === this.currentIncidentId) {
            this._updateRefreshButton(status !== 'idle');
        }
    },

    async handleRefreshComplete(msg) {
        this._refreshingIncidents.delete(msg.incident_id);
        this._updateSidebarDot(msg.incident_id);
        UI._removeSidebarRefreshStatus(msg.incident_id);
        delete UI._progressState[msg.incident_id];
        UI._reindexQueuePositions();
        this.renderSidebar();

        if (msg.incident_id === this.currentIncidentId) {
            this._updateRefreshButton(false);
            await this.loadIncidentDetail(msg.incident_id);

            // Progress-Popup nicht sofort ausblenden — auf refresh_summary warten
            this._pendingComplete = msg.incident_id;
            if (this._pendingCompleteTimer) clearTimeout(this._pendingCompleteTimer);
            this._pendingCompleteTimer = setTimeout(() => {
                if (this._pendingComplete === msg.incident_id) {
                    this._pendingComplete = null;
                    UI.hideProgress(msg.incident_id);
                }
            }, 5000);
        }

        await this.loadIncidents();
    },



    handleRefreshSummary(msg) {
        const d = msg.data;
        const title = d.incident_title || 'Lage';

        // Abschluss-Animation auslösen wenn pending
        if (this._pendingComplete === msg.incident_id) {
            if (this._pendingCompleteTimer) {
                clearTimeout(this._pendingCompleteTimer);
                this._pendingCompleteTimer = null;
            }
            this._pendingComplete = null;
            UI.showProgressComplete(d, msg.incident_id);
        }

        // Toast-Text zusammenbauen
        const parts = [];
        if (d.new_articles > 0) {
            parts.push(`${d.new_articles} neue Meldung${d.new_articles !== 1 ? 'en' : ''}`);
        }
        if (d.confirmed_count > 0) {
            parts.push(`${d.confirmed_count} bestätigt`);
        }
        if (d.contradicted_count > 0) {
            parts.push(`${d.contradicted_count} widersprochen`);
        }
        if (d.status_changes && d.status_changes.length > 0) {
            parts.push(`${d.status_changes.length} Statusänderung${d.status_changes.length !== 1 ? 'en' : ''}`);
        }

        const summaryText = parts.length > 0
            ? parts.join(', ')
            : 'Keine neuen Entwicklungen';

        // 1 Toast statt 5-10
        UI.showToast(`Recherche abgeschlossen: ${summaryText}`, 'success', 6000);

        // Ins NotificationCenter eintragen
        NotificationCenter.add({
            incident_id: msg.incident_id,
            title: title,
            text: `Recherche: ${summaryText}`,
            icon: d.contradicted_count > 0 ? 'warning' : 'success',
        });

        // Status-Änderungen als separate Einträge
        if (d.status_changes) {
            d.status_changes.forEach(sc => {
                const oldLabel = this._translateStatus(sc.old_status);
                const newLabel = this._translateStatus(sc.new_status);
                NotificationCenter.add({
                    incident_id: msg.incident_id,
                    title: title,
                    text: `${sc.claim}: ${oldLabel} \u2192 ${newLabel}`,
                    icon: sc.new_status === 'contradicted' || sc.new_status === 'disputed' ? 'error' : 'success',
                });
            });
        }

        // Sidebar-Dot blinken
        const dot = document.getElementById(`dot-${msg.incident_id}`);
        if (dot) {
            dot.classList.add('has-notification');
            setTimeout(() => dot.classList.remove('has-notification'), 10000);
        }
    },

    _translateStatus(status) {
        const map = {
            confirmed: 'Bestätigt',
            established: 'Gesichert',
            unconfirmed: 'Unbestätigt',
            contradicted: 'Widersprüchlich',
            false: 'Widerlegt',
            disputed: 'Umstritten',
            developing: 'In Entwicklung',
            unverified: 'Ungeprüft',
        };
        return map[status] || status;
    },

    handleRefreshError(msg) {
        this._refreshingIncidents.delete(msg.incident_id);
        this._updateSidebarDot(msg.incident_id, 'error');
        UI._removeSidebarRefreshStatus(msg.incident_id);
        delete UI._progressState[msg.incident_id];
        UI._reindexQueuePositions();
        this.renderSidebar();
        if (msg.incident_id === this.currentIncidentId) {
            this._updateRefreshButton(false);
            // Pending-Complete aufräumen
            if (this._pendingCompleteTimer) {
                clearTimeout(this._pendingCompleteTimer);
                this._pendingCompleteTimer = null;
            }
            this._pendingComplete = null;
            UI.showProgressError(msg.data.error, false, 0, msg.incident_id);
        }
        UI.showToast(`Recherche-Fehler: ${msg.data.error}`, 'error');
    },

    handleRefreshCancelled(msg) {
        this._refreshingIncidents.delete(msg.incident_id);
        this._updateSidebarDot(msg.incident_id);
        UI._removeSidebarRefreshStatus(msg.incident_id);
        delete UI._progressState[msg.incident_id];
        UI._reindexQueuePositions();
        this.renderSidebar();
        if (msg.incident_id === this.currentIncidentId) {
            this._updateRefreshButton(false);
            if (this._pendingCompleteTimer) {
                clearTimeout(this._pendingCompleteTimer);
                this._pendingCompleteTimer = null;
            }
            this._pendingComplete = null;
            UI.hideProgress(msg.incident_id);
        }
        UI.showToast((typeof T === 'function' ? T('toast.research_cancelled', 'Recherche abgebrochen.') : 'Recherche abgebrochen.'), 'info');
    },

    /**
     * Gleicht den lokalen Refresh-Status mit dem Server ab.
     * Bereinigt verwaiste Status-Anzeigen, die durch verpasste WebSocket-Nachrichten entstehen.
     */
    async syncRefreshStatus() {
        if (this._refreshingIncidents.size === 0) return;
        try {
            const data = await API.getRefreshingIncidents();
            const serverRefreshing = new Set(data.refreshing || []);
            const serverQueued = new Set(data.queued || []);
            const serverAll = new Set([...serverRefreshing, ...serverQueued]);

            // Finde lokal als refreshing/queued markierte IDs, die serverseitig nicht mehr laufen
            const stale = [];
            this._refreshingIncidents.forEach(id => {
                if (!serverAll.has(id)) stale.push(id);
            });

            if (stale.length > 0) {
                console.log('Status-Sync: Bereinige verwaiste Refreshes:', stale);
                stale.forEach(id => {
                    this._refreshingIncidents.delete(id);
                    this._updateSidebarDot(id);
                    UI._removeSidebarRefreshStatus(id);
                    delete UI._progressState[id];
                    if (id === this.currentIncidentId) {
                        this._updateRefreshButton(false);
                        UI.hideProgress(id);
                    }
                });
                UI._reindexQueuePositions();
                this.renderSidebar();
            }
        } catch (e) {
            // Netzwerkfehler ignorieren, naechster Zyklus probiert erneut
        }
    },

    minimizeProgress() {
        UI.minimizeProgress(this.currentIncidentId);
    },

    openProgressPopup() {
        UI.openProgressPopup(this.currentIncidentId);
    },

    async cancelRefresh() {
        if (!this.currentIncidentId) return;

        // Temporarily hide progress popup so confirm dialog is fully visible
        const progressOverlay = document.getElementById('progress-overlay');
        if (progressOverlay) progressOverlay.style.display = 'none';
        UI._syncHeaderAccess();

        const ok = await confirmDialog((typeof T === 'function' ? T('confirm.cancel_running_research', 'Laufende Recherche abbrechen?') : 'Laufende Recherche abbrechen?'));

        // Restore progress popup if not confirmed
        if (!ok) {
            const state = UI._progressState[this.currentIncidentId];
            if (state && progressOverlay) progressOverlay.style.display = 'flex';
            UI._syncHeaderAccess();
            return;
        }

        // Show cancelling state in popup
        if (progressOverlay) progressOverlay.style.display = 'flex';
        UI._syncHeaderAccess();
        const btn = document.getElementById('progress-cancel-btn');
        if (btn) {
            btn.textContent = (typeof T === 'function' ? T('action.cancelling', 'Wird abgebrochen...') : 'Wird abgebrochen...');
            btn.disabled = true;
        }
        const titleEl = document.getElementById('progress-popup-title');
        if (titleEl) titleEl.textContent = (typeof T === 'function' ? T('action.cancelling', 'Wird abgebrochen...') : 'Wird abgebrochen...');

        try {
            const result = await API.cancelRefresh(this.currentIncidentId);
            if (!result) {
                UI.showToast((typeof T === 'function' ? T('toast.no_active_refresh', 'Kein aktiver Refresh zum Abbrechen gefunden.') : 'Kein aktiver Refresh zum Abbrechen gefunden.'), 'info');
                if (btn) { btn.textContent = 'Abbrechen'; btn.disabled = false; }
                if (titleEl) titleEl.textContent = (typeof T === 'function' ? T('progress.title.refresh', 'Aktualisierung läuft') : 'Aktualisierung läuft');
            }
        } catch (err) {
            UI.showToast('Abbrechen fehlgeschlagen: ' + err.message, 'error');
            if (btn) { btn.textContent = 'Abbrechen'; btn.disabled = false; }
            if (titleEl) titleEl.textContent = 'Aktualisierung l\u00e4uft';
        }
    },

    // === Export ===

    openExportModal() {
        if (!this.currentIncidentId) return;
        openModal('modal-export');
    },

    async submitExport() {
        if (!this.currentIncidentId) return;
        const checked = document.querySelectorAll('input[name="export-section"]:checked');
        const sections = Array.from(checked).map(cb => cb.value);
        if (sections.length === 0) {
            UI.showToast('Bitte mindestens einen Bereich ausw\u00e4hlen.', 'warning');
            return;
        }
        const format = document.querySelector('input[name="export-format"]:checked').value;
        const brandingEl = document.querySelector('input[name="export-branding"]:checked');
        const includeBranding = !brandingEl || brandingEl.value === 'on';
        const ersteller = (document.getElementById('export-ersteller')?.value || '').trim();

        const btn = document.getElementById('export-submit-btn');
        const origText = btn.textContent;
        btn.disabled = true;
        btn.textContent = (typeof T === 'function' ? T('action.creating', 'Wird erstellt...') : 'Wird erstellt...');

        try {
            const response = await API.exportReport(this.currentIncidentId, format, null, sections, includeBranding, ersteller);
            if (!response.ok) {
                const err = await response.json().catch(() => ({}));
                throw new Error(err.detail || 'Fehler ' + response.status);
            }
            const blob = await response.blob();
            const disposition = response.headers.get('Content-Disposition') || '';
            let filename = 'bericht.' + format;
            const match = disposition.match(/filename="?([^"]+)"?/);
            if (match) filename = match[1];
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            closeModal('modal-export');
            UI.showToast((typeof T === 'function' ? T('toast.report_downloaded', 'Bericht heruntergeladen') : 'Bericht heruntergeladen'), 'success');
        } catch (err) {
            UI.showToast('Export fehlgeschlagen: ' + err.message, 'error');
        } finally {
            btn.disabled = false;
            btn.textContent = origText;
        }
    },

    // === Sidebar-Stats ===

    async updateSidebarStats() {
        const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        const lblSources = _t('sidebar.stat.sources_suffix', 'Quellen');
        const lblArticles = _t('sidebar.stat.articles_suffix', 'Artikel');
        try {
            const stats = await API.getSourceStats();
            const srcCount = document.getElementById('stat-sources-count');
            const artCount = document.getElementById('stat-articles-count');
            if (srcCount) srcCount.textContent = `${stats.total_sources} ${lblSources}`;
            if (artCount) artCount.textContent = `${stats.total_articles} ${lblArticles}`;
        } catch {
            // Fallback: aus Lagen berechnen
            const totalArticles = this.incidents.reduce((sum, i) => sum + i.article_count, 0);
            const totalSources = this.incidents.reduce((sum, i) => sum + i.source_count, 0);
            const srcCount = document.getElementById('stat-sources-count');
            const artCount = document.getElementById('stat-articles-count');
            if (srcCount) srcCount.textContent = `${totalSources} ${lblSources}`;
            if (artCount) artCount.textContent = `${totalArticles} ${lblArticles}`;
        }
    },

    // === Soft-Refresh (F5) ===

    async softRefresh() {
        try {
            await this.loadIncidents();
            if (this.currentIncidentId) {
                await this.selectIncident(this.currentIncidentId);
            }
            UI.showToast((typeof T === 'function' ? T('toast.data_updated', 'Daten aktualisiert.') : 'Daten aktualisiert.'), 'success', 2000);
        } catch (err) {
            UI.showToast('Aktualisierung fehlgeschlagen: ' + err.message, 'error');
        }
    },

    // === Feedback ===

    openFeedback() {
        const form = document.getElementById('feedback-form');
        if (form) form.reset();
        const counter = document.getElementById('fb-char-count');
        if (counter) counter.textContent = '0';
        openModal('modal-feedback');
    },

    async submitFeedback(e) {
        e.preventDefault();
        const form = document.getElementById('feedback-form');
        this._clearFormErrors(form);

        const btn = document.getElementById('fb-submit-btn');
        const category = document.getElementById('fb-category').value;
        const msgField = document.getElementById('fb-message');
        const message = msgField.value.trim();

        if (message.length < 10) {
            this._showFieldError(msgField, 'Bitte mindestens 10 Zeichen eingeben.');
            msgField.focus();
            return;
        }

        // Dateien pruefen
        const fileInput = document.getElementById('fb-files');
        const files = fileInput ? Array.from(fileInput.files) : [];
        if (files.length > 3) {
            UI.showToast('Maximal 3 Bilder erlaubt.', 'error');
            return;
        }
        for (const f of files) {
            if (f.size > 5 * 1024 * 1024) {
                UI.showToast('Datei "' + f.name + '" ist groesser als 5 MB.', 'error');
                return;
            }
        }

        btn.disabled = true;
        btn.textContent = (typeof T === 'function' ? T('action.sending', 'Wird gesendet...') : 'Wird gesendet...');
        try {
            const formData = new FormData();
            formData.append('category', category);
            formData.append('message', message);
            for (const f of files) {
                formData.append('files', f);
            }
            await API.sendFeedbackForm(formData);
            closeModal('modal-feedback');
            UI.showToast('Feedback gesendet. Vielen Dank!', 'success');
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        } finally {
            btn.disabled = false;
            btn.textContent = 'Absenden';
        }
    },

    // === Sidebar Sektionen ein-/ausklappen ===

    toggleSidebarSection(sectionId) {
        const list = document.getElementById(sectionId);
        if (!list) return;
        const chevron = document.getElementById('chevron-' + sectionId);
        const isHidden = list.style.display === 'none';
        list.style.display = isHidden ? '' : 'none';
        if (chevron) {
            chevron.classList.toggle('open', isHidden);
        }
        // aria-expanded auf dem Section-Title synchronisieren
        const title = chevron ? chevron.closest('.sidebar-section-title') : null;
        if (title) title.setAttribute('aria-expanded', String(isHidden));
    },

    // === Quellenverwaltung ===

    async openSourceManagement() {
        openModal('modal-sources');
        await this.loadSources();
    },

    async loadSources() {
        try {
            const [sources, stats, myExclusions] = await Promise.all([
                API.listSources(),
                API.getSourceStats(),
                API.getMyExclusions(),
            ]);
            this._allSources = sources;
            this._sourcesOnly = sources.filter(s => s.source_type !== 'excluded');
            this._myExclusions = myExclusions || [];

            this.renderSourceStats(stats);
            this.renderSourceList();
        } catch (err) {
            UI.showToast('Fehler beim Laden der Quellen: ' + err.message, 'error');
        }
    },

    renderSourceStats(stats) {
        const bar = document.getElementById('sources-stats-bar');
        if (!bar) return;

        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>
        `;
    },

    /**
     * Quellen nach Domain gruppiert rendern.
     */
    renderSourceList() {
        const list = document.getElementById('sources-list');
        if (!list) return;

        // Filter anwenden
        const typeFilter = document.getElementById('sources-filter-type')?.value || '';
        const catFilter = document.getElementById('sources-filter-category')?.value || '';
        const politicalFilter = document.getElementById('sources-filter-political')?.value || '';
        const mediaTypeFilter = document.getElementById('sources-filter-mediatype')?.value || '';
        const reliabilityFilter = document.getElementById('sources-filter-reliability')?.value || '';
        const alignmentFilter = document.getElementById('sources-filter-alignment')?.value || '';
        const externFilter = document.getElementById('sources-filter-extern')?.value || '';
        const search = (document.getElementById('sources-search')?.value || '').toLowerCase();

        // Alle Quellen nach Domain gruppieren
        const groups = new Map();
        const excludedDomains = new Set();
        const excludedNotes = {};

        // User-Ausschlüsse sammeln
        this._myExclusions.forEach(e => {
            const domain = (e.domain || '').toLowerCase();
            if (domain) {
                excludedDomains.add(domain);
                excludedNotes[domain] = e.notes || '';
            }
        });

        // Feeds nach Domain gruppieren
        this._sourcesOnly.forEach(s => {
            const domain = (s.domain || '').toLowerCase() || `_single_${s.id}`;
            if (!groups.has(domain)) groups.set(domain, []);
            groups.get(domain).push(s);
        });

        // Ausgeschlossene Domains die keine Feeds haben auch als Gruppe
        this._myExclusions.forEach(e => {
            const domain = (e.domain || '').toLowerCase();
            if (domain && !groups.has(domain)) {
                groups.set(domain, []);
            }
        });

        // Filter auf Gruppen anwenden
        let filteredGroups = [];
        for (const [domain, feeds] of groups) {
            const isExcluded = excludedDomains.has(domain);
            const isGlobal = feeds.some(f => f.is_global);

            // Typ-Filter
            if (typeFilter === 'excluded' && !isExcluded) continue;
            if (typeFilter && typeFilter !== 'excluded') {
                const hasMatchingType = feeds.some(f => f.source_type === typeFilter);
                if (!hasMatchingType) continue;
            }

            // Kategorie-Filter
            if (catFilter) {
                const hasMatchingCat = feeds.some(f => f.category === catFilter);
                if (!hasMatchingCat) continue;
            }

            // Klassifikations-Filter
            if (politicalFilter) {
                if (!feeds.some(f => (f.political_orientation || 'na') === politicalFilter)) continue;
            }
            if (mediaTypeFilter) {
                if (!feeds.some(f => (f.media_type || 'sonstige') === mediaTypeFilter)) continue;
            }
            if (reliabilityFilter) {
                if (!feeds.some(f => (f.reliability || 'na') === reliabilityFilter)) continue;
            }
            if (alignmentFilter) {
                if (!feeds.some(f => Array.isArray(f.alignments) && f.alignments.includes(alignmentFilter))) continue;
            }
            if (externFilter === 'ifcn') {
                if (!feeds.some(f => f.ifcn_signatory)) continue;
            } else if (externFilter === 'eu_disinfo') {
                if (!feeds.some(f => f.eu_disinfo_listed)) continue;
            }

            // Suche
            if (search) {
                const groupText = feeds.map(f =>
                    `${f.name} ${f.domain || ''} ${f.url || ''} ${f.notes || ''}`
                ).join(' ').toLowerCase() + ' ' + domain;
                if (!groupText.includes(search)) continue;
            }

            filteredGroups.push({ domain, feeds, isExcluded, isGlobal });
        }

        if (filteredGroups.length === 0) {
            list.innerHTML = '<div style="padding:24px;text-align:center;font-size:13px;color:var(--text-disabled);">Keine Quellen gefunden</div>';
            return;
        }

        // Sortierung: Aktive zuerst (alphabetisch), dann ausgeschlossene
        filteredGroups.sort((a, b) => {
            if (a.isExcluded !== b.isExcluded) return a.isExcluded ? 1 : -1;
            return a.domain.localeCompare(b.domain);
        });

        list.innerHTML = filteredGroups.map(g =>
            UI.renderSourceGroup(g.domain, g.feeds, g.isExcluded, excludedNotes[g.domain] || '', g.isGlobal)
        ).join('');

        // Erweiterte Gruppen wiederherstellen
        this._expandedGroups.forEach(domain => {
            const feedsEl = list.querySelector(`.source-group-feeds[data-domain="${CSS.escape(domain)}"]`);
            if (feedsEl) {
                feedsEl.classList.add('expanded');
                const header = feedsEl.previousElementSibling;
                if (header) header.classList.add('expanded');
            }
        });
    },

    filterSources() {
        this.renderSourceList();
    },

    /**
     * Domain-Gruppe auf-/zuklappen.
     */
    toggleSourceOverview() {
        const content = document.getElementById('source-overview-content');
        const chevron = document.getElementById('source-overview-chevron');
        if (!content) return;
        const isHidden = content.style.display === 'none';
        content.style.display = isHidden ? '' : 'none';
        if (chevron) {
            chevron.classList.toggle('open', isHidden);
            chevron.title = isHidden ? 'Einklappen' : 'Aufklappen';
        }
        // aria-expanded auf dem Header-Toggle synchronisieren
        const header = chevron ? chevron.closest('[role="button"]') : null;
        if (header) header.setAttribute('aria-expanded', String(isHidden));
        // Tab-Modus: Panel waechst mit Inhalt, kein Resize noetig
    },

    toggleGroup(domain) {
        const list = document.getElementById('sources-list');
        if (!list) return;
        const feedsEl = list.querySelector(`.source-group-feeds[data-domain="${CSS.escape(domain)}"]`);
        if (!feedsEl) return;

        const isExpanded = feedsEl.classList.toggle('expanded');
        const header = feedsEl.previousElementSibling;
        if (header) {
            header.classList.toggle('expanded', isExpanded);
            header.setAttribute('aria-expanded', String(isExpanded));
        }

        if (isExpanded) {
            this._expandedGroups.add(domain);
        } else {
            this._expandedGroups.delete(domain);
        }
    },

    /**
     * Domain ausschließen (aus dem Inline-Formular).
     */
    async blockDomain() {
        const input = document.getElementById('block-domain-input');
        const domain = (input?.value || '').trim();
        if (!domain) {
            UI.showToast('Domain ist erforderlich.', 'warning');
            return;
        }

        const notes = (document.getElementById('block-domain-notes')?.value || '').trim() || null;

        try {
            await API.blockDomain(domain, notes);
            UI.showToast(`${domain} ausgeschlossen.`, 'success');
            this.showBlockDomainDialog(false);
            await this.loadSources();
            this.updateSidebarStats();
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    /**
     * Faktencheck-Filter umschalten.
     */
    toggleFactCheckFilter(status) {
        const checkbox = document.querySelector(`.fc-dropdown-item[data-status="${status}"] input`);
        if (!checkbox) return;
        const isActive = checkbox.checked;

        document.querySelectorAll(`.factcheck-item[data-fc-status="${status}"]`).forEach(el => {
            el.style.display = isActive ? '' : 'none';
        });
    },

    toggleFcDropdown(e) {
        e.stopPropagation();
        const btn = e.target.closest('.fc-dropdown-toggle');
        const menu = btn ? btn.nextElementSibling : document.getElementById('fc-dropdown-menu');
        if (!menu) return;
        const isOpen = menu.classList.toggle('open');
        if (btn) btn.setAttribute('aria-expanded', String(isOpen));
        if (isOpen) {
            const close = (ev) => {
                if (!menu.contains(ev.target)) {
                    menu.classList.remove('open');
                    document.removeEventListener('click', close);
                }
            };
            setTimeout(() => document.addEventListener('click', close), 0);
        }
    },

    filterModalTimeline(searchTerm) {
        const filterBtn = document.querySelector('.ht-modal-filter-btn.active');
        const filterType = filterBtn ? filterBtn.dataset.filter : 'all';
        const body = document.getElementById('content-viewer-body');
        if (!body) return;
        body.innerHTML = this._buildFullVerticalTimeline(filterType, (searchTerm || '').toLowerCase());
    },

    filterModalTimelineType(filterType, btn) {
        document.querySelectorAll('.ht-modal-filter-btn').forEach(b => b.classList.remove('active'));
        if (btn) btn.classList.add('active');
        const searchInput = document.querySelector('#content-viewer-header-extra .timeline-filter-input');
        const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
        const body = document.getElementById('content-viewer-body');
        if (!body) return;
        body.innerHTML = this._buildFullVerticalTimeline(filterType, searchTerm);
    },

    /**
     * Domain direkt ausschließen (aus der Gruppenliste).
     */
    async blockDomainDirect(domain) {
        if (!await confirmDialog(`"${domain}" wirklich ausschließen? Artikel dieser Domain werden bei allen deinen Recherchen ignoriert. Dies betrifft nicht andere Nutzer deiner Organisation.`)) return;

        try {
            await API.blockDomain(domain);
            UI.showToast(`${domain} ausgeschlossen.`, 'success');
            await this.loadSources();
            this.updateSidebarStats();
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    /**
     * Domain-Ausschluss aufheben.
     */
    async unblockDomain(domain) {
        try {
            await API.unblockDomain(domain);
            UI.showToast(`${domain} Ausschluss aufgehoben.`, 'success');
            await this.loadSources();
            this.updateSidebarStats();
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    /**
     * Alle Quellen einer Domain löschen.
     */
    async deleteDomain(domain) {
        if (!await confirmDialog(`Alle Quellen von "${domain}" wirklich löschen?`)) return;

        try {
            await API.deleteDomain(domain);
            UI.showToast(`${domain} gelöscht.`, 'success');
            await this.loadSources();
            this.updateSidebarStats();
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    /**
     * Einzelnen Feed löschen.
     */
    async deleteSingleFeed(sourceId) {
        try {
            await API.deleteSource(sourceId);
            this._allSources = this._allSources.filter(s => s.id !== sourceId);
            this._sourcesOnly = this._sourcesOnly.filter(s => s.id !== sourceId);
            this.renderSourceList();
            this.updateSidebarStats();
            UI.showToast('Feed gelöscht.', 'success');
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    /**
     * "Domain ausschließen" Dialog ein-/ausblenden.
     */
    showBlockDomainDialog(show) {
        const form = document.getElementById('sources-block-form');
        if (!form) return;

        if (show === undefined || show === true) {
            form.style.display = 'block';
            document.getElementById('block-domain-input').value = '';
            document.getElementById('block-domain-notes').value = '';
            // Add-Form ausblenden
            const addForm = document.getElementById('sources-add-form');
            if (addForm) addForm.style.display = 'none';
        } else {
            form.style.display = 'none';
        }
    },

    _discoveredData: null,

    openPdfUpload() {
        const form = document.getElementById("pdf-upload-form");
        if (form) form.reset();
        const err = document.getElementById("pdf-upload-error");
        if (err) { err.style.display = "none"; err.textContent = ""; }
        const prog = document.getElementById("pdf-upload-progress");
        if (prog) prog.style.display = "none";
        openModal("modal-pdf-upload");
        this._bindPdfUploadFormOnce();
    },

    _bindPdfUploadFormOnce() {
        const form = document.getElementById("pdf-upload-form");
        if (!form || form.dataset.bound === "1") return;
        form.dataset.bound = "1";
        form.addEventListener("submit", async (e) => {
            e.preventDefault();
            const errEl = document.getElementById("pdf-upload-error");
            const progEl = document.getElementById("pdf-upload-progress");
            const submitBtn = document.getElementById("pdf-upload-submit");
            errEl.style.display = "none";

            const fileInput = document.getElementById("pdf-upload-file");
            const f = fileInput && fileInput.files && fileInput.files[0];
            if (!f) {
                errEl.textContent = "Bitte eine PDF-Datei auswaehlen.";
                errEl.style.display = "block";
                return;
            }
            if (f.size > 50 * 1024 * 1024) {
                errEl.textContent = "Datei ueberschreitet 50 MB.";
                errEl.style.display = "block";
                return;
            }

            const fd = new FormData();
            fd.append("file", f);
            const nm = (document.getElementById("pdf-upload-name").value || "").trim();
            if (nm) fd.append("name", nm);
            fd.append("category", document.getElementById("pdf-upload-category").value || "sonstige");
            const lng = (document.getElementById("pdf-upload-language").value || "").trim();
            if (lng) fd.append("language", lng);
            const nt = (document.getElementById("pdf-upload-notes").value || "").trim();
            if (nt) fd.append("notes", nt);

            submitBtn.disabled = true;
            progEl.style.display = "block";
            try {
                await API.upload("/sources/upload-pdf", fd);
                closeModal("modal-pdf-upload");
                if (typeof UI !== "undefined" && UI.showToast) {
                    UI.showToast("PDF hochgeladen -- Verarbeitung laeuft im Hintergrund", "success");
                }
                await App.loadSources();
            } catch (err) {
                errEl.textContent = err && err.message ? err.message : "Upload fehlgeschlagen";
                errEl.style.display = "block";
            } finally {
                submitBtn.disabled = false;
                progEl.style.display = "none";
            }
        });
    },

    toggleSourceForm(show) {
        const form = document.getElementById('sources-add-form');
        if (!form) return;

        if (show === undefined) {
            show = form.style.display === 'none';
        }

        form.style.display = show ? 'block' : 'none';

        if (show) {
            this._editingSourceId = null;
            this._discoveredData = null;
            document.getElementById('src-discover-url').value = '';
            document.getElementById('src-discovery-result').style.display = 'none';
            document.getElementById('src-discover-btn').disabled = false;
            document.getElementById('src-discover-btn').textContent = 'Erkennen';
            document.getElementById('src-type-select').value = 'rss_feed';
            // 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';
        }
    },

    async discoverSource() {
        const urlInput = document.getElementById('src-discover-url');
        const urlVal = urlInput.value.trim();

        // Telegram-URLs direkt behandeln (kein Discovery noetig)
        if (urlVal.match(/^(https?:\/\/)?(t\.me|telegram\.me)\//i)) {
            const channelName = urlVal.replace(/^(https?:\/\/)?(t\.me|telegram\.me)\//, '').replace(/\/$/, '');
            const tgUrl = 't.me/' + channelName;
            this._discoveredData = {
                name: '@' + channelName,
                domain: 't.me',
                source_type: 'telegram_channel',
                rss_url: null,
            };
            document.getElementById('src-name').value = '@' + channelName;
            document.getElementById('src-type-select').value = 'telegram_channel';
            document.getElementById('src-type-display').value = 'Telegram';
            document.getElementById('src-domain').value = tgUrl;
            document.getElementById('src-rss-url-group').style.display = 'none';
            document.getElementById('src-discovery-result').style.display = 'block';
            const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
            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');
            return;
        }

        // Prüfen ob Domain ausgeschlossen ist
        const inputDomain = url.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].toLowerCase();
        const isBlocked = inputDomain && this._myExclusions.some(e => (e.domain || '').toLowerCase() === inputDomain);

        if (isBlocked) {
            if (!await confirmDialog(`"${inputDomain}" ist ausgeschlossen. Trotzdem hinzufügen? Der Ausschluss wird dabei aufgehoben.`)) return;
            await API.unblockDomain(inputDomain);
        }

        const btn = document.getElementById('src-discover-btn');
        btn.disabled = true;
        btn.textContent = (typeof T === 'function' ? T('action.searching_feeds', 'Suche Feeds...') : 'Suche Feeds...');

        try {
            const result = await API.discoverMulti(url);

            if (result.fallback_single) {
                this._discoveredData = {
                    name: result.domain,
                    domain: result.domain,
                    category: result.category,
                    source_type: result.total_found > 0 ? 'rss_feed' : 'web_source',
                    rss_url: result.sources.length > 0 ? result.sources[0].url : null,
                };
                if (result.sources.length > 0) {
                    this._discoveredData.name = result.sources[0].name;
                }

                document.getElementById('src-name').value = this._discoveredData.name || '';
                document.getElementById('src-category').value = this._discoveredData.category || 'sonstige';
                document.getElementById('src-domain').value = this._discoveredData.domain || '';
                document.getElementById('src-notes').value = '';

                const typeLabel = this._discoveredData.source_type === 'rss_feed' ? 'RSS-Feed' : this._discoveredData.source_type === 'telegram_channel' ? 'Telegram' : 'Web-Quelle';
                const typeSelect = document.getElementById('src-type-select');
                if (typeSelect) typeSelect.value = this._discoveredData.source_type || 'web_source';
                document.getElementById('src-type-display').value = typeLabel;

                const rssGroup = document.getElementById('src-rss-url-group');
                const rssInput = document.getElementById('src-rss-url');
                if (this._discoveredData.rss_url) {
                    rssInput.value = this._discoveredData.rss_url;
                    rssGroup.style.display = 'block';
                } else {
                    rssInput.value = '';
                    rssGroup.style.display = 'none';
                }

                document.getElementById('src-discovery-result').style.display = 'block';

                if (result.added_count > 0) {
                    UI.showToast(`${result.domain}: Feed wurde automatisch hinzugefügt.`, 'success');
                    this.toggleSourceForm(false);
                    await this.loadSources();
                } else if (result.total_found === 0) {
                    UI.showToast((typeof T === 'function' ? T('toast.no_rss_save_as_web', 'Kein RSS-Feed gefunden. Als Web-Quelle speichern?') : 'Kein RSS-Feed gefunden. Als Web-Quelle speichern?'), 'info');
                } else {
                    UI.showToast('Feed bereits vorhanden.', 'info');
                }
            } else {
                document.getElementById('src-discovery-result').style.display = 'none';

                if (result.added_count > 0) {
                    UI.showToast(`${result.domain}: ${result.added_count} Feeds hinzugefügt` +
                        (result.skipped_count > 0 ? ` (${result.skipped_count} bereits vorhanden)` : ''),
                        'success');
                } else if (result.skipped_count > 0) {
                    UI.showToast(`${result.domain}: Alle ${result.skipped_count} Feeds bereits vorhanden.`, 'info');
                } else {
                    UI.showToast(`${result.domain}: Keine relevanten Feeds gefunden.`, 'info');
                }

                this.toggleSourceForm(false);
                await this.loadSources();
            }
        } catch (err) {
            UI.showToast('Erkennung fehlgeschlagen: ' + err.message, 'error');
        } finally {
            btn.disabled = false;
            btn.textContent = 'Erkennen';
        }
    },

    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' : 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;

        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 = (typeof T === 'function' ? T('action.save_source', 'Quelle speichern') : '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) {
            UI.showToast('Name ist erforderlich. Bitte erst "Erkennen" klicken.', 'warning');
            return;
        }

        const discovered = this._discoveredData || {};
        const data = {
            name,
            source_type: discovered.source_type || 'web_source',
            category: document.getElementById('src-category').value,
            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,
        };

        if (!data.domain && discovered.domain) {
            data.domain = discovered.domain;
        }

        try {
            if (this._editingSourceId) {
                await API.updateSource(this._editingSourceId, data);
                UI.showToast((typeof T === 'function' ? T('toast.source_updated', 'Quelle aktualisiert.') : 'Quelle aktualisiert.'), 'success');
            } else {
                await API.createSource(data);
                UI.showToast((typeof T === 'function' ? T('toast.source_added', 'Quelle hinzugefügt.') : 'Quelle hinzugefügt.'), 'success');
            }

            this.toggleSourceForm(false);
            await this.loadSources();
            this.updateSidebarStats();
        } catch (err) {
            UI.showToast('Fehler: ' + err.message, 'error');
        }
    },

    // --- Global Admin: Org-Switcher (herausnehmbar) ---
    async _initOrgSwitcher(currentTenantId) {
        const section = document.getElementById('org-switcher-section');
        const select = document.getElementById('org-switcher-select');
        if (!section || !select) return;

        try {
            const orgs = await API.listOrganizations();
            if (!orgs || orgs.length < 2) return;

            section.style.display = 'block';
            select.innerHTML = '';
            orgs.forEach(org => {
                const opt = document.createElement('option');
                opt.value = org.id;
                opt.textContent = org.name + (org.is_active ? '' : ' (inaktiv)');
                if (org.id === currentTenantId) opt.selected = true;
                select.appendChild(opt);
            });

            select.addEventListener('change', async () => {
                const orgId = parseInt(select.value, 10);
                if (orgId === currentTenantId) return;
                try {
                    const result = await API.switchOrg(orgId);
                    localStorage.setItem('osint_token', result.access_token);
                    window.location.reload();
                } catch (err) {
                    console.error('Org-Wechsel fehlgeschlagen:', err);
                }
            });
        } catch {
            // Kein Global Admin oder Fehler - Switcher bleibt versteckt
        }
    },

    logout() {
        localStorage.removeItem('osint_token');
        localStorage.removeItem('osint_username');
        this._sessionWarningShown = false;
        WS.disconnect();
        window.location.href = '/';
    },
};

// === Barrierefreier Bestätigungsdialog ===

function confirmDialog(message) {
    return new Promise((resolve) => {
        // Overlay erstellen
        const overlay = document.createElement('div');
        overlay.className = 'modal-overlay active';
        overlay.setAttribute('role', 'alertdialog');
        overlay.setAttribute('aria-modal', 'true');
        overlay.setAttribute('aria-labelledby', 'confirm-dialog-msg');

        const modal = document.createElement('div');
        modal.className = 'modal';
        modal.style.maxWidth = '420px';
        modal.innerHTML = `
            <div class="modal-header">
                <div class="modal-title">Bestätigung</div>
            </div>
            <div class="modal-body" style="padding:16px 24px;">
                <p id="confirm-dialog-msg" style="margin:0;font-size:14px;color:var(--text-primary);line-height:1.5;">${message.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</p>
            </div>
            <div class="modal-footer">
                <button class="btn btn-secondary" id="confirm-cancel">Abbrechen</button>
                <button class="btn btn-primary" id="confirm-ok">Bestätigen</button>
            </div>
        `;
        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        const previousFocus = document.activeElement;

        const cleanup = (result) => {
            releaseFocus(overlay);
            overlay.remove();
            if (previousFocus) previousFocus.focus();
            resolve(result);
        };

        modal.querySelector('#confirm-cancel').addEventListener('click', () => cleanup(false));
        modal.querySelector('#confirm-ok').addEventListener('click', () => cleanup(true));
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) cleanup(false);
        });
        overlay.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') cleanup(false);
        });

        trapFocus(overlay);
    });
}

// === Globale Hilfsfunktionen ===

// --- Focus-Trap für Modals (WCAG 2.4.3) ---
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

function trapFocus(modalEl) {
    const handler = (e) => {
        if (e.key !== 'Tab') return;
        const focusable = Array.from(modalEl.querySelectorAll(FOCUSABLE_SELECTOR)).filter(el => el.offsetParent !== null);
        if (focusable.length === 0) return;
        const first = focusable[0];
        const last = focusable[focusable.length - 1];
        if (e.shiftKey && document.activeElement === first) {
            e.preventDefault();
            last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
            e.preventDefault();
            first.focus();
        }
    };
    modalEl._focusTrapHandler = handler;
    modalEl.addEventListener('keydown', handler);
    // Fokus auf erstes Element setzen
    requestAnimationFrame(() => {
        const focusable = Array.from(modalEl.querySelectorAll(FOCUSABLE_SELECTOR)).filter(el => el.offsetParent !== null);
        if (focusable.length > 0) focusable[0].focus();
    });
}

function releaseFocus(modalEl) {
    if (modalEl._focusTrapHandler) {
        modalEl.removeEventListener('keydown', modalEl._focusTrapHandler);
        delete modalEl._focusTrapHandler;
    }
}

function openModal(id) {
    if (id === 'modal-new' && !App._editingIncidentId) {
        // Create-Modus: Formular zurücksetzen
        document.getElementById('new-incident-form').reset();
        document.getElementById('modal-new-title').textContent = (typeof T === 'function') ? T('modal.new_incident.title2', 'Neue Lage anlegen') : 'Neue Lage anlegen';
        document.getElementById('modal-new-submit').textContent = (typeof T === 'function') ? T('modal.new_incident.submit', 'Lage anlegen') : 'Lage anlegen';
        { const _b = document.getElementById('btn-enhance-description'); if (_b) _b.disabled = true; }
        { const _t = document.getElementById("inc-description"); if (_t) { _t.style.height = ""; _autoResizeTextarea(_t); } }
        // E-Mail-Checkboxen zuruecksetzen
        document.getElementById('inc-notify-summary').checked = false;
        document.getElementById('inc-notify-new-articles').checked = false;
        document.getElementById('inc-notify-status-change').checked = false;
        toggleTypeDefaults();
        toggleRefreshInterval();
        updateIntervalMin();
    }
    const modal = document.getElementById(id);
    modal._previousFocus = document.activeElement;
    modal.classList.add('active');
    trapFocus(modal);
}

function closeModal(id) {
    // Laufenden Beschreibung-generieren-Request abbrechen
    if (id === 'modal-new' && App._enhanceController) {
        App._enhanceController.abort();
        App._enhanceController = null;
        const ta = document.getElementById('inc-description');
        if (ta) { ta.readOnly = false; ta.classList.remove('textarea--loading'); }
    }
    const modal = document.getElementById(id);
    releaseFocus(modal);
    modal.classList.remove('active');
    if (modal._previousFocus) {
        modal._previousFocus.focus();
        delete modal._previousFocus;
    }
    if (id === 'modal-new') {
        App._editingIncidentId = null;
        document.getElementById('modal-new-title').textContent = (typeof T === 'function') ? T('modal.new_incident.title2', 'Neue Lage anlegen') : 'Neue Lage anlegen';
        document.getElementById('modal-new-submit').textContent = (typeof T === 'function') ? T('modal.new_incident.submit', 'Lage anlegen') : 'Lage anlegen';
    }
}

function openContentModal(title, sourceElementId) {
    const source = document.getElementById(sourceElementId);
    if (!source) return;

    document.getElementById('content-viewer-title').textContent = title;
    const body = document.getElementById('content-viewer-body');
    const headerExtra = document.getElementById('content-viewer-header-extra');
    headerExtra.innerHTML = '';

    if (sourceElementId === 'factcheck-list') {
        // Faktencheck: Filter in den Modal-Header, Liste in den Body
        const filters = document.getElementById('fc-filters');
        if (filters && filters.innerHTML.trim()) {
            headerExtra.innerHTML = `<div class="fc-filter-bar">${filters.innerHTML}</div>`;
        }
        body.innerHTML = source.innerHTML;
        // Filter im Modal auf Modal-Items umleiten
        headerExtra.querySelectorAll('.fc-dropdown-item input[type="checkbox"]').forEach(cb => {
            cb.onchange = function() {
                const status = this.closest('.fc-dropdown-item').dataset.status;
                body.querySelectorAll(`.factcheck-item[data-fc-status="${status}"]`).forEach(el => {
                    el.style.display = cb.checked ? '' : 'none';
                });
            };
        });
    } else if (sourceElementId === 'source-overview-content') {
        // Quellenübersicht: Detailansicht mit Suchleiste
        headerExtra.innerHTML = '<input type="text" class="timeline-filter-input" placeholder="Quellen durchsuchen..." oninput="App.filterModalSources(this.value)" style="width:200px;">';
        body.innerHTML = buildDetailedSourceOverview();
    } else if (sourceElementId === 'timeline') {
        // Timeline: Vollständige vertikale Timeline im Modal mit Filter + Suche
        headerExtra.innerHTML = `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
            <div class="ht-filter-group">
                <button class="ht-modal-filter-btn active" data-filter="all" onclick="App.filterModalTimelineType('all', this)">Alle</button>
                <button class="ht-modal-filter-btn" data-filter="articles" onclick="App.filterModalTimelineType('articles', this)">Meldungen</button>
                <button class="ht-modal-filter-btn" data-filter="snapshots" onclick="App.filterModalTimelineType('snapshots', this)">Lageberichte</button>
            </div>
            <input type="text" class="timeline-filter-input" placeholder="Suche..." oninput="App.filterModalTimeline(this.value)" style="width:180px;">
        </div>`;
        body.innerHTML = App._buildFullVerticalTimeline('all', '');
    } else {
        body.innerHTML = source.innerHTML;
    }

    openModal('modal-content-viewer');
}

App.filterModalSources = function(query) {
    const q = query.toLowerCase().trim();
    const details = document.querySelectorAll('#content-viewer-body details');
    details.forEach(d => {
        if (!q) {
            d.style.display = '';
            d.removeAttribute('open');
            return;
        }
        const name = d.querySelector('summary').textContent.toLowerCase();
        // Quellenname oder Artikel-Headlines durchsuchen
        const articles = d.querySelectorAll('div > div');
        let articleMatch = false;
        articles.forEach(a => {
            const text = a.textContent.toLowerCase();
            const hit = text.includes(q);
            a.style.display = hit ? '' : 'none';
            if (hit) articleMatch = true;
        });
        const match = name.includes(q) || articleMatch;
        d.style.display = match ? '' : 'none';
        // Bei Artikeltreffer aufklappen, bei Namens-Match alle Artikel zeigen
        if (match && articleMatch && !name.includes(q)) {
            d.setAttribute('open', '');
        } else if (name.includes(q)) {
            articles.forEach(a => a.style.display = '');
        }
    });
};

function buildDetailedSourceOverview() {
    const articles = App._currentArticles || [];
    if (!articles.length) return '<div style="padding:24px;color:var(--text-disabled);">Keine Artikel vorhanden</div>';

    // Nach Quelle gruppieren
    const sourceMap = {};
    articles.forEach(a => {
        const name = a.source || 'Unbekannt';
        if (!sourceMap[name]) sourceMap[name] = { articles: [], languages: new Set() };
        sourceMap[name].articles.push(a);
        sourceMap[name].languages.add((a.language || 'de').toUpperCase());
    });

    const sources = Object.entries(sourceMap).sort((a, b) => b[1].articles.length - a[1].articles.length);

    // Sprach-Statistik Header
    const langCount = {};
    articles.forEach(a => {
        const lang = (a.language || 'de').toUpperCase();
        langCount[lang] = (langCount[lang] || 0) + 1;
    });
    const langChips = Object.entries(langCount)
        .sort((a, b) => b[1] - a[1])
        .map(([lang, count]) => `<span class="source-lang-chip">${lang} <strong>${count}</strong></span>`)
        .join('');

    let html = `<div class="source-overview-header">
        <span class="source-overview-stat">${articles.length} Artikel aus ${sources.length} Quellen</span>
        <div class="source-lang-chips">${langChips}</div>
    </div>`;

    sources.forEach(([name, data]) => {
        const langs = [...data.languages].join('/');
        const escapedName = UI.escape(name);
        html += `<details style="border:1px solid var(--border);border-radius:4px;margin-bottom:6px;">
            <summary style="display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;background:var(--bg-secondary);color:var(--text-primary);font-size:13px;list-style:none;">
                <span style="color:var(--text-secondary);font-size:12px;flex-shrink:0;">&#9656;</span>
                <span style="flex:1;font-weight:500;">${escapedName}</span>
                <span style="font-size:10px;color:var(--text-secondary);flex-shrink:0;">${langs}</span>
                <span style="font-size:12px;font-weight:700;color:var(--accent);background:var(--tint-accent);padding:1px 6px;border-radius:4px;flex-shrink:0;">${data.articles.length}</span>
            </summary>
            <div style="border-top:1px solid var(--border);">`;
        data.articles.forEach(a => {
            const headline = UI.escape(a.headline_de || a.headline || 'Ohne Titel');
            const time = a.collected_at
                ? (parseUTC(a.collected_at) || new Date(a.collected_at)).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })
                : '';
            const langBadge = a.language && a.language !== 'de'
                ? `<span class="lang-badge">${a.language.toUpperCase()}</span>` : '';
            const link = a.source_url
                ? `<a href="${UI.escape(a.source_url)}" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none;font-size:14px;flex-shrink:0;opacity:0.6;" title="Artikel öffnen">&#8599;</a>` : '';
            html += `<div style="display:flex;align-items:center;gap:10px;padding:8px 14px 8px 36px;font-size:12px;border-bottom:1px solid var(--border);">
                <span style="color:var(--text-secondary);flex-shrink:0;min-width:90px;font-size:11px;">${time}</span>
                <span style="flex:1;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${headline}</span>
                ${langBadge}
                ${link}
            </div>`;
        });
        html += `</div></details>`;
    });

    return html;
}




function toggleRefreshInterval() {
    const mode = document.getElementById('inc-refresh-mode').value;
    const field = document.getElementById('refresh-interval-field');
    const startField = document.getElementById('refresh-starttime-field');
    field.classList.toggle('visible', mode === 'auto');
    if (startField) startField.classList.toggle('visible', mode === 'auto');
}

function _getMinIntervalMinutes() {
    // Mindest-Intervall (Minuten) je nach Quellen: 30 Basis, 45 bei X oder Telegram, 60 bei beiden. International zaehlt nicht.
    const tg = document.getElementById('inc-telegram');
    const x = document.getElementById('inc-x');
    const tgOn = !!(tg && tg.checked);
    const xOn = !!(x && x.checked);
    if (tgOn && xOn) return 60;
    if (tgOn || xOn) return 45;
    return 30;
}

function updateIntervalMin() {
    const unit = parseInt(document.getElementById('inc-refresh-unit').value);
    const input = document.getElementById('inc-refresh-value');
    const minMinutes = _getMinIntervalMinutes();
    const hint = document.getElementById('interval-min-hint');
    if (unit === 1) {
        // Minuten: dynamisches Minimum (30 / 45 bei X oder Telegram / 60 bei beiden)
        input.min = minMinutes;
        if (isNaN(parseInt(input.value)) || parseInt(input.value) < minMinutes) input.value = minMinutes;
        if (hint) {
            let zusatz = '';
            if (minMinutes === 45) zusatz = ' (X oder Telegram aktiv)';
            else if (minMinutes === 60) zusatz = ' (X und Telegram aktiv)';
            hint.textContent = 'Mindestens ' + minMinutes + ' Minuten' + zusatz;
            hint.style.display = '';
        }
    } else {
        // Stunden/Tage/Wochen: eine Einheit liegt ueber jedem Minuten-Minimum
        input.min = 1;
        if (isNaN(parseInt(input.value)) || parseInt(input.value) < 1) input.value = 1;
        if (hint) hint.style.display = 'none';
    }
}

function updateVisibilityHint() {
    const isPublic = document.getElementById('inc-visibility').checked;
    const text = document.getElementById('visibility-text');
    if (text) {
        const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
        text.textContent = isPublic
            ? _t('modal.toggle.visibility_public_text', 'Öffentlich — für alle Nutzer sichtbar')
            : _t('modal.toggle.visibility_private_text', 'Privat — nur für dich sichtbar');
    }
}

function updateSourcesHint() {
    const intl = document.getElementById('inc-international').checked;
    const hint = document.getElementById('sources-hint');
    if (hint) {
        hint.textContent = intl
            ? 'DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)'
            : (typeof T === 'function' ? T('modal.hint.sources_german_only', 'Nur deutschsprachige Quellen (DE, AT, CH)') : 'Nur deutschsprachige Quellen (DE, AT, CH)');
    }
}

function toggleTypeDefaults(preserveMode = false) {
    const type = document.getElementById('inc-type').value;
    const hint = document.getElementById('type-hint');
    const refreshMode = document.getElementById('inc-refresh-mode');

    const _t = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
    if (type === 'research') {
        hint.textContent = _t('modal.hint.type_research', 'Recherchiert in Tiefe: Nachrichtenarchive, Parlamentsdokumente, Fachmedien, Expertenquellen. Empfohlen: Manuell starten und bei Bedarf vertiefen.');
        // Nur bei Typ-Wechsel/Neuanlage Modus zurückziehen, beim Edit bestehender Lagen DB-Wert respektieren
        if (!preserveMode) {
            refreshMode.value = 'manual';
            toggleRefreshInterval();
        }
    } else {
        hint.textContent = _t('modal.hint.type_adhoc', 'Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.');
    }

    // Beschreibungs-Tooltip je nach Typ wechseln
    const descIcon = document.getElementById('description-info-icon');
    if (descIcon) {
        descIcon.setAttribute('data-tooltip', type === 'research'
            ? 'Nenne das vollst\u00e4ndige Thema, gew\u00fcnschte Schwerpunkte und relevante URLs.\nBeispiel: "Muster GmbH: Fokus auf F\u00fchrungspersonen, Kontroversen, Finanzkennzahlen"'
            : 'Beschreibe den Vorfall m\u00f6glichst genau: Was ist passiert? Wo? Wer ist beteiligt?\nJe pr\u00e4ziser, desto bessere Ergebnisse.');
    }
}

// Tab-Fokus: Nur Tab-Badge (Titel-Counter) zurücksetzen, nicht alle Notifications
window.addEventListener('focus', () => {
    document.title = App._originalTitle;
});

// ESC schließt Modals
// F5: Daten aktualisieren statt Seite neu laden
document.addEventListener('keydown', (e) => {
    if (e.key === 'F5') {
        e.preventDefault();
        App.softRefresh();
    }
});

document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
        // Schließ-Reihenfolge: A11y-Panel > Notification-Panel > Export-Dropdown > FC-Dropdown > Modals
        if (A11yManager._isOpen) {
            A11yManager._closePanel();
            return;
        }
        if (NotificationCenter._isOpen) {
            NotificationCenter.close();
            return;
        }

        const fcMenu = document.querySelector('.fc-dropdown-menu.open');
        if (fcMenu) {
            fcMenu.classList.remove('open');
            const fcBtn = fcMenu.previousElementSibling;
            if (fcBtn) fcBtn.setAttribute('aria-expanded', 'false');
            return;
        }
        document.querySelectorAll('.modal-overlay.active').forEach(m => {
            closeModal(m.id);
        });
    }
});

// Keyboard-Handler: Enter/Space auf [role="button"] löst click aus (WCAG 2.1.1)
document.addEventListener('keydown', (e) => {
    if ((e.key === 'Enter' || e.key === ' ') && e.target.matches('[role="button"]')) {
        e.preventDefault();
        e.target.click();
    }
});

// Session-Ablauf prüfen (alle 60 Sekunden)
setInterval(() => {
    const token = localStorage.getItem('osint_token');
    if (!token) return;
    try {
        const payload = JSON.parse(atob(token.split('.')[1]));
        const expiresAt = payload.exp * 1000;
        const remaining = expiresAt - Date.now();
        const fiveMinutes = 5 * 60 * 1000;
        if (remaining <= 0) {
            App.logout();
        } else if (remaining <= fiveMinutes && !App._sessionWarningShown) {
            App._sessionWarningShown = true;
            const mins = Math.ceil(remaining / 60000);
            UI.showToast(`Session läuft in ${mins} Minute${mins !== 1 ? 'n' : ''} ab. Bitte erneut anmelden.`, 'warning', 15000);
        }
    } catch (e) { /* Token nicht parsbar */ }
}, 60000);

// Modal-Overlays: Klick auf Backdrop schließt NICHT mehr (nur X-Button)
document.addEventListener('click', (e) => {
    if (e.target.classList.contains('modal-overlay') && e.target.classList.contains('active')) {
        // closeModal deaktiviert - Modal nur ueber X-Button schliessbar
    }
});

// App starten
document.addEventListener('click', (e) => {

});
document.addEventListener('DOMContentLoaded', () => App.init());


// Auto-Resize fuer Textarea
function _autoResizeTextarea(el) {
    if (!el) return;
    el.style.height = 'auto';
    el.style.height = Math.max(80, el.scrollHeight) + 'px';
}

// Titel-Input: Button aktivieren + Textarea Auto-Resize
document.addEventListener('DOMContentLoaded', () => {
    const titleInput = document.getElementById('inc-title');
    if (titleInput) {
        titleInput.addEventListener('input', function() {
            const btn = document.getElementById('btn-enhance-description');
            if (btn) btn.disabled = this.value.trim().length < 3;
        });
    }
    const descInput = document.getElementById('inc-description');
    if (descInput) {
        descInput.addEventListener('input', function() { _autoResizeTextarea(this); });
    }
});
