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:
@@ -2136,7 +2136,7 @@ a.dev-source-pill:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* === Blur for First Refresh === */
|
/* === Blur for First Refresh === */
|
||||||
.grid-stack.blurred .grid-stack-item-content {
|
.tab-panels.blurred .tab-panel {
|
||||||
filter: blur(8px);
|
filter: blur(8px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -3989,74 +3989,55 @@ a.dev-source-pill:hover {
|
|||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === gridstack Dashboard-Layout === */
|
/* === Tab-basiertes Dashboard-Layout === */
|
||||||
.grid-stack {
|
.tab-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 18px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
.tab-panels {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tab-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tab-panel > .card {
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tab-panel .map-container {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
.tab-panel .ht-timeline-container {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
.tab-panels.blurred { filter: blur(4px); pointer-events: none; }
|
||||||
.grid-stack > .grid-stack-item > .grid-stack-item-content {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-stack-item-content > .card {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-stack-item-content .incident-analysis-summary > #summary-content,
|
|
||||||
.grid-stack-item-content .incident-analysis-factcheck > .factcheck-list,
|
|
||||||
.grid-stack-item-content #timeline,
|
|
||||||
.grid-stack-item-content #zusammenfassung-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
min-height: 0;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--text-disabled) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-stack-item-content .incident-analysis-summary > #summary-content::-webkit-scrollbar,
|
|
||||||
.grid-stack-item-content .incident-analysis-factcheck > .factcheck-list::-webkit-scrollbar,
|
|
||||||
.grid-stack-item-content #timeline::-webkit-scrollbar,
|
|
||||||
.grid-stack-item-content #zusammenfassung-content::-webkit-scrollbar { width: 6px; }
|
|
||||||
|
|
||||||
.grid-stack-item-content .incident-analysis-summary > #summary-content::-webkit-scrollbar-track,
|
|
||||||
.grid-stack-item-content .incident-analysis-factcheck > .factcheck-list::-webkit-scrollbar-track,
|
|
||||||
.grid-stack-item-content #timeline::-webkit-scrollbar-track,
|
|
||||||
.grid-stack-item-content #zusammenfassung-content::-webkit-scrollbar-track { background: transparent; border-radius: 3px; }
|
|
||||||
|
|
||||||
.grid-stack-item-content .incident-analysis-summary > #summary-content::-webkit-scrollbar-thumb,
|
|
||||||
.grid-stack-item-content .incident-analysis-factcheck > .factcheck-list::-webkit-scrollbar-thumb,
|
|
||||||
.grid-stack-item-content #timeline::-webkit-scrollbar-thumb,
|
|
||||||
.grid-stack-item-content #zusammenfassung-content::-webkit-scrollbar-thumb { background: var(--text-disabled); border-radius: 3px; }
|
|
||||||
|
|
||||||
.grid-stack-item-content .incident-analysis-summary > #summary-content::-webkit-scrollbar-thumb:hover,
|
|
||||||
.grid-stack-item-content .incident-analysis-factcheck > .factcheck-list::-webkit-scrollbar-thumb:hover,
|
|
||||||
.grid-stack-item-content #timeline::-webkit-scrollbar-thumb:hover,
|
|
||||||
.grid-stack-item-content #zusammenfassung-content::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
|
|
||||||
|
|
||||||
/* Quellenübersicht: Kachel wächst beim Aufklappen via resizeTileToContent */
|
|
||||||
.grid-stack-item-content #source-overview-content {
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--text-disabled) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-stack-item-content #source-overview-content::-webkit-scrollbar { width: 6px; }
|
|
||||||
.grid-stack-item-content #source-overview-content::-webkit-scrollbar-track { background: transparent; border-radius: 3px; }
|
|
||||||
.grid-stack-item-content #source-overview-content::-webkit-scrollbar-thumb { background: var(--text-disabled); border-radius: 3px; }
|
|
||||||
.grid-stack-item-content #source-overview-content::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
|
|
||||||
|
|
||||||
.grid-stack-item-content .incident-analysis-summary,
|
|
||||||
.grid-stack-item-content .incident-analysis-factcheck {
|
|
||||||
max-height: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-stack .card-header {
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-stack .card-header:active {
|
.grid-stack .card-header:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
@@ -4073,56 +4054,6 @@ a.dev-source-pill:hover {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout-Toolbar */
|
|
||||||
.layout-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: var(--sp-md) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-toggles {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-toggle-btn {
|
|
||||||
padding: var(--sp-xs) var(--sp-lg);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-disabled);
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: var(--font-body);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-toggle-btn.active {
|
|
||||||
background: var(--tint-accent);
|
|
||||||
color: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-toggle-btn:hover {
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Temporäre Messklasse: hebt alle Höhenbeschränkungen auf */
|
|
||||||
.grid-stack-item.gs-measuring {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gs-measuring > .grid-stack-item-content {
|
|
||||||
height: auto !important;
|
|
||||||
position: relative !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gs-measuring .grid-stack-item-content > .card {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Barrierefreiheit (A11y) === */
|
/* === Barrierefreiheit (A11y) === */
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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://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/leaflet.css">
|
||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
||||||
@@ -190,131 +189,106 @@
|
|||||||
<span class="progress-mini-timer" id="progress-mini-timer"></span>
|
<span class="progress-mini-timer" id="progress-mini-timer"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Layout-Toolbar -->
|
<!-- Tab-Navigation -->
|
||||||
<div class="layout-toolbar" id="layout-toolbar" style="display:none;">
|
<div class="tab-nav" id="tab-nav" style="display:none;">
|
||||||
<div class="layout-toggles">
|
<button class="tab-btn active" data-tab="zusammenfassung">Neueste Entwicklungen</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="zusammenfassung" onclick="LayoutManager.toggleTile('zusammenfassung')" aria-pressed="true">Zusammenfassung</button>
|
<button class="tab-btn" data-tab="lagebild">Lagebild</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="lagebild" onclick="LayoutManager.toggleTile('lagebild')" aria-pressed="true">Lagebild</button>
|
<button class="tab-btn" data-tab="timeline">Ereignis-Timeline</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="faktencheck" onclick="LayoutManager.toggleTile('faktencheck')" aria-pressed="true">Faktencheck</button>
|
<button class="tab-btn" data-tab="karte">Geografische Verteilung</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="quellen" onclick="LayoutManager.toggleTile('quellen')" aria-pressed="true">Quellen</button>
|
<button class="tab-btn" data-tab="faktencheck">Faktencheck</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="timeline" onclick="LayoutManager.toggleTile('timeline')" aria-pressed="true">Timeline</button>
|
<button class="tab-btn" data-tab="quellen">Quellenübersicht</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- gridstack Dashboard-Grid -->
|
<!-- Tab-Panels -->
|
||||||
<div class="grid-stack">
|
<div class="tab-panels">
|
||||||
<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="tab-panel active" id="panel-zusammenfassung">
|
||||||
<div class="grid-stack-item-content">
|
<div class="card" id="zusammenfassung-card">
|
||||||
<div class="card" id="zusammenfassung-card">
|
<div class="card-header">
|
||||||
<div class="card-header">
|
<div class="card-title">Zusammenfassung</div>
|
||||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Zusammenfassung', 'zusammenfassung-content')">Zusammenfassung</div>
|
</div>
|
||||||
</div>
|
<div id="zusammenfassung-content">
|
||||||
<div id="zusammenfassung-content">
|
<div id="zusammenfassung-text" class="summary-text" style="padding:8px 16px;"></div>
|
||||||
<div id="zusammenfassung-text" class="summary-text" style="padding:8px 16px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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="tab-panel" id="panel-lagebild">
|
||||||
<div class="grid-stack-item-content">
|
<div class="card incident-analysis-summary">
|
||||||
<div class="card incident-analysis-summary">
|
<div class="card-header">
|
||||||
<div class="card-header">
|
<div class="card-title">Lagebild</div>
|
||||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Lagebild', 'summary-content')">Lagebild</div>
|
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
||||||
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
</div>
|
||||||
</div>
|
<div id="summary-content">
|
||||||
<div id="summary-content">
|
<div id="summary-text" class="summary-text"></div>
|
||||||
<div id="summary-text" class="summary-text"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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="tab-panel" id="panel-timeline">
|
||||||
<div class="grid-stack-item-content">
|
<div class="card timeline-card">
|
||||||
<div class="card incident-analysis-factcheck" id="factcheck-card">
|
<div class="card-header">
|
||||||
<div class="card-header">
|
<div class="card-title">Ereignis-Timeline</div>
|
||||||
<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. Unbestätigt/Ungeprüft = noch nicht ausreichend verifiziert. 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="ht-controls">
|
||||||
<div class="fc-filter-bar" id="fc-filters"></div>
|
<div class="ht-filter-group">
|
||||||
</div>
|
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
|
||||||
<div class="factcheck-list" id="factcheck-list">
|
<button class="ht-filter-btn" data-filter="articles" onclick="App.setTimelineFilter('articles')" aria-pressed="false">Meldungen</button>
|
||||||
<div class="empty-state" style="padding:20px;">
|
<button class="ht-filter-btn" data-filter="snapshots" onclick="App.setTimelineFilter('snapshots')" aria-pressed="false">Lageberichte</button>
|
||||||
<div class="empty-state-text">Noch keine Fakten geprüft</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="ht-count" id="article-count"></span>
|
||||||
</div>
|
<div class="ht-range-group">
|
||||||
</div>
|
<button class="ht-range-btn" data-range="24h" onclick="App.setTimelineRange('24h')" aria-pressed="false">24h</button>
|
||||||
</div>
|
<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 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">▸</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()">
|
|
||||||
</div>
|
</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>
|
||||||
<div id="timeline" class="ht-timeline-container">
|
</div>
|
||||||
<div class="ht-empty">Noch keine Meldungen</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. Unbestätigt/Ungeprüft = noch nicht ausreichend verifiziert. 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>
|
</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="tab-panel" id="panel-quellen">
|
||||||
<div class="grid-stack-item-content">
|
<div class="card source-overview-card">
|
||||||
<div class="card map-card">
|
<div class="card-header">
|
||||||
<div class="card-header">
|
<div class="card-title">Quellenübersicht</div>
|
||||||
<div class="card-title">Geografische Verteilung</div>
|
<span class="source-overview-header-stats" id="source-overview-header-stats"></span>
|
||||||
<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>
|
</div>
|
||||||
|
<div id="source-overview-content"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Parkplatz für ausgeblendete Kacheln -->
|
|
||||||
<div id="tile-parking" style="display:none;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -646,7 +620,6 @@
|
|||||||
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
|
<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/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.js"></script>
|
||||||
<script src="/static/vendor/leaflet.markercluster.js"></script>
|
<script src="/static/vendor/leaflet.markercluster.js"></script>
|
||||||
<script src="/static/js/api.js?v=20260316c"></script>
|
<script src="/static/js/api.js?v=20260316c"></script>
|
||||||
|
|||||||
@@ -716,7 +716,7 @@ const App = {
|
|||||||
|
|
||||||
// GridStack-Animation deaktivieren und Scroll komplett sperren
|
// GridStack-Animation deaktivieren und Scroll komplett sperren
|
||||||
// bis alle Tile-Resize-Operationen (doppeltes rAF) abgeschlossen sind
|
// 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');
|
if (gridEl) gridEl.classList.remove('grid-stack-animate');
|
||||||
var scrollLock = function() { mc.scrollTop = 0; };
|
var scrollLock = function() { mc.scrollTop = 0; };
|
||||||
mc.addEventListener('scroll', scrollLock);
|
mc.addEventListener('scroll', scrollLock);
|
||||||
@@ -732,7 +732,7 @@ const App = {
|
|||||||
if (prevOverlay) prevOverlay.style.display = 'none';
|
if (prevOverlay) prevOverlay.style.display = 'none';
|
||||||
const prevMini = document.getElementById('progress-mini');
|
const prevMini = document.getElementById('progress-mini');
|
||||||
if (prevMini) prevMini.style.display = 'none';
|
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 (grid) grid.classList.remove('blurred');
|
||||||
|
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
@@ -825,10 +825,11 @@ const App = {
|
|||||||
|
|
||||||
// Kachel-Label: 'Recherchebericht' fuer Recherche-Lagen, 'Lagebild' fuer Live-Monitoring
|
// Kachel-Label: 'Recherchebericht' fuer Recherche-Lagen, 'Lagebild' fuer Live-Monitoring
|
||||||
const _lbLabel = incident.type === 'research' ? 'Recherchebericht' : 'Lagebild';
|
const _lbLabel = incident.type === 'research' ? 'Recherchebericht' : 'Lagebild';
|
||||||
const _cardTitle = document.querySelector('[gs-id="lagebild"] .card-title');
|
const _cardTitle = document.querySelector('#panel-lagebild .card-title');
|
||||||
if (_cardTitle) { _cardTitle.textContent = _lbLabel; _cardTitle.setAttribute("onclick", "openContentModal('" + _lbLabel + "', 'summary-content')"); }
|
if (_cardTitle) _cardTitle.textContent = _lbLabel;
|
||||||
const _toggleBtn = document.querySelector('.layout-toggle-btn[data-tile="lagebild"]');
|
if (typeof LayoutManager !== 'undefined' && typeof LayoutManager.applyTypeLabels === 'function') {
|
||||||
if (_toggleBtn) _toggleBtn.textContent = _lbLabel;
|
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; } }
|
{ 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
|
// Archiv-Button Text
|
||||||
@@ -855,7 +856,6 @@ const App = {
|
|||||||
if (incident.type === 'research') {
|
if (incident.type === 'research') {
|
||||||
// Recherche: ZUSAMMENFASSUNG-Sektion aus Briefing extrahieren
|
// Recherche: ZUSAMMENFASSUNG-Sektion aus Briefing extrahieren
|
||||||
if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Zusammenfassung';
|
if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Zusammenfassung';
|
||||||
if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Zusammenfassung', 'zusammenfassung-content')");
|
|
||||||
if (incident.summary) {
|
if (incident.summary) {
|
||||||
const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
|
const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
|
||||||
if (zusammenfassung) {
|
if (zusammenfassung) {
|
||||||
@@ -874,7 +874,6 @@ const App = {
|
|||||||
} else {
|
} else {
|
||||||
// Live-Monitoring (adhoc): Kachel zeigt "Neueste Entwicklungen" (max 8 Bullets mit Zeitstempel)
|
// Live-Monitoring (adhoc): Kachel zeigt "Neueste Entwicklungen" (max 8 Bullets mit Zeitstempel)
|
||||||
if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Neueste Entwicklungen';
|
if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Neueste Entwicklungen';
|
||||||
if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Neueste Entwicklungen', 'zusammenfassung-content')");
|
|
||||||
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
|
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
|
||||||
const devText = (incident.latest_developments || '').trim();
|
const devText = (incident.latest_developments || '').trim();
|
||||||
if (devText) {
|
if (devText) {
|
||||||
@@ -949,30 +948,18 @@ const App = {
|
|||||||
const _soSources = new Set(articles.map(a => a.source).filter(Boolean));
|
const _soSources = new Set(articles.map(a => a.source).filter(Boolean));
|
||||||
_soStats.textContent = articles.length + " Artikel aus " + _soSources.size + " Quellen";
|
_soStats.textContent = articles.length + " Artikel aus " + _soSources.size + " Quellen";
|
||||||
}
|
}
|
||||||
// Kachel an Inhalt anpassen
|
// Im Tab-Modus wird die Kachel vom Seiten-Layout bestimmt — kein Resize noetig
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline - Artikel + Snapshots zwischenspeichern und rendern
|
// Timeline - Artikel + Snapshots zwischenspeichern und rendern
|
||||||
this._currentArticles = articles;
|
this._currentArticles = articles;
|
||||||
this._currentSnapshots = snapshots || [];
|
this._currentSnapshots = snapshots || [];
|
||||||
this._currentIncidentType = incident.type;
|
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._timelineFilter = 'all';
|
||||||
this._timelineRange = 'all';
|
this._timelineRange = 'all';
|
||||||
this._activePointIndex = null;
|
this._activePointIndex = null;
|
||||||
@@ -1360,35 +1347,15 @@ const App = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_resizeTimelineTile() {
|
_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(() => {
|
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 card = document.querySelector('.timeline-card');
|
||||||
const main = document.querySelector('.main-content');
|
if (!card) return;
|
||||||
if (!card || !main) return;
|
|
||||||
const cardBottom = card.getBoundingClientRect().bottom;
|
const cardBottom = card.getBoundingClientRect().bottom;
|
||||||
const mainBottom = main.getBoundingClientRect().bottom;
|
const viewBottom = window.innerHeight;
|
||||||
if (cardBottom > mainBottom) {
|
if (cardBottom > viewBottom) {
|
||||||
main.scrollBy({ top: cardBottom - mainBottom + 16, behavior: document.documentElement.dataset.a11yMotion ? 'instant' : 'smooth' });
|
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
|
// aria-expanded auf dem Header-Toggle synchronisieren
|
||||||
const header = chevron ? chevron.closest('[role="button"]') : null;
|
const header = chevron ? chevron.closest('[role="button"]') : null;
|
||||||
if (header) header.setAttribute('aria-expanded', String(isHidden));
|
if (header) header.setAttribute('aria-expanded', String(isHidden));
|
||||||
// gridstack-Kachel an Inhalt anpassen (doppelter rAF für vollständiges Layout)
|
// Tab-Modus: Panel waechst mit Inhalt, kein Resize noetig
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleGroup(domain) {
|
toggleGroup(domain) {
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ const UI = {
|
|||||||
if (state.isFirst) {
|
if (state.isFirst) {
|
||||||
overlay.classList.add('blocking');
|
overlay.classList.add('blocking');
|
||||||
// Apply blur to grid
|
// Apply blur to grid
|
||||||
const grid = document.querySelector('.grid-stack');
|
const grid = document.querySelector('.tab-panels');
|
||||||
if (grid) grid.classList.add('blurred');
|
if (grid) grid.classList.add('blurred');
|
||||||
} else {
|
} else {
|
||||||
overlay.classList.remove('blocking');
|
overlay.classList.remove('blocking');
|
||||||
@@ -465,7 +465,7 @@ const UI = {
|
|||||||
|
|
||||||
if (incidentId === App.currentIncidentId) {
|
if (incidentId === App.currentIncidentId) {
|
||||||
// Remove blur
|
// Remove blur
|
||||||
const grid = document.querySelector('.grid-stack');
|
const grid = document.querySelector('.tab-panels');
|
||||||
if (grid) grid.classList.remove('blurred');
|
if (grid) grid.classList.remove('blurred');
|
||||||
|
|
||||||
const overlay = document.getElementById('progress-overlay');
|
const overlay = document.getElementById('progress-overlay');
|
||||||
@@ -559,7 +559,7 @@ const UI = {
|
|||||||
if (!incidentId) incidentId = App.currentIncidentId;
|
if (!incidentId) incidentId = App.currentIncidentId;
|
||||||
|
|
||||||
// Remove blur
|
// Remove blur
|
||||||
const grid = document.querySelector('.grid-stack');
|
const grid = document.querySelector('.tab-panels');
|
||||||
if (grid) grid.classList.remove('blurred');
|
if (grid) grid.classList.remove('blurred');
|
||||||
|
|
||||||
if (incidentId === App.currentIncidentId) {
|
if (incidentId === App.currentIncidentId) {
|
||||||
@@ -787,11 +787,15 @@ const UI = {
|
|||||||
|
|
||||||
const buildPill = (src, fallbackName) => {
|
const buildPill = (src, fallbackName) => {
|
||||||
const displayName = src ? (src.name || fallbackName) : 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) {
|
if (src && src.url) {
|
||||||
return `<a href="${this.escape(src.url)}" target="_blank" rel="noopener" class="dev-source-pill" title="${esc}">${esc}</a>`;
|
return `<a href="${this.escape(src.url)}" target="_blank" rel="noopener" class="dev-source-pill" title="${titleEsc}">${esc}</a>`;
|
||||||
}
|
}
|
||||||
return `<span class="dev-source-pill" title="${esc}">${esc}</span>`;
|
return `<span class="dev-source-pill" title="${titleEsc}">${esc}</span>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cards = bulletLines.map(line => {
|
const cards = bulletLines.map(line => {
|
||||||
|
|||||||
@@ -1,295 +1,75 @@
|
|||||||
/**
|
/**
|
||||||
* LayoutManager: Drag & Resize Dashboard-Layout mit gridstack.js
|
* LayoutManager: Tab-Navigation fuer das Monitor-Dashboard.
|
||||||
* Persistenz über localStorage, Reset auf Standard-Layout möglich.
|
* Nur ein Tab-Panel gleichzeitig sichtbar, pro Lage gemerkt in localStorage.
|
||||||
*/
|
*/
|
||||||
const LayoutManager = {
|
const LayoutManager = {
|
||||||
_grid: null,
|
TAB_ORDER: ['zusammenfassung', 'lagebild', 'timeline', 'karte', 'faktencheck', 'quellen'],
|
||||||
_storageKey: 'osint_layout',
|
_currentIncidentId: null,
|
||||||
_initialized: false,
|
_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() {
|
init() {
|
||||||
if (this._initialized) return;
|
if (this._initialized) return;
|
||||||
|
const nav = document.getElementById('tab-nav');
|
||||||
|
if (!nav) return;
|
||||||
|
|
||||||
const container = document.querySelector('.grid-stack');
|
nav.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
if (!container) return;
|
btn.addEventListener('click', () => {
|
||||||
|
const tab = btn.getAttribute('data-tab');
|
||||||
this._grid = GridStack.init({
|
if (tab) this.switchTab(tab);
|
||||||
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 });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
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');
|
nav.style.display = '';
|
||||||
if (toolbar) toolbar.style.display = 'flex';
|
|
||||||
|
|
||||||
this._syncToggles();
|
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
_applyLayout(layout) {
|
switchTab(tabId, save = true) {
|
||||||
if (!this._grid) return;
|
if (!this.TAB_ORDER.includes(tabId)) tabId = 'zusammenfassung';
|
||||||
|
|
||||||
this._hiddenTiles = {};
|
document.querySelectorAll('#tab-nav .tab-btn').forEach(b => {
|
||||||
|
b.classList.toggle('active', b.getAttribute('data-tab') === tabId);
|
||||||
layout.forEach(item => {
|
});
|
||||||
const el = this._grid.engine.nodes.find(n => n.el && n.el.getAttribute('gs-id') === item.id);
|
document.querySelectorAll('.tab-panel').forEach(p => {
|
||||||
if (!el) return;
|
p.classList.toggle('active', p.id === 'panel-' + tabId);
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
restoreTabFor(incidentId) {
|
||||||
if (!this._grid) return;
|
this._currentIncidentId = incidentId;
|
||||||
|
let target = 'zusammenfassung';
|
||||||
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 });
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(this._storageKey, JSON.stringify(items));
|
const saved = localStorage.getItem('osint_tab_' + incidentId);
|
||||||
} catch (e) { /* quota */ }
|
if (saved && this.TAB_ORDER.includes(saved)) target = saved;
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
this.switchTab(target, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
_debouncedSave() {
|
/** Tab-Labels je Incident-Typ anpassen (adhoc vs. research). */
|
||||||
clearTimeout(this._saveTimeout);
|
applyTypeLabels(incidentType) {
|
||||||
this._saveTimeout = setTimeout(() => this.save(), 300);
|
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() {
|
// Legacy-API-Stubs: falls alte Aufrufe im Code liegen, stumm schlucken statt crashen.
|
||||||
try {
|
toggleTile() { /* legacy no-op */ },
|
||||||
const raw = localStorage.getItem(this._storageKey);
|
reset() { /* legacy no-op */ },
|
||||||
if (!raw) return null;
|
save() { /* legacy no-op */ },
|
||||||
const parsed = JSON.parse(raw);
|
resizeTileToContent() { /* legacy no-op */ },
|
||||||
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
destroy() { /* legacy no-op */ },
|
||||||
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 = {};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => LayoutManager.init());
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren