feat: Tutorial-Fortschritt serverseitig persistieren (Resume/Restart)
- Neuer Router /api/tutorial mit GET/PUT/DELETE für Fortschritt pro User - DB-Migration: tutorial_step + tutorial_completed in users-Tabelle - Resume-Dialog bei abgebrochenem Tutorial (Fortsetzen/Neu starten) - Chat-Hinweis passt sich dem Tutorial-Status dynamisch an - API-Methoden: getTutorialState, saveTutorialState, resetTutorialState Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -12,6 +12,7 @@ const Tutorial = {
|
||||
_resizeHandler: null,
|
||||
_demoRunning: false,
|
||||
_lastExitedStep: -1,
|
||||
_highestStep: -1,
|
||||
_stepTimers: [], // setTimeout-IDs fuer den aktuellen Step
|
||||
_savedState: null, // Dashboard-Zustand vor dem Tutorial
|
||||
|
||||
@@ -1152,14 +1153,52 @@ const Tutorial = {
|
||||
// -----------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -----------------------------------------------------------------------
|
||||
start() {
|
||||
async start(forceRestart) {
|
||||
if (this._isActive) return;
|
||||
this._isActive = true;
|
||||
this._currentStep = -1;
|
||||
|
||||
// Chat schließen falls offen
|
||||
// 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 = '<div class=tutorial-resume-dialog>'
|
||||
+ '<p>Sie haben den Rundgang bei <strong>Schritt ' + (step + 1) + '/' + this._steps.length + '</strong> unterbrochen.</p>'
|
||||
+ '<div class=tutorial-resume-actions>'
|
||||
+ '<button class=tutorial-btn tutorial-btn-next id=tutorial-resume-btn>Fortsetzen</button>'
|
||||
+ '<button class=tutorial-btn tutorial-btn-secondary id=tutorial-restart-btn>Neu starten</button>'
|
||||
+ '</div></div>';
|
||||
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');
|
||||
@@ -1172,7 +1211,11 @@ const Tutorial = {
|
||||
this._resizeHandler = this._onResize.bind(this);
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
|
||||
this.next();
|
||||
if (resumeStep && resumeStep > 0) {
|
||||
this.goToStep(resumeStep);
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
@@ -1230,7 +1273,14 @@ const Tutorial = {
|
||||
this._resizeHandler = null;
|
||||
}
|
||||
|
||||
this._markSeen();
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -1258,6 +1308,7 @@ const Tutorial = {
|
||||
|
||||
if (this._currentStep >= 0) this._exitStep(this._currentStep);
|
||||
this._currentStep = index;
|
||||
if (index > this._highestStep) this._highestStep = index;
|
||||
this._enterStep(index);
|
||||
},
|
||||
|
||||
@@ -2231,6 +2282,7 @@ const Tutorial = {
|
||||
// -----------------------------------------------------------------------
|
||||
_markSeen() {
|
||||
try { localStorage.setItem('osint_tutorial_seen', '1'); } catch(e) {}
|
||||
API.saveTutorialState({ completed: true, current_step: null }).catch(function() {});
|
||||
},
|
||||
|
||||
_hasSeen() {
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren