From d5022f0d6fe178436d10b98156bdcdaabb2508e5 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 16 Mar 2026 14:32:56 +0100 Subject: [PATCH] Tutorial: Demo-Lage mit Platzhaltern, detaillierte Erklaerungen, Scroll-to-View Kompletter Umbau des Tutorial-Systems: - Tutorial funktioniert jetzt ohne bestehende Lagen - Injiziert Demo-Lage (Explosion Hamburger Hafen) mit realistischen Platzhaltern in Sidebar, Lagebild, Faktencheck, Timeline, Quellen und Karte - 25 Steps statt 20: Neue Lage vs Recherche erklaert, jede Kachel detailliert - ScrollIntoView vor jedem Step (wichtig fuer Karte etc.) - Sub-Element-Highlighting: Markiert spezifische Funktionen innerhalb der Kacheln (Quellenverweise, Filter, Buttons, Kartensteuerung) - Sauberes Aufraumen: Demo-Daten werden nach Tutorial entfernt, Dashboard-Zustand wird vollstaendig wiederhergestellt Co-Authored-By: Claude Opus 4.6 (1M context) --- src/static/css/style.css | 16 + src/static/dashboard.html | 4 +- src/static/js/tutorial.js | 1952 +++++++++++++++++++++++-------------- 3 files changed, 1227 insertions(+), 745 deletions(-) diff --git a/src/static/css/style.css b/src/static/css/style.css index bed0256..dc2e9ef 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -5187,3 +5187,19 @@ a.map-popup-article:hover { .chat-tutorial-hint strong { color: var(--accent); } + + +/* Sub-Element Highlight innerhalb von Tutorial-Steps */ +.tutorial-sub-highlight { + outline: 2px solid var(--accent) !important; + outline-offset: 3px; + border-radius: var(--radius); + animation: tutorial-sub-pulse 1.5s ease-in-out infinite; + position: relative; + z-index: 9002; +} + +@keyframes tutorial-sub-pulse { + 0%, 100% { outline-color: var(--accent); } + 50% { outline-color: rgba(150, 121, 26, 0.4); } +} diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 433e6ca..6f37e4e 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -17,7 +17,7 @@ - + @@ -764,7 +764,7 @@ - + diff --git a/src/static/js/tutorial.js b/src/static/js/tutorial.js index 0e0751f..eaa2248 100644 --- a/src/static/js/tutorial.js +++ b/src/static/js/tutorial.js @@ -1,743 +1,1209 @@ -/** - * AegisSight Tutorial - Interaktiver Rundgang durch den Monitor. - */ -const Tutorial = { - _steps: [], - _currentStep: -1, - _isActive: false, - _cleanupFns: [], - _resizeTimer: null, - _keyHandler: null, - _resizeHandler: null, - _demoRunning: false, - - // 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'); - }, - - // ----------------------------------------------------------------------- - // Step-Definitionen - // ----------------------------------------------------------------------- - _defineSteps() { - this._steps = [ - // 0 - Welcome - { - id: 'welcome', - target: '#main-content', - title: 'Willkommen im AegisSight Monitor', - text: 'Dieser kurze Rundgang zeigt Ihnen die wichtigsten Funktionen des Monitors. Sie können jederzeit mit Escape abbrechen oder mit den Pfeiltasten navigieren.', - position: 'center', - }, - // 1 - Sidebar - { - id: 'sidebar', - target: '.sidebar', - title: 'Seitenleiste: Ihre Lagen', - text: 'Hier finden Sie alle Ihre Lagen, unterteilt in Live-Monitoring, Recherchen und das Archiv. Klicken Sie auf eine Lage, um sie zu öffnen.', - position: 'right', - }, - // 2 - Neue Lage Button - { - id: 'new-incident-btn', - target: '#new-incident-btn', - title: 'Neue Lage anlegen', - text: 'Mit diesem Button erstellen Sie eine neue Lage. Sie können zwischen Live-Monitoring und Recherche wählen.', - position: 'right', - onEnter: function() { - setTimeout(function() { - var btn = document.getElementById('new-incident-btn'); - if (btn) btn.click(); - }, 1500); - }, - }, - // 3 - Neue Lage Modal - { - id: 'new-incident-modal', - target: '#modal-new .modal', - title: 'Lage konfigurieren', - text: 'Hier geben Sie den Titel, die Beschreibung und den Typ der Lage ein. Bei Live-Monitoring-Lagen können Sie zusätzlich Suchbegriffe und Quellen festlegen.', - position: 'right', - requireModal: '#modal-new', - onEnter: function() { - var overlay = document.getElementById('modal-new'); - if (overlay && !overlay.classList.contains('active')) { - overlay.classList.add('active'); - } - if (overlay) overlay.style.zIndex = '9002'; - }, - onExit: function() { - var overlay = document.getElementById('modal-new'); - if (overlay) { - overlay.classList.remove('active'); - overlay.style.zIndex = ''; - } - }, - }, - // 4 - Sidebar Filter - { - id: 'sidebar-filters', - target: '.sidebar-filter', - title: 'Lagen filtern', - text: 'Wechseln Sie zwischen "Alle" Lagen und "Eigene", um nur Ihre selbst erstellten Lagen anzuzeigen.', - position: 'right', - }, - // 5 - Lagebild - { - id: 'lagebild', - target: '[gs-id="lagebild"]', - title: 'Lagebild', - text: 'Das Lagebild fasst die aktuelle Situation zusammen. Quellenverweise sind als Nummern markiert und führen direkt zu den Originalartikeln. Über das Pfeil-Symbol öffnen Sie die Vollansicht.', - position: 'top', - requireIncident: true, - }, - // 6 - Faktencheck - { - id: 'faktencheck', - target: '[gs-id="faktencheck"]', - title: 'Faktencheck', - text: 'Hier werden Behauptungen automatisch geprüft und in Kategorien eingeteilt: Bestätigt, Widersprüchlich, Unbelegt und Falsch. Jeder Eintrag zeigt die Quellen der Bewertung.', - position: 'top', - requireIncident: true, - }, - // 7 - Quellen - { - id: 'quellen', - target: '[gs-id="quellen"]', - title: 'Quellenübersicht', - text: 'Alle verwendeten Quellen auf einen Blick, gruppiert nach Kategorie. Klicken Sie auf eine Gruppe, um die einzelnen Quellen aufzuklappen.', - position: 'top', - requireIncident: true, - }, - // 8 - Timeline - { - id: 'timeline', - target: '[gs-id="timeline"]', - title: 'Ereignis-Timeline', - text: 'Die Timeline zeigt alle Ereignisse chronologisch. Nutzen Sie die Filter, um nach Zeitraum oder Kategorie einzugrenzen.', - position: 'top', - requireIncident: true, - }, - // 9 - Karte - { - id: 'karte', - target: '[gs-id="karte"]', - title: 'Geografische Verteilung', - text: 'Die Karte zeigt automatisch erkannte Orte aus den Quellen. Klicken Sie auf Marker für Details oder nutzen Sie das Vollbild-Symbol für eine größere Ansicht.', - position: 'top', - requireIncident: true, - }, - // 10 - Layout Toolbar - { - id: 'layout-toolbar', - target: '#layout-toolbar', - title: 'Layout-Steuerung', - text: 'Mit diesen Schaltern blenden Sie einzelne Kacheln ein oder aus. So passen Sie das Dashboard an Ihre Bedürfnisse an.', - position: 'bottom', - requireIncident: true, - }, - // 11 - Drag Demo - { - id: 'drag-demo', - target: '[gs-id="lagebild"] .card-header', - title: 'Kacheln verschieben', - text: 'Sie können jede Kachel per Drag-and-Drop an eine andere Position verschieben. Greifen Sie dazu die Kopfzeile der Kachel und ziehen Sie sie an die gewünschte Stelle.', - position: 'top', - requireIncident: true, - disableNav: true, - onEnter: function() { - Tutorial._simulateDrag(); - }, - }, - // 12 - Resize Demo - { - id: 'resize-demo', - target: '[gs-id="faktencheck"]', - title: 'Kacheln anpassen', - text: 'Ziehen Sie am rechten unteren Rand einer Kachel, um ihre Größe anzupassen. So können Sie wichtigen Inhalten mehr Platz einräumen.', - position: 'top', - requireIncident: true, - disableNav: true, - onEnter: function() { - Tutorial._simulateResize(); - }, - }, - // 13 - Theme - { - id: 'theme', - target: '#theme-toggle', - title: 'Design umschalten', - text: 'Wechseln Sie zwischen dem dunklen und hellen Design. Ihre Auswahl wird gespeichert.', - position: 'bottom', - }, - // 14 - Refresh - { - id: 'refresh', - target: '#refresh-btn', - title: 'Aktualisieren', - text: 'Starten Sie eine manuelle Aktualisierung der Lage. Bei Live-Monitoring-Lagen erfolgt die Aktualisierung zusätzlich automatisch in regelmaessigen Abstaenden.', - position: 'bottom', - requireIncident: true, - }, - // 15 - Quellen-Button - { - id: 'sources-btn', - target: '.sidebar-sources-link button:first-child', - title: 'Quellenverwaltung', - text: 'Hier öffnen Sie die Quellenverwaltung, um Quellen hinzuzufügen, zu bearbeiten oder zu deaktivieren.', - position: 'right', - onEnter: function() { - setTimeout(function() { - if (typeof App !== 'undefined' && App.openSourceManagement) { - App.openSourceManagement(); - } - }, 1500); - }, - }, - // 16 - Quellen Modal - { - id: 'sources-modal', - target: '#modal-sources .modal', - title: 'Quellen verwalten', - text: 'In der Quellenverwaltung sehen Sie alle konfigurierten Quellen. Sie können neue Quellen hinzufuegen, bestehende bearbeiten oder einzelne Quellen für bestimmte Lagen ausschließen.', - position: 'right', - requireModal: '#modal-sources', - 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'; - }, - onExit: function() { - var overlay = document.getElementById('modal-sources'); - if (overlay) { - overlay.classList.remove('active'); - overlay.style.zIndex = ''; - } - }, - }, - // 17 - Export - { - id: 'export', - target: '#export-dropdown', - title: 'Exportieren', - text: 'Exportieren Sie den Lagebericht als Markdown, JSON oder drucken Sie ihn als PDF. Waehlen Sie zwischen dem kompakten Lagebericht und dem Vollexport mit allen Details.', - position: 'bottom', - requireIncident: true, - }, - // 18 - Chat - { - id: 'chat', - target: '#chat-toggle-btn', - title: 'Chat-Assistent', - text: 'Der Chat-Assistent hilft Ihnen bei Fragen zur Bedienung des Monitors. Stellen Sie einfach Ihre Frage und erhalten Sie sofort eine Antwort.', - position: 'left', - }, - // 19 - Ende - { - id: 'end', - target: null, - title: 'Rundgang abgeschlossen', - text: 'Sie kennen jetzt die wichtigsten Funktionen des AegisSight Monitors. Bei weiteren Fragen steht Ihnen der Chat-Assistent oder unser Support unter support@aegis-sight.de zur Verfügung.', - position: 'center', - }, - ]; - }, - - // ----------------------------------------------------------------------- - // Lifecycle - // ----------------------------------------------------------------------- - start() { - if (this._isActive) return; - this._isActive = true; - this._currentStep = -1; - - // Chat schliessen falls offen - if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close(); - - // Overlay einblenden - this._els.overlay.classList.add('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); - - this.next(); - }, - - stop() { - if (!this._isActive) return; - - // Aktuellen Step aufraumen - if (this._currentStep >= 0) this._exitStep(this._currentStep); - - this._isActive = false; - this._currentStep = -1; - this._demoRunning = false; - - // Overlay ausblenden - this._els.overlay.classList.remove('active'); - this._els.spotlight.style.opacity = '0'; - this._els.bubble.classList.remove('visible'); - this._hideCursor(); - - // Events entfernen - if (this._keyHandler) { - document.removeEventListener('keydown', this._keyHandler); - this._keyHandler = null; - } - if (this._resizeHandler) { - window.removeEventListener('resize', this._resizeHandler); - this._resizeHandler = null; - } - - // Cleanup-Callbacks - this._cleanupFns.forEach(function(fn) { try { fn(); } catch(e) {} }); - this._cleanupFns = []; - - this._markSeen(); - }, - - // ----------------------------------------------------------------------- - // Navigation - // ----------------------------------------------------------------------- - next() { - if (!this._isActive || this._demoRunning) return; - var nextIdx = this._currentStep + 1; - - // Skip Steps deren Voraussetzungen nicht erfuellt sind - while (nextIdx < this._steps.length && this._shouldSkip(nextIdx)) { - nextIdx++; - } - - if (nextIdx >= this._steps.length) { - this.stop(); - return; - } - - this.goToStep(nextIdx); - }, - - prev() { - if (!this._isActive || this._demoRunning) return; - var prevIdx = this._currentStep - 1; - - while (prevIdx >= 0 && this._shouldSkip(prevIdx)) { - prevIdx--; - } - - 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; - this._enterStep(index); - }, - - _shouldSkip(index) { - var step = this._steps[index]; - if (!step) return true; - - // Steps die eine offene Lage brauchen - if (step.requireIncident) { - var view = document.getElementById('incident-view'); - if (!view || view.style.display === 'none' || !view.offsetParent) return true; - } - - // Target muss existieren (ausser center-Steps) - if (step.target && step.position !== 'center') { - var el = document.querySelector(step.target); - if (!el) return true; - } - - return false; - }, - - // ----------------------------------------------------------------------- - // Step Enter/Exit - // ----------------------------------------------------------------------- - _enterStep(i) { - var step = this._steps[i]; - var self = this; - - // Spotlight positionieren - if (step.target && step.position !== 'center') { - this._spotlightElement(step.target); - } else { - // Kein Spotlight / zentrierte Bubble - 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]; - - // onExit-Callback - if (step.onExit) { - step.onExit(); - } - - this._hideCursor(); - }, - - // ----------------------------------------------------------------------- - // Spotlight - // ----------------------------------------------------------------------- - _spotlightElement(selector) { - var el = document.querySelector(selector); - if (!el) { - this._els.spotlight.style.opacity = '0'; - return; - } - - var rect = el.getBoundingClientRect(); - var pad = 8; - var s = this._els.spotlight.style; - - s.top = (rect.top - pad) + 'px'; - s.left = (rect.left - pad) + 'px'; - s.width = (rect.width + pad * 2) + 'px'; - s.height = (rect.height + pad * 2) + '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 (index > 0 && !step.disableNav) { - navHtml += ''; - } else { - navHtml += ''; - } - if (index < total - 1 && !step.disableNav) { - navHtml += ''; - } else if (index === total - 1) { - navHtml += ''; - } else { - navHtml += ''; - } - navHtml += '
'; - - // Close-Button - var closeHtml = ''; - - bubble.innerHTML = closeHtml + counterHtml + titleHtml + textHtml + dotsHtml + navHtml; - - // Positionierung - this._positionBubble(step); - - // Sichtbar machen - bubble.classList.add('visible'); - }, - - _positionBubble(step) { - var bubble = this._els.bubble; - var bw = 340; - - // 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%)'; - 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%)'; - return; - } - - var rect = el.getBoundingClientRect(); - var vw = window.innerWidth; - var vh = window.innerHeight; - var gap = 16; - - // Reset transform - bubble.style.transform = ''; - - // Automatische Positionswahl falls nicht genug Platz - var pos = step.position || 'bottom'; - var bubbleHeight = 250; // Schaetzung - - if (pos === 'bottom' && (rect.bottom + gap + bubbleHeight > vh)) pos = 'top'; - if (pos === 'top' && (rect.top - gap - bubbleHeight < 0)) pos = 'bottom'; - if (pos === 'right' && (rect.right + gap + bw > vw)) pos = 'left'; - if (pos === 'left' && (rect.left - gap - bw < 0)) pos = 'right'; - - bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos; - - switch (pos) { - case 'bottom': - bubble.style.top = (rect.bottom + gap) + 'px'; - bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px'; - break; - case 'top': - bubble.style.top = (rect.top - gap) + 'px'; - bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px'; - bubble.style.transform = 'translateY(-100%)'; - break; - case 'right': - bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px'; - bubble.style.left = (rect.right + gap) + 'px'; - break; - case 'left': - bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px'; - bubble.style.left = (rect.left - gap - bw) + '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) { - var self = this; - return new Promise(function(resolve) { - setTimeout(function() { resolve(); }, ms); - }); - }, - - // ----------------------------------------------------------------------- - // Drag-Demo (Step 11) - // ----------------------------------------------------------------------- - async _simulateDrag() { - this._demoRunning = true; - var el = document.querySelector('[gs-id="lagebild"] .card-header'); - if (!el) { this._demoRunning = false; this.next(); return; } - - var rect = el.getBoundingClientRect(); - var startX = rect.left + rect.width / 2; - var startY = rect.top + rect.height / 2; - var endX = startX + 200; - var endY = startY; - - // 1. Cursor erscheint - this._showCursor(startX, startY, 'default'); - await this._wait(500); - - // 2. Grabbing-State - this._els.cursor.classList.remove('tutorial-cursor-default'); - this._els.cursor.classList.add('tutorial-cursor-grabbing'); - await this._wait(300); - - // 3. Nach rechts gleiten - await this._animateCursor(startX, startY, endX, endY, 1500); - await this._wait(200); - - // 4. Release + zurück - this._els.cursor.classList.remove('tutorial-cursor-grabbing'); - this._els.cursor.classList.add('tutorial-cursor-default'); - await this._animateCursor(endX, endY, startX, startY, 800); - await this._wait(300); - - // 5. Cursor verschwindet - this._hideCursor(); - this._demoRunning = false; - this._enableNavAfterDemo(); - }, - - // ----------------------------------------------------------------------- - // Resize-Demo (Step 12) - // ----------------------------------------------------------------------- - async _simulateResize() { - this._demoRunning = true; - var el = document.querySelector('[gs-id="faktencheck"]'); - if (!el) { this._demoRunning = false; this.next(); return; } - - var rect = el.getBoundingClientRect(); - var startX = rect.right - 4; - var startY = rect.bottom - 4; - var endX = startX + 100; - var endY = startY + 60; - - // 1. Cursor an unterer rechter Ecke - this._showCursor(startX, startY, 'resize'); - await this._wait(500); - - // 2. Diagonal ziehen - await this._animateCursor(startX, startY, endX, endY, 1200); - await this._wait(200); - - // 3. Zurück - await this._animateCursor(endX, endY, startX, startY, 800); - await this._wait(300); - - // 4. Cursor verschwindet - this._hideCursor(); - this._demoRunning = false; - this._enableNavAfterDemo(); - }, - - _enableNavAfterDemo() { - // Nav-Buttons nachträglich einblenden - var bubble = this._els.bubble; - var nav = bubble.querySelector('.tutorial-bubble-nav'); - if (!nav) return; - var step = this._steps[this._currentStep]; - 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) {} - }, - - _hasSeen() { - try { return localStorage.getItem('osint_tutorial_seen') === '1'; } catch(e) { return false; } - }, -}; +/** + * 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, + _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 Morgen des 16. März 2026 kam es im Hamburger Hafen zu einer schweren Explosion ' + + 'in einem Containerterminal am Burchardkai. Nach übereinstimmenden Berichten von ' + + 'dpa [1], ' + + 'Reuters [2] und ' + + 'NDR [3] ' + + 'ereignete sich die Detonation gegen 06:45 Uhr Ortszeit in einem Lagerbereich für Gefahrgut.

