-
-
+
+
+
+
+
+
+
+
+
+
Noch keine Fakten geprüft
-
@@ -646,7 +620,6 @@
-
diff --git a/src/static/js/app.js b/src/static/js/app.js
index 95c9344..2040592 100644
--- a/src/static/js/app.js
+++ b/src/static/js/app.js
@@ -716,7 +716,7 @@ const App = {
// GridStack-Animation deaktivieren und Scroll komplett sperren
// bis alle Tile-Resize-Operationen (doppeltes rAF) abgeschlossen sind
- var gridEl = document.querySelector('.grid-stack');
+ var gridEl = document.querySelector('.tab-panels');
if (gridEl) gridEl.classList.remove('grid-stack-animate');
var scrollLock = function() { mc.scrollTop = 0; };
mc.addEventListener('scroll', scrollLock);
@@ -732,7 +732,7 @@ const App = {
if (prevOverlay) prevOverlay.style.display = 'none';
const prevMini = document.getElementById('progress-mini');
if (prevMini) prevMini.style.display = 'none';
- const grid = document.querySelector('.grid-stack');
+ const grid = document.querySelector('.tab-panels');
if (grid) grid.classList.remove('blurred');
if (isRefreshing) {
@@ -825,10 +825,11 @@ const App = {
// Kachel-Label: 'Recherchebericht' fuer Recherche-Lagen, 'Lagebild' fuer Live-Monitoring
const _lbLabel = incident.type === 'research' ? 'Recherchebericht' : 'Lagebild';
- const _cardTitle = document.querySelector('[gs-id="lagebild"] .card-title');
- if (_cardTitle) { _cardTitle.textContent = _lbLabel; _cardTitle.setAttribute("onclick", "openContentModal('" + _lbLabel + "', 'summary-content')"); }
- const _toggleBtn = document.querySelector('.layout-toggle-btn[data-tile="lagebild"]');
- if (_toggleBtn) _toggleBtn.textContent = _lbLabel;
+ 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 = "Neues " + _lbLabel; } }
// Archiv-Button Text
@@ -855,7 +856,6 @@ const App = {
if (incident.type === 'research') {
// Recherche: ZUSAMMENFASSUNG-Sektion aus Briefing extrahieren
if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Zusammenfassung';
- if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Zusammenfassung', 'zusammenfassung-content')");
if (incident.summary) {
const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
if (zusammenfassung) {
@@ -874,7 +874,6 @@ const App = {
} else {
// Live-Monitoring (adhoc): Kachel zeigt "Neueste Entwicklungen" (max 8 Bullets mit Zeitstempel)
if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Neueste Entwicklungen';
- if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Neueste Entwicklungen', 'zusammenfassung-content')");
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
const devText = (incident.latest_developments || '').trim();
if (devText) {
@@ -949,30 +948,18 @@ const App = {
const _soSources = new Set(articles.map(a => a.source).filter(Boolean));
_soStats.textContent = articles.length + " Artikel aus " + _soSources.size + " Quellen";
}
- // Kachel an Inhalt anpassen
- if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) {
- if (sourceOverview.style.display !== 'none') {
- // Offen → an Inhalt anpassen
- requestAnimationFrame(() => requestAnimationFrame(() => {
- LayoutManager.resizeTileToContent('quellen');
- }));
- } else {
- // Geschlossen → einheitliche Default-Höhe
- const defaults = LayoutManager.DEFAULT_LAYOUT.find(d => d.id === 'quellen');
- if (defaults) {
- const node = LayoutManager._grid.engine.nodes.find(
- n => n.el && n.el.getAttribute('gs-id') === 'quellen'
- );
- if (node) LayoutManager._grid.update(node.el, { h: defaults.h });
- }
- }
- }
+ // Im Tab-Modus wird die Kachel vom Seiten-Layout bestimmt — kein Resize noetig
}
// Timeline - Artikel + Snapshots zwischenspeichern und rendern
this._currentArticles = articles;
this._currentSnapshots = snapshots || [];
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._activePointIndex = null;
@@ -1360,35 +1347,15 @@ const App = {
},
_resizeTimelineTile() {
- if (typeof LayoutManager === 'undefined' || !LayoutManager._grid) return;
+ // Tab-Modus: Kein internes Resize noetig, Panel waechst mit Inhalt.
+ // Wir scrollen lediglich ein offenes Detail in den sichtbaren Bereich.
requestAnimationFrame(() => { requestAnimationFrame(() => {
- // Prüfen ob Detail-Panel oder expandierter Eintrag offen ist
- const hasDetail = document.querySelector('.ht-detail-panel') !== null;
- const hasExpanded = document.querySelector('.timeline-card .vt-entry.expanded') !== null;
-
- if (hasDetail || hasExpanded) {
- LayoutManager.resizeTileToContent('timeline');
- } else {
- // Zurück auf Default-Höhe
- const defaults = LayoutManager.DEFAULT_LAYOUT.find(d => d.id === 'timeline');
- if (defaults) {
- const node = LayoutManager._grid.engine.nodes.find(
- n => n.el && n.el.getAttribute('gs-id') === 'timeline'
- );
- if (node) {
- LayoutManager._grid.update(node.el, { h: defaults.h });
- LayoutManager._debouncedSave();
- }
- }
- }
- // Scroll in Sicht
const card = document.querySelector('.timeline-card');
- const main = document.querySelector('.main-content');
- if (!card || !main) return;
+ if (!card) return;
const cardBottom = card.getBoundingClientRect().bottom;
- const mainBottom = main.getBoundingClientRect().bottom;
- if (cardBottom > mainBottom) {
- main.scrollBy({ top: cardBottom - mainBottom + 16, behavior: document.documentElement.dataset.a11yMotion ? 'instant' : 'smooth' });
+ const viewBottom = window.innerHeight;
+ if (cardBottom > viewBottom) {
+ window.scrollBy({ top: cardBottom - viewBottom + 16, behavior: document.documentElement.dataset.a11yMotion ? 'instant' : 'smooth' });
}
}); });
},
@@ -2669,27 +2636,7 @@ async handleRefresh() {
// aria-expanded auf dem Header-Toggle synchronisieren
const header = chevron ? chevron.closest('[role="button"]') : null;
if (header) header.setAttribute('aria-expanded', String(isHidden));
- // gridstack-Kachel an Inhalt anpassen (doppelter rAF für vollständiges Layout)
- if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) {
- if (isHidden) {
- // Aufgeklappt → Inhalt muss erst layouten
- requestAnimationFrame(() => requestAnimationFrame(() => {
- LayoutManager.resizeTileToContent('quellen');
- }));
- } else {
- // Zugeklappt → auf Default-Höhe zurück
- const defaults = LayoutManager.DEFAULT_LAYOUT.find(d => d.id === 'quellen');
- if (defaults) {
- const node = LayoutManager._grid.engine.nodes.find(
- n => n.el && n.el.getAttribute('gs-id') === 'quellen'
- );
- if (node) {
- LayoutManager._grid.update(node.el, { h: defaults.h });
- LayoutManager._debouncedSave();
- }
- }
- }
- }
+ // Tab-Modus: Panel waechst mit Inhalt, kein Resize noetig
},
toggleGroup(domain) {
diff --git a/src/static/js/components.js b/src/static/js/components.js
index 6627c0c..060d09f 100644
--- a/src/static/js/components.js
+++ b/src/static/js/components.js
@@ -335,7 +335,7 @@ const UI = {
if (state.isFirst) {
overlay.classList.add('blocking');
// Apply blur to grid
- const grid = document.querySelector('.grid-stack');
+ const grid = document.querySelector('.tab-panels');
if (grid) grid.classList.add('blurred');
} else {
overlay.classList.remove('blocking');
@@ -465,7 +465,7 @@ const UI = {
if (incidentId === App.currentIncidentId) {
// Remove blur
- const grid = document.querySelector('.grid-stack');
+ const grid = document.querySelector('.tab-panels');
if (grid) grid.classList.remove('blurred');
const overlay = document.getElementById('progress-overlay');
@@ -559,7 +559,7 @@ const UI = {
if (!incidentId) incidentId = App.currentIncidentId;
// Remove blur
- const grid = document.querySelector('.grid-stack');
+ const grid = document.querySelector('.tab-panels');
if (grid) grid.classList.remove('blurred');
if (incidentId === App.currentIncidentId) {
@@ -787,11 +787,15 @@ const UI = {
const buildPill = (src, fallbackName) => {
const displayName = src ? (src.name || fallbackName) : fallbackName;
- const esc = this.escape(displayName);
+ const url = (src && src.url) || '';
+ const isTelegram = /^https?:\/\/t\.me\//i.test(url);
+ const label = isTelegram ? displayName + ' (Telegram-Link)' : displayName;
+ const esc = this.escape(label);
+ const titleEsc = this.escape(displayName);
if (src && src.url) {
- return `
${esc}`;
+ return `
${esc}`;
}
- return `
${esc}`;
+ return `
${esc}`;
};
const cards = bulletLines.map(line => {
diff --git a/src/static/js/layout.js b/src/static/js/layout.js
index fdf58f1..e7d6e41 100644
--- a/src/static/js/layout.js
+++ b/src/static/js/layout.js
@@ -1,295 +1,75 @@
/**
- * LayoutManager: Drag & Resize Dashboard-Layout mit gridstack.js
- * Persistenz über localStorage, Reset auf Standard-Layout möglich.
+ * LayoutManager: Tab-Navigation fuer das Monitor-Dashboard.
+ * Nur ein Tab-Panel gleichzeitig sichtbar, pro Lage gemerkt in localStorage.
*/
const LayoutManager = {
- _grid: null,
- _storageKey: 'osint_layout',
+ TAB_ORDER: ['zusammenfassung', 'lagebild', 'timeline', 'karte', 'faktencheck', 'quellen'],
+ _currentIncidentId: null,
_initialized: false,
- _saveTimeout: null,
- _hiddenTiles: {},
-
- DEFAULT_LAYOUT: [
- { id: 'zusammenfassung', x: 0, y: 0, w: 12, h: 2, minW: 4, minH: 2 },
- { id: 'lagebild', x: 0, y: 2, w: 6, h: 4, minW: 4, minH: 4 },
- { id: 'faktencheck', x: 6, y: 0, w: 6, h: 4, minW: 4, minH: 4 },
- { id: 'quellen', x: 0, y: 4, w: 12, h: 2, minW: 6, minH: 2 },
- { id: 'timeline', x: 0, y: 5, w: 12, h: 4, minW: 6, minH: 4 },
- { id: 'karte', x: 0, y: 9, w: 12, h: 8, minW: 6, minH: 3 },
- ],
-
- TILE_MAP: {
- zusammenfassung: '#zusammenfassung-card',
- lagebild: '.incident-analysis-summary',
- faktencheck: '.incident-analysis-factcheck',
- quellen: '.source-overview-card',
- timeline: '.timeline-card',
- karte: '.map-card',
- },
init() {
if (this._initialized) return;
+ const nav = document.getElementById('tab-nav');
+ if (!nav) return;
- const container = document.querySelector('.grid-stack');
- if (!container) return;
-
- this._grid = GridStack.init({
- column: 12,
- cellHeight: 80,
- margin: 12,
- animate: true,
- handle: '.card-header',
- float: false,
- disableOneColumnMode: true,
- }, container);
-
- const saved = this._load();
- if (saved) {
- // Migration: Neue Kacheln ergaenzen die in alten Layouts fehlen
- this.DEFAULT_LAYOUT.forEach(def => {
- if (!saved.find(s => s.id === def.id)) {
- saved.unshift({ id: def.id, x: def.x, y: def.y, w: def.w, h: def.h, visible: true });
- }
+ nav.querySelectorAll('.tab-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const tab = btn.getAttribute('data-tab');
+ if (tab) this.switchTab(tab);
});
- this._applyLayout(saved);
- }
-
- this._grid.on('change', () => {
- this._debouncedSave();
- // Leaflet-Map bei Resize invalidieren
- if (typeof UI !== 'undefined') UI.invalidateMap();
});
- const toolbar = document.getElementById('layout-toolbar');
- if (toolbar) toolbar.style.display = 'flex';
-
- this._syncToggles();
+ nav.style.display = '';
this._initialized = true;
},
- _applyLayout(layout) {
- if (!this._grid) return;
+ switchTab(tabId, save = true) {
+ if (!this.TAB_ORDER.includes(tabId)) tabId = 'zusammenfassung';
- this._hiddenTiles = {};
-
- layout.forEach(item => {
- const el = this._grid.engine.nodes.find(n => n.el && n.el.getAttribute('gs-id') === item.id);
- if (!el) return;
-
- if (item.visible === false) {
- this._hiddenTiles[item.id] = item;
- // Card in tile-parking retten bevor Widget entfernt wird
- const selector = this.TILE_MAP[item.id];
- if (selector) {
- const cardEl = el.el.querySelector(selector);
- if (cardEl) {
- const parking = document.getElementById("tile-parking");
- if (parking) parking.appendChild(cardEl);
- }
- }
- this._grid.removeWidget(el.el, true, false);
- } else {
- this._grid.update(el.el, { x: item.x, y: item.y, w: item.w, h: item.h });
- }
+ document.querySelectorAll('#tab-nav .tab-btn').forEach(b => {
+ b.classList.toggle('active', b.getAttribute('data-tab') === tabId);
+ });
+ document.querySelectorAll('.tab-panel').forEach(p => {
+ p.classList.toggle('active', p.id === 'panel-' + tabId);
});
- this._syncToggles();
+ // Leaflet-Karte: invalidateSize nach Panel-Wechsel, damit Tiles korrekt rendern
+ if (tabId === 'karte' && typeof UI !== 'undefined' && UI._map) {
+ setTimeout(() => { try { UI._map.invalidateSize(); } catch (e) { /* ignore */ } }, 50);
+ }
+
+ if (save && this._currentIncidentId != null) {
+ try {
+ localStorage.setItem('osint_tab_' + this._currentIncidentId, tabId);
+ } catch (e) { /* quota */ }
+ }
},
- save() {
- if (!this._grid) return;
-
- const items = [];
- this._grid.engine.nodes.forEach(node => {
- const id = node.el ? node.el.getAttribute('gs-id') : null;
- if (!id) return;
- items.push({
- id, x: node.x, y: node.y, w: node.w, h: node.h, visible: true,
- });
- });
-
- Object.keys(this._hiddenTiles).forEach(id => {
- items.push({ ...this._hiddenTiles[id], visible: false });
- });
-
+ restoreTabFor(incidentId) {
+ this._currentIncidentId = incidentId;
+ let target = 'zusammenfassung';
try {
- localStorage.setItem(this._storageKey, JSON.stringify(items));
- } catch (e) { /* quota */ }
+ const saved = localStorage.getItem('osint_tab_' + incidentId);
+ if (saved && this.TAB_ORDER.includes(saved)) target = saved;
+ } catch (e) { /* ignore */ }
+ this.switchTab(target, false);
},
- _debouncedSave() {
- clearTimeout(this._saveTimeout);
- this._saveTimeout = setTimeout(() => this.save(), 300);
+ /** Tab-Labels je Incident-Typ anpassen (adhoc vs. research). */
+ applyTypeLabels(incidentType) {
+ const isResearch = incidentType === 'research';
+ const zf = document.querySelector('#tab-nav .tab-btn[data-tab="zusammenfassung"]');
+ const lb = document.querySelector('#tab-nav .tab-btn[data-tab="lagebild"]');
+ if (zf) zf.textContent = isResearch ? 'Zusammenfassung' : 'Neueste Entwicklungen';
+ if (lb) lb.textContent = isResearch ? 'Recherchebericht' : 'Lagebild';
},
- _load() {
- try {
- const raw = localStorage.getItem(this._storageKey);
- if (!raw) return null;
- const parsed = JSON.parse(raw);
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
- return parsed;
- } catch (e) {
- return null;
- }
- },
-
- toggleTile(tileId) {
- if (!this._grid) return;
-
- const selector = this.TILE_MAP[tileId];
- if (!selector) return;
-
- if (this._hiddenTiles[tileId]) {
- // Kachel einblenden
- const cfg = this._hiddenTiles[tileId];
- delete this._hiddenTiles[tileId];
-
- const cardEl = document.querySelector(selector);
- if (!cardEl) return;
-
- // Wrapper erstellen
- const wrapper = document.createElement('div');
- wrapper.className = 'grid-stack-item';
- wrapper.setAttribute('gs-id', tileId);
- wrapper.setAttribute('gs-x', cfg.x);
- wrapper.setAttribute('gs-y', cfg.y);
- wrapper.setAttribute('gs-w', cfg.w);
- wrapper.setAttribute('gs-h', cfg.h);
- wrapper.setAttribute('gs-min-w', cfg.minW || '');
- wrapper.setAttribute('gs-min-h', cfg.minH || '');
- const content = document.createElement('div');
- content.className = 'grid-stack-item-content';
- content.appendChild(cardEl);
- wrapper.appendChild(content);
-
- this._grid.addWidget(wrapper);
- } else {
- // Kachel ausblenden
- const node = this._grid.engine.nodes.find(
- n => n.el && n.el.getAttribute('gs-id') === tileId
- );
- if (!node) return;
-
- const defaults = this.DEFAULT_LAYOUT.find(d => d.id === tileId);
- this._hiddenTiles[tileId] = {
- id: tileId,
- x: node.x, y: node.y, w: node.w, h: node.h,
- minW: defaults ? defaults.minW : 4,
- minH: defaults ? defaults.minH : 2,
- visible: false,
- };
-
- // Card aus dem Widget retten bevor es entfernt wird
- const cardEl = node.el.querySelector(selector);
- if (cardEl) {
- // Temporär im incident-view parken (unsichtbar)
- const parking = document.getElementById('tile-parking');
- if (parking) parking.appendChild(cardEl);
- }
-
- this._grid.removeWidget(node.el, true, false);
- }
-
- this._syncToggles();
- this.save();
- },
-
- _syncToggles() {
- document.querySelectorAll('.layout-toggle-btn').forEach(btn => {
- const tileId = btn.getAttribute('data-tile');
- const isHidden = !!this._hiddenTiles[tileId];
- btn.classList.toggle('active', !isHidden);
- btn.setAttribute('aria-pressed', String(!isHidden));
- });
- },
-
- reset() {
- localStorage.removeItem(this._storageKey);
-
- // Cards einsammeln BEVOR der Grid zerstört wird (aus Grid + Parking)
- const cards = {};
- Object.entries(this.TILE_MAP).forEach(([id, selector]) => {
- const card = document.querySelector(selector);
- if (card) cards[id] = card;
- });
-
- this._hiddenTiles = {};
-
- if (this._grid) {
- this._grid.destroy(false);
- this._grid = null;
- }
- this._initialized = false;
-
- const gridEl = document.querySelector('.grid-stack');
- if (!gridEl) return;
-
- // Grid leeren (Cards sind bereits in cards-Map gesichert)
- gridEl.innerHTML = '';
-
- // Cards in Default-Layout neu aufbauen
- this.DEFAULT_LAYOUT.forEach(cfg => {
- const cardEl = cards[cfg.id];
- if (!cardEl) return;
-
- const wrapper = document.createElement('div');
- wrapper.className = 'grid-stack-item';
- wrapper.setAttribute('gs-id', cfg.id);
- wrapper.setAttribute('gs-x', cfg.x);
- wrapper.setAttribute('gs-y', cfg.y);
- wrapper.setAttribute('gs-w', cfg.w);
- wrapper.setAttribute('gs-h', cfg.h);
- wrapper.setAttribute('gs-min-w', cfg.minW);
- wrapper.setAttribute('gs-min-h', cfg.minH);
-
- const content = document.createElement('div');
- content.className = 'grid-stack-item-content';
- content.appendChild(cardEl);
- wrapper.appendChild(content);
- gridEl.appendChild(wrapper);
- });
-
- this.init();
- },
-
- resizeTileToContent(tileId) {
- if (!this._grid) return;
-
- const node = this._grid.engine.nodes.find(
- n => n.el && n.el.getAttribute('gs-id') === tileId
- );
- if (!node || !node.el) return;
-
- const wrapper = node.el.querySelector('.grid-stack-item-content');
- if (!wrapper) return;
-
- const card = wrapper.firstElementChild;
- if (!card) return;
-
- const cellH = this._grid.opts.cellHeight || 80;
- const margin = this._grid.opts.margin || 12;
-
- // Temporär alle height-Constraints aufheben
- node.el.classList.add('gs-measuring');
- const naturalHeight = card.scrollHeight;
- node.el.classList.remove('gs-measuring');
-
- // In Grid-Units umrechnen (aufrunden + 1 Puffer)
- const neededH = Math.ceil(naturalHeight / (cellH + margin)) + 1;
- const minH = node.minH || 2;
- const finalH = Math.max(neededH, minH);
-
- this._grid.update(node.el, { h: finalH });
- this._debouncedSave();
- },
-
- destroy() {
- if (this._grid) {
- this._grid.destroy(false);
- this._grid = null;
- }
- this._initialized = false;
- this._hiddenTiles = {};
- },
+ // Legacy-API-Stubs: falls alte Aufrufe im Code liegen, stumm schlucken statt crashen.
+ toggleTile() { /* legacy no-op */ },
+ reset() { /* legacy no-op */ },
+ save() { /* legacy no-op */ },
+ resizeTileToContent() { /* legacy no-op */ },
+ destroy() { /* legacy no-op */ },
};
+
+document.addEventListener('DOMContentLoaded', () => LayoutManager.init());