Dashboard: GridStack durch Tab-Navigation ersetzen

Der Monitor-Dashboard zeigte bisher alle sechs Kacheln gleichzeitig in
einem GridStack-Layout (Drag/Resize, je Kachel eigenes Scrolling). Nutzer-
wunsch: Analog zur Lagebild-Seite nur ein Tab-Panel gleichzeitig, maximiert
auf volle Breite, Seiten-Scroll statt interne Scrollbars.

Aenderungen:
- dashboard.html: Layout-Toolbar + grid-stack-Wrapper entfernt; neue tab-nav
  mit 6 Buttons + tab-panels mit 6 Panels. GridStack CDN-Links raus.
- layout.js: GridStack-Init/toggleTile/reset komplett entfernt. Neu:
  switchTab(tabId) + restoreTabFor(incidentId) mit localStorage-Persistenz
  pro Lage osint_tab_id. applyTypeLabels fuer adhoc vs. research. Legacy-
  Methoden sind No-Op-Stubs.
- app.js: renderIncidentDetail ruft LayoutManager.restoreTabFor und
  applyTypeLabels auf. openContentModal-Trigger aus Card-Titeln raus.
  Tile-Resize-Bloecke fuer Quellen und Timeline entfernt.
- components.js: Telegram-Pills bekommen Suffix Telegram-Link, wenn die
  URL auf t.me verweist.
- style.css: grid-stack/layout-toggle Klassen raus; neue tab-nav/tab-btn/
  tab-panel Klassen. Internes Scrolling entfernt. map-container 600px.

Alte osint_layout-Eintraege werden ignoriert.
Dieser Commit ist enthalten in:
claude-dev
2026-04-18 22:34:36 +00:00
Ursprung 3b9e9e25c2
Commit e15ed0c21e
5 geänderte Dateien mit 206 neuen und 571 gelöschten Zeilen

Datei anzeigen

