/**
* 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 += '
';
});
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:
'
+ '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:
'
+ '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 += '