' + + '

Die Hamburger Feuerwehr ist mit einem Großaufgebot vor Ort. Laut ' + + 'Hamburger Abendblatt [4] ' + + 'wurden mindestens 12 Personen verletzt, davon 3 schwer. Die Ursache der Explosion ist noch unklar. ' + + 'Die Polizei Hamburg hat den Bereich weiträumig abgesperrt.

' + + '

Der Hamburger Hafen, Europas drittgrößter Seehafen, hat den Betrieb im betroffenen Terminal vorübergehend ' + + 'eingestellt. Auswirkungen auf den Schiffsverkehr werden derzeit geprüft ' + + 'HPA [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' }, + ], + + // ----------------------------------------------------------------------- + // Demo-View injizieren / entfernen + // ----------------------------------------------------------------------- + _injectDemoView() { + // Zustand sichern + var incidentView = document.getElementById('incident-view'); + var emptyState = document.getElementById('empty-state'); + this._savedState = { + 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 = ''; + + // 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 + var timeline = document.getElementById('timeline'); + if (timeline) { + var tlHtml = '
'; + this._DEMO_TIMELINE.forEach(function(ev) { + var cls = ev.type === 'snapshot' ? 'ht-entry ht-snapshot' : 'ht-entry'; + tlHtml += '
' + + '
' + ev.time + '
' + + '
' + + '
' + + '
' + ev.title + '
' + + '
' + ev.source + '
' + + '
'; + }); + tlHtml += '
'; + timeline.innerHTML = tlHtml; + } + + // Karte: "Keine Orte" ausblenden, Platzhalter einsetzen + var mapEmpty = document.getElementById('map-empty'); + if (mapEmpty) mapEmpty.style.display = 'none'; + var mapContainer = document.getElementById('map-container'); + if (mapContainer) { + var mapPlaceholder = document.createElement('div'); + mapPlaceholder.className = 'tutorial-demo tutorial-map-placeholder'; + mapPlaceholder.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;' + + 'background:var(--bg-secondary);color:var(--text-secondary);font-size:13px;'; + mapPlaceholder.innerHTML = '
' + + '
🌎
' + + '
3 Orte erkannt: Hamburg, Burchardkai, Elbe
'; + mapContainer.appendChild(mapPlaceholder); + } + var mapStats = document.getElementById('map-stats'); + if (mapStats) mapStats.textContent = '3 Orte'; + + // 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; + + // Map-Platzhalter entfernen + var mapPlaceholder = document.querySelector('.tutorial-map-placeholder'); + if (mapPlaceholder) mapPlaceholder.remove(); + + // 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 = ''; + + this._savedState = null; + }, + + // ----------------------------------------------------------------------- + // Highlight-Helfer: Einzelnes Sub-Element innerhalb einer Kachel markieren + // ----------------------------------------------------------------------- + _highlightSub(selector) { + var el = document.querySelector(selector); + if (!el) return; + el.classList.add('tutorial-sub-highlight'); + this._cleanupFns.push(function() { + el.classList.remove('tutorial-sub-highlight'); + }); + }, + + _clearSubHighlights() { + document.querySelectorAll('.tutorial-sub-highlight').forEach(function(el) { + el.classList.remove('tutorial-sub-highlight'); + }); + }, + + _cleanupFns: [], + + // ----------------------------------------------------------------------- + // 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: 'Hier starten Sie die Erstellung einer neuen Lage. Im nächsten Schritt zeigen wir Ihnen das Formular dazu.', + position: 'right', + onEnter: function() { + setTimeout(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 - Neue Lage Modal: Überblick + { + id: 'new-incident-modal', + target: '#modal-new .modal', + title: 'Lage konfigurieren', + text: 'Das Formular zur Lage-Erstellung. Hier legen Sie fest:

' + + 'Titel - Kurze Bezeichnung des Vorfalls
' + + 'Beschreibung - Kontext und Hintergrundinformationen
' + + 'Art der Lage - Live-Monitoring oder Analyse/Recherche
' + + 'Quellen - Internationale und Telegram-Quellen ein/aus
' + + 'Sichtbarkeit - Öffentlich oder privat für Ihr Team
' + + 'Aktualisierung - Manuell oder automatisch mit Intervall', + position: 'left', + onEnter: function() { + var overlay = document.getElementById('modal-new'); + if (overlay && !overlay.classList.contains('active')) { + overlay.classList.add('active'); + } + if (overlay) overlay.style.zIndex = '9002'; + // Demo: Titel vorausfüllen + var titleInput = document.getElementById('inc-title'); + if (titleInput) titleInput.value = 'Explosion in Hamburger Hafen'; + var descInput = document.getElementById('inc-description'); + if (descInput) descInput.value = 'Schwere Explosion im Hamburger Hafengebiet, Burchardkai-Terminal'; + }, + onExit: function() { + var overlay = document.getElementById('modal-new'); + if (overlay) { + overlay.classList.remove('active'); + overlay.style.zIndex = ''; + } + var titleInput = document.getElementById('inc-title'); + if (titleInput) titleInput.value = ''; + var descInput = document.getElementById('inc-description'); + if (descInput) descInput.value = ''; + }, + }, + // 4 - Lage-Typ: Live vs Recherche + { + id: 'incident-type', + target: '#modal-new .modal', + title: 'Live-Monitoring vs. Recherche', + text: 'Live-Monitoring beobachtet ein Ereignis in Echtzeit. ' + + 'Hunderte Nachrichtenquellen werden automatisch durchsucht. ' + + 'Ideal für aktuelle Vorfälle, Krisen oder sich entwickelnde Lagen.

' + + 'Analyse/Recherche untersucht ein Thema tiefergehend. ' + + 'Keine automatischen Updates, stattdessen gezielte Recherche mit eigenen Suchbegriffen. ' + + 'Ideal für Hintergrundanalysen und Lageberichte.', + position: 'left', + 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._highlightSub('#inc-type'); + }, + 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() { + 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 + { + id: 'karte', + target: '[gs-id="karte"]', + title: 'Geografische Verteilung', + text: 'Die Karte zeigt automatisch erkannte Orte aus den gesammelten Artikeln (Geoparsing).

' + + 'Marker - Klicken Sie auf einen Marker für Details zum Ort und verknüpfte Artikel
' + + 'Cluster - Bei vielen Markern werden nahe Orte gruppiert
' + + 'Orte einlesen - Startet das Geoparsing manuell neu
' + + 'Vollbild - Vergrößert die Karte auf den gesamten Bildschirm

' + + 'Besonders bei internationalen Lagen bietet die Karte einen schnellen Überblick über die räumliche Verteilung.', + position: 'top', + onEnter: function() { + Tutorial._highlightSub('#geoparse-btn'); + setTimeout(function() { + Tutorial._clearSubHighlights(); + Tutorial._highlightSub('#map-expand-btn'); + }, 3000); + }, + onExit: function() { + 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._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._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() { + setTimeout(function() { + var overlay = document.getElementById('modal-sources'); + if (overlay && !overlay.classList.contains('active')) { + if (typeof App !== 'undefined' && App.openSourceManagement) { + App.openSourceManagement(); + } + } + }, 1500); + }, + }, + // 22 - Quellen Modal + { + id: 'sources-modal', + target: '#modal-sources .modal', + title: 'Quellen verwalten', + text: 'Die Quellenverwaltung zeigt alle konfigurierten Nachrichtenquellen. ' + + 'Hier sehen Sie für jede Quelle:

' + + 'Name und URL - Die Quellbezeichnung und Adresse
' + + 'Kategorie - Nachrichtenagentur, Qualitätszeitung, etc.
' + + 'Sprache - Deutsch, Englisch oder Mehrsprachig
' + + 'Status - Aktiv oder deaktiviert

' + + 'Sie können Quellen auch für einzelne Lagen ausschließen, ohne sie global zu deaktivieren.', + position: 'left', + 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'; + }, + onExit: function() { + var overlay = document.getElementById('modal-sources'); + if (overlay) { + overlay.classList.remove('active'); + overlay.style.zIndex = ''; + } + }, + }, + // 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 + // ----------------------------------------------------------------------- + start() { + if (this._isActive) return; + this._isActive = true; + this._currentStep = -1; + + // Chat schließen falls offen + if (typeof Chat !== 'undefined' && Chat._isOpen) Chat.close(); + + // Overlay einblenden + this._els.overlay.classList.add('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); + + 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 + this._els.overlay.classList.remove('active'); + this._els.spotlight.style.opacity = '0'; + this._els.bubble.classList.remove('visible'); + this._hideCursor(); + this._clearSubHighlights(); + + // Cleanup-Callbacks + this._cleanupFns.forEach(function(fn) { try { fn(); } catch(e) {} }); + this._cleanupFns = []; + + // 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; + } + + 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; + this._enterStep(index); + }, + + // ----------------------------------------------------------------------- + // Step Enter/Exit + // ----------------------------------------------------------------------- + async _enterStep(i) { + var step = this._steps[i]; + + // Erst scrollen, dann spotlighten + if (step.target && step.position !== 'center') { + 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]; + 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 pad = 8; + var s = this._els.spotlight.style; + + s.top = (rect.top - pad) + 'px'; + s.left = (rect.left - pad) + 'px'; + s.width = (rect.width + pad * 2) + 'px'; + s.height = (rect.height + pad * 2) + '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 (index > 0 && !step.disableNav) { + navHtml += ''; + } else { + navHtml += ''; + } + if (index < total - 1 && !step.disableNav) { + navHtml += ''; + } else if (index === total - 1) { + navHtml += ''; + } else { + navHtml += ''; + } + navHtml += '
'; + + // Close-Button + var closeHtml = ''; + + bubble.innerHTML = closeHtml + counterHtml + titleHtml + textHtml + dotsHtml + navHtml; + + // Positionierung + this._positionBubble(step); + + // Sichtbar machen + bubble.classList.add('visible'); + }, + + _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; + + // 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' && (rect.bottom + gap + bubbleHeight > vh)) pos = 'top'; + if (pos === 'top' && (rect.top - gap - bubbleHeight < 0)) pos = 'bottom'; + if (pos === 'right' && (rect.right + gap + bw > vw)) pos = 'left'; + if (pos === 'left' && (rect.left - gap - bw < 0)) pos = 'right'; + + bubble.className = 'tutorial-bubble visible tutorial-pos-' + pos; + + switch (pos) { + case 'bottom': + bubble.style.top = (rect.bottom + gap) + 'px'; + bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px'; + break; + case 'top': + bubble.style.top = (rect.top - gap) + 'px'; + bubble.style.left = Math.max(8, Math.min(rect.left + rect.width / 2 - bw / 2, vw - bw - 8)) + 'px'; + bubble.style.transform = 'translateY(-100%)'; + break; + case 'right': + bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px'; + bubble.style.left = (rect.right + gap) + 'px'; + break; + case 'left': + bubble.style.top = Math.max(8, rect.top + rect.height / 2 - bubbleHeight / 2) + 'px'; + bubble.style.left = (rect.left - gap - bw) + '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); + }); + }, + + // ----------------------------------------------------------------------- + // Drag-Demo + // ----------------------------------------------------------------------- + async _simulateDrag() { + this._demoRunning = true; + var el = document.querySelector('[gs-id="lagebild"] .card-header'); + if (!el) { this._demoRunning = false; this._enableNavAfterDemo(); return; } + + var rect = el.getBoundingClientRect(); + var startX = rect.left + rect.width / 2; + var startY = rect.top + rect.height / 2; + var endX = startX + 200; + var endY = startY; + + this._showCursor(startX, startY, 'default'); + await this._wait(500); + + this._els.cursor.classList.remove('tutorial-cursor-default'); + this._els.cursor.classList.add('tutorial-cursor-grabbing'); + await this._wait(300); + + await this._animateCursor(startX, startY, endX, endY, 1500); + await this._wait(200); + + this._els.cursor.classList.remove('tutorial-cursor-grabbing'); + this._els.cursor.classList.add('tutorial-cursor-default'); + await this._animateCursor(endX, endY, startX, startY, 800); + await this._wait(300); + + this._hideCursor(); + this._demoRunning = false; + this._enableNavAfterDemo(); + }, + + // ----------------------------------------------------------------------- + // Resize-Demo + // ----------------------------------------------------------------------- + async _simulateResize() { + this._demoRunning = true; + var el = document.querySelector('[gs-id="faktencheck"]'); + if (!el) { this._demoRunning = false; this._enableNavAfterDemo(); return; } + + var rect = el.getBoundingClientRect(); + var startX = rect.right - 4; + var startY = rect.bottom - 4; + var endX = startX + 100; + var endY = startY + 60; + + this._showCursor(startX, startY, 'resize'); + await this._wait(500); + + await this._animateCursor(startX, startY, endX, endY, 1200); + await this._wait(200); + + await this._animateCursor(endX, endY, startX, startY, 800); + await this._wait(300); + + this._hideCursor(); + this._demoRunning = false; + this._enableNavAfterDemo(); + }, + + _enableNavAfterDemo() { + var bubble = this._els.bubble; + 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) {} + }, + + _hasSeen() { + try { return localStorage.getItem('osint_tutorial_seen') === '1'; } catch(e) { return false; } + }, +};