@@ -10,7 +10,6 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/vendor/leaflet.css">
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
@@ -190,131 +189,106 @@
<span class="progress-mini-timer" id="progress-mini-timer"></span>
</div>
<!-- Layout-Toolbar -->
<div class="layout-toolbar" id="layout-toolbar" style="display:none;">
<div class="layout-toggles">
<button class="layout-toggle-btn active" data-tile="zusammenfassung" onclick="LayoutManager.toggleTile('zusammenfassung')" aria-pressed="true">Zusammenfassung</button>
<button class="layout-toggle-btn active" data-tile="lagebild" onclick="LayoutManager.toggleTile('lagebild')" aria-pressed="true">Lagebild</button>
<button class="layout-toggle-btn active" data-tile="faktencheck" onclick="LayoutManager.toggleTile('faktencheck')" aria-pressed="true">Faktencheck</button>
<button class="layout-toggle-btn active" data-tile="quellen" onclick="LayoutManager.toggleTile('quellen')" aria-pressed="true">Quellen</button>
<button class="layout-toggle-btn active" data-tile="timeline" onclick="LayoutManager.toggleTile('timeline')" aria-pressed="true">Timeline</button>
<button class="layout-toggle-btn active" data-tile="karte" onclick="LayoutManager.toggleTile('karte')" aria-pressed="true">Karte</button>
</div>
<button class="btn btn-secondary btn-small" onclick="LayoutManager.reset()">Layout zurücksetzen</button>
<!-- Tab-Navigation -->
<div class="tab-nav" id="tab-nav" style="display:none;">
<button class="tab-btn active" data-tab="zusammenfassung">Neueste Entwicklungen</button>
<button class="tab-btn" data-tab="lagebild">Lagebild</button>
<button class="tab-btn" data-tab="timeline">Ereignis-Timeline</button>
<button class="tab-btn" data-tab="karte">Geografische Verteilung</button>
<button class="tab-btn" data-tab="faktencheck">Faktencheck</button>
<button class="tab-btn" data-tab="quellen">Quellenübersicht</button>
</div>
<!-- gridstack Dashboard-Grid -->
<div class="grid-stack">
<div class="grid-stack-item" gs-id="zusammenfassung" gs-x="0" gs-y="0" gs-w="12" gs-h="2" gs-min-w="4" gs-min-h="2">
<div class="grid-stack-item-content">
<div class="card" id="zusammenfassung-card">
<div class="card-header">
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Zusammenfassung', 'zusammenfassung-content')">Zusammenfassung</div>
</div>
<div id="zusammenfassung-content">
<div id="zusammenfassung-text" class="summary-text" style="padding:8px 16px;"></div>
</div>
<!-- Tab-Panels -->
<div class="tab-panels">
<div class="tab-panel active" id="panel-zusammenfassung">
<div class="card" id="zusammenfassung-card">
<div class="card-header">
<div class="card-title">Zusammenfassung</div>
</div>
<div id="zusammenfassung-content">
<div id="zusammenfassung-text" class="summary-text" style="padding:8px 16px;"></div>
</div>
</div>
</div>
<div class="grid-stack-item" gs-id="lagebild" gs-x="0" gs-y="2" gs-w="6" gs-h="4" gs-min-w="4" gs-min-h="4">
<div class="grid-stack-item-content">
<div class="card incident-analysis-summary">
<div class="card-header">
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Lagebild', 'summary-content')">Lagebild</div>
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
</div>
<div id="summary-content">
<div id="summary-text" class="summary-text"></div>
</div>
<div class="tab-panel" id="panel-lagebild">
<div class="card incident-analysis-summary">
<div class="card-header">
<div class="card-title">Lagebild</div>
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
</div>
<div id="summary-content">
<div id="summary-text" class="summary-text"></div>
</div>
</div>
</div>
<div class="grid-stack-item" gs-id="faktencheck" gs-x="6" gs-y="0" gs-w="6" gs-h="4" gs-min-w="4" gs-min-h="4">
<div class="grid-stack-item-content">
<div class="card incident-analysis-factcheck" id="factcheck-card">
<div class="card-header">
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Faktencheck', 'factcheck-list')">Faktencheck <span class="info-icon" data-tooltip="Gesichert/Bestätigt = durch mehrere unabhängige Quellen belegt.&#10;&#10;Unbestätigt/Ungeprüft = noch nicht ausreichend verifiziert.&#10;&#10;Widerlegt/Umstritten = Quellen widersprechen sich."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></div>
<div class="fc-filter-bar" id="fc-filters"></div>
</div>
<div class="factcheck-list" id="factcheck-list">
<div class="empty-state" style="padding:20px;">
<div class="empty-state-text">Noch keine Fakten geprüft</div>
<div class="tab-panel" id="panel-timeline">
<div class="card timeline-card">
<div class="card-header">
<div class="card-title">Ereignis-Timeline</div>
<div class="ht-controls">
<div class="ht-filter-group">
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
<button class="ht-filter-btn" data-filter="articles" onclick="App.setTimelineFilter('articles')" aria-pressed="false">Meldungen</button>
<button class="ht-filter-btn" data-filter="snapshots" onclick="App.setTimelineFilter('snapshots')" aria-pressed="false">Lageberichte</button>
</div>
</div>
</div>
</div>
</div>
<div class="grid-stack-item" gs-id="quellen" gs-x="0" gs-y="4" gs-w="12" gs-h="2" gs-min-w="6" gs-min-h="2">
<div class="grid-stack-item-content">
<div class="card source-overview-card">
<div class="card-header source-overview-header-toggle" onclick="App.toggleSourceOverview()" role="button" tabindex="0" aria-expanded="false">
<span class="source-overview-chevron" id="source-overview-chevron" title="Aufklappen" aria-hidden="true">&#9656;</span>
<div class="card-title clickable">Quellenübersicht</div>
<button class="btn btn-secondary btn-small source-detail-btn" onclick="event.stopPropagation(); openContentModal('Quellenübersicht', 'source-overview-content')">Detailansicht</button>
</div>
<div class="source-overview-subheader" onclick="App.toggleSourceOverview()" role="button">
<span class="source-overview-header-stats" id="source-overview-header-stats"></span>
</div>
<div id="source-overview-content" style="display:none;"></div>
</div>
</div>
</div>
<div class="grid-stack-item" gs-id="timeline" gs-x="0" gs-y="5" gs-w="12" gs-h="4" gs-min-w="6" gs-min-h="4">
<div class="grid-stack-item-content">
<div class="card timeline-card">
<div class="card-header">
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Ereignis-Timeline', 'timeline')">Ereignis-Timeline</div>
<div class="ht-controls">
<div class="ht-filter-group">
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
<button class="ht-filter-btn" data-filter="articles" onclick="App.setTimelineFilter('articles')" aria-pressed="false">Meldungen</button>
<button class="ht-filter-btn" data-filter="snapshots" onclick="App.setTimelineFilter('snapshots')" aria-pressed="false">Lageberichte</button>
</div>
<span class="ht-count" id="article-count"></span>
<div class="ht-range-group">
<button class="ht-range-btn" data-range="24h" onclick="App.setTimelineRange('24h')" aria-pressed="false">24h</button>
<button class="ht-range-btn" data-range="7d" onclick="App.setTimelineRange('7d')" aria-pressed="false">7T</button>
<button class="ht-range-btn active" data-range="all" onclick="App.setTimelineRange('all')" aria-pressed="true">Alles</button>
</div>
<label for="timeline-search" class="sr-only">Timeline durchsuchen</label>
<input type="text" id="timeline-search" class="timeline-filter-input" placeholder="Suche..." oninput="App.debouncedRerenderTimeline()">
<span class="ht-count" id="article-count"></span>
<div class="ht-range-group">
<button class="ht-range-btn" data-range="24h" onclick="App.setTimelineRange('24h')" aria-pressed="false">24h</button>
<button class="ht-range-btn" data-range="7d" onclick="App.setTimelineRange('7d')" aria-pressed="false">7T</button>
<button class="ht-range-btn active" data-range="all" onclick="App.setTimelineRange('all')" aria-pressed="true">Alles</button>
</div>
<label for="timeline-search" class="sr-only">Timeline durchsuchen</label>
<input type="text" id="timeline-search" class="timeline-filter-input" placeholder="Suche..." oninput="App.debouncedRerenderTimeline()">
</div>
<div id="timeline" class="ht-timeline-container">
<div class="ht-empty">Noch keine Meldungen</div>
</div>
<div id="timeline" class="ht-timeline-container">
<div class="ht-empty">Noch keine Meldungen</div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-karte">
<div class="card map-card">
<div class="card-header">
<div class="card-title">Geografische Verteilung</div>
<span class="map-stats" id="map-stats"></span>
<div class="card-header-actions">
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln einlesen">Orte einlesen</button>
</div>
</div>
<div class="map-container" id="map-container">
<div class="map-empty" id="map-empty">Keine Orte erkannt</div>
</div>
</div>
</div>
<div class="tab-panel" id="panel-faktencheck">
<div class="card incident-analysis-factcheck" id="factcheck-card">
<div class="card-header">
<div class="card-title">Faktencheck <span class="info-icon" data-tooltip="Gesichert/Bestätigt = durch mehrere unabhängige Quellen belegt.&#10;&#10;Unbestätigt/Ungeprüft = noch nicht ausreichend verifiziert.&#10;&#10;Widerlegt/Umstritten = Quellen widersprechen sich."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></div>
<div class="fc-filter-bar" id="fc-filters"></div>
</div>
<div class="factcheck-list" id="factcheck-list">
<div class="empty-state" style="padding:20px;">
<div class="empty-state-text">Noch keine Fakten geprüft</div>
</div>
</div>
</div>
</div>
<div class="grid-stack-item" gs-id="karte" gs-x="0" gs-y="9" gs-w="12" gs-h="8" gs-min-w="6" gs-min-h="3">
<div class="grid-stack-item-content">
<div class="card map-card">
<div class="card-header">
<div class="card-title">Geografische Verteilung</div>
<span class="map-stats" id="map-stats"></span>
<div class="card-header-actions">
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln einlesen">Orte einlesen</button>
<button class="btn btn-secondary btn-small map-expand-btn" id="map-expand-btn" onclick="UI.toggleMapFullscreen()" title="Vollbild" aria-label="Karte im Vollbild anzeigen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
</button>
</div>
</div>
<div class="map-container" id="map-container">
<div class="map-empty" id="map-empty">Keine Orte erkannt</div>
</div>
<div class="tab-panel" id="panel-quellen">
<div class="card source-overview-card">
<div class="card-header">
<div class="card-title">Quellenübersicht</div>
<span class="source-overview-header-stats" id="source-overview-header-stats"></span>
</div>
<div id="source-overview-content"></div>
</div>
</div>
</div>
<!-- Parkplatz für ausgeblendete Kacheln -->
<div id="tile-parking" style="display:none;"></div>
</div>
</main>
</div>
@@ -646,7 +620,6 @@
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack-all.js"></script>
<script src="/static/vendor/leaflet.js"></script>
<script src="/static/vendor/leaflet.markercluster.js"></script>
<script src="/static/js/api.js?v=20260316c"></script>