Rechte Sidebar + Monitor-Feed nur Naturkatastrophen
SIDEBAR (neu): - Aufklappbare Uebersicht aller aktiven Datenpunkte rechts - Suchfeld filtert alle Eintraege - Sektionen: OSINT Monitor, Katastrophen (auffaecherbar) - Flug/Schiff/Satelliten als Zaehler - Klick auf Eintrag fliegt zur Position - Ein-/ausklappbar per Pfeil-Button am rechten Rand MONITOR-FEED FIX: - Nur Lage 45 (Naturkatastrophen international) wird abgefragt - Keine anderen Lagen mehr im Globe sichtbar
Dieser Commit ist enthalten in:
@@ -16,9 +16,9 @@ _cache: dict[str, tuple] = {}
|
||||
|
||||
|
||||
@router.get("/monitor-feed")
|
||||
async def get_monitor_feed(incident_id: int = Query(None)):
|
||||
async def get_monitor_feed(incident_id: int = Query(45)):
|
||||
"""Holt OSINT-Daten vom AegisSight Monitor."""
|
||||
cache_key = f"monitor:{incident_id or 'all'}"
|
||||
cache_key = f"monitor:{incident_id}"
|
||||
if cache_key in _cache and time.time() - _cache[cache_key][0] < 120:
|
||||
return _cache[cache_key][1]
|
||||
|
||||
|
||||
@@ -256,3 +256,159 @@ html, body { height: 100%; overflow: hidden; background: var(--bg-primary); colo
|
||||
line-height: 1.4;
|
||||
}
|
||||
.layer-toggle, .layer-name, .vmode-btn { position: relative; }
|
||||
|
||||
|
||||
/* === Rechte Sidebar === */
|
||||
.sidebar-right {
|
||||
position: fixed;
|
||||
top: 44px;
|
||||
right: 0;
|
||||
bottom: 28px;
|
||||
width: 280px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.sidebar-right.collapsed {
|
||||
transform: translateX(280px);
|
||||
}
|
||||
.sidebar-right.collapsed .sidebar-toggle {
|
||||
transform: translateX(-32px);
|
||||
}
|
||||
.sidebar-toggle {
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: 12px;
|
||||
width: 28px;
|
||||
height: 36px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
border-radius: 6px 0 0 6px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
z-index: 101;
|
||||
}
|
||||
.sidebar-toggle:hover { color: var(--accent); border-color: var(--accent); }
|
||||
|
||||
.sidebar-inner {
|
||||
flex: 1;
|
||||
background: var(--bg-panel);
|
||||
border-left: 1px solid var(--border);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sidebar-search-wrap {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-search {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
outline: none;
|
||||
}
|
||||
.sidebar-search:focus { border-color: var(--accent); }
|
||||
.sidebar-search::placeholder { color: var(--text-dim); }
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.sidebar-content::-webkit-scrollbar { width: 4px; }
|
||||
.sidebar-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
/* Sections */
|
||||
.sb-section { border-bottom: 1px solid rgba(255,255,255,0.04); }
|
||||
.sb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.sb-header:hover { background: rgba(255,255,255,0.04); }
|
||||
.sb-header-count { cursor: default; }
|
||||
.sb-chevron {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
transition: transform 0.2s;
|
||||
width: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.sb-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
}
|
||||
.sb-badge {
|
||||
font-size: 9px;
|
||||
color: var(--accent);
|
||||
background: rgba(0,255,136,0.1);
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Items */
|
||||
.sb-list { padding: 0 4px 4px; }
|
||||
.sb-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.sb-item:hover { background: rgba(0,255,136,0.06); }
|
||||
.sb-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.sb-item-text { min-width: 0; flex: 1; }
|
||||
.sb-item-name {
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sb-item-sub {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sb-more {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim);
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.sb-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
@@ -126,6 +126,22 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
<!-- Rechte Sidebar: Datenpunkt-Uebersicht -->
|
||||
<aside id="sidebar-right" class="sidebar-right">
|
||||
<button id="sidebar-toggle" class="sidebar-toggle" title="Seitenleiste ein-/ausblenden">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
<div class="sidebar-inner">
|
||||
<div class="sidebar-search-wrap">
|
||||
<input type="text" id="sidebar-search" class="sidebar-search" placeholder="Suchen...">
|
||||
</div>
|
||||
<div id="sidebar-content" class="sidebar-content">
|
||||
<div class="sb-empty">Keine Layer aktiv</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- CesiumJS Container -->
|
||||
<div id="cesiumContainer"></div>
|
||||
<div id="visual-overlay" class="visual-overlay" style="display:none"></div>
|
||||
@@ -145,6 +161,7 @@
|
||||
<script src="/static/js/layers/gdelt.js"></script>
|
||||
<script src="/static/js/layers/satellites.js"></script>
|
||||
<script src="/static/js/layers/disasters.js"></script>
|
||||
<script src="/static/js/ui/sidebar.js"></script>
|
||||
<script src="/static/js/layers/monitor.js"></script>
|
||||
<script src="/static/js/layers/visualmodes.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -82,6 +82,7 @@ const Globe = {
|
||||
|
||||
this._toggleLabels(true);
|
||||
if (typeof VisualModes !== 'undefined') VisualModes.init();
|
||||
if (typeof Sidebar !== 'undefined') Sidebar.init();
|
||||
document.getElementById('bottom-stats').textContent = 'Globe initialisiert — Lade Daten...';
|
||||
},
|
||||
|
||||
|
||||
171
static/js/ui/sidebar.js
Normale Datei
171
static/js/ui/sidebar.js
Normale Datei
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Rechte Sidebar: Uebersicht aller aktiven Datenpunkte mit Suche.
|
||||
*/
|
||||
const Sidebar = {
|
||||
_open: true,
|
||||
_searchTerm: '',
|
||||
|
||||
init() {
|
||||
var self = this;
|
||||
var toggle = document.getElementById('sidebar-toggle');
|
||||
var search = document.getElementById('sidebar-search');
|
||||
if (toggle) toggle.addEventListener('click', function() { self.toggle(); });
|
||||
if (search) search.addEventListener('input', function() {
|
||||
self._searchTerm = this.value.toLowerCase();
|
||||
self.update();
|
||||
});
|
||||
// Periodisch aktualisieren
|
||||
setInterval(function() { self.update(); }, 5000);
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this._open = !this._open;
|
||||
var panel = document.getElementById('sidebar-right');
|
||||
var toggle = document.getElementById('sidebar-toggle');
|
||||
if (panel) panel.classList.toggle('collapsed', !this._open);
|
||||
if (toggle) toggle.innerHTML = this._open
|
||||
? '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>'
|
||||
: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>';
|
||||
},
|
||||
|
||||
update() {
|
||||
var container = document.getElementById('sidebar-content');
|
||||
if (!container) return;
|
||||
var html = '';
|
||||
var term = this._searchTerm;
|
||||
|
||||
// Monitor OSINT
|
||||
if (typeof MonitorLayer !== 'undefined' && MonitorLayer._data.length) {
|
||||
html += this._renderSection('monitor', 'OSINT Monitor', MonitorLayer._data, function(f) {
|
||||
var p = f.properties;
|
||||
return {
|
||||
name: p.name || '?',
|
||||
sub: (p.incident_title || '') + (p.article_count > 1 ? ' (' + p.article_count + ' Artikel)' : ''),
|
||||
color: { primary: '#ff2222', secondary: '#ff8800', tertiary: '#4499ff', mentioned: '#888' }[p.category] || '#888',
|
||||
lat: f.geometry.coordinates[1],
|
||||
lon: f.geometry.coordinates[0],
|
||||
};
|
||||
}, term);
|
||||
}
|
||||
|
||||
// Katastrophen
|
||||
if (typeof DisastersLayer !== 'undefined' && DisastersLayer._dataSource && DisastersLayer._count > 0) {
|
||||
var entities = DisastersLayer._dataSource.entities.values;
|
||||
var items = [];
|
||||
for (var i = 0; i < entities.length; i++) {
|
||||
var e = entities[i];
|
||||
var name = e.name || (e._name && e._name.getValue ? e._name.getValue() : '');
|
||||
if (!name && e.description) {
|
||||
var match = e.description.getValue().match(/<strong[^>]*>([^<]+)<\/strong>/);
|
||||
if (match) name = match[1];
|
||||
}
|
||||
var pos = e.position ? e.position.getValue(Cesium.JulianDate.now()) : null;
|
||||
var carto = pos ? Cesium.Cartographic.fromCartesian(pos) : null;
|
||||
items.push({
|
||||
name: name || 'Ereignis',
|
||||
sub: '',
|
||||
color: '#ff4400',
|
||||
lat: carto ? Cesium.Math.toDegrees(carto.latitude) : 0,
|
||||
lon: carto ? Cesium.Math.toDegrees(carto.longitude) : 0,
|
||||
entity: e,
|
||||
});
|
||||
}
|
||||
html += this._renderEntitySection('disasters', 'Katastrophen + Erdbeben', items, term);
|
||||
}
|
||||
|
||||
// Flugverkehr (nur count, keine Liste — zu viele)
|
||||
if (typeof FlightsLayer !== 'undefined' && FlightsLayer._count > 0) {
|
||||
html += this._renderCountSection('flights', 'Flugverkehr', FlightsLayer._count, '#00ff88');
|
||||
}
|
||||
|
||||
// Schiffsverkehr
|
||||
if (typeof ShipsLayer !== 'undefined' && ShipsLayer._count > 0) {
|
||||
html += this._renderCountSection('ships', 'Schiffsverkehr', ShipsLayer._count, '#4499ff');
|
||||
}
|
||||
|
||||
// Satelliten
|
||||
if (typeof SatellitesLayer !== 'undefined' && SatellitesLayer._count > 0) {
|
||||
html += this._renderCountSection('satellites', 'Satelliten', SatellitesLayer._count, '#ff4444');
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<div class="sb-empty">Keine Layer aktiv</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
this._attachListeners();
|
||||
},
|
||||
|
||||
_renderSection(id, title, features, mapFn, term) {
|
||||
var items = features.map(mapFn).filter(function(item) {
|
||||
if (!term) return true;
|
||||
return (item.name + ' ' + item.sub).toLowerCase().indexOf(term) >= 0;
|
||||
});
|
||||
if (!items.length) return '';
|
||||
return '<div class="sb-section" id="sb-' + id + '">' +
|
||||
'<div class="sb-header" onclick="Sidebar._toggleSection(\'' + id + '\')">' +
|
||||
'<span class="sb-chevron" id="sb-chev-' + id + '">▾</span>' +
|
||||
'<span class="sb-title">' + title + '</span>' +
|
||||
'<span class="sb-badge">' + items.length + '</span></div>' +
|
||||
'<div class="sb-list" id="sb-list-' + id + '">' +
|
||||
items.slice(0, 100).map(function(item) {
|
||||
return '<div class="sb-item" onclick="Sidebar._flyTo(' + item.lat + ',' + item.lon + ')" title="' + item.name + '">' +
|
||||
'<span class="sb-dot" style="background:' + item.color + '"></span>' +
|
||||
'<div class="sb-item-text"><div class="sb-item-name">' + item.name + '</div>' +
|
||||
(item.sub ? '<div class="sb-item-sub">' + item.sub + '</div>' : '') +
|
||||
'</div></div>';
|
||||
}).join('') +
|
||||
(items.length > 100 ? '<div class="sb-more">+ ' + (items.length - 100) + ' weitere</div>' : '') +
|
||||
'</div></div>';
|
||||
},
|
||||
|
||||
_renderEntitySection(id, title, items, term) {
|
||||
if (term) {
|
||||
items = items.filter(function(item) { return item.name.toLowerCase().indexOf(term) >= 0; });
|
||||
}
|
||||
if (!items.length) return '';
|
||||
return '<div class="sb-section" id="sb-' + id + '">' +
|
||||
'<div class="sb-header" onclick="Sidebar._toggleSection(\'' + id + '\')">' +
|
||||
'<span class="sb-chevron" id="sb-chev-' + id + '">▾</span>' +
|
||||
'<span class="sb-title">' + title + '</span>' +
|
||||
'<span class="sb-badge">' + items.length + '</span></div>' +
|
||||
'<div class="sb-list" id="sb-list-' + id + '">' +
|
||||
items.slice(0, 100).map(function(item, idx) {
|
||||
return '<div class="sb-item" onclick="Sidebar._flyTo(' + item.lat + ',' + item.lon + ')" title="' + item.name + '">' +
|
||||
'<span class="sb-dot" style="background:' + item.color + '"></span>' +
|
||||
'<div class="sb-item-text"><div class="sb-item-name">' + item.name + '</div></div></div>';
|
||||
}).join('') +
|
||||
'</div></div>';
|
||||
},
|
||||
|
||||
_renderCountSection(id, title, count, color) {
|
||||
return '<div class="sb-section">' +
|
||||
'<div class="sb-header sb-header-count">' +
|
||||
'<span class="sb-dot" style="background:' + color + '"></span>' +
|
||||
'<span class="sb-title">' + title + '</span>' +
|
||||
'<span class="sb-badge">' + count.toLocaleString('de-DE') + '</span></div></div>';
|
||||
},
|
||||
|
||||
_toggleSection(id) {
|
||||
var list = document.getElementById('sb-list-' + id);
|
||||
var chev = document.getElementById('sb-chev-' + id);
|
||||
if (list) {
|
||||
var open = list.style.display !== 'none';
|
||||
list.style.display = open ? 'none' : 'block';
|
||||
if (chev) chev.style.transform = open ? 'rotate(-90deg)' : '';
|
||||
}
|
||||
},
|
||||
|
||||
_flyTo(lat, lon) {
|
||||
if (typeof Globe !== 'undefined' && Globe.viewer) {
|
||||
Globe.viewer.camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 800000),
|
||||
duration: 1.5,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_attachListeners() {
|
||||
// Wird nach jedem Update aufgerufen
|
||||
},
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren