Commits vergleichen
2 Commits
4b9ed6439a
...
4d6d022bee
| Autor | SHA1 | Datum | |
|---|---|---|---|
|
|
4d6d022bee | ||
|
|
5e194d43e0 |
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -464,6 +464,13 @@ async def init_db():
|
||||
await db.execute("ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1")
|
||||
await db.commit()
|
||||
|
||||
# Migration: Tutorial-Fortschritt pro User
|
||||
if "tutorial_step" not in user_columns:
|
||||
await db.execute("ALTER TABLE users ADD COLUMN tutorial_step INTEGER DEFAULT NULL")
|
||||
await db.execute("ALTER TABLE users ADD COLUMN tutorial_completed INTEGER DEFAULT 0")
|
||||
await db.commit()
|
||||
logger.info("Migration: tutorial_step + tutorial_completed zu users hinzugefuegt")
|
||||
|
||||
if "last_login_at" not in user_columns:
|
||||
await db.execute("ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP")
|
||||
await db.commit()
|
||||
|
||||
@@ -333,6 +333,7 @@ from routers.feedback import router as feedback_router
|
||||
from routers.public_api import router as public_api_router
|
||||
from routers.chat import router as chat_router
|
||||
from routers.network_analysis import router as network_analysis_router
|
||||
from routers.tutorial import router as tutorial_router
|
||||
|
||||
app.include_router(auth_router)
|
||||
app.include_router(incidents_router)
|
||||
@@ -342,6 +343,7 @@ app.include_router(feedback_router)
|
||||
app.include_router(public_api_router)
|
||||
app.include_router(chat_router, prefix="/api/chat")
|
||||
app.include_router(network_analysis_router)
|
||||
app.include_router(tutorial_router)
|
||||
|
||||
|
||||
@app.websocket("/api/ws")
|
||||
|
||||
77
src/routers/tutorial.py
Normale Datei
77
src/routers/tutorial.py
Normale Datei
@@ -0,0 +1,77 @@
|
||||
"""Tutorial-Router: Fortschritt serverseitig pro User speichern."""
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from auth import get_current_user
|
||||
from database import db_dependency
|
||||
import aiosqlite
|
||||
|
||||
logger = logging.getLogger("osint.tutorial")
|
||||
|
||||
router = APIRouter(prefix="/api/tutorial", tags=["tutorial"])
|
||||
|
||||
|
||||
@router.get("/state")
|
||||
async def get_tutorial_state(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Tutorial-Fortschritt des aktuellen Nutzers abrufen."""
|
||||
cursor = await db.execute(
|
||||
"SELECT tutorial_step, tutorial_completed FROM users WHERE id = ?",
|
||||
(current_user["id"],),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return {"current_step": None, "completed": False}
|
||||
return {
|
||||
"current_step": row["tutorial_step"],
|
||||
"completed": bool(row["tutorial_completed"]),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/state")
|
||||
async def save_tutorial_state(
|
||||
body: dict,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Tutorial-Fortschritt speichern (current_step und/oder completed)."""
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if "current_step" in body:
|
||||
step = body["current_step"]
|
||||
if step is not None and (not isinstance(step, int) or step < 0 or step > 31):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=422, detail="current_step muss 0-31 oder null sein")
|
||||
updates.append("tutorial_step = ?")
|
||||
params.append(step)
|
||||
|
||||
if "completed" in body:
|
||||
updates.append("tutorial_completed = ?")
|
||||
params.append(1 if body["completed"] else 0)
|
||||
|
||||
if not updates:
|
||||
return {"ok": True}
|
||||
|
||||
params.append(current_user["id"])
|
||||
await db.execute(
|
||||
f"UPDATE users SET {', '.join(updates)} WHERE id = ?",
|
||||
params,
|
||||
)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/state")
|
||||
async def reset_tutorial_state(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Tutorial-Fortschritt zuruecksetzen (fuer Neustart)."""
|
||||
await db.execute(
|
||||
"UPDATE users SET tutorial_step = NULL, tutorial_completed = 0 WHERE id = ?",
|
||||
(current_user["id"],),
|
||||
)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
@@ -215,6 +215,19 @@ const API = {
|
||||
},
|
||||
|
||||
// Export
|
||||
|
||||
// Tutorial-Fortschritt
|
||||
getTutorialState() {
|
||||
return this._request('GET', '/tutorial/state');
|
||||
},
|
||||
|
||||
saveTutorialState(data) {
|
||||
return this._request('PUT', '/tutorial/state', data);
|
||||
},
|
||||
|
||||
resetTutorialState() {
|
||||
return this._request('DELETE', '/tutorial/state');
|
||||
},
|
||||
exportIncident(id, format, scope) {
|
||||
const token = localStorage.getItem('osint_token');
|
||||
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}`, {
|
||||
|
||||
@@ -6,6 +6,7 @@ const Chat = {
|
||||
_isOpen: false,
|
||||
_isLoading: false,
|
||||
_hasGreeted: false,
|
||||
_tutorialHintDismissed: false,
|
||||
_isFullscreen: false,
|
||||
|
||||
init() {
|
||||
@@ -64,10 +65,13 @@ const Chat = {
|
||||
if (!this._hasGreeted) {
|
||||
this._hasGreeted = true;
|
||||
this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent. Stell mir gerne jede Frage rund um die Bedienung des Monitors, ich helfe dir weiter.');
|
||||
// Tutorial-Hinweis beim ersten Chat-Oeffnen der Session
|
||||
if (typeof Tutorial !== 'undefined' && !sessionStorage.getItem('osint_tutorial_hint_dismissed')) {
|
||||
this._showTutorialHint();
|
||||
}
|
||||
}
|
||||
|
||||
// Tutorial-Hinweis bei jedem Oeffnen aktualisieren (wenn nicht dismissed)
|
||||
if (typeof Tutorial !== 'undefined' && !this._tutorialHintDismissed) {
|
||||
var oldHint = document.getElementById('chat-tutorial-hint');
|
||||
if (oldHint) oldHint.remove();
|
||||
this._showTutorialHint();
|
||||
}
|
||||
|
||||
// Focus auf Input
|
||||
@@ -288,29 +292,57 @@ const Chat = {
|
||||
}
|
||||
},
|
||||
|
||||
_showTutorialHint() {
|
||||
async _showTutorialHint() {
|
||||
var container = document.getElementById('chat-messages');
|
||||
if (!container) return;
|
||||
|
||||
// API-State laden (Fallback: Standard-Hint)
|
||||
var state = null;
|
||||
try { state = await API.getTutorialState(); } catch(e) {}
|
||||
|
||||
var hint = document.createElement('div');
|
||||
hint.className = 'chat-tutorial-hint';
|
||||
hint.id = 'chat-tutorial-hint';
|
||||
var textDiv = document.createElement('div');
|
||||
textDiv.className = 'chat-tutorial-hint-text';
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt f\u00fcr Schritt alle Funktionen des Monitors. Klicken Sie hier, um ihn zu starten.';
|
||||
textDiv.style.cursor = 'pointer';
|
||||
textDiv.addEventListener('click', function() {
|
||||
Chat.close();
|
||||
sessionStorage.setItem('osint_tutorial_hint_dismissed', '1');
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||
});
|
||||
|
||||
if (state && !state.completed && state.current_step !== null && state.current_step > 0) {
|
||||
// Mittendrin abgebrochen
|
||||
var totalSteps = (typeof Tutorial !== 'undefined') ? Tutorial._steps.length : 32;
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bei Schritt ' + (state.current_step + 1) + '/' + totalSteps + ' unterbrochen. Klicken Sie hier, um fortzusetzen.';
|
||||
textDiv.addEventListener('click', function() {
|
||||
Chat.close();
|
||||
Chat._tutorialHintDismissed = true;
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||
});
|
||||
} else if (state && state.completed) {
|
||||
// Bereits abgeschlossen
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bereits abgeschlossen. <span style="text-decoration:underline;">Erneut starten?</span>';
|
||||
textDiv.addEventListener('click', async function() {
|
||||
Chat.close();
|
||||
Chat._tutorialHintDismissed = true;
|
||||
try { await API.resetTutorialState(); } catch(e) {}
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start(true);
|
||||
});
|
||||
} else {
|
||||
// Nie gestartet
|
||||
textDiv.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt für Schritt alle Funktionen des Monitors. Klicken Sie hier, um ihn zu starten.';
|
||||
textDiv.addEventListener('click', function() {
|
||||
Chat.close();
|
||||
Chat._tutorialHintDismissed = true;
|
||||
if (typeof Tutorial !== 'undefined') Tutorial.start();
|
||||
});
|
||||
}
|
||||
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'chat-tutorial-hint-close';
|
||||
closeBtn.title = 'Schlie\u00dfen';
|
||||
closeBtn.title = 'Schließen';
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
hint.remove();
|
||||
sessionStorage.setItem('osint_tutorial_hint_dismissed', '1');
|
||||
Chat._tutorialHintDismissed = true;
|
||||
});
|
||||
hint.appendChild(textDiv);
|
||||
hint.appendChild(closeBtn);
|
||||
|
||||
@@ -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