/** * AegisSight Tutorial - Interaktiver Rundgang durch den Monitor. * Erzeugt eine Demo-Lage mit Platzhalter-Inhalten, sodass der Rundgang * auch ohne bestehende Lagen funktioniert. */ const Tutorial = { _steps: [], _currentStep: -1, _isActive: false, _resizeTimer: null, _keyHandler: null, _resizeHandler: null, _demoRunning: false, _lastExitedStep: -1, _highestStep: -1, _stepTimers: [], // setTimeout-IDs fuer den aktuellen Step _savedState: null, // Dashboard-Zustand vor dem Tutorial // DOM-Referenzen _els: { overlay: null, spotlight: null, bubble: null, cursor: null, }, init() { this._defineSteps(); this._els.overlay = document.getElementById('tutorial-overlay'); this._els.spotlight = document.getElementById('tutorial-spotlight'); this._els.bubble = document.getElementById('tutorial-bubble'); this._els.cursor = document.getElementById('tutorial-cursor'); }, // ----------------------------------------------------------------------- // Demo-Daten // ----------------------------------------------------------------------- _DEMO_SIDEBAR_ITEM: '
' + '' + '
' + '
Explosion in Hamburger Hafen
' + '
14 Artikel · demo-user
' + '
' + '' + '
', _DEMO_SIDEBAR_RESEARCH: '
' + '' + '
' + '
Analyse: Cyberangriffe auf Kritische Infrastruktur 2026
' + '
8 Artikel · demo-user
' + '
' + '
', _DEMO_SUMMARY: '

Am 16. März 2026 kam es im Hamburger Hafen zu einer Explosion ' + 'im Containerterminal Burchardkai ' + '[1] ' + '[2]. ' + 'Die Detonation ereignete sich gegen 06:45 Uhr in einem Gefahrgutbereich. ' + 'Mindestens 12 Personen wurden verletzt ' + '[3].

' + '

Die Feuerwehr ist mit einem Großaufgebot vor Ort. Der Hafenbetrieb im betroffenen Terminal wurde ' + 'vorübergehend eingestellt ' + '[4]. ' + 'Die Ursache ist noch unklar ' + '[5].

', _DEMO_FACTCHECKS: [ { status: 'confirmed', icon: '✓', claim: 'Eine Explosion ereignete sich am 16.03.2026 gegen 06:45 Uhr im Hamburger Hafen.', sources: 5 }, { status: 'confirmed', icon: '✓', claim: 'Mindestens 12 Personen wurden bei dem Vorfall verletzt.', sources: 3 }, { status: 'established', icon: '✓', claim: 'Die Explosion fand im Bereich des Burchardkai-Terminals statt.', sources: 4 }, { status: 'unconfirmed', icon: '?', claim: 'Es soll sich um eine Fehlfunktion in einem Gefahrgutcontainer handeln.', sources: 1 }, { status: 'disputed', icon: '⚠', claim: 'Die Rauchwolke soll gesundheitsgefährdende Stoffe enthalten.', sources: 2 }, { status: 'contradicted', icon: '✗', claim: 'Der gesamte Hamburger Hafen wurde geschlossen.', sources: 2 }, ], _DEMO_SOURCES_STATS: '5 Kategorien · 14 Quellen · 14 Artikel', _DEMO_TIMELINE: [ { time: '06:45', title: 'Explosion im Containerterminal Burchardkai', source: 'dpa', type: 'article' }, { time: '07:02', title: 'Hamburger Feuerwehr rückt mit Großaufgebot aus', source: 'NDR', type: 'article' }, { time: '07:15', title: 'Polizei sperrt Hafengebiet weiträumig ab', source: 'Hamburger Abendblatt', type: 'article' }, { time: '07:30', title: 'Erste Meldungen über Verletzte', source: 'Reuters', type: 'article' }, { time: '08:00', title: 'Hamburg Port Authority stoppt Betrieb im Terminal', source: 'HPA', type: 'article' }, { time: '08:22', title: 'Lagebericht: Erster Überblick zum Vorfall', source: 'AegisSight', type: 'snapshot' }, { time: '09:10', title: 'Gefahrgut-Spezialisten am Einsatzort eingetroffen', source: 'tagesschau.de', type: 'article' }, ], _buildDemoTimelineHTML() { // Achsen-basierte Timeline wie im Original var entries = this._DEMO_TIMELINE; var times = ['06:45','07:02','07:15','07:30','08:00','08:22','09:10']; var startMin = 6*60+45; var endMin = 9*60+10; var range = endMin - startMin; var html = '
'; // Datums-Marker html += '
'; html += '
16. Mär.
'; html += '
'; // Punkte html += '
'; entries.forEach(function(e, i) { var parts = e.time.split(':'); var min = parseInt(parts[0])*60 + parseInt(parts[1]); var pos = ((min - startMin) / range) * 92 + 4; var isSnapshot = e.type === 'snapshot'; var cls = 'ht-point' + (isSnapshot ? ' ht-snapshot-point' : '') + (i === 0 ? ' active' : ''); var size = isSnapshot ? 14 : 10; html += '
'; html += '
' + e.time + ': ' + e.title + '
'; html += '
'; }); html += '
'; // Achsenlinie html += '
'; // Labels html += '
'; ['07:00','08:00','09:00'].forEach(function(t) { var parts = t.split(':'); var min = parseInt(parts[0])*60; var pos = ((min - startMin) / range) * 92 + 4; html += '
' + t + '
'; }); html += '
'; html += '
'; // Detail-Panel fuer aktiven Punkt html += '
'; var first = entries[0]; html += '
16.03.2026, ' + first.time + '1 Eintrag
'; html += '
'; html += '
' + first.source + '
' + first.title + '
'; html += '
'; return html; }, // ----------------------------------------------------------------------- // Demo-View injizieren / entfernen // ----------------------------------------------------------------------- _injectDemoView() { // Zustand sichern var incidentView = document.getElementById('incident-view'); var emptyState = document.getElementById('empty-state'); this._savedState = { gridWasInitialized: typeof LayoutManager !== 'undefined' && LayoutManager._initialized, incidentViewDisplay: incidentView ? incidentView.style.display : 'none', emptyStateDisplay: emptyState ? emptyState.style.display : '', incidentTitle: document.getElementById('incident-title') ? document.getElementById('incident-title').textContent : '', summaryText: document.getElementById('summary-text') ? document.getElementById('summary-text').innerHTML : '', factcheckList: document.getElementById('factcheck-list') ? document.getElementById('factcheck-list').innerHTML : '', sourceStats: document.getElementById('source-overview-header-stats') ? document.getElementById('source-overview-header-stats').innerHTML : '', timeline: document.getElementById('timeline') ? document.getElementById('timeline').innerHTML : '', mapEmpty: document.getElementById('map-empty') ? document.getElementById('map-empty').style.display : '', layoutToolbar: document.getElementById('layout-toolbar') ? document.getElementById('layout-toolbar').style.display : 'none', typeBadge: document.getElementById('incident-type-badge') ? document.getElementById('incident-type-badge').innerHTML : '', refreshMode: document.getElementById('meta-refresh-mode') ? document.getElementById('meta-refresh-mode').innerHTML : '', headerStrip: document.getElementById('incident-header-strip') ? document.getElementById('incident-header-strip').style.display : '', fcFilters: document.getElementById('fc-filters') ? document.getElementById('fc-filters').innerHTML : '', mapStats: document.getElementById('map-stats') ? document.getElementById('map-stats').innerHTML : '', sidebarItems: null, }; // Sidebar: Demo-Einträge hinzufügen var activeList = document.getElementById('active-incidents'); if (activeList) { this._savedState.sidebarItems = activeList.innerHTML; activeList.insertAdjacentHTML('afterbegin', this._DEMO_SIDEBAR_ITEM); } // Empty-State verstecken, Incident-View anzeigen if (emptyState) emptyState.style.display = 'none'; if (incidentView) incidentView.style.display = ''; // Header var title = document.getElementById('incident-title'); if (title) title.textContent = 'Explosion in Hamburger Hafen'; var badge = document.getElementById('incident-type-badge'); if (badge) { badge.textContent = 'Live-Monitoring'; badge.className = 'incident-type-badge type-adhoc'; } var refreshMode = document.getElementById('meta-refresh-mode'); if (refreshMode) refreshMode.innerHTML = 'Auto-Refresh: 15 Min'; var creator = document.getElementById('incident-creator'); if (creator) creator.textContent = 'demo-user'; var desc = document.getElementById('incident-description'); if (desc) desc.textContent = 'Schwere Explosion im Hamburger Hafengebiet, Burchardkai-Terminal'; // Layout-Toolbar var toolbar = document.getElementById('layout-toolbar'); if (toolbar) toolbar.style.display = ''; // GridStack initialisieren falls noch nicht geschehen if (typeof LayoutManager !== 'undefined' && !LayoutManager._initialized) { LayoutManager.init(); } // Aktuelles Layout sichern und Standard-Layout erzwingen if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) { this._savedState.savedLayout = LayoutManager._grid.save(false); LayoutManager._applyLayout(LayoutManager.DEFAULT_LAYOUT); } // GridStack Resize triggern damit Kacheln korrekt positioniert werden if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) { LayoutManager._grid.engine.nodes.forEach(function(n) { LayoutManager._grid.update(n.el, { x: n.x, y: n.y, w: n.w, h: n.h }); }); // Sicherheits-Relayout setTimeout(function() { if (LayoutManager._grid) { LayoutManager._grid.compact(); LayoutManager._grid.engine.nodes.forEach(function(n) { if (n.el) n.el.style.position = ''; }); } }, 100); } // Lagebild var summaryText = document.getElementById('summary-text'); if (summaryText) summaryText.innerHTML = this._DEMO_SUMMARY; // Timestamp var ts = document.getElementById('lagebild-timestamp'); if (ts) ts.textContent = '16.03.2026, 08:22'; // Faktencheck var fcList = document.getElementById('factcheck-list'); if (fcList) { var fcHtml = ''; this._DEMO_FACTCHECKS.forEach(function(fc) { fcHtml += '
' + '' + '
' + '
' + fc.claim + '
' + '
' + '' + fc.sources + ' Quelle' + (fc.sources !== 1 ? 'n' : '') + '' + '
'; }); fcList.innerHTML = fcHtml; } // Faktencheck-Filter var fcFilters = document.getElementById('fc-filters'); if (fcFilters) { fcFilters.innerHTML = ''; } // Quellenübersicht var sourceStats = document.getElementById('source-overview-header-stats'); if (sourceStats) sourceStats.innerHTML = this._DEMO_SOURCES_STATS; // Timeline (Achsen-basiert wie im Original) var timeline = document.getElementById('timeline'); if (timeline) { timeline.innerHTML = this._buildDemoTimelineHTML(); } var articleCount = document.getElementById('article-count'); if (articleCount) articleCount.textContent = '7 Einträge'; // Karte: Echte Leaflet-Map in der Kachel initialisieren var mapEmpty = document.getElementById('map-empty'); if (mapEmpty) mapEmpty.style.display = 'none'; var mapStats = document.getElementById('map-stats'); if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel'; this._initDemoMapInTile(); // Meta var metaUpdated = document.getElementById('meta-updated'); if (metaUpdated) metaUpdated.textContent = 'Aktualisiert: 16.03.2026, 09:10'; }, _removeDemoView() { if (!this._savedState) return; var s = this._savedState; // Sidebar Demo-Einträge entfernen document.querySelectorAll('.tutorial-demo').forEach(function(el) { el.remove(); }); // Sidebar wiederherstellen var activeList = document.getElementById('active-incidents'); if (activeList && s.sidebarItems !== null) activeList.innerHTML = s.sidebarItems; // Views wiederherstellen var incidentView = document.getElementById('incident-view'); if (incidentView) incidentView.style.display = s.incidentViewDisplay; var emptyState = document.getElementById('empty-state'); if (emptyState) emptyState.style.display = s.emptyStateDisplay; // Inhalte wiederherstellen var title = document.getElementById('incident-title'); if (title) title.textContent = s.incidentTitle; var summaryText = document.getElementById('summary-text'); if (summaryText) summaryText.innerHTML = s.summaryText; var fcList = document.getElementById('factcheck-list'); if (fcList) fcList.innerHTML = s.factcheckList; var sourceStats = document.getElementById('source-overview-header-stats'); if (sourceStats) sourceStats.innerHTML = s.sourceStats; var timeline = document.getElementById('timeline'); if (timeline) timeline.innerHTML = s.timeline; var mapEmpty = document.getElementById('map-empty'); if (mapEmpty) mapEmpty.style.display = s.mapEmpty; var toolbar = document.getElementById('layout-toolbar'); if (toolbar) toolbar.style.display = s.layoutToolbar; var badge = document.getElementById('incident-type-badge'); if (badge) badge.innerHTML = s.typeBadge; var refreshMode = document.getElementById('meta-refresh-mode'); if (refreshMode) refreshMode.innerHTML = s.refreshMode; var fcFilters = document.getElementById('fc-filters'); if (fcFilters) fcFilters.innerHTML = s.fcFilters; var mapStats = document.getElementById('map-stats'); if (mapStats) mapStats.innerHTML = s.mapStats; // Demo-Map entfernen (Kachel + Fullscreen) this._destroyDemoMap(); // Meta var metaUpdated = document.getElementById('meta-updated'); if (metaUpdated) metaUpdated.textContent = ''; var creator = document.getElementById('incident-creator'); if (creator) creator.textContent = ''; var desc = document.getElementById('incident-description'); if (desc) desc.textContent = ''; var ts = document.getElementById('lagebild-timestamp'); if (ts) ts.textContent = ''; // Gespeichertes Layout wiederherstellen if (typeof LayoutManager !== 'undefined' && LayoutManager._grid && s.savedLayout) { LayoutManager._applyLayout(s.savedLayout); } this._savedState = null; }, // ----------------------------------------------------------------------- // Demo-Karte mit Leaflet // ----------------------------------------------------------------------- _demoMap: null, _demoMapMarkers: [], _demoMapLegend: null, _demoMapTileMap: null, // Map-Instanz in der Kachel _DEMO_MAP_LOCATIONS: [ { lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' }, { lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' }, { lat: 53.5380, lon: 9.9400, name: 'Elbe / Hafengebiet', articles: 1, cat: 'tertiary' }, ], _DEMO_MAP_COLORS: { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' }, _DEMO_MAP_LABELS: { primary: 'Hauptereignisort', secondary: 'Erwähnt', tertiary: 'Kontext' }, _createDemoMarkers(map) { var markers = []; var self = this; this._DEMO_MAP_LOCATIONS.forEach(function(loc) { var color = self._DEMO_MAP_COLORS[loc.cat]; var icon = L.divIcon({ className: 'tutorial-map-marker', html: '
', iconSize: [14, 14], iconAnchor: [7, 7], }); var marker = L.marker([loc.lat, loc.lon], { icon: icon }); var label = self._DEMO_MAP_LABELS[loc.cat]; var popupHtml = '
' + '
' + loc.name + '
' + '
' + label + '
' + '
' + loc.articles + ' Artikel
' + '
'; marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' }); marker.addTo(map); markers.push(marker); }); return markers; }, _addDemoLegend(map) { var self = this; var legend = L.control({ position: 'bottomright' }); legend.onAdd = function() { var div = L.DomUtil.create('div', 'map-legend-ctrl'); L.DomEvent.disableClickPropagation(div); var html = 'Legende'; ['primary', 'secondary', 'tertiary'].forEach(function(cat) { html += '
' + '' + '' + self._DEMO_MAP_LABELS[cat] + '
'; }); div.innerHTML = html; return div; }; legend.addTo(map); return legend; }, // Map in der Dashboard-Kachel initialisieren _initDemoMapInTile() { if (typeof L === 'undefined') return; var container = document.getElementById('map-container'); if (!container) return; // Container-Höhe sicherstellen var gsItem = container.closest('.grid-stack-item'); if (gsItem) { var hdr = container.closest('.map-card'); var headerEl = hdr ? hdr.querySelector('.card-header') : null; var headerH = headerEl ? headerEl.offsetHeight : 40; var available = gsItem.offsetHeight - headerH - 4; container.style.height = Math.max(available, 200) + 'px'; } this._demoMapTileMap = L.map(container, { zoomControl: true, attributionControl: false, }).setView([53.545, 9.98], 12); L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { maxZoom: 18, noWrap: true, }).addTo(this._demoMapTileMap); this._createDemoMarkers(this._demoMapTileMap); this._addDemoLegend(this._demoMapTileMap); var map = this._demoMapTileMap; [200, 500, 1000].forEach(function(d) { setTimeout(function() { if (map) map.invalidateSize(); }, d); }); }, _destroyDemoMap() { if (this._demoMap) { this._demoMap.remove(); this._demoMap = null; } if (this._demoMapTileMap) { this._demoMapTileMap.remove(); this._demoMapTileMap = null; } this._demoMapMarkers = []; this._demoMapLegend = null; }, // ----------------------------------------------------------------------- // Highlight-Helfer: Einzelnes Sub-Element innerhalb einer Kachel markieren // ----------------------------------------------------------------------- _highlightSub(selector) { // Ignorieren wenn der Step bereits verlassen wurde if (!this._isActive) return; var el = document.querySelector(selector); if (!el) return; el.classList.add('tutorial-sub-highlight'); }, _clearSubHighlights() { document.querySelectorAll('.tutorial-sub-highlight').forEach(function(el) { el.classList.remove('tutorial-sub-highlight'); }); }, _cleanupFns: [], // Timer-Helfer: Timeouts die beim Step-Wechsel automatisch gecancelt werden _stepTimeout(fn, ms) { var id = setTimeout(fn, ms); this._stepTimers.push(id); return id; }, _clearStepTimers() { this._stepTimers.forEach(function(id) { clearTimeout(id); }); this._stepTimers = []; }, // Sichere Demo-Ausfuehrung: Faengt Fehler ab und stellt Navigation sicher _runDemo(fn) { var self = this; var finished = false; function done() { if (finished) return; finished = true; self._hideCursor(); self._demoRunning = false; self._enableNavAfterDemo(); } // Fallback-Timeout: Nach 30s wird Demo auf jeden Fall beendet var fallback = setTimeout(done, 30000); try { var result = fn.call(this); if (result && typeof result.then === 'function') { result.then(function() { clearTimeout(fallback); // Demo-Methode hat _enableNavAfterDemo selbst aufgerufen, // aber falls nicht, machen wir es hier if (self._demoRunning) done(); }).catch(function(e) { clearTimeout(fallback); done(); }); } else { // Synchron beendet clearTimeout(fallback); if (self._demoRunning) done(); } } catch(e) { clearTimeout(fallback); done(); } }, // ----------------------------------------------------------------------- // Scroll-Helfer: Element in den sichtbaren Bereich scrollen // ----------------------------------------------------------------------- _scrollToTarget(selector) { return new Promise(function(resolve) { var el = document.querySelector(selector); if (!el) { resolve(); return; } var rect = el.getBoundingClientRect(); var vh = window.innerHeight; if (rect.top >= 0 && rect.bottom <= vh) { resolve(); return; } el.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(resolve, 500); }); }, // ----------------------------------------------------------------------- // Step-Definitionen // ----------------------------------------------------------------------- _defineSteps() { this._steps = [ // 0 - Welcome { id: 'welcome', target: '#main-content', title: 'Willkommen im AegisSight Monitor', text: 'Dieser interaktive Rundgang führt Sie durch alle Funktionen des OSINT-Monitors. ' + 'Wir werden gemeinsam eine Demo-Lage anlegen und alle Bereiche des Dashboards erkunden.

' + 'Navigieren Sie mit den Pfeiltasten oder den Buttons. Mit Escape können Sie jederzeit abbrechen.', position: 'center', }, // 1 - Sidebar { id: 'sidebar', target: '.sidebar', title: 'Seitenleiste: Ihre Lagen', text: 'Die Seitenleiste zeigt all Ihre Lagen in drei Bereichen:

' + 'Live-Monitoring - Laufende Ereignisbeobachtung mit automatischer Aktualisierung
' + 'Recherchen - Themenanalysen ohne Echtzeit-Updates
' + 'Archiv - Abgeschlossene Lagen zur späteren Einsicht

' + 'Klicken Sie auf eine Lage, um sie im Hauptbereich zu öffnen.', position: 'right', }, // 2 - Neue Lage Button { id: 'new-incident-btn', target: '#new-incident-btn', title: 'Neue Lage anlegen', text: 'Mit diesem Button öffnen Sie das Formular zur Erstellung einer neuen Lage. ' + 'Wir gehen jetzt gemeinsam alle Felder durch.', position: 'right', onEnter: function() { Tutorial._stepTimeout(function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); }, 1500); }, onExit: function() { var overlay = document.getElementById('modal-new'); if (overlay) overlay.classList.remove('active'); }, }, // 3 - Titel und Beschreibung (Cursor-Demo) { id: 'form-title-desc', target: '#modal-new .modal', title: 'Titel und Beschreibung', text: 'Geben Sie einen aussagekräftigen Titel ein, der das Ereignis klar beschreibt. ' + 'Die Beschreibung liefert zusätzlichen Kontext für die Recherche.

' + 'Beobachten Sie die Eingabe:', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; var modalBody = document.querySelector('#modal-new .modal-body'); if (modalBody) modalBody.scrollTop = 0; var t = document.getElementById('inc-title'); if (t) t.value = ''; var d = document.getElementById('inc-description'); if (d) d.value = ''; Tutorial._runDemo(Tutorial._simulateFormTitleDesc); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 4 - Art der Lage (Cursor-Demo) { id: 'form-type', target: '#modal-new .modal', title: 'Art der Lage', text: 'Live-Monitoring beobachtet ein Ereignis in Echtzeit. Hunderte Quellen werden ' + 'laufend durchsucht. Ideal für aktuelle Krisen und sich entwickelnde Lagen.

' + 'Analyse/Recherche untersucht ein Thema tiefergehend ohne automatische Updates. ' + 'Ideal für Hintergrundanalysen und Lageberichte.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; var modalBody = document.querySelector('#modal-new .modal-body'); if (modalBody) modalBody.scrollTo({ top: 0, behavior: 'smooth' }); Tutorial._stepTimeout(function() { Tutorial._highlightSub('#inc-type'); Tutorial._runDemo(Tutorial._simulateTypeSwitch); }, 500); }, onExit: function() { var sel = document.getElementById('inc-type'); if (sel) { sel.value = 'adhoc'; try { sel.dispatchEvent(new Event('change')); } catch(e) {} } Tutorial._clearSubHighlights(); }, }, // 5 - Quellen { id: 'form-sources', target: '#modal-new .modal', title: 'Quellen konfigurieren', text: 'Internationale Quellen bezieht englischsprachige und internationale Medien ' + 'ein (Reuters, BBC, Al Jazeera etc.). Erhöht die Abdeckung, aber auch den Analyseumfang.

' + 'Telegram-Kanäle liefern oft frühzeitige OSINT-Informationen, ' + 'können aber auch unbestätigte Meldungen enthalten. Für sensible Lagen empfohlen.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; Tutorial._runDemo(Tutorial._simulateFormSources); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 6 - Sichtbarkeit { id: 'form-visibility', target: '#modal-new .modal', title: 'Sichtbarkeit', text: 'Öffentlich bedeutet, dass alle Nutzer Ihrer Organisation diese Lage sehen ' + 'und darauf zugreifen können.

' + 'Privat macht die Lage nur für Sie persönlich sichtbar. ' + 'Nützlich für persönliche Recherchen oder sensible Vorgänge.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; Tutorial._runDemo(Tutorial._simulateFormVisibility); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 7 - Aktualisierung und Intervall { id: 'form-refresh', target: '#modal-new .modal', title: 'Aktualisierung', text: 'Manuell: Sie starten Aktualisierungen selbst per Button.
' + 'Automatisch: Der Monitor aktualisiert im eingestellten Intervall.

' + 'Wichtig: Kürzere Intervalle liefern aktuellere Daten, ' + 'erhöhen aber den Creditverbrauch. Für die meisten Lagen sind 15 bis 30 Minuten ein guter Richtwert.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; Tutorial._runDemo(Tutorial._simulateFormRefresh); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 8 - Aufbewahrung { id: 'form-retention', target: '#modal-new .modal', title: 'Aufbewahrung', text: 'Legen Sie fest, wie lange die Lage aktiv bleibt. Nach Ablauf der Frist ' + 'wird sie automatisch ins Archiv verschoben.

' + 'Setzen Sie den Wert auf 0 für unbegrenzte Aufbewahrung. ' + 'Standard sind 30 Tage.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; Tutorial._runDemo(Tutorial._simulateFormRetention); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 9 - E-Mail-Benachrichtigungen { id: 'form-notifications', target: '#modal-new .modal', title: 'E-Mail-Benachrichtigungen', text: 'Lassen Sie sich per E-Mail informieren bei:

' + 'Neues Lagebild - Wenn eine aktualisierte Zusammenfassung vorliegt
' + 'Neue Artikel - Wenn neue Quellen gefunden werden
' + 'Statusänderung Faktencheck - Wenn sich die Bewertung einer Behauptung ändert

' + 'So bleiben Sie auch ohne ständiges Einloggen auf dem Laufenden.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-new'); if (overlay && !overlay.classList.contains('active')) overlay.classList.add('active'); if (overlay) overlay.style.zIndex = '9002'; Tutorial._runDemo(Tutorial._simulateFormNotifications); }, onExit: function() { var overlay = document.getElementById('modal-new'); if (overlay) { overlay.classList.remove('active'); overlay.style.zIndex = ''; } Tutorial._clearSubHighlights(); }, }, // 5 - Sidebar Filter { id: 'sidebar-filters', target: '.sidebar-filter', title: 'Lagen filtern', text: 'Mit diesen Filtern steuern Sie, welche Lagen angezeigt werden:

' + 'Alle - Zeigt sämtliche Lagen Ihrer Organisation
' + 'Eigene - Nur Lagen, die Sie selbst erstellt haben

' + 'Bei vielen Lagen hilft dies, den Überblick zu behalten.', position: 'right', }, // 6 - Demo-Lage einführen { id: 'demo-intro', target: '.incident-item.active.tutorial-demo', title: 'Unsere Demo-Lage', text: 'Für diesen Rundgang haben wir eine Demo-Lage erstellt: ' + '"Explosion in Hamburger Hafen". Sie sehen sie hier in der Seitenleiste ' + 'als aktive Lage mit Auto-Refresh.

' + 'In den folgenden Schritten erkunden wir alle Bereiche, ' + 'die nach dem Öffnen einer Lage im Hauptbereich erscheinen.', position: 'right', onEnter: function() { Tutorial._injectDemoView(); }, }, // 7 - Header-Bereich { id: 'incident-header', target: '#incident-header-strip', title: 'Lage-Kopfbereich', text: 'Der Kopfbereich zeigt alle wichtigen Informationen auf einen Blick:

' + 'Typ-Badge - Live-Monitoring oder Recherche
' + 'Auto-Refresh - Zeigt das Aktualisierungsintervall
' + 'Aktionsleiste - Aktualisieren, Bearbeiten, Exportieren, Archivieren und Löschen

' + 'Der Zeitstempel zeigt, wann die letzte Aktualisierung stattfand.', position: 'bottom', }, // 8 - Refresh-Button { id: 'refresh', target: '#refresh-btn', title: 'Manuelle Aktualisierung', text: 'Mit diesem Button starten Sie eine sofortige Aktualisierung der Lage. ' + 'Der Monitor durchsucht dann alle konfigurierten Quellen nach neuen Meldungen, ' + 'erstellt ein aktualisiertes Lagebild und führt einen neuen Faktencheck durch.

' + 'Bei Live-Monitoring-Lagen mit Auto-Refresh geschieht dies zusätzlich automatisch im eingestellten Intervall.', position: 'bottom', onEnter: function() { Tutorial._highlightSub('#refresh-btn'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 9 - Export { id: 'export', target: '#export-dropdown', title: 'Lagebericht exportieren', text: 'Exportieren Sie Ihre Lage in verschiedenen Formaten:

' + 'Lagebericht (Markdown/JSON) - Kompakte Zusammenfassung mit Faktencheck
' + 'Vollexport - Alle Daten inklusive Artikel und Quellen
' + 'Drucken / PDF - Druckoptimierte Ansicht für Berichte

' + 'Ideal, um Ergebnisse mit Kollegen zu teilen, die keinen Monitor-Zugang haben.', position: 'bottom', onEnter: function() { Tutorial._highlightSub('#export-dropdown'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 10 - Layout Toolbar { id: 'layout-toolbar', target: '#layout-toolbar', title: 'Layout-Steuerung', text: 'Mit der Layout-Steuerung passen Sie Ihr Dashboard individuell an:

' + 'Jeder Button steht für eine Kachel: Lagebild, Faktencheck, ' + 'Quellen, Timeline und Karte.

' + 'Klicken Sie auf einen Button, um die entsprechende Kachel ein- oder auszublenden. ' + 'Mit "Layout zurücksetzen" stellen Sie die Standardansicht wieder her.', position: 'bottom', }, // 11 - Lagebild (detailliert) { id: 'lagebild', target: '[gs-id="lagebild"]', title: 'Lagebild', text: 'Das Lagebild ist das Herzstück jeder Lage. Es wird automatisch aus allen gesammelten ' + 'Quellen erstellt und bei jeder Aktualisierung neu generiert.

' + 'Quellenverweise - Die nummerierten Verweise [1], [2] etc. verlinken ' + 'direkt zu den Originalartikeln. So können Sie jede Aussage nachprüfen.
' + 'Vollansicht - Klicken Sie auf "Lagebild" in der Kopfzeile für eine ' + 'große Darstellung mit mehr Platz zum Lesen.
' + 'Zeitstempel - Zeigt, wann dieses Lagebild zuletzt generiert wurde.', position: 'right', onEnter: function() { Tutorial._highlightSub('[gs-id="lagebild"] .card-title'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 12 - Quellenverweise im Lagebild { id: 'lagebild-sources', target: '#summary-text', title: 'Quellenverweise im Detail', text: 'Die farbigen Nummern im Text sind Quellenverweise. Wenn Sie mit der Maus darüberfahren, ' + 'sehen Sie den Namen der Quelle. Ein Klick öffnet den Originalartikel.

' + 'So können Sie jede Behauptung im Lagebild direkt an der Originalquelle überprüfen. ' + 'Je mehr unabhängige Quellen eine Information stützen, desto verlässlicher ist sie.', position: 'right', onEnter: function() { Tutorial._highlightSub('.source-ref[data-index="1"]'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 13 - Faktencheck (detailliert) { id: 'faktencheck', target: '[gs-id="faktencheck"]', title: 'Faktencheck', text: 'Der Faktencheck prüft automatisch die wichtigsten Behauptungen anhand aller verfügbaren Quellen. ' + 'Jeder Eintrag erhält einen Status:

' + '✓ Bestätigt/Gesichert - Durch mehrere unabhängige Quellen belegt
' + '? Unbestätigt - Nur aus einer Quelle bekannt
' + '⚠ Umstritten - Quellen widersprechen sich
' + '✗ Widerlegt - Zuverlässige Quellen widersprechen', position: 'left', onEnter: function() { Tutorial._highlightSub('#factcheck-card'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 14 - Faktencheck: Einzelner Eintrag { id: 'faktencheck-detail', target: '.factcheck-item[data-fc-status="confirmed"]', title: 'Faktencheck-Eintrag', text: 'Jeder Faktencheck-Eintrag besteht aus:

' + 'Status-Symbol - Farbcodiert für schnelle Einordnung (links)
' + 'Behauptung - Die geprüfte Aussage
' + 'Quellenanzahl - Wie viele Quellen diese Behauptung stützen

' + 'Die Filterfunktion oben rechts ermöglicht es, nach Status zu filtern, ' + 'z.B. nur unbestätigte Meldungen anzeigen.', position: 'left', onEnter: function() { // Zum nicht-verifizierten Eintrag scrollen var fcList = document.getElementById('factcheck-list'); if (fcList) fcList.scrollTo({ top: fcList.scrollHeight, behavior: 'smooth' }); Tutorial._stepTimeout(function() { if (fcList) fcList.scrollTo({ top: 0, behavior: 'smooth' }); }, 2000); var item = document.querySelector('.factcheck-item[data-fc-status="confirmed"]'); if (item) item.classList.add('tutorial-sub-highlight'); Tutorial._cleanupFns.push(function() { if (item) item.classList.remove('tutorial-sub-highlight'); }); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 15 - Quellen { id: 'quellen', target: '[gs-id="quellen"]', title: 'Quellenübersicht', text: 'Die Quellenübersicht zeigt alle für diese Lage verwendeten Quellen, gruppiert nach Kategorie:

' + 'Nachrichtenagenturen - dpa, Reuters, AFP
' + 'Öffentlich-Rechtlich - tagesschau, ZDF, NDR
' + 'Qualitätszeitungen - FAZ, SZ, ZEIT
' + 'Behörden - Offizielle Stellen und Pressemitteilungen

' + 'Klicken Sie auf die Kopfzeile, um die Gruppen aufzuklappen. ' + 'Die "Detailansicht" zeigt alle Quellen mit einzelnen Artikeln.', position: 'top', onEnter: function() { Tutorial._highlightSub('.source-overview-header-toggle'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 16 - Timeline { id: 'timeline', target: '[gs-id="timeline"]', title: 'Ereignis-Timeline', text: 'Die Timeline zeigt den chronologischen Verlauf aller Ereignisse:

' + 'Meldungen - Einzelne Artikel und Nachrichten aus den Quellen
' + 'Lageberichte - Automatisch erstellte Zusammenfassungen (hervorgehoben)

' + 'Nutzen Sie die Filter oben:
' + 'Alle/Meldungen/Lageberichte - Typ-Filter
' + '24h/7T/Alles - Zeitraum eingrenzen
' + 'Suchfeld - Freitextsuche in allen Einträgen', position: 'top', onEnter: function() { Tutorial._highlightSub('.ht-controls'); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 17 - Karte: Kachel-Ansicht { id: 'karte', target: '[gs-id="karte"]', title: 'Geografische Verteilung', text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.

' + 'Orte einlesen - Startet das Geoparsing manuell neu
' + 'Vollbild - Vergr\u00f6\u00dfert die Karte auf den gesamten Bildschirm

' + 'Im n\u00e4chsten Schritt \u00f6ffnen wir die Vollbildansicht und schauen uns die Marker im Detail an.', position: 'top', onEnter: function() { // Tile-Map Resize triggern if (Tutorial._demoMapTileMap) { Tutorial._demoMapTileMap.invalidateSize(); setTimeout(function() { if (Tutorial._demoMapTileMap) Tutorial._demoMapTileMap.setView([53.545, 9.98], 12); }, 200); } Tutorial._highlightSub('#geoparse-btn'); Tutorial._stepTimeout(function() { Tutorial._clearSubHighlights(); Tutorial._highlightSub('#map-expand-btn'); }, 2500); }, onExit: function() { Tutorial._clearSubHighlights(); }, }, // 18 - Karte: Vollbild + Zoom + Marker-Demo { id: 'karte-fullscreen', target: '.map-fullscreen-header', title: 'Karte im Vollbild', text: 'Die Karte zoomt jetzt auf den Ereignisort. Die Marker zeigen:

' + '● Hauptereignisort - Burchardkai Terminal (6 Artikel)
' + '● Erw\u00e4hnt - Hamburg Innenstadt (2 Artikel)
' + '● Kontext - Elbe / Hafengebiet (1 Artikel)

' + 'Die Legende unten rechts erkl\u00e4rt die Farbkategorien. ' + 'Klicken Sie auf einen Marker f\u00fcr Details und verkn\u00fcpfte Artikel.', position: 'left', disableNav: true, onEnter: function() { var chatBtn = document.getElementById('chat-toggle-btn'); if (chatBtn) chatBtn.style.display = 'none'; Tutorial._openDemoMapFullscreen(); }, onExit: function() { Tutorial._closeDemoMapFullscreen(); var chatBtn = document.getElementById('chat-toggle-btn'); if (chatBtn) chatBtn.style.display = ''; Tutorial._clearSubHighlights(); }, }, // 18 - Drag Demo { id: 'drag-demo', target: '[gs-id="lagebild"] .card-header', title: 'Kacheln verschieben', text: 'Alle Kacheln im Dashboard lassen sich frei per Drag-and-Drop verschieben. ' + 'Greifen Sie dazu die Kopfzeile einer Kachel und ziehen Sie sie an die gewünschte Position.

' + 'Beobachten Sie die virtuelle Maus-Demo:', position: 'right', disableNav: true, onEnter: function() { Tutorial._runDemo(Tutorial._simulateDrag); }, }, // 19 - Resize Demo { id: 'resize-demo', target: '[gs-id="faktencheck"]', title: 'Kacheln in der Größe anpassen', text: 'Ziehen Sie am rechten unteren Rand einer Kachel, um ihre Größe zu verändern. ' + 'So können Sie wichtigen Inhalten wie dem Faktencheck mehr Platz einräumen.

' + 'Beobachten Sie die virtuelle Maus-Demo:', position: 'left', disableNav: true, onEnter: function() { Tutorial._runDemo(Tutorial._simulateResize); }, }, // 20 - Theme { id: 'theme', target: '#theme-toggle', title: 'Design umschalten', text: 'Wechseln Sie zwischen dem dunklen und hellen Design. ' + 'Ihre Auswahl wird automatisch gespeichert und beim nächsten Besuch beibehalten.', position: 'bottom', }, // 21 - Quellen verwalten { id: 'sources-btn', target: '.sidebar-sources-link button:first-child', title: 'Quellenverwaltung öffnen', text: 'In der Seitenleiste ganz unten finden Sie den Zugang zur Quellenverwaltung. ' + 'Hier können Sie:

' + 'Neue Quellen hinzufügen - URL eingeben oder automatisch erkennen lassen
' + 'Bestehende Quellen bearbeiten - Kategorie, Sprache, Notizen anpassen
' + 'Quellen deaktivieren - Temporär oder dauerhaft ausschließen', position: 'right', onEnter: function() { Tutorial._stepTimeout(function() { var overlay = document.getElementById('modal-sources'); if (overlay && !overlay.classList.contains('active')) { if (typeof App !== 'undefined' && App.openSourceManagement) { App.openSourceManagement(); } } }, 1500); }, onExit: function() { var overlay = document.getElementById('modal-sources'); if (overlay) { overlay.classList.remove('active'); overlay.style.zIndex = ''; } }, }, // 22 - Quellen Modal: Info-Icon + Tooltip { id: 'sources-modal', target: '#modal-sources .modal', title: 'Quellendetails anzeigen', text: 'Jede Quelle hat ein Info-Symbol (i), das Details wie Typ, Sprache und ' + 'Ausrichtung anzeigt. Beobachten Sie den Tooltip:

' + 'Klicken Sie auf Weiter, wenn Sie den Tooltip gelesen haben.', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-sources'); if (overlay && !overlay.classList.contains('active')) { if (typeof App !== 'undefined' && App.openSourceManagement) { App.openSourceManagement(); } } if (overlay) overlay.style.zIndex = '9002'; Tutorial._stepTimeout(function() { Tutorial._runDemo(Tutorial._simulateSourcesInfoIcon); }, 1500); }, onExit: function() { var tip = document.getElementById('tutorial-tooltip'); if (tip) tip.remove(); Tutorial._clearSubHighlights(); Tutorial._hideCursor(); }, }, // 23 - Quellen Modal: Aktionen { id: 'sources-modal-actions', target: '#modal-sources .modal', title: 'Quellen verwalten', text: '+ Quelle - Neue Quellen per URL oder Domain hinzuf\u00fcgen. ' + 'Der Monitor erkennt RSS-Feeds automatisch.

' + 'Ausschlie\u00dfen - Sperrt eine Quelle f\u00fcr einzelne Lagen, ' + 'ohne sie global zu deaktivieren.

' + 'Kategorie-Badge - Farbige Kennzeichnung der Quellkategorie ' + '(Nachrichtenagentur, \u00d6ffentlich-Rechtlich, etc.)', position: 'left', disableNav: true, onEnter: function() { var overlay = document.getElementById('modal-sources'); if (overlay && !overlay.classList.contains('active')) { if (typeof App !== 'undefined' && App.openSourceManagement) { App.openSourceManagement(); } } if (overlay) overlay.style.zIndex = '9002'; Tutorial._runDemo(Tutorial._simulateSourcesActions); }, onExit: function() { var overlay = document.getElementById('modal-sources'); if (overlay) { overlay.classList.remove('active'); overlay.style.zIndex = ''; } Tutorial._clearSubHighlights(); Tutorial._hideCursor(); }, }, // 23 - Chat { id: 'chat', target: '#chat-toggle-btn', title: 'Chat-Assistent', text: 'Der Chat-Assistent steht Ihnen jederzeit zur Verfügung. ' + 'Stellen Sie Fragen zur Bedienung des Monitors und erhalten Sie sofort eine Antwort.

' + 'Beispiele:
' + '"Wie erstelle ich eine neue Lage?"
' + '"Was bedeuten die Faktencheck-Status?"
' + '"Wie exportiere ich einen Lagebericht?"', position: 'left', }, // 24 - Ende { id: 'end', target: null, title: 'Rundgang abgeschlossen', text: 'Sie kennen jetzt alle wichtigen Funktionen des AegisSight Monitors.

' + 'Die Demo-Daten werden nach dem Schließen entfernt. ' + 'Erstellen Sie Ihre erste eigene Lage über den Button "+ Neue Lage" in der Seitenleiste.

' + 'Bei weiteren Fragen steht Ihnen der Chat-Assistent ' + 'oder unser Support unter support@aegis-sight.de zur Verfügung.', position: 'center', }, ]; }, // ----------------------------------------------------------------------- // Lifecycle // ----------------------------------------------------------------------- async start(forceRestart) { if (this._isActive) return; // Chat schliessen falls offen if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close(); // Server-State laden (Fallback: direkt starten) var state = null; try { state = await API.getTutorialState(); } catch(e) {} // Resume-Dialog wenn mittendrin abgebrochen if (!forceRestart && state && !state.completed && state.current_step !== null && state.current_step > 0) { this._showResumeDialog(state.current_step); return; } this._startInternal(forceRestart ? 0 : null); }, _showResumeDialog(step) { var self = this; var overlay = document.createElement('div'); overlay.className = 'tutorial-resume-overlay'; overlay.innerHTML = '
' + '

Sie haben den Rundgang bei Schritt ' + (step + 1) + '/' + this._steps.length + ' unterbrochen.

' + '
' + '' + '' + '
'; document.body.appendChild(overlay); document.getElementById('tutorial-resume-btn').addEventListener('click', function() { overlay.remove(); self._startInternal(step); }); document.getElementById('tutorial-restart-btn').addEventListener('click', async function() { overlay.remove(); try { await API.resetTutorialState(); } catch(e) {} self._startInternal(0); }); }, _startInternal(resumeStep) { this._isActive = true; this._highestStep = -1; this._currentStep = -1; // Overlay einblenden + Klicks blockieren this._els.overlay.classList.add('active'); document.body.classList.add('tutorial-active'); // Keyboard this._keyHandler = this._onKey.bind(this); document.addEventListener('keydown', this._keyHandler); // Resize this._resizeHandler = this._onResize.bind(this); window.addEventListener('resize', this._resizeHandler); if (resumeStep && resumeStep > 0) { this.goToStep(resumeStep); } else { this.next(); } }, stop() { if (!this._isActive) return; // Aktuellen Step aufräumen if (this._currentStep >= 0) this._exitStep(this._currentStep); this._isActive = false; this._currentStep = -1; this._demoRunning = false; // Overlay ausblenden + Klicks freigeben this._els.overlay.classList.remove('active'); document.body.classList.remove('tutorial-active'); this._els.spotlight.style.opacity = '0'; this._els.bubble.classList.remove('visible'); this._hideCursor(); this._clearSubHighlights(); // Step-Timers canceln this._clearStepTimers(); // Alle Modals schliessen die das Tutorial geoeffnet haben koennte ['modal-new', 'modal-sources'].forEach(function(id) { var overlay = document.getElementById(id); if (overlay) { overlay.classList.remove('active'); overlay.style.zIndex = ''; } }); // Cleanup-Callbacks this._cleanupFns.forEach(function(fn) { try { fn(); } catch(e) {} }); this._cleanupFns = []; // Formular-Inputs zuruecksetzen var titleInput = document.getElementById('inc-title'); if (titleInput) titleInput.value = ''; var descInput = document.getElementById('inc-description'); if (descInput) descInput.value = ''; var selType = document.getElementById('inc-type'); if (selType) { selType.value = 'adhoc'; try { selType.dispatchEvent(new Event('change')); } catch(e) {} } // Demo-View entfernen this._removeDemoView(); // Events entfernen if (this._keyHandler) { document.removeEventListener('keydown', this._keyHandler); this._keyHandler = null; } if (this._resizeHandler) { window.removeEventListener('resize', this._resizeHandler); this._resizeHandler = null; } // Fortschritt serverseitig speichern if (this._lastExitedStep >= 0 && this._lastExitedStep < this._steps.length - 1) { // Mittendrin abgebrochen — Schritt speichern API.saveTutorialState({ current_step: this._lastExitedStep }).catch(function() {}); } else { // Komplett durchlaufen oder letzter Schritt this._markSeen(); } }, // ----------------------------------------------------------------------- // Navigation // ----------------------------------------------------------------------- next() { if (!this._isActive || this._demoRunning) return; var nextIdx = this._currentStep + 1; if (nextIdx >= this._steps.length) { this.stop(); return; } this.goToStep(nextIdx); }, prev() { if (!this._isActive || this._demoRunning) return; var prevIdx = this._currentStep - 1; if (prevIdx < 0) return; this.goToStep(prevIdx); }, goToStep(index) { if (index < 0 || index >= this._steps.length) return; if (this._currentStep >= 0) this._exitStep(this._currentStep); this._currentStep = index; if (index > this._highestStep) this._highestStep = index; this._enterStep(index); }, // ----------------------------------------------------------------------- // Step Enter/Exit // ----------------------------------------------------------------------- async _enterStep(i) { var step = this._steps[i]; // Bei Modal-Steps: Spotlight ausblenden (Modal hat eigene Abdunkelung) var isModalStep = step.target && (step.target.indexOf('#modal-') !== -1); if (step.target && step.position !== 'center' && !isModalStep) { await this._scrollToTarget(step.target); this._spotlightElement(step.target); } else { this._els.spotlight.style.opacity = '0'; } // Bubble konfigurieren und anzeigen this._showBubble(step, i); // onEnter-Callback if (step.onEnter) { step.onEnter(); } }, _exitStep(i) { var step = this._steps[i]; this._clearStepTimers(); // Laufende Demo abbrechen this._demoRunning = false; // Step-Index merken um verspaetete Highlight-Aufrufe zu ignorieren this._lastExitedStep = i; if (step.onExit) { step.onExit(); } this._hideCursor(); this._clearSubHighlights(); }, // ----------------------------------------------------------------------- // Spotlight // ----------------------------------------------------------------------- _spotlightElement(selector) { var el = document.querySelector(selector); if (!el) { this._els.spotlight.style.opacity = '0'; return; } var rect = el.getBoundingClientRect(); var vw = window.innerWidth; var vh = window.innerHeight; var pad = 8; // Sichtbaren Bereich berechnen (auf Viewport beschränkt) var top = Math.max(rect.top, 0) - pad; var left = Math.max(rect.left, 0) - pad; var bottom = Math.min(rect.bottom, vh) + pad; var right = Math.min(rect.right, vw) + pad; var s = this._els.spotlight.style; s.top = top + 'px'; s.left = left + 'px'; s.width = (right - left) + 'px'; s.height = (bottom - top) + 'px'; s.borderRadius = '8px'; s.opacity = '1'; }, // ----------------------------------------------------------------------- // Bubble // ----------------------------------------------------------------------- _showBubble(step, index) { var bubble = this._els.bubble; var total = this._steps.length; // Inhalt var counterHtml = '
Schritt ' + (index + 1) + ' von ' + total + '
'; var titleHtml = '
' + step.title + '
'; var textHtml = '
' + step.text + '
'; // Fortschrittspunkte var dotsHtml = '
'; for (var d = 0; d < total; d++) { dotsHtml += ''; } dotsHtml += '
'; // Navigation var navHtml = '
'; if (step.disableNav) { // Automatische Demo läuft - keine Buttons, wird pulsieren navHtml += 'Demo läuft...'; } else { if (index > 0) { navHtml += ''; } else { navHtml += ''; } if (index < total - 1) { navHtml += ''; } else { navHtml += ''; } } navHtml += '
'; // Close-Button var closeHtml = ''; bubble.innerHTML = closeHtml + counterHtml + titleHtml + textHtml + dotsHtml + navHtml; // Positionierung this._positionBubble(step); // Pulsieren bei automatischen Demos if (step.disableNav) { bubble.classList.add('tutorial-bubble-pulsing'); } else { bubble.classList.remove('tutorial-bubble-pulsing'); } // Sichtbar machen bubble.classList.add('visible'); // Sicherheitscheck: Bubble nicht unter Viewport-Rand var self = this; requestAnimationFrame(function() { var bRect = bubble.getBoundingClientRect(); var vHeight = window.innerHeight; if (bRect.bottom > vHeight - 8) { var shift = bRect.bottom - vHeight + 16; var currentTop = parseFloat(bubble.style.top) || 0; bubble.style.top = (currentTop - shift) + 'px'; } }); }, _positionBubble(step) { var bubble = this._els.bubble; var bw = 360; // Zentriert (Welcome / End) if (step.position === 'center' || !step.target) { bubble.className = 'tutorial-bubble visible tutorial-pos-center'; bubble.style.top = '50%'; bubble.style.left = '50%'; bubble.style.transform = 'translate(-50%, -50%)'; bubble.style.width = bw + 'px'; return; } var el = document.querySelector(step.target); if (!el) { bubble.className = 'tutorial-bubble visible tutorial-pos-center'; bubble.style.top = '50%'; bubble.style.left = '50%'; bubble.style.transform = 'translate(-50%, -50%)'; bubble.style.width = bw + 'px'; return; } var rect = el.getBoundingClientRect(); var vw = window.innerWidth; var vh = window.innerHeight; var gap = 16; // Sichtbaren Bereich des Elements berechnen (auf Viewport beschraenkt) var visTop = Math.max(rect.top, 0); var visBottom = Math.min(rect.bottom, vh); var visLeft = Math.max(rect.left, 0); var visRight = Math.min(rect.right, vw); var visCenterY = (visTop + visBottom) / 2; var visCenterX = (visLeft + visRight) / 2; // Reset transform bubble.style.transform = ''; bubble.style.width = bw + 'px'; // Automatische Positionswahl falls nicht genug Platz var pos = step.position || 'bottom'; var bubbleHeight = 300; if (pos === 'bottom' && (visBottom + gap + bubbleHeight > vh)) pos = 'top'; if (pos === 'top' && (visTop - gap - bubbleHeight < 0)) pos = 'bottom'; if (pos === 'right' && (visRight + gap + bw > vw)) pos = 'left'; if (pos === 'left' && (visLeft - gap - bw < 0)) pos = 'right'; bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos; // Bubble-Top immer im sichtbaren Bereich halten var clampTop = function(t) { return Math.max(8, Math.min(t, vh - bubbleHeight - 8)); }; switch (pos) { case 'bottom': bubble.style.top = Math.min(visBottom + gap, vh - bubbleHeight - 8) + 'px'; bubble.style.left = Math.max(8, Math.min(visCenterX - bw / 2, vw - bw - 8)) + 'px'; break; case 'top': bubble.style.top = Math.max(visTop - gap, 8) + 'px'; bubble.style.left = Math.max(8, Math.min(visCenterX - bw / 2, vw - bw - 8)) + 'px'; bubble.style.transform = 'translateY(-100%)'; break; case 'right': bubble.style.top = clampTop(visCenterY - bubbleHeight / 2) + 'px'; bubble.style.left = Math.min(visRight + gap, vw - bw - 8) + 'px'; break; case 'left': bubble.style.top = clampTop(visCenterY - bubbleHeight / 2) + 'px'; bubble.style.left = Math.max(visLeft - gap - bw, 8) + 'px'; break; } }, // ----------------------------------------------------------------------- // Cursor (virtuelle Maus) // ----------------------------------------------------------------------- _showCursor(x, y, type) { var c = this._els.cursor; c.className = 'tutorial-cursor'; if (type) c.classList.add('tutorial-cursor-' + type); c.style.left = x + 'px'; c.style.top = y + 'px'; c.classList.add('visible'); }, _hideCursor() { this._els.cursor.classList.remove('visible'); }, _animateCursor(fromX, fromY, toX, toY, ms) { var self = this; return new Promise(function(resolve) { var c = self._els.cursor; var start = null; function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function frame(timestamp) { if (!self._isActive) { resolve(); return; } if (!start) start = timestamp; var elapsed = timestamp - start; var progress = Math.min(elapsed / ms, 1); var eased = easeInOutCubic(progress); var cx = fromX + (toX - fromX) * eased; var cy = fromY + (toY - fromY) * eased; c.style.left = cx + 'px'; c.style.top = cy + 'px'; if (progress < 1) { requestAnimationFrame(frame); } else { resolve(); } } requestAnimationFrame(frame); }); }, _wait(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }, // ----------------------------------------------------------------------- // Karten-Demo: Vollbild mit Markern und Cursor-Demo // ----------------------------------------------------------------------- _openDemoMapFullscreen() { var overlay = document.getElementById('map-fullscreen-overlay'); var fsContainer = document.getElementById('map-fullscreen-container'); var fsStats = document.getElementById('map-fullscreen-stats'); if (!overlay || !fsContainer) return; // Overlay anzeigen overlay.classList.add('active'); overlay.style.zIndex = '9998'; if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel'; // Alte Demo-Map entfernen this._destroyDemoMap(); // Map im Fullscreen-Container fsContainer.innerHTML = ''; // Explizite Hoehe setzen damit Leaflet korrekt rendert fsContainer.style.flex = '1'; fsContainer.style.minHeight = '0'; var mapDiv = document.createElement('div'); mapDiv.id = 'tutorial-fs-map'; mapDiv.style.cssText = 'width:100%;height:100%;min-height:400px;'; fsContainer.appendChild(mapDiv); // Start weit herausgezoomt (Europa) this._demoMap = L.map(mapDiv, { zoomControl: true, attributionControl: true, }).setView([51.0, 10.0], 5); // Gleiche Tile-Quelle wie die echte App (deutsche OSM-Kacheln) L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 18, noWrap: true, }).addTo(this._demoMap); // Marker + Legende hinzufügen this._demoMapMarkers = this._createDemoMarkers(this._demoMap); this._demoMapLegend = this._addDemoLegend(this._demoMap); // Resize + animierter Zoom auf Hamburg var map = this._demoMap; // Mehrere invalidateSize-Aufrufe damit Leaflet die Container-Groesse erkennt [100, 300, 600].forEach(function(delay) { setTimeout(function() { if (map) map.invalidateSize(); }, delay); }); // Warten bis Tiles geladen, dann sanft auf Hamburg zoomen setTimeout(function() { if (map) { map.invalidateSize(); map.flyTo([53.54, 9.97], 13, { duration: 2.5 }); } // Nach Zoom: Demo starten setTimeout(function() { self._runDemo(self._simulateMapDemo); }, 3200); }, 1200); }, _closeDemoMapFullscreen() { var overlay = document.getElementById('map-fullscreen-overlay'); if (overlay) { overlay.classList.remove('active'); overlay.style.zIndex = ''; } var fsContainer = document.getElementById('map-fullscreen-container'); if (fsContainer) { fsContainer.innerHTML = ''; fsContainer.style.flex = ''; fsContainer.style.minHeight = ''; } this._destroyDemoMap(); }, async _simulateMapDemo() { this._demoRunning = true; await this._wait(800); if (!this._demoMapMarkers.length || !this._demoMap) { this._demoRunning = false; this._enableNavAfterDemo(); return; } var mapEl = document.getElementById('tutorial-fs-map'); if (!mapEl) { this._demoRunning = false; this._enableNavAfterDemo(); return; } var mapRect = mapEl.getBoundingClientRect(); // Alle 3 Marker nacheinander besuchen var names = ['Burchardkai Terminal (Hauptereignisort)', 'Hamburg Innenstadt (Erwähnt)', 'Elbe / Hafengebiet (Kontext)']; var prevX, prevY; for (var i = 0; i < this._demoMapMarkers.length; i++) { if (!this._isActive) break; var marker = this._demoMapMarkers[i]; var latLng = marker.getLatLng(); var point = this._demoMap.latLngToContainerPoint(latLng); var mx = mapRect.left + point.x; var my = mapRect.top + point.y; if (prevX !== undefined) { await this._animateCursor(prevX, prevY, mx, my, 600); } else { this._showCursor(mx - 60, my - 50, 'default'); await this._wait(200); await this._animateCursor(mx - 60, my - 50, mx, my, 500); } await this._wait(200); marker.openPopup(); await this._wait(2500); marker.closePopup(); prevX = mx; prevY = my; } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Tipp-Simulation: Text Zeichen fuer Zeichen eingeben // ----------------------------------------------------------------------- _simulateTyping(input, text, ms) { var self = this; var charDelay = ms / text.length; return new Promise(function(resolve) { var i = 0; function typeNext() { if (!self._isActive || i >= text.length) { resolve(); return; } input.value += text[i]; input.dispatchEvent(new Event('input', { bubbles: true })); i++; setTimeout(typeNext, charDelay); } typeNext(); }); }, // Helfer: Cursor zu einem Element bewegen async _cursorToElement(selector, fromX, fromY) { var el = document.querySelector(selector); if (!el) return { x: fromX || 400, y: fromY || 300 }; var rect = el.getBoundingClientRect(); if (!rect.width && !rect.height) return { x: fromX || 400, y: fromY || 300 }; var tx = rect.left + Math.min(rect.width / 2, 60); var ty = rect.top + rect.height / 2; if (fromX !== undefined && fromY !== undefined) { await this._animateCursor(fromX, fromY, tx, ty, 500); } else { this._showCursor(tx, ty, 'default'); } await this._wait(200); return { x: tx, y: ty }; }, // Helfer: Modal-Body zu einem Element scrollen _scrollModalTo(selector) { var el = document.querySelector(selector); var modalBody = document.querySelector('#modal-new .modal-body'); if (!el || !modalBody) return; var elTop = el.offsetTop - modalBody.offsetTop; modalBody.scrollTo({ top: Math.max(0, elTop - 20), behavior: 'smooth' }); }, // ----------------------------------------------------------------------- // Step 3: Titel + Beschreibung // ----------------------------------------------------------------------- async _simulateFormTitleDesc() { this._demoRunning = true; // Warten bis Modal vollstaendig gerendert await this._wait(600); var titleInput = document.getElementById('inc-title'); var descInput = document.getElementById('inc-description'); if (!titleInput) { this._demoRunning = false; this._enableNavAfterDemo(); return; } // Cursor zum Titel this._highlightSub('#inc-title'); var pos = await this._cursorToElement('#inc-title'); titleInput.focus(); await this._simulateTyping(titleInput, 'Explosion in Hamburger Hafen', 1200); await this._wait(400); this._clearSubHighlights(); // Cursor zur Beschreibung if (descInput) { this._highlightSub('#inc-description'); pos = await this._cursorToElement('#inc-description', pos.x, pos.y); descInput.focus(); await this._simulateTyping(descInput, 'Schwere Explosion im Hafengebiet, Burchardkai-Terminal', 1200); await this._wait(400); this._clearSubHighlights(); } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Step 4: Art der Lage (Typ-Wechsel) // ----------------------------------------------------------------------- async _simulateTypeSwitch() { this._demoRunning = true; var sel = document.getElementById('inc-type'); if (!sel) { this._demoRunning = false; this._enableNavAfterDemo(); return; } var pos = await this._cursorToElement('#inc-type'); await this._wait(300); // Wechsel zu Recherche sel.value = 'research'; sel.dispatchEvent(new Event('change')); await this._wait(2000); // Zur\u00fcck zu Live-Monitoring sel.value = 'adhoc'; sel.dispatchEvent(new Event('change')); await this._wait(800); this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Step 5: Quellen (International + Telegram toggles) // ----------------------------------------------------------------------- async _simulateFormSources() { this._demoRunning = true; this._scrollModalTo('#inc-international'); await this._wait(400); // International-Toggle highlighten var intlCheckbox = document.getElementById('inc-international'); var intlLabel = intlCheckbox ? intlCheckbox.closest('.toggle-label') : null; if (intlLabel) { this._highlightSub('#inc-international'); var pos = await this._cursorToElement('#inc-international'); await this._wait(1500); this._clearSubHighlights(); // Telegram-Toggle var telegramCheckbox = document.getElementById('inc-telegram'); if (telegramCheckbox) { this._highlightSub('#inc-telegram'); pos = await this._cursorToElement('#inc-telegram', pos.x, pos.y); // Aktivieren telegramCheckbox.checked = true; await this._wait(1500); this._clearSubHighlights(); } } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Step 6: Sichtbarkeit // ----------------------------------------------------------------------- async _simulateFormVisibility() { this._demoRunning = true; this._scrollModalTo('#inc-visibility'); await this._wait(400); var checkbox = document.getElementById('inc-visibility'); if (checkbox) { this._highlightSub('#inc-visibility'); var pos = await this._cursorToElement('#inc-visibility'); await this._wait(1000); // Umschalten auf Privat checkbox.checked = false; var textEl = document.getElementById('visibility-text'); if (textEl) textEl.textContent = 'Privat \u2014 nur f\u00fcr dich sichtbar'; await this._wait(1500); // Zur\u00fcck auf \u00d6ffentlich checkbox.checked = true; if (textEl) textEl.textContent = '\u00d6ffentlich \u2014 f\u00fcr alle Nutzer sichtbar'; await this._wait(800); this._clearSubHighlights(); } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Step 7: Aktualisierung + Intervall // ----------------------------------------------------------------------- async _simulateFormRefresh() { this._demoRunning = true; this._scrollModalTo('#inc-refresh-mode'); await this._wait(400); var refreshSelect = document.getElementById('inc-refresh-mode'); if (refreshSelect) { this._highlightSub('#inc-refresh-mode'); var pos = await this._cursorToElement('#inc-refresh-mode'); await this._wait(800); // Auf Auto wechseln refreshSelect.value = 'auto'; try { refreshSelect.dispatchEvent(new Event('change')); } catch(e) {} await this._wait(1000); this._clearSubHighlights(); // Intervall-Feld highlighten var intervalField = document.getElementById('refresh-interval-field'); var intervalInput = document.getElementById('inc-refresh-value'); if (intervalField && intervalInput) { this._highlightSub('#inc-refresh-value'); pos = await this._cursorToElement('#inc-refresh-value', pos.x, pos.y); await this._wait(1500); this._clearSubHighlights(); } } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Step 8: Aufbewahrung // ----------------------------------------------------------------------- async _simulateFormRetention() { this._demoRunning = true; this._scrollModalTo('#inc-retention'); await this._wait(400); var retentionInput = document.getElementById('inc-retention'); if (retentionInput) { this._highlightSub('#inc-retention'); var pos = await this._cursorToElement('#inc-retention'); await this._wait(2000); this._clearSubHighlights(); } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Step 9: E-Mail-Benachrichtigungen // ----------------------------------------------------------------------- async _simulateFormNotifications() { this._demoRunning = true; this._scrollModalTo('#inc-notify-summary'); await this._wait(400); var checks = ['#inc-notify-summary', '#inc-notify-new-articles', '#inc-notify-status-change']; var pos; for (var i = 0; i < checks.length; i++) { var cb = document.querySelector(checks[i]); if (!cb) continue; this._highlightSub(checks[i]); if (pos) { pos = await this._cursorToElement(checks[i], pos.x, pos.y); } else { pos = await this._cursorToElement(checks[i]); } cb.checked = true; await this._wait(1000); this._clearSubHighlights(); } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Drag-Demo: Kachel visuell verschieben // ----------------------------------------------------------------------- async _simulateDrag() { this._demoRunning = true; var tile = document.querySelector('[gs-id="lagebild"]'); var header = tile ? tile.querySelector('.card-header') : null; if (!header || !tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; } var rect = header.getBoundingClientRect(); var startX = rect.left + rect.width / 2; var startY = rect.top + rect.height / 2; var moveX = 150; // Cursor erscheint this._showCursor(startX - 50, startY - 30, 'default'); await this._wait(300); await this._animateCursor(startX - 50, startY - 30, startX, startY, 500); await this._wait(300); // Grabbing this._els.cursor.classList.remove('tutorial-cursor-default'); this._els.cursor.classList.add('tutorial-cursor-grabbing'); await this._wait(200); // Kachel visuell verschieben (CSS transform) tile.style.transition = 'none'; tile.style.zIndex = '9002'; var self = this; var start = null; await new Promise(function(resolve) { function frame(ts) { if (!self._isActive) { resolve(); return; } if (!start) start = ts; var progress = Math.min((ts - start) / 1200, 1); var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2; var dx = moveX * eased; tile.style.transform = 'translateX(' + dx + 'px)'; self._els.cursor.style.left = (startX + dx) + 'px'; if (progress < 1) requestAnimationFrame(frame); else resolve(); } requestAnimationFrame(frame); }); await this._wait(300); // Zurück this._els.cursor.classList.remove('tutorial-cursor-grabbing'); this._els.cursor.classList.add('tutorial-cursor-default'); start = null; await new Promise(function(resolve) { function frame(ts) { if (!self._isActive) { resolve(); return; } if (!start) start = ts; var progress = Math.min((ts - start) / 800, 1); var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2; var dx = moveX * (1 - eased); tile.style.transform = 'translateX(' + dx + 'px)'; self._els.cursor.style.left = (startX + dx) + 'px'; if (progress < 1) requestAnimationFrame(frame); else resolve(); } requestAnimationFrame(frame); }); // Aufräumen tile.style.transform = ''; tile.style.transition = ''; tile.style.zIndex = ''; this._hideCursor(); await this._wait(200); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Resize-Demo: Kachel visuell vergrößern // ----------------------------------------------------------------------- async _simulateResize() { this._demoRunning = true; var tile = document.querySelector('[gs-id="faktencheck"]'); if (!tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; } var rect = tile.getBoundingClientRect(); var startX = rect.right - 6; var startY = rect.bottom - 6; var expandX = 80; var expandY = 50; // Cursor am Rand this._showCursor(startX - 40, startY - 40, 'default'); await this._wait(300); await this._animateCursor(startX - 40, startY - 40, startX, startY, 400); await this._wait(200); // Resize-Cursor this._els.cursor.classList.remove('tutorial-cursor-default'); this._els.cursor.classList.add('tutorial-cursor-resize'); await this._wait(200); // Rein visuelle Vergrößerung per CSS transform (kein width/height!) // So bleibt GridStack komplett unberührt tile.style.transition = 'none'; tile.style.zIndex = '9002'; tile.style.transformOrigin = 'top left'; var self = this; var origW = rect.width; var origH = rect.height; var start = null; await new Promise(function(resolve) { function frame(ts) { if (!self._isActive) { resolve(); return; } if (!start) start = ts; var progress = Math.min((ts - start) / 1000, 1); var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2; var scaleX = 1 + (expandX / origW) * eased; var scaleY = 1 + (expandY / origH) * eased; tile.style.transform = 'scale(' + scaleX + ', ' + scaleY + ')'; self._els.cursor.style.left = (startX + expandX * eased) + 'px'; self._els.cursor.style.top = (startY + expandY * eased) + 'px'; if (progress < 1) requestAnimationFrame(frame); else resolve(); } requestAnimationFrame(frame); }); await this._wait(500); // Zurück start = null; await new Promise(function(resolve) { function frame(ts) { if (!self._isActive) { resolve(); return; } if (!start) start = ts; var progress = Math.min((ts - start) / 700, 1); var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2; var scaleX = 1 + (expandX / origW) * (1 - eased); var scaleY = 1 + (expandY / origH) * (1 - eased); tile.style.transform = 'scale(' + scaleX + ', ' + scaleY + ')'; self._els.cursor.style.left = (startX + expandX * (1 - eased)) + 'px'; self._els.cursor.style.top = (startY + expandY * (1 - eased)) + 'px'; if (progress < 1) requestAnimationFrame(frame); else resolve(); } requestAnimationFrame(frame); }); // Aufräumen tile.style.transform = ''; tile.style.transformOrigin = ''; tile.style.transition = ''; tile.style.zIndex = ''; this._hideCursor(); await this._wait(200); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Quellenverwaltung-Demo: Info-Icon hover + Tooltip zeigen // ----------------------------------------------------------------------- // Quellen-Demo Teil 1: Info-Icon + Tooltip, dann Weiter-Button async _simulateSourcesInfoIcon() { this._demoRunning = true; // Warten bis Quellen geladen sind var attempts = 0; while (attempts < 10) { var infoIcon = document.querySelector('#sources-list .info-icon'); if (infoIcon) break; await this._wait(300); attempts++; } var infoIcon = document.querySelector('#sources-list .info-icon'); if (!infoIcon) { this._demoRunning = false; this._enableNavAfterDemo(); return; } // Cursor zum Info-Icon var rect = infoIcon.getBoundingClientRect(); var targetX = rect.left + rect.width / 2; var targetY = rect.top + rect.height / 2; this._showCursor(targetX - 80, targetY - 60, 'default'); await this._wait(300); await this._animateCursor(targetX - 80, targetY - 60, targetX, targetY, 600); await this._wait(200); // Info-Icon highlighten this._highlightSub('#sources-list .info-icon'); // Tooltip manuell anzeigen var tooltipText = infoIcon.getAttribute('data-tooltip') || ''; if (tooltipText) { var tooltip = document.createElement('div'); tooltip.id = 'tutorial-tooltip'; tooltip.style.cssText = 'position:fixed;z-index:9999;background:var(--bg-card);color:var(--text-primary);' + 'border:1px solid var(--accent);border-radius:var(--radius);padding:8px 12px;font-size:12px;' + 'line-height:1.5;max-width:250px;white-space:pre-line;box-shadow:var(--shadow-md);' + 'pointer-events:none;'; tooltip.textContent = tooltipText; document.body.appendChild(tooltip); tooltip.style.left = Math.max(8, rect.left - 20) + 'px'; tooltip.style.top = (rect.bottom + 8) + 'px'; } // Demo fertig - Weiter-Button einblenden, Tooltip bleibt stehen this._demoRunning = false; this._enableNavAfterDemo(); }, // Quellen-Demo Teil 2: + Quelle und Ausschlie\u00dfen Buttons async _simulateSourcesActions() { this._demoRunning = true; // Zum "+ Quelle" Button var addBtn = document.querySelector('.sources-toolbar-actions .btn-primary'); if (addBtn) { var addRect = addBtn.getBoundingClientRect(); this._showCursor(addRect.left - 40, addRect.top - 30, 'default'); await this._wait(300); await this._animateCursor(addRect.left - 40, addRect.top - 30, addRect.left + addRect.width / 2, addRect.top + addRect.height / 2, 500); this._highlightSub('.sources-toolbar-actions .btn-primary'); await this._wait(2500); this._clearSubHighlights(); } // Zum "Ausschlie\u00dfen" Button der ersten Quelle var excludeBtn = document.querySelector('#sources-list .source-group-actions .btn-secondary'); if (excludeBtn) { var exRect = excludeBtn.getBoundingClientRect(); var prevX = addBtn ? addBtn.getBoundingClientRect().left + addBtn.getBoundingClientRect().width / 2 : exRect.left; var prevY = addBtn ? addBtn.getBoundingClientRect().top + addBtn.getBoundingClientRect().height / 2 : exRect.top; await this._animateCursor(prevX, prevY, exRect.left + exRect.width / 2, exRect.top + exRect.height / 2, 500); this._highlightSub('#sources-list .source-group-actions .btn-secondary'); await this._wait(2500); this._clearSubHighlights(); } this._hideCursor(); this._demoRunning = false; this._enableNavAfterDemo(); }, // ----------------------------------------------------------------------- // Navigation nach Demo-Ende freigeben // ----------------------------------------------------------------------- _enableNavAfterDemo() { var bubble = this._els.bubble; if (!bubble) return; // Pulsieren stoppen bubble.classList.remove('tutorial-bubble-pulsing'); var nav = bubble.querySelector('.tutorial-bubble-nav'); if (!nav) return; var index = this._currentStep; var total = this._steps.length; var navHtml = ''; if (index > 0) { navHtml += ''; } else { navHtml += ''; } if (index < total - 1) { navHtml += ''; } else { navHtml += ''; } nav.innerHTML = navHtml; }, // ----------------------------------------------------------------------- // Keyboard // ----------------------------------------------------------------------- _onKey(e) { if (!this._isActive) return; switch (e.key) { case 'Escape': e.preventDefault(); this.stop(); break; case 'ArrowRight': case 'Enter': e.preventDefault(); this.next(); break; case 'ArrowLeft': e.preventDefault(); this.prev(); break; } }, // ----------------------------------------------------------------------- // Resize // ----------------------------------------------------------------------- _onResize() { if (!this._isActive) return; clearTimeout(this._resizeTimer); var self = this; this._resizeTimer = setTimeout(function() { if (!self._isActive || self._currentStep < 0) return; var step = self._steps[self._currentStep]; if (step.target && step.position !== 'center') { self._spotlightElement(step.target); } self._positionBubble(step); }, 150); }, // ----------------------------------------------------------------------- // Persistenz // ----------------------------------------------------------------------- _markSeen() { try { localStorage.setItem('osint_tutorial_seen', '1'); } catch(e) {} API.saveTutorialState({ completed: true, current_step: null }).catch(function() {}); }, _hasSeen() { try { return localStorage.getItem('osint_tutorial_seen') === '1'; } catch(e) { return false; } }, };