Compare commits
108 Commits
d255c7b4db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 063cbf944f | |||
| 0e66aff47d | |||
| 6a7e90116a | |||
| 4a9d1f4ada | |||
| dbed1ad79f | |||
| 69b15aa4df | |||
| 0b3e5d39cf | |||
| 1c46eb2421 | |||
| 955bdb7057 | |||
| 3bc6c8f75c | |||
| 9ebdb7ed3e | |||
| b1671aa29a | |||
| 1bfbd53062 | |||
| 983e57cd8d | |||
| f8f068150e | |||
| 501ecc0c1e | |||
| 3a1bd759c7 | |||
| 0b3286af4c | |||
| e853ca8e93 | |||
| e52cada445 | |||
| c5009d1718 | |||
| 860b1ffba9 | |||
| bb1773e32e | |||
| 2b86466a7e | |||
| 2612d4eaec | |||
| 56e455c45d | |||
| ce64427008 | |||
| 742f19db8c | |||
| 664e3adaeb | |||
| 4a07ac8dbf | |||
| 565cf1e23c | |||
| 5abaa6d933 | |||
| dbe95809c2 | |||
| e33569f1ae | |||
| c31438806c | |||
| e875c9a7d3 | |||
| 305e3aa4b6 | |||
| 7f47261c78 | |||
| f3ac6ed252 | |||
| 5fcbdfb8c8 | |||
| ce1975b47a | |||
| 80f556b1d0 | |||
| 3f23f223ce | |||
| ad94c232f6 | |||
| ac5e6e6496 | |||
| 42d0342981 | |||
| f2ea8936c3 | |||
| f9e6516124 | |||
| fb487be037 | |||
| 85e8647cdd | |||
| 49d588b609 | |||
| a484416ea0 | |||
| d5d4add293 | |||
| 7a97e6f24e | |||
| bd76cd11de | |||
| 56cd17520e | |||
| f9ee3092a3 | |||
| 2e01583d8f | |||
| 95df002e24 | |||
| f90f21589d | |||
| 3f32fcd6c1 | |||
| b8f830dce0 | |||
| 27c409bfb3 | |||
| 010a294151 | |||
| c665857ce8 | |||
| 07f6693954 | |||
| d718a09561 | |||
| b0d8dd48db | |||
| 73e74a2859 | |||
| 3eef7cdb40 | |||
| 6d243c8987 | |||
| 6bde27a241 | |||
| 0f08c93a67 | |||
| abc9e0e96d | |||
| 64234ad84c | |||
| 48dfde17eb | |||
| dbd1960424 | |||
| ea1ba38cf0 | |||
| 548a90f15e | |||
| a23a1b4e7e | |||
| 87cd63e373 | |||
| 4bf019747a | |||
| 333af0253e | |||
| 018efdee0f | |||
| 0f2c9eae86 | |||
| 17702554f8 | |||
| 4935d00b4e | |||
| 0b0360be9d | |||
| edb5b36408 | |||
| 986f6226ef | |||
| 4f99593d34 | |||
| 6195a11501 | |||
| 02975ebb25 | |||
| bcd216fe60 | |||
| 005d2d7d8c | |||
| 4f5042dc43 | |||
| db6b5bbb1a | |||
| 762812649c | |||
| 6fd8f8ecc8 | |||
| b4fbc972f2 | |||
| 21c594445f | |||
| 035d603504 | |||
| adb5646b5e | |||
| 08e5e2b089 | |||
| b88962da6f | |||
| 55a09cb093 | |||
| f2db7f286b | |||
| 4599dd12b8 |
@@ -22,8 +22,27 @@ jobs:
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
MINDFLUX_DB_PASSWORD: ${{ secrets.MINDFLUX_DB_PASSWORD }}
|
||||
MINDFLUX_ADMIN_PASSWORD: ${{ secrets.MINDFLUX_ADMIN_PASSWORD }}
|
||||
MINDFLUX_ADMIN2_PASSWORD: ${{ secrets.MINDFLUX_ADMIN2_PASSWORD }}
|
||||
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
|
||||
run: |
|
||||
test -n "$MINDFLUX_DB_PASSWORD" || { echo "MINDFLUX_DB_PASSWORD fehlt" >&2; exit 1; }
|
||||
docker exec -i -e NEW_DB_PASSWORD="$MINDFLUX_DB_PASSWORD" postgres \
|
||||
psql -v ON_ERROR_STOP=1 -U mindflux -d mindfluxdb <<'SQL'
|
||||
\getenv new_db_password NEW_DB_PASSWORD
|
||||
ALTER USER mindflux WITH PASSWORD :'new_db_password';
|
||||
SQL
|
||||
docker compose -f docker-compose.yml pull
|
||||
<<<<<<< HEAD
|
||||
docker compose -f docker-compose.yml up -d --wait --wait-timeout 90 || {
|
||||
docker logs --tail 200 mindflux >&2
|
||||
exit 1
|
||||
}
|
||||
=======
|
||||
docker compose -f docker-compose.yml up -d
|
||||
- name: Cleanup
|
||||
run: |
|
||||
docker image prune -f
|
||||
docker builder prune -f --filter "until=168h"
|
||||
>>>>>>> f79df17 (CI: dangling images und alten build cache aufraeumen)
|
||||
|
||||
+562
-1
@@ -1,9 +1,570 @@
|
||||
# Changelog
|
||||
|
||||
Alle nennenswerten Änderungen an **Mindflux** werden hier dokumentiert.
|
||||
Alle nennenswerten Änderungen an **SysnaptiK Neural** werden hier dokumentiert.
|
||||
Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
||||
Versionierung [semantisch](https://semver.org/lang/de/).
|
||||
|
||||
## [Unreleased] – 2026-07-21 · Gesamt-Audit: Light Mode, Barrierefreiheit, AP2-Inhalte, DSGVO
|
||||
|
||||
### Registrierung & Kontozugang
|
||||
- **Sofort nutzbare Accounts:** Die Registrierung hängt nicht mehr an der Zustellung
|
||||
einer Bestätigungs-E-Mail. Neue Accounts können sich direkt anmelden; bereits
|
||||
wartende Konten werden mit Migration V23 aktiviert. Nicht mehr benötigte
|
||||
Bestätigungs-Token werden gelöscht. E-Mail bleibt für den Passwort-Reset erhalten.
|
||||
|
||||
### Light Mode & Design
|
||||
- **Browser-Icon:** Das Neural-Emblem wird unter `/favicon.ico` ausgeliefert und
|
||||
direkt auf der Startseite verlinkt. Die Versionierung der Bild-URL ersetzt auch
|
||||
bereits gecachte Standard-Globus-Symbole im Browser-Tab.
|
||||
- **Preismodell offen formuliert:** Die öffentliche Einstiegsseite verspricht keine
|
||||
kostenlose Nutzung mehr; Beschreibung, Registrierungs-Teaser und Account-Hinweis
|
||||
bleiben bewusst preisneutral.
|
||||
- **Produktzweck klarer:** Die Login-Seite erklärt nun Zielgruppe, Lernformen und
|
||||
den Nutzen eines Accounts; im Dashboard fasst ein kompakter Einstieg den Lernzweck
|
||||
zusammen.
|
||||
- **TechTacular-Fragen überspringbar:** Eine noch nicht beantwortete Frage kann per
|
||||
×, Klick außerhalb oder Escape folgenlos geschlossen und später wieder geöffnet
|
||||
werden.
|
||||
- **Light-Mode-Fundament:** zentrale UI-Effekte sind jetzt als Theme-Primitives
|
||||
organisiert. Dark Mode behält Neon-Emissionen; Light Mode nutzt warme Elevation,
|
||||
entfernt Text-/Icon-Halos und erhält einen sichtbaren Scrollbar-Thumb. Native
|
||||
Controls folgen über `color-scheme` dem gewählten Theme.
|
||||
- **Theme-Start & FinalProtocol:** Erstbesucher folgen der OS-Farbpräferenz, die
|
||||
Browser-Chrome erhält ein passendes `theme-color` und FinalProtocol lädt keinen
|
||||
ungenutzten Legacy-Style mehr; die statischen Lernseiten erhalten daraus nur noch
|
||||
eine minimale Reset-Basis.
|
||||
- **Kritische Light-Mode-Fälle behoben:** Login-Overlay, SyntaxSaga-Bühne,
|
||||
CodeEscape-Terminal, NetWorker-Guide und die Farbenblind-Palette sind in heller
|
||||
Darstellung wieder lesbar und kontraststark. A11y-Widget und FinalProtocol
|
||||
verwenden ebenfalls themefähige Farben.
|
||||
- **Ansichtswechsel abgesichert:** zentrale SPA-Übergänge starten oben, ohne laufendes
|
||||
Antwortfeedback zu versetzen. FinalProtocol scrollt Ergebnisse und Neustarts gezielt
|
||||
ins Sichtfeld; SyntaxSaga und DevLab fokussieren ohne Scrollsprung.
|
||||
- **Echter Light Mode statt Invertierung:** Light-Tokens auf WCAG AA nachgeschärft
|
||||
(Akzent, Muted, Sekundär, Spielakzente), neues `--warn`-Token. Die Spielkarten der
|
||||
Plattform-Hülle erhalten im Light Mode farbton-treu abgedunkelte Akzente statt
|
||||
durchschlagender Neon-Werte.
|
||||
- **ServiceClash, NetWorker und BugHunter** haben jetzt eine echte helle Variante
|
||||
(zuvor dauerhaft dunkel): alle Farben laufen über scoped CSS-Custom-Properties auf
|
||||
der Body-Klasse, Dark bleibt pixelgleich, die Light-Paletten sind handentworfen und
|
||||
vollständig WCAG-AA-geprüft; der Theme-Umschalter wurde im Header ergänzt. Der bei
|
||||
NetWorker fälschlich globale `var(--text)`/`var(--muted)`-Zugriff ist behoben.
|
||||
- **Header-Icons vereinheitlicht:** alle Icon-Buttons einheitlich 42×42 px, Radius 10,
|
||||
Glyphe 20×20, Outline-Stil (Surface-Fill, Akzent nur über Farbe/Rahmen).
|
||||
|
||||
### Barrierefreiheit
|
||||
- **Widget perfektioniert:** Fokus-Falle im Panel, `aria-modal`, Schließen per Klick
|
||||
außerhalb; der Textgrößen-Button nutzt ein dynamisches `aria-label` statt eines
|
||||
falschen `aria-pressed`, der Reset-Button ist als Aktion ausgezeichnet; Hochkontrast
|
||||
ist als „immer dunkel" kenntlich gemacht. Toter Legacy-Dock-Code entfernt.
|
||||
- **Spielanleitungen & Hints:** gemeinsames „So funktioniert's"-Overlay mit ?-Button
|
||||
und automatischem Erstaufruf für sieben Spiele (NetWorker behält seinen Inline-Guide);
|
||||
MindMatch erhält einen „Paar-Blitz"-Tipp, der kurz ein Paar aufdeckt und dafür einen
|
||||
Zug kostet (rein clientseitig, keine Backend-Änderung).
|
||||
|
||||
### FinalProtocol – Abschlussprüfung Teil 2
|
||||
- **200 neue Prüfungsfragen:** je fünf Themen für Systemintegration (Netzwerkplanung,
|
||||
IT-Sicherheit, Virtualisierung, Monitoring & Backup, Skripting) und Anwendungs-
|
||||
entwicklung (SQL, UML, OOP, Algorithmen, Testverfahren) mit je 20 Fragen, fünf
|
||||
Kategorien und ausgewogener Antwortverteilung. Titel und Navigation decken jetzt
|
||||
AP1 **und** AP2 ab.
|
||||
|
||||
### DSGVO & Recht
|
||||
- **Rechtslinks von jeder Seite erreichbar:** das bislang ungenutzte Footer-Fragment
|
||||
wird jetzt auf allen Seiten inkl. Spiel-, Fehler- und Anmeldeseiten verwendet
|
||||
(26 duplizierte Inline-Footer ersetzt, 11 fehlende ergänzt).
|
||||
- **Datenschutzerklärung** um NeuralNet-Nachrichten, E-Mail-Token (SHA-256, 24 h),
|
||||
SMTP-Versand und konkrete Speicherfristen ergänzt; **Impressum** nach § 5 DDG
|
||||
strukturiert (auffällig markierte Pflichtfeld-Platzhalter).
|
||||
- **Datenexport (Art. 20):** `/profile/export` liefert alle Kontodaten (ohne Passwort)
|
||||
als JSON-Download.
|
||||
- **Demo-Konto „NeuralTest"** wird per Migration aus produktiven Datenbanken entfernt.
|
||||
|
||||
### Technik & Qualität
|
||||
- **E-Mail-Versand diagnostizierbar:** Bestätigungs- und Reset-Mails schreiben jetzt
|
||||
datensparsame, korrelierbare Ereignislogs für Token-Erstellung, SMTP-Übergabe,
|
||||
Fehler, erneute Anforderung und Verifikation. Roh-Tokens und Bestätigungslinks
|
||||
werden nie protokolliert; eine SMTP-Annahme wird nicht als Postfachzustellung
|
||||
ausgegeben.
|
||||
- **Legacy aufgeräumt:** die nicht referenzierten Styles für Achievements, Progress,
|
||||
Settings und Soziales sowie `finalflux-check.js` entfernt; der Styleguide dokumentiert
|
||||
die neuen Effekt-Primitives.
|
||||
- **Chart.js lokal ausgeliefert** statt vom CDN – das einzige externe Asset entfällt,
|
||||
wodurch die Datenschutzerklärung wieder zur Realität passt.
|
||||
- **Security-Header** ergänzt: Content-Security-Policy (self-hosted), Referrer-Policy;
|
||||
HSTS bleibt auf HTTPS beschränkt.
|
||||
- **Umlaute** in den Spielinhalten flächendeckend korrigiert (wort-/morphemgenau,
|
||||
Slugs und Code-Bezeichner geschont), mit Reseed-Migration für bereits befüllte
|
||||
Datenbanken.
|
||||
- **AvatarService** aus dem ProfileController extrahiert (Bildverarbeitung und
|
||||
Pfad-Logik zentralisiert); **Bean Validation** eingeführt (Start mit ScoreSubmission).
|
||||
- Aufräumen: totes Scaffolding (`DummyController`, `dummy.html`, `hello.txt`) und
|
||||
obsoleter CSS-Code entfernt.
|
||||
|
||||
## [Unreleased] – 2026-07-14 · SysnaptiK Neural Rebrand
|
||||
|
||||
### TechTacular
|
||||
- **50:50-Joker:** Pro Runde kann im Frage-Overlay ein Joker zwei garantiert falsche
|
||||
Antworten ausblenden und deaktivieren. Die Lösung bleibt vollständig serverseitig;
|
||||
ein verwendeter Joker halbiert die Punkte der betreffenden Frage nur, wenn der
|
||||
serverseitige Rundenzustand den Einsatz bestätigt.
|
||||
|
||||
### Konto & Sicherheit
|
||||
- **E-Mail-Bestätigung:** Neue Accounts erhalten einen einmaligen, 24 Stunden
|
||||
gültigen Bestätigungslink und werden erst nach erfolgreicher Verifikation zum
|
||||
Login freigeschaltet. Bestehende Accounts bleiben ohne Unterbrechung bestätigt.
|
||||
- **Passwort-Reset per E-Mail:** Über „Passwort vergessen?“ können bestätigte
|
||||
Nutzerinnen und Nutzer einen zeitlich begrenzten Reset-Link anfordern und ihr
|
||||
Passwort selbst neu vergeben. Token werden ausschließlich gehasht gespeichert.
|
||||
- **Admin-Reset vereinheitlicht:** Manuelle Passwort-Resets im Adminbereich
|
||||
verwenden jetzt verdeckte Eingabefelder und dieselbe Mindestlänge von acht Zeichen.
|
||||
- **Zustellungsweg repariert:** Mindflux verwendet für SMTP den Zertifikatsnamen
|
||||
`mail.sysnaptik.de`, der im internen `backend_network` direkt auf den
|
||||
Mailserver aufgelöst wird. STARTTLS prüft damit den korrekten Hostnamen,
|
||||
ohne die interne Container-Verbindung zu verlassen.
|
||||
- **Deployment verifiziert:** Der Deployment-Workflow wartet jetzt bis zu 90
|
||||
Sekunden auf den Container-Healthcheck und bricht mit den App-Logs ab, falls
|
||||
die Anwendung nicht gesund startet. Dadurch werden fehlerhafte Deployments
|
||||
nicht mehr fälschlich als erfolgreich angezeigt.
|
||||
- **Registrierung kompakter:** „Bestätigungslink erneut senden“ ist nun eine
|
||||
einklappbare Aktion; das Eingabefeld erscheint erst auf Klick. Bei längeren
|
||||
Formularen bleibt der Registrierungsbereich innerhalb des Panels scrollbar.
|
||||
- **Registrierung eindeutig bestätigt:** Nach einer erfolgreichen Anmeldung
|
||||
bleibt der Registrierungsbereich geöffnet und zeigt die Versandbestätigung
|
||||
direkt am Formular, statt unauffällig zum Login zu wechseln.
|
||||
- **Registrierungsroute korrigiert:** Der direkte Aufruf von `/register` öffnet
|
||||
nun wieder den Registrierungsmodus statt auf dem Login zu landen.
|
||||
- **Passwort-Reset sichtbar verlinkt:** „Passwort vergessen?“ steht jetzt direkt
|
||||
unter dem Passwortfeld im Login-Bereich.
|
||||
- **Token-Schema nachgeschärft:** Folge-Migrationen richten UTC-Zeitstempel und
|
||||
den Token-Hash-Typ an das JPA-Modell aus, damit Schema-Validierung und
|
||||
Container-Start zuverlässig funktionieren.
|
||||
|
||||
### Marke & Plattform
|
||||
- **Neural Lightmode & Kontrast:** Die helle Plattformvariante ist wieder ein
|
||||
vollständiges Mint-/Blau-Neural-Theme, inklusive Sidebar, Karten und Login.
|
||||
Hochkontrast nutzt jetzt dieselbe Mint-/Cyan-Sprache statt der alten Amber-Palette.
|
||||
- **NeuralNet aufgeräumt:** Der leere Gesprächshinweis verschwindet beim Öffnen
|
||||
wirklich, die letzte Verbindung wird pro Konto gespeichert und beim nächsten
|
||||
Besuch direkt wieder geöffnet. Die Sprechblase bleibt neutral und invertiert
|
||||
nur bei ungelesenen Nachrichten.
|
||||
- **NeuralNet-Benachrichtigung:** Die Dashboard-Glocke ist jetzt eine
|
||||
Sprechblase. Sie zeigt den echten Zähler ungelesener Direktnachrichten und
|
||||
leuchtet in der Neural-Akzentfarbe, bis die Nachrichten gelesen sind.
|
||||
- **NeuralNet-Chatlayout geschlossen:** Chatkopf, Nachrichtenliste und Eingabefeld
|
||||
bilden nun eine feste Flex-Spalte im Panel. Der Footer kann nicht mehr zwischen
|
||||
Gespräch und Eingabe rutschen.
|
||||
- **NeuralNet-Buttons direkt gebunden:** Kontakt- und Chat-Buttons erhalten nach
|
||||
jedem Rendern eigene Handler. Damit öffnet eine Verbindung unabhängig von der
|
||||
Ereignisweitergabe des umgebenden Panels zuverlässig den Nachrichtenbereich.
|
||||
- **NeuralNet-Chat öffnet sofort:** Der Klick auf eine Verbindung schaltet das
|
||||
Gespräch samt Eingabefeld nun unabhängig vom anschließenden Verlaufsabruf frei;
|
||||
Chat-Aktionen sind explizit als Buttons markiert.
|
||||
- **NeuralNet-Chat repariert:** Leere, erfolgreiche API-Antworten werden nicht mehr
|
||||
fälschlich als JSON gelesen. Kontaktaktionen aktualisieren die Oberfläche wieder
|
||||
zuverlässig und bestätigte Verbindungen lassen sich direkt im Chat öffnen.
|
||||
- **NeuralNet-Testkonto:** Ein klar benanntes, für NeuralNet aktiviertes Dummykonto
|
||||
erleichtert das Prüfen von Kontaktanfragen und privaten Nachrichten.
|
||||
- **NeuralNet-MVP:** Der frühere Soziales-Platzhalter ist jetzt ein Opt-in-Hub für
|
||||
Kontaktanfragen, Annahme/Ablehnung/Rückzug und private 1:1-Nachrichten. Ohne
|
||||
bestätigte Verbindung lässt der Server keine Nachrichten zu.
|
||||
- **NeuralNet geplant:** Der kommende soziale Bereich heißt NeuralNet und priorisiert
|
||||
private Lernverbindungen: Kontaktanfragen müssen erst ausdrücklich angenommen werden,
|
||||
bevor private Nachrichten möglich sind.
|
||||
- **Farbschema ohne Aufblitzen:** Die Neural-Hülle wird nun statisch mit dem
|
||||
Grundtheme geladen und ihre Klasse bereits im `<head>` gesetzt. Theme- und
|
||||
Bootstrap-Dateien tragen neue Versionsparameter, damit Login, Logout und jede
|
||||
Navigation direkt im SysnaptiK-Neural-Schema zeichnen.
|
||||
- **Fortschritt & Barrierefreiheit repariert:** Die Diagramme warten jetzt auf die
|
||||
Neural-Plattformklasse und erscheinen dadurch in Mint statt im alten Orange.
|
||||
Hochkontrast und Farbenblind-Modus überschreiben nun auch lokale Plattform- und
|
||||
Spieltokens; der Farbenblind-Modus nutzt eine klar unterscheidbare Palette.
|
||||
- **Echter Lernnetz-Status:** Der Dashboard-Status prüft nun serverseitig den
|
||||
Fortschrittsspeicher und zeigt nachvollziehbar „bereit“ oder „eingeschränkt“ samt
|
||||
Zeitstempel und registrierten Lernmodulen. Der dekorative 100%-Ring wurde entfernt.
|
||||
- **Akzente klar getrennt:** MindMatch ist nun Magenta, SyntaxSaga ein kühleres Violett,
|
||||
SystemEscape Gelb-Bernstein und FinalProtocol Koralle. Die Plattformkarten sind damit
|
||||
auf einen Blick unterscheidbar, ohne die Farben in den Spielen selbst anzutasten.
|
||||
- **Fortschrittsdiagramme korrigiert:** Donut- und Wochenchart lesen den Neural-Akzent
|
||||
nun von der Plattformhülle statt den alten globalen Orange-Token zu verwenden.
|
||||
- **Alte Platzhalter-Copy entfernt:** Die Registrierungsrakete und der Zeitreise-Spruch
|
||||
auf der Vorbereitungsseite wurden durch neutrale SysnaptiK-Neural-Texte ersetzt.
|
||||
- **Admin-Umfang transparent:** Das Admin-Dashboard benennt jetzt klar, welche
|
||||
Inhaltsverwaltungen vorhanden sind und dass ServiceClash, NetWorker und FinalProtocol
|
||||
noch direkt mit der Anwendung ausgeliefert werden.
|
||||
- **Bestehendes Emblem bewahrt:** Die kurzfristig ergänzte CSS-Knotenmarke wurde
|
||||
zurückgenommen. Das vorhandene Emblem bleibt als Wiedererkennungszeichen bestehen,
|
||||
weil es die neue neuronale Metapher bereits passend transportiert.
|
||||
- **Sichtbarer Markenwechsel:** Mindflux heißt auf der Plattform nun **SysnaptiK Neural**;
|
||||
FinalFlux wurde zu **FinalProtocol**. Bestehende technische Routen, Datenbanktabellen,
|
||||
Spiel-Keys und Paketnamen bleiben für Kompatibilität unverändert.
|
||||
- **Neue Plattformwelt:** Die bisherige Zeitreise-/BTTF-Anmutung wurde durch ein dunkles
|
||||
neuronales Netzwerk mit Knoten, Verbindungen, Petrolflächen und grünen Glows ersetzt.
|
||||
Login/Registrierung, Dashboard, Profilseiten, Admin- und Rechtsseiten nutzen dieselbe Hülle.
|
||||
- **Dashboard nach Spielknoten:** Jede Plattform-Spielkarte trägt einen eigenen Akzent
|
||||
(u. a. DevLab Mint, SyntaxSaga Violett, BugHunter Crimson, NetWorker Blau,
|
||||
FinalProtocol Orange) – ohne die internen Spieloberflächen zu verändern.
|
||||
- **Zeitreise-Copy entfernt:** Die sichtbare Pausenfunktion heißt nun **NeuralPause**;
|
||||
BTTF-Referenzen und Sprüche wurden durch neutrale Lernpausen-Texte ersetzt.
|
||||
- **FinalProtocol durchgängig benannt:** Hub, Lernseiten, Navigation, Rücklinks und
|
||||
Seitentitel zeigen den neuen Namen, während die bestehende FinalFlux-URL als kompatibler
|
||||
technischer Pfad erhalten bleibt.
|
||||
|
||||
## [Unreleased] – 2026-07-13 · BugHunter, NetWorker & echter Lernfortschritt
|
||||
|
||||
### Planung
|
||||
- **S3-Sicherheitsstand nachgezogen:** Der Zukunftsplan markiert die serverseitige
|
||||
Spielwertung nun korrekt als umgesetzt: alle vier neuen Spiele halten Lösungen,
|
||||
Reveal- und Versuchsstatus serverseitig und berechnen Ergebnisse unabhängig vom Client.
|
||||
|
||||
### Eigenständige Spielwelten
|
||||
- **Bestandsspiele gestalterisch getrennt:** MindMatch erhielt ein Violett-/Mint-
|
||||
„Neural Archive“, TechTacular eine Magenta-/Blaue Cyber-Quizshow, SyntaxSaga ein
|
||||
phosphoriges Terminal und FinalFlux eine ruhige Gold-/Indigo-Prüfungszentrale.
|
||||
- **DevLab als Werkstatt:** Petrol/Orange, Schaltkreisraster und Build-Pipeline-Akzente
|
||||
schärfen die Oberfläche nach. Die schwebende Kolben-/dlFloat-Animation bleibt
|
||||
unverändert erhalten.
|
||||
- **Zugänglichkeit bleibt stimmig:** Das Accessibility-Widget erkennt und verwendet nun
|
||||
auch für diese fünf Lernwelten automatisch deren lokalen Akzent; Statusfarben und
|
||||
Spielmechaniken bleiben unverändert.
|
||||
|
||||
### ServiceClash
|
||||
- **Auswertung repariert:** Nach dem letzten Ticket wurde die Abschlussansicht nicht
|
||||
geöffnet, weil der Renderer noch auf ein nicht vorhandenes Folgeticket zugriff. Die
|
||||
Abschlussprüfung erfolgt nun vor jeder Ticketzustands-Abfrage.
|
||||
- **Letztes geplantes Spiel ausgeliefert:** drei Service-Desk-Schichten mit insgesamt
|
||||
**24 kuratierten Tickets** aus Incident-, Request-, Change-, Monitoring-, Backup- und
|
||||
Security-Szenarien. Jede Aufgabe führt verständlich durch Art, Priorität und passende
|
||||
Erstmaßnahme.
|
||||
- **Echte Wertung und Fortschritt:** V15-Datenmodell, lösungsfreie Sessions und
|
||||
serverseitige Prüfung bewerten jeden der drei Schritte samt Versuchsleiter. Score,
|
||||
Sterne, Highscore, Dauer und Fortschritt werden gespeichert; „Leitstand-Einsatz“ und
|
||||
der fehlerfreie „Incident Commander“ sind als Achievements integriert.
|
||||
- **Eigenständiger Leitstand:** Cyanfarbene Queue, Wellen, Fortschrittsanzeige,
|
||||
Ticket-Debriefs sowie bewusstes Weiterklicken nach einer Lösung. Die Lobby erklärt
|
||||
Incident vs. Anfrage und die Priorisierung vor der ersten Schicht.
|
||||
- **Integration abgeschlossen:** Dashboard-Karte öffnet `/serviceclash`, die API ist
|
||||
CSRF-konform eingebunden und das Accessibility-Widget übernimmt den ServiceClash-Akzent.
|
||||
|
||||
### BugHunter
|
||||
- **Vollständiges Fallakten-Spiel ausgeliefert:** serverautoritatives Drei-Phasen-Debugging
|
||||
(Orten → Klassifizieren → Fixen), Score, Combo, Sterne, Highscore, Admin-Pflege und
|
||||
lösungsfreie Sessions über eigene V12-Tabellen, Seeder und API.
|
||||
- **Inhalte ausgebaut und nach Schwierigkeit sortiert:** 12 schlüssige Fallakten von Junior
|
||||
über Senior bis Principal mit längeren Briefings, Mehrdatei-Fällen und mehreren Bugs.
|
||||
- **Archiv und Ablauf verfeinert:** Die Lobby ist ein Aktenschrank statt eines unlesbaren
|
||||
Kartenstapels; Akten lassen sich klar öffnen. Nach jeder Prüfung bleibt das Debriefing
|
||||
stehen und der nächste Schritt erfolgt bewusst per Klick.
|
||||
- **Wiederholbarkeit:** Eine fehlerfrei gelöste Akte erhält einen diagonalen „GELÖST“-Stempel.
|
||||
Ein Reset startet eine passende Variantenlage, sodass Fehler und Hinweise nicht immer an
|
||||
derselben Stelle liegen.
|
||||
- **Sprache und Gestaltung bereinigt:** Fallakten- statt „Investigation Bureau“-Vokabular,
|
||||
korrigierte Schreibfehler sowie Crimson-Akzent ohne auffällig eingefärbte Dashboard-Karte.
|
||||
|
||||
### NetWorker
|
||||
- **Neues Netzwerk-Puzzle ausgeliefert:** V14-Datenmodell, Seeder, Server-API und
|
||||
serverseitiger `NetSimulator` prüfen IPv4-/CIDR-, Host-/Broadcast-, Gateway-,
|
||||
Duplikat-IP- und Routing-Fälle unabhängig vom Client.
|
||||
- **15 Aufgaben in drei Welten:** adressieren, diagnostizieren und routen; Ergebnisse,
|
||||
Highscores und Fortschritt werden wie bei den übrigen Spielen gespeichert.
|
||||
- **Blaupausen-Optik nachgeschärft:** korrekte Schreibweise „NetWorker“, sichtbares Raster
|
||||
auf Seiten- und Netzwerkboard-Hintergrund sowie blaues Accessibility-Widget.
|
||||
- **Einsteigerführung ergänzt:** Lobby, Weltstart und die Levelansicht erklären Ablauf,
|
||||
IP-Adresse, Präfix, Gateway und Ping. Je Aufgabentyp gibt es konkrete Handlungsanweisungen
|
||||
samt Beispielen für `/24`, Hostadressen und statische Routen.
|
||||
- **Direkter Rückweg:** Die Kopfzeile führt mit „NETZPLÄNE“ zurück zur NetWorker-Übersicht,
|
||||
ohne den Umweg über das Dashboard.
|
||||
|
||||
### Navigation & Fehlerbehebung
|
||||
- **BugHunter und NetWorker:** „AKTENSCHRANK“ bzw. „NETZPLÄNE“ brechen einen laufenden,
|
||||
noch nicht gespeicherten Durchgang bewusst ab und öffnen direkt die jeweilige Übersicht.
|
||||
- **Fortschrittsseite repariert:** Der JavaScript-Inliner in `progress.html` konnte zwei
|
||||
Ausdrücke in einer Rechenoperation nicht parsen und erzeugte deshalb einen Fehler 500.
|
||||
Die Werte werden nun erst in JavaScript-Konstanten übergeben.
|
||||
|
||||
### Fortschritt, Achievements & Knowledge Core
|
||||
- **Spielbezogene Achievements ergänzt:** MindMatch, TechTacular, DevLab, SyntaxSaga,
|
||||
SystemEscape, BugHunter und NetWorker können nun passende Erfolge auslösen; zusätzliche
|
||||
Spezialerfolge belohnen perfekte SystemEscape-/BugHunter-/NetWorker-Runden.
|
||||
- **Dashboard und Statistik von Platzhaltern befreit:** Wochen-XP, Verlauf und Top-Spiele
|
||||
kommen aus `game_scores`; die Statistik zeigt den realen XP-Fortschritt und die gemessene
|
||||
Dauer abgeschlossener Runden. Nicht messbare Runden werden bewusst nicht als Spielzeit
|
||||
behauptet.
|
||||
- **Bezeichnungen präzisiert:** Das Level ist transparent ein XP-Level (1.000 XP je Stufe),
|
||||
und die frühere Zahl „korrekte Antworten“ heißt nun „erfasste Spielaktionen“.
|
||||
- **Knowledge Core aufgebaut:** Lernzentrale mit AP1-, Netzwerk-/Systemintegration- sowie
|
||||
Debugging-/Security-Pfaden, Verweisen auf passende Module und persönlichem, datenbasiertem
|
||||
Status statt vorgetäuschter Themenanalyse.
|
||||
|
||||
### Barrierefreiheit & Qualität
|
||||
- Accessibility-Steuerung übernimmt in SystemEscape, BugHunter und NetWorker den jeweiligen
|
||||
Spielakzent und bleibt in der Kopfzeile reserviert.
|
||||
- Zusätzliche Tests sichern den Wochenaggregator und den kompletten `/progress`-Renderpfad;
|
||||
insgesamt **46 Tests** grün.
|
||||
|
||||
## [Unreleased] – 2026-07-12 · Großer Verbesserungs-Sweep (Sicherheit, Daten, Navigation)
|
||||
|
||||
Weitgehende Umsetzung des Verbesserungskatalogs aus `mindfluxfuture.md` (Teil 2;
|
||||
bewusst zurückgestellte Restpunkte sind dort weiterhin als offen markiert):
|
||||
Sicherheit, Datenintegrität, echtes XP/Achievement-System, Navigations-Umbau,
|
||||
Docker/CI-Härtung und erste Tests.
|
||||
|
||||
### Responsive Darstellung & Barrierefreiheit
|
||||
- Logout-Navigation in Dashboard, DevLab, MindMatch, SyntaxSaga und TechTacular auf ein
|
||||
einheitliches Icon mit zugänglichem Namen und Tooltip „Abmelden“ vereinheitlicht.
|
||||
- **Globales Accessibility-Widget:** Ein schwebender Rollstuhl-Button öffnet auf jeder Seite
|
||||
ein tastaturbedienbares Panel. Einstellungen werden ausschließlich lokal im Browser gespeichert.
|
||||
- Enthalten sind vier Textgrößen, Hochkontrast, farbenblindfreundliche Statusfarben, Monochrom,
|
||||
einen Lesemodus für Dyslexie mit Verdana und angepassten Abständen, unterstrichene Links, reduzierte Animationen,
|
||||
großer Cursor und ein zentraler Reset.
|
||||
- Floating-Button an die Mindflux-Palette angepasst; der Schrift-Schalter heißt nun transparent
|
||||
„Lesemodus (Dyslexie)“ und weist auf Verdana plus vergrößerte Abstände hin.
|
||||
- Der dauerhaft erreichbare Auslöser besitzt nun reservierten Layoutplatz statt Spielkarten oder
|
||||
Formulare zu überdecken: seitlich auf Desktop und in einer unteren Schutzzone auf kleinen Displays.
|
||||
- Accessibility-Zugriff in die jeweilige sticky Kopfzeile neben Theme/Logout verschoben; Seiten
|
||||
ohne Werkzeugleiste erhalten eine eigene reservierte Utility-Zeile statt eines Inhalts-Overlays.
|
||||
- Accessibility-Schaltfläche auf dieselben 38 px Außen- und 18 px Iconmaße wie die übrigen
|
||||
Kopfzeilen-Werkzeuge vereinheitlicht.
|
||||
- SystemEscape behält seine Terminal-Typografie im Spiel, verwendet für globale Navigation und
|
||||
Werkzeuge aber die einheitliche Mindflux-Sans-Serif-Schrift.
|
||||
- TechTacular-Kategoriekarten auf identische Höhe gesetzt und zweizeilige Titel mittig ausgerichtet.
|
||||
- Das Panel besitzt Dialogsemantik, sichtbaren Tastaturfokus, Escape-Schließen und verständliche
|
||||
Aktivzustände; Spielzustände oder Benutzerkonten werden dabei nicht verändert.
|
||||
- **DevLab-Spielansicht nutzt große Bildschirme aus:** Die bisherige starre Begrenzung auf
|
||||
1100 px wurde durch ein fluides, bis 2200 px breites Viewport-Layout ersetzt. Hauptbereich,
|
||||
Seitenpanel, Abstände und Grundschrift skalieren mit der verfügbaren Bildschirmgröße.
|
||||
- Auf Mobilgeräten bleibt das Layout einspaltig und setzt die Grundschrift auf mindestens 16 px.
|
||||
|
||||
### Sicherheit
|
||||
- **DB-Passwort aus dem Repo entfernt:** `docker-compose.yml` liest jetzt `${MINDFLUX_DB_PASSWORD}`
|
||||
aus einem Gitea-Secret (wie die Admin-Passwörter).
|
||||
Compose bricht bei einem fehlenden oder leeren Secret jetzt sofort mit einer eindeutigen
|
||||
Fehlermeldung ab, statt einen App-Container mit leerem DB-Passwort zu starten.
|
||||
Der Gitea-Runner gleicht das PostgreSQL-Passwort vor jedem Deployment automatisch mit dem
|
||||
Secret ab; für die Rotation ist daher kein separater SSH-Zugang erforderlich.
|
||||
**Produktionsrotation abgeschlossen am 2026-07-12:** Secret angelegt, DB-Rolle aktualisiert
|
||||
und Mindflux über den erfolgreichen Gitea-Workflow-Lauf 43 neu gestartet (Commit `4f5042d`).
|
||||
- **Unsicherer Legacy-Endpunkt `/users/save` gelöscht** (speicherte User ohne Hashing, mit frei
|
||||
wählbarer Rolle) inkl. `UserViewController`, `user-form.html`, `user-success.html`.
|
||||
- **Logout nur noch per POST** mit CSRF-Token (alle Templates umgestellt).
|
||||
- **Login-Brute-Force-Schutz:** Nach 5 Fehlversuchen wird der Benutzername 15 Minuten gesperrt
|
||||
(`LoginAttemptService`, In-Memory).
|
||||
- **Registrierung gehärtet:** Passwort-Mindestlänge 8, E-Mail-Formatprüfung, case-insensitiver
|
||||
Unique-Index auf `users.username` (Migration V9) schließt das Registrierungs-Race.
|
||||
- **Bootstrap-Admins abgesichert:** Ein Admin wird nur noch angelegt, wenn sein Passwort-Secret
|
||||
tatsächlich gesetzt ist; fehlende optionale Admin-Konfiguration wird protokolliert und sicher
|
||||
übersprungen.
|
||||
- Dockerfile läuft nicht mehr als root (`USER mindflux`), zweistufig mit Dependency-Cache;
|
||||
Tests laufen im Image-Build mit. Compose-Healthcheck auf `/actuator/health` (Actuator neu).
|
||||
|
||||
### Daten & Backend
|
||||
- **Migration V10:** `UNIQUE(lower(email))`, Fremdschlüssel von `game_scores`/`achievements`
|
||||
auf `users(username)` mit `ON DELETE CASCADE ON UPDATE CASCADE`; verwaiste Altdaten werden
|
||||
bereinigt, E-Mail-Dubletten aufgelöst (älteste Zeile behält die Adresse).
|
||||
- **Echte XP/Level/Streak statt Dummy-Daten:** `ProgressService` aggregiert aus `game_scores`
|
||||
(XP = Score-Summe, 1000 XP je Level, Tages-Streak mit 1 Tag Karenz); Dashboard-Sidebar zeigt
|
||||
echte Werte.
|
||||
- **Achievements sind live:** Katalog aus 6 Regeln (Erster Sieg, 1000er-Club, Highscore-Jäger,
|
||||
Dauerbrenner, Punktesammler, Allrounder), Vergabe automatisch bei jedem gespeicherten Score;
|
||||
Dummy-Daten entfernt, gesperrte Karten zeigen den Katalog.
|
||||
- **User-Logik konsolidiert:** Registrierung, REST- und Admin-Anlage laufen über einen
|
||||
`UserService` (eine Stelle für BCrypt, Rollen-Whitelist, Eindeutigkeit);
|
||||
`AdminUserService`/`AdminUserRepository` (Duplikate) entfernt.
|
||||
- **Globales Error-Handling:** eigene Fehlerseite (`error.html`) statt Whitelabel, JSON-Fehler
|
||||
mit korrekten Statuscodes für alle APIs, unbekannte URLs liefern eine echte 404-Seite.
|
||||
- **N+1-Queries behoben:** TechTacular-Board lädt Kategorien/Fragen/Antworten in 2 Queries
|
||||
(Fetch-Join), Admin-Fragenzähler als eine GROUP-BY-Query.
|
||||
- **Logging:** alle `System.out`/`printStackTrace` durch SLF4J ersetzt.
|
||||
- `spring-boot-starter-validation` + `-actuator` ergänzt, redundantes `spring-security-crypto`
|
||||
entfernt, Java auf 21 vereinheitlicht (pom + Dockerfile).
|
||||
|
||||
### Navigation & UI
|
||||
- **Layout-Fragmente eingeführt** (`fragments/layout.html`): App-Sidebar, Profil-Tab-Leiste und
|
||||
Footer sind jetzt gemeinsame Fragmente (erster Schritt gegen die Template-Duplikation).
|
||||
- **Sidebar hat einen „Profil"-Eintrag**; der User-Block verlor den irreführenden Dropdown-Pfeil.
|
||||
- **Profil-Tab-Leiste auf allen Unterseiten** (Profil · Statistiken · Erfolge · Soziales ·
|
||||
Einstellungen) mit einheitlichen Namen; Rück-Links führen einheitlich zum Dashboard.
|
||||
- **Einstellungen aufgeräumt:** Theme-Auswahl funktioniert jetzt wirklich (gekoppelt an
|
||||
`theme.js`/localStorage); funktionslose Sprach-/Benachrichtigungs-Attrappen entfernt;
|
||||
`SettingsController` in den `ProfileController` gemergt; Token-Farbe statt Hardcode.
|
||||
- **Spielnamen vereinheitlicht** (englisch, zusammengeschrieben): DevLab, SyntaxSaga sowie
|
||||
SystemEscape, ServiceClash, BugHunter, NetWorker für die geplanten Spiele (Keys in
|
||||
`KNOWN_GAMES`/`ProgressService` mit umbenannt – noch keine Scores vorhanden).
|
||||
- **DevLab erscheint jetzt auf der Statistik-Seite** (fehlte in `ProgressService.GAMES`).
|
||||
- Tote Templates entfernt: `login.html`, `register.html` (Formular lebt auf der Startseite),
|
||||
`AppStartCheck` u. a. Kleinkram.
|
||||
|
||||
### FinalFlux
|
||||
- **AP2-Gerüst:** neue Sidebar-Blöcke „AP2 Anwendungsentwicklung" und „AP2 Systemintegration"
|
||||
mit den ersten Themenseiten **Entwurfsmuster** (FIAE) und **Hochverfügbarkeit** (FISI) inkl.
|
||||
FluxChecks – FinalFlux deckt damit den Weg zur gesamten Ausbildung ab (AP1 + AP2).
|
||||
|
||||
### Tests (neu – vorher 0)
|
||||
- 33 Tests: Score-Formel, XP/Level/Streak-Berechnung, Registrierungsregeln, Achievement-Vergabe,
|
||||
Login-Sperre sowie `@WebMvcTest` der Security-Regeln (Login-Zwang, Admin-Schutz,
|
||||
gelöschter Legacy-Endpunkt). Die CI baut das Image jetzt inklusive Testlauf.
|
||||
|
||||
### Dokumentation
|
||||
- **`mindfluxfuture.md`:** konsolidierter Verbesserungs- und Ausbaukatalog mit umgesetzten,
|
||||
bewusst zurückgestellten und künftig geplanten Punkten.
|
||||
- **`pw_anleitung.md`:** Betriebsanleitung für DB-Secret, automatische Runner-Rotation,
|
||||
manuellen Notfallweg und Verifikation nach dem Deployment.
|
||||
|
||||
### Zukunftsfundament & FluxTimer
|
||||
- **Plan technisch aktualisiert:** freie Flyway-Reihenfolge V11–V14, persistiertes
|
||||
`exam_scope`-Tagging und verbindlicher S3-Anti-Cheat-Vertrag für alle neuen Spiele.
|
||||
- **Spiel-Akzentbasis:** Dark-/Light-Tokens für SystemEscape, BugHunter, ServiceClash und NetWorker.
|
||||
- **FluxTimer-MVP:** lokaler Pausentimer mit 5–240 Minuten Intervall, rotierenden Erinnerungen,
|
||||
eigenem Text, Snooze, Probelauf, optionalem Sound und lokaler Browser-Benachrichtigung.
|
||||
Countdown und Overlay laufen über Navigationen weiter; ein eigener Topbar-Button öffnet den
|
||||
Schnellzugriff und die Einstellungen enthalten die vollständige Konfiguration.
|
||||
- **Dashboard-Topbar aufgeräumt:** generische Winkehand und dekorative Pulsline entfernt.
|
||||
FluxTimer besitzt statt der Glocke jetzt ein eigenes Uhr-mit-Blitz-Icon im Zeitreise-Look;
|
||||
die Glocke bleibt frei für spätere echte Benachrichtigungen.
|
||||
- **Login entschlackt:** das generische Schloss-Emoji aus dem Login-Button entfernt; die klare
|
||||
Beschriftung reicht als Handlungsaufforderung und passt besser zur eigenständigen Designsprache.
|
||||
- Datenschutzerklärung um lokale Speicherung und Browser-Benachrichtigung ergänzt.
|
||||
|
||||
### SystemEscape (interner Key `codeescape`)
|
||||
- **Variable SystemEscape-Runs:** Jedes Szenario besitzt nun acht Räume; ein Durchlauf wählt
|
||||
serverseitig vier davon in zufälliger Reihenfolge. Der Pool wächst damit auf 24 Räume und
|
||||
69 Rätsel, mit 1.680 möglichen geordneten Raumkombinationen je Szenario.
|
||||
- Die konkrete Raum- und Rätselauswahl wird im Server-Run festgeschrieben. Checks, Hinweise,
|
||||
Türen, Finish, Maximalpunktzahl und Sterne akzeptieren ausschließlich IDs dieses Runs.
|
||||
- Lobby erklärt Poolgröße und Run-Länge; ein neuer Start erzeugt ausdrücklich eine neue Route.
|
||||
- **Sichtbarer Name zu SystemEscape geändert:** Der neue Name bildet Netzwerk-, Security-,
|
||||
Hardware-, Backup- und Entwicklungsthemen besser ab. Interner Key, Route und Datenmodell
|
||||
bleiben für bestehende Scores und Migrationen kompatibel bei `codeescape`.
|
||||
- **Spielablauf verdeutlicht und Schleusenfehler behoben:** dauerhafte Drei-Schritt-Führung
|
||||
„Rätsel lösen → Fragmente sammeln → Türcode bilden“, nummerierte Fragmentreihenfolge,
|
||||
optionales automatisches Einsetzen und klare Rückmeldungen bei Prüfung, Erfolg und Fehler.
|
||||
- **Produktive Türcodes repariert:** ergänzende V11.1-Datenmigration gleicht alle neun Türcodes
|
||||
mit ihren sichtbaren Fragmenten ab. Die Lösereihenfolge der Rätsel bleibt bewusst frei.
|
||||
- Schleusen lassen sich serverseitig erst nach Abschluss aller Rätsel öffnen; ein zusätzlicher
|
||||
Test sichert diesen Ablauf und die hilfreiche Fehlermeldung ab.
|
||||
- **Neues vollständiges Spiel:** drei Facility-Szenarien mit neun Räumen und 24 IT-Rätseln
|
||||
von AP1 bis AP2 (FIAE/FISI/Cross), Fragment-Inventar, Türcodes, Countdown, Alarmzustand,
|
||||
Hinweise, Reveal, Zeitbonus, Sterne, Highscore und Ergebnis-Debriefing.
|
||||
- **Serverautoritativ:** V11-Schema mit `exam_scope`, lösungsfreie Session-DTOs, serverseitig
|
||||
protokollierte Versuche/Hinweise/Reveals/Türen und vollständige Score-Neuberechnung beim Finish.
|
||||
Fremde oder doppelte IDs, überlange Listen und unplausible Zeiten werden abgewiesen.
|
||||
- **Vier Mechaniken:** freie Code-Eingabe, Choice, Reihenfolge und Matching; alle Lösungen bleiben
|
||||
bis zur expliziten Auflösung auf dem Server.
|
||||
- **Eigene visuelle Welt:** dunkle Facility-/CRT-Optik, Scanlines, Terminal-Dialoge,
|
||||
Fragment-Chips und SystemEscape-Akzent im Dashboard; responsive für mobile Ansichten.
|
||||
- **Admin-CRUD:** Szenarien, Räume und Rätsel inklusive JSON-Payload und Prüfungsbereich pflegbar.
|
||||
- Dashboard, Security-CSRF-Ausnahme, Score-/XP-/Achievement-System und Admin-Dashboard integriert.
|
||||
- Vier neue Service-Tests prüfen Lösungsfreiheit, serverseitige Hint-/Versuchsabrechnung sowie
|
||||
Schutz vor fremden IDs und unplausiblen Laufzeiten.
|
||||
|
||||
## [Unreleased] – 2026-06-30 · Login, Registrierung & Konten
|
||||
|
||||
Reihe von Fixes und Verbesserungen rund um Anmeldung, Registrierung und
|
||||
Konto-Verwaltung – ausgelöst durch einen Login-Bug, bei dem ein frisch
|
||||
registriertes Konto sich nicht anmelden konnte.
|
||||
|
||||
### Behoben
|
||||
- **Login scheiterte an Groß-/Kleinschreibung des Benutzernamens:** Die Registrierung
|
||||
speicherte den Namen klein, der Login suchte aber **case-sensitiv** – wer sich z. B. als
|
||||
„LostAtWonderland" registrierte, kam mit genau dieser Schreibweise nicht hinein. Das wirkte
|
||||
wie ein Passwort-Problem, war aber der Benutzername-Abgleich (das Passwort lag korrekt
|
||||
verschlüsselt in der DB). Login **und** Dublettenprüfung sind jetzt **case-insensitiv**
|
||||
(`findByUsernameIgnoreCase`); Admin-Logins funktionieren unverändert in jeder Schreibweise.
|
||||
- **Profilbild wurde beim Bearbeiten im Admin genullt:** Das Benutzer-Edit-Formular sendet kein
|
||||
`profile_picture`, wodurch ein Speichern das Bild löschte. Beim Bearbeiten bleibt das
|
||||
bestehende Profilbild jetzt erhalten.
|
||||
- **Dev-Lab-Pfad-Icon „Debugging & Problemlösung" war leer:** Das Symbol `</>` wurde per
|
||||
`innerHTML` als (kaputtes) HTML interpretiert statt als Text gerendert. Der Marker wird jetzt
|
||||
escaped (`esc(p.marker)`, Cache-Buster `v4`); alle drei Trainingspfade zeigen ihr Icon
|
||||
(`{}` · `</>` · `[]`).
|
||||
|
||||
### Geändert / Hinzugefügt
|
||||
- **Registrierung behält die Schreibweise des Benutzernamens** (vorher klein) und trimmt nur noch.
|
||||
- **Passwort-Wiederholung bei der Registrierung:** neues Feld „Passwort wiederholen" mit Client-
|
||||
**und** Server-Prüfung; bei Abweichung wird der Account nicht angelegt (Schutz vor Tippfehlern,
|
||||
da die Passworteingabe verdeckt ist). Eigene Fehlermeldung, die das Signup-Panel direkt öffnet.
|
||||
- **Admin-Passwort-Reset (ohne E-Mail):** In der Benutzerverwaltung lässt sich pro Nutzer direkt
|
||||
ein neues Passwort setzen („PW zurücksetzen"); das Bearbeiten-Formular setzt ein eingegebenes
|
||||
Passwort ebenfalls neu (leer = unverändert). Dient als Konto-Recovery (Phase 1).
|
||||
|
||||
### Hinweise / offen
|
||||
- **„Passwort vergessen" als E-Mail-Self-Service (Phase 2)** ist noch offen: Der Server hat aktuell
|
||||
keinen Mailversand. Geplant über ein SMTP-Relay (z. B. Brevo/Mailjet, EU/DSGVO) bzw. ein
|
||||
vorhandenes Postfach, sobald SMTP-Zugangsdaten vorliegen – Reset dann per zeitlich begrenztem
|
||||
Token-Link. (Ein eigener Mailserver ist wegen Zustellbarkeit/Reputation nicht vorgesehen.)
|
||||
|
||||
## [Unreleased] – 2026-06-30 · Dev Lab
|
||||
|
||||
Neues Spiel **Dev Lab**: ein Entwicklerlabor, das mehrere kleine Disziplinen bündelt
|
||||
(SQL, Java/Debugging, Git, HTML, CSS, Regex, PHP, REST). Im Gegensatz zur ersten,
|
||||
rein clientseitigen Skizze ist Dev Lab jetzt **server-autoritativ, DB-gestützt und im
|
||||
Admin pflegbar** – mit **echter Mechanik-Vielfalt** statt überall demselben Lückentext.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Spiel: Dev Lab (`/devlab`)** mit Hub (drei Trainingspfade: Syntax · Debugging · Praxis)
|
||||
und **11 Disziplinen** (`query-builder`, `sql-detective`, `debug-hunt`, `git-mission`,
|
||||
`html-architect`, `css-designer`, `css-cascade`, `flexbox-factory`, `regex-master`,
|
||||
`php-request`, `api-explorer`), zusammen **189 Aufgaben** (16–21 je Spiel).
|
||||
- **Sechs unterschiedliche Spielmechaniken** – passend zum Thema gemischt:
|
||||
**cloze** (Code-Lücke tippen), **choice** (Multiple-Choice-Chips), **order**
|
||||
(Bausteine klick-basiert zusammensetzen/sortieren), **spot** (den fehlerhaften Token
|
||||
anklicken), **match** (Paare zuordnen) und **regex** (Muster schreiben mit
|
||||
**Live-Treffer-Vorschau** gegen Beispiel-Strings).
|
||||
- **Server-autoritativ & Anti-Cheat:** Aus dem JSON-Payload jeder Aufgabe baut der Server
|
||||
eine bereinigte Sicht **ohne Lösung**; geprüft und gewertet wird ausschließlich serverseitig
|
||||
(Text-/Reihenfolge-/Verhaltensvergleich). `correctIndex`/`bugIndex`/Lösungen/Pattern verlassen
|
||||
nie das Backend. Score über die generische `game_scores`-Tabelle (Kennung `devlab`, `deck`=Spiel-Slug).
|
||||
- **Fehler-Mechanik** wie bei Syntax Saga: bis zu drei Versuche mit Hinweis, dann „Auflösung",
|
||||
kein Game-Over; Punkte nach Versuch, Sterne 0–3.
|
||||
- **Admin-Verwaltung (`/admin/devlab`):** Spiele und Aufgaben (Typ + JSON-Payload) anlegen,
|
||||
bearbeiten, löschen, aktiv/inaktiv schalten. Inhalte aus `devlab/content.json` (idempotenter Seeder).
|
||||
|
||||
### Behoben / geändert (ggü. der ersten Dev-Lab-Skizze)
|
||||
- **Platzhalter-Leak entfernt:** Die Eingabefelder zeigten vorher die Lösung als Platzhalter –
|
||||
jetzt sind es neutrale Kategorie-Labels („Keyword", „Operator", „Tag" …). Geprüft wird serverseitig.
|
||||
- **Echte Mechanik-Abwechslung** statt 11×Lückentext; **deutlich mehr Aufgaben** je Disziplin.
|
||||
|
||||
### Technik
|
||||
- Neue Entities `DevLabGame`/`DevLabTask` (JSON-`payload`) + Flyway `V8__devlab.sql`;
|
||||
`DevLabService` mit sechs Mechanik-Checkern; JSON-API `/api/devlab/**`
|
||||
(`catalog`/`session`/`check`/`finish`, CSRF-ausgenommen, `devlab` in `KNOWN_GAMES`).
|
||||
Frontend: polymorphe Engine `devlabgame.js`, DB-getriebener Hub `devlab.js`, `devlabgame.css` (Prefix `dlg-`).
|
||||
|
||||
## [Unreleased] – 2026-06-30
|
||||
|
||||
Neues Spiel **Syntax Saga**: interaktive IT-Geschichten, in denen man den fehlenden
|
||||
Code (Schlüsselwörter, Operatoren, ganze Zeilen) in eine kleine Story tippt. Server-autoritativ
|
||||
nach dem TechTacular-Muster, Inhalte DB-gestützt und im Admin pflegbar. Die Dashboard-Kachel
|
||||
„Syntax Saga" ist damit aktiv (vorher „coming soon").
|
||||
|
||||
### Hinzugefügt
|
||||
- **Spiel: Syntax Saga (`/syntaxsaga`):** Missions-Saga in drei **wählbaren Schwierigkeitsstufen**
|
||||
(Junior = eine Lücke, Professional = mehrere Lücken, Senior = ganze Zeile rekonstruieren). Jede
|
||||
Mission besteht aus Erzähltext + optionalem Dialog + Codeblock mit `{{n}}`-Lücken. Eine Lücke kann
|
||||
**mehrere gültige Antworten** haben (z. B. `localhost` / `192.168.0.1`) und optional
|
||||
groß-/kleinschreibungs-unabhängig prüfen (z. B. SQL-Keywords). Eingabe per Freitext; **Prüfung und
|
||||
Score ausschließlich serverseitig** – die Lösungen verlassen nie das Backend (Anti-Cheat wie bei
|
||||
TechTacular/MindMatch).
|
||||
- **Fehler-Mechanik:** pro Lücke bis zu drei Versuche mit Hinweis, danach **„Auflösung anzeigen"**; die
|
||||
Geschichte läuft immer weiter (**kein Game-Over**). Punkte je nach Versuch (Erstversuch voll, danach
|
||||
weniger), aufgelöste Lücke = 0 Punkte; Sternewertung 0–3.
|
||||
- **Admin-Verwaltung (`/admin/syntaxsaga`):** Missionen, Lücken und Lösungen anlegen/bearbeiten/löschen,
|
||||
aktiv/inaktiv schalten. Mitgeliefert: **34 Seed-Missionen** (16 Junior · 10 Professional · 8 Senior,
|
||||
zusammen 44 Lücken) aus `syntaxsaga/content.json` (idempotenter Seeder, lädt nur in leere Tabellen).
|
||||
Themen quer durch den Stack: **Java** (Schleifen, OOP/`class`/`this`, Exceptions, Collections,
|
||||
Operatoren), **SQL** (`WHERE`/`ORDER BY`/`JOIN … ON`/`INSERT`/`UPDATE`/`DELETE`), **Bash**
|
||||
(`cd`/`grep`/Pipe), **Git** (`commit`), **HTML** (Link/Formular) und **Python** (`print`); Senior
|
||||
rekonstruiert ganze Zeilen (z. B. `public static void main(String[] args)`,
|
||||
`Scanner sc = new Scanner(System.in)`) mit ein paar tolerierten Schreibvarianten.
|
||||
- Spielstände laufen über die bestehende generische `game_scores`-Tabelle (Spiel-Kennung `syntax-saga`),
|
||||
inkl. Highscore/„Neuer Bestwert" und Statistik im Spiel-HUD.
|
||||
|
||||
### Technik
|
||||
- Neue Entities `SyntaxSagaMission` / `SyntaxSagaGap` / `SyntaxSagaSolution` + Flyway-Migration
|
||||
`V7__syntax_saga.sql`; server-autoritativer `SyntaxSagaService` (Story ohne Lösungen / serverseitige
|
||||
Prüfung / Score-Recompute beim Abschluss); JSON-API `/api/syntaxsaga/**` (CSRF-ausgenommen). Frontend
|
||||
unter `syntaxsaga.html` / `syntaxsaga.js` / `syntaxsaga.css` (Prefix `ss-`, zentrale `theme.css`-Tokens).
|
||||
|
||||
## [Unreleased] – 2026-06-29
|
||||
|
||||
Inhaltlicher und optischer Feinschliff: echte Rechtstexte, vollständig
|
||||
|
||||
+19
-2
@@ -3,17 +3,34 @@ FROM maven:3.9-eclipse-temurin-21 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
# Dependency-Layer cachen: erst pom.xml auflösen, dann erst den Quellcode kopieren.
|
||||
COPY pom.xml .
|
||||
RUN mvn -q dependency:go-offline
|
||||
|
||||
RUN mvn clean package -DskipTests
|
||||
COPY src ./src
|
||||
|
||||
# Tests laufen im Build mit – ein roter Test stoppt das Image (CI-Teststufe inklusive).
|
||||
RUN mvn -q package
|
||||
|
||||
# ---- Runtime Stage ----
|
||||
FROM eclipse-temurin:21-jre
|
||||
|
||||
# curl für den Container-Healthcheck (siehe docker-compose.yml)
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Nicht als root laufen; Upload-Verzeichnis für den App-User beschreibbar machen.
|
||||
RUN useradd --system --home /app --shell /usr/sbin/nologin mindflux \
|
||||
&& mkdir -p /app/uploads \
|
||||
&& chown -R mindflux:mindflux /app
|
||||
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
USER mindflux
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["java","-jar","app.jar"]
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# Mindflux – Style Guide
|
||||
|
||||
Verbindliche Design-Referenz für **Mindflux**. **Single Source of Truth ist
|
||||
`src/main/resources/static/css/theme.css`** – diese Datei dokumentiert nur, was dort definiert
|
||||
ist. Bei Änderungen immer zuerst `theme.css` anpassen und diese Datei nachziehen.
|
||||
|
||||
> **Grundregel:** Niemals Hex-/RGBA-Werte hart in Templates oder Spiele-CSS schreiben. **Immer**
|
||||
> die Tokens (`var(--accent)`, `var(--surface)` …) verwenden, damit Dark/Light und künftige
|
||||
> Palettenwechsel automatisch greifen.
|
||||
|
||||
Theme-Umschaltung: über das Attribut `<html data-theme="dark|light">` (gesetzt/persistiert von
|
||||
`theme.js`; injiziert auch den mobilen Off-Canvas-Drawer). **Dark ist Default.**
|
||||
|
||||
---
|
||||
|
||||
## Farbkonzept – „Back to the Future"
|
||||
|
||||
- **Primärakzent = Feuer-Amber** (Dark `#f0a81a` / Light `#d65116`) + **Gelb-Glow** (`--accent-2`)
|
||||
für Verläufe und Leuchten.
|
||||
- **Zweitakzent = Flux-Blau** (`#2d9cce`) – für **Links** und das Spiel-Feedback **„richtig"**.
|
||||
- **Warmer Bernstein-Glow** statt Matrix-Grün; dunkler bzw. sehr heller Grund je Theme.
|
||||
- **Kein Grün** im Erfolgs-Feedback: **richtig = Blau ✓**, **falsch = Rot ✗** (Rot ist ein kühles
|
||||
Crimson, damit es sich klar vom orangen Akzent abhebt – v. a. im Light-Mode).
|
||||
|
||||
---
|
||||
|
||||
## Token-Referenz (aus `theme.css`)
|
||||
|
||||
| Token | Dark | Light | Verwendung |
|
||||
|---|---|---|---|
|
||||
| `--bg` | `#0a0806` | `#fbf6ec` | Seitenhintergrund (unter `--bg-glow`) |
|
||||
| `--bg-glow` | radial `rgba(240,168,26,.10)` | radial `rgba(214,81,22,.10)` | weicher Lichtschein oben |
|
||||
| `--panel` | `#13100b` | `#ffffff` | Sidebar, Topbar, Footer |
|
||||
| `--panel-2` | `#171209` | `#fdf7ea` | Panel-Variante |
|
||||
| `--surface` | `#120f0a` | `#ffffff` | Karten / Panels / Eingaben (Grundfläche) |
|
||||
| `--surface-2` | `#1b1610` | `#fbf4e6` | erhöhte Fläche, Hover, Felder |
|
||||
| `--border` | `#2a2117` | `#ece1cf` | Standard-Rahmen |
|
||||
| `--line` | `#221b12` | `#f1e8d8` | feine Linien, Scrollbar, Chart-Achsen |
|
||||
| `--border-accent` | `rgba(240,168,26,.45)` | `rgba(194,65,12,.40)` | akzentuierte Rahmen |
|
||||
| `--accent` | `#f0a81a` | `#c2410c` | **Primärakzent**: Buttons, aktive Nav, Icons, Headline-Akzent |
|
||||
| `--accent-2` | `#f7dc11` | `#b45309` | Sekundär-Akzent für Verläufe (Gelb-Glow) |
|
||||
| `--accent-ink` | `#1c1100` | `#ffffff` | Textfarbe **auf** Akzentflächen |
|
||||
| `--secondary` | `#2d9cce` | `#146b96` | **Flux-Blau**: Links + „richtig" |
|
||||
| `--secondary-2` | `#5cc4e8` | `#0f5a80` | helleres Blau (Hover/Variante) |
|
||||
| `--text` | `#ece3d2` | `#33271c` | Fließtext |
|
||||
| `--muted` | `#8c8170` | `#6b5f4c` | gedämpfter Text, Labels, Captions |
|
||||
| `--heading` | `#ffffff` | `#241a12` | Überschriften |
|
||||
| `--success` | `#2d9cce` | `#146b96` | **„richtig"** (= Flux-Blau, bewusst kein Grün) |
|
||||
| `--danger` | `#ff4d6d` | `#be123c` | **„falsch"** / Fehler / Löschen (kühles Crimson) |
|
||||
| `--warn` | `#edcb4c` | `#8a6a06` | **„eingeschränkt"** / Warnstatus (Light-Wert WCAG-AA) |
|
||||
| `--glow-s` | `rgba(240,168,26,.55)` | `rgba(214,81,22,.40)` | **starker** Glow (Text-/Drop-Shadow auf Akzent) |
|
||||
| `--glow-w` | `rgba(240,168,26,.16)` | `rgba(214,81,22,.12)` | **weicher** Ambient-Glow |
|
||||
| `--g-soft` | `0 0 18px var(--glow-w)` | `0 6px 18px var(--glow-w)` | Standard-Panel-Schatten |
|
||||
|
||||
### Semantik kurz
|
||||
- **Akzent (Amber/Orange):** alles Primäre/Interaktive. Text auf Akzent immer `--accent-ink`.
|
||||
- **`--accent` + `--accent-2`** zusammen ergeben die typischen Verläufe (`linear-gradient(…,var(--accent),var(--accent-2))`).
|
||||
- **Blau (`--secondary` / `--success`):** Links und Erfolg.
|
||||
- **Rot (`--danger`):** Fehler, Löschen-Buttons, „falsch".
|
||||
|
||||
---
|
||||
|
||||
## Spiele-Feedback (verbindlich)
|
||||
|
||||
In allen Spielen (FinalFlux, MindMatch, TechTacular, Syntax Saga) gilt einheitlich:
|
||||
|
||||
- **richtig** → `var(--success)` (Blau) + Häkchen `✓`
|
||||
- **falsch** → `var(--danger)` (Rot) + Kreuz `✗`
|
||||
- aufgelöst/neutral (Syntax Saga) → `var(--secondary)`
|
||||
|
||||
Muster (siehe z. B. `syntaxsaga.css` / `techtacular.css`):
|
||||
```css
|
||||
.x-feedback.correct { color: var(--success); }
|
||||
.x-feedback.correct::before { content: "\2713 "; }
|
||||
.x-feedback.wrong { color: var(--danger); }
|
||||
.x-feedback.wrong::before { content: "\2717 "; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typografie
|
||||
|
||||
- **UI/Fließtext:** `"Inter","Segoe UI",system-ui,-apple-system,Roboto,sans-serif`
|
||||
- **Code/Monospace:** `"JetBrains Mono","Fira Code","Consolas","Courier New",monospace`
|
||||
- **Überschriften:** `font-weight:800`, Farbe `--heading`.
|
||||
- **Eyebrow/Labels (`.mf-eyebrow`, Panel-`h4`):** `--accent`, `text-transform:uppercase`,
|
||||
`letter-spacing:.08–.12em`, kleine Schrift.
|
||||
|
||||
---
|
||||
|
||||
## Glow, Schatten & Radien
|
||||
|
||||
- **Glow stark** `--glow-s`: `text-shadow`/`drop-shadow` auf leuchtenden Akzentelementen.
|
||||
- **Glow weich** `--glow-w`: dezenter Schein um Panels/Buttons.
|
||||
- **Panel-Schatten:** `box-shadow: var(--g-soft)`.
|
||||
- **Radien (Konvention):** Badges/Buttons ~`7–9px`, Inputs/Icon-Buttons `10px`, Nav `11px`,
|
||||
Panels `13–14px`, Standalone-Karten `16–18px`, Pills/Bars `99px`.
|
||||
- **Header-Icon-Buttons** (`.mf-iconbtn`, `.mf-theme`, `.mf-a11y-trigger`, `.ft-trigger`):
|
||||
einheitlich **`42×42px`**, Radius `10px`, **Outline-Stil** (Surface-Fill, 1px Border,
|
||||
Akzent nur über `color`/`border-color`), Glyphe **`20×20px`**. Der `.mf-theme`-Toggle behält
|
||||
Akzentfarbe + Glow als Erkennungsmerkmal.
|
||||
- **Spielkarten** (`.mf-card`) haben eine **abgeschrägte Ecke** via `clip-path` (`--n:18px`) statt
|
||||
rundem Radius – ein bewusstes „HUD"-Detail.
|
||||
|
||||
---
|
||||
|
||||
## Layout-Shell & Responsivität
|
||||
|
||||
App-Seiten nutzen das Grid `.mf-shell` (definiert in `theme.css`):
|
||||
|
||||
```
|
||||
┌── Sidebar 264px ──┬─────── Topbar (Spalte 2–3) ───────┐
|
||||
│ ├──── Main 1fr ────┬─ Rightbar 332px ┤
|
||||
└───────────────────┴──────────────────┴─────────────────┘
|
||||
```
|
||||
- `.mf-shell.no-right` → nur `264px | 1fr` (ohne rechte Spalte).
|
||||
- **Breakpoints:** `≤1180px` rechte Spalte aus, 2-spaltig · `≤820px` Shell stapelt, Sidebar wird
|
||||
zum **Off-Canvas-Drawer** (Hamburger + Backdrop, via `theme.js`) · `≤560px` Spielkarten-Grid 1-spaltig.
|
||||
- Spielseiten (MindMatch/TechTacular/Syntax Saga) haben eigene, schlankere Shells, nutzen aber
|
||||
dieselben Tokens und sind ebenfalls mobil-tauglich.
|
||||
|
||||
---
|
||||
|
||||
## Komponenten-Cheatsheet (`.mf-*` aus `theme.css`)
|
||||
|
||||
| Klasse | Zweck |
|
||||
|---|---|
|
||||
| `.mf-btn` | Primär-Button (Akzent). Varianten: `.outline`, `.ghost`, `.danger` |
|
||||
| `.mf-panel` | Karte/Box; `.accent` = akzentuierter Rahmen + Glow |
|
||||
| `.mf-card` / `.mf-grid` | HUD-Spielkarte (abgeschrägte Ecke) im 3er-Grid |
|
||||
| `.mf-input`, `.mf-select`, `textarea.mf-input` | Formularfelder (Fokus = Akzentrahmen + Glow) |
|
||||
| `.mf-field`, `.mf-label` | Formular-Zeile + Label |
|
||||
| `.mf-table` | Tabellen (Kopf in Akzent, mobil horizontal scrollbar) |
|
||||
| `.mf-badge` | kleines Status-Label |
|
||||
| `.mf-eyebrow` | Kicker/Overline über Headlines |
|
||||
| `.mf-nav` / `.mf-sidebar` | linke Navigation (aktiv = Akzent-Verlauf) |
|
||||
| `.mf-bar` + `> i` | Fortschrittsbalken |
|
||||
| `.mf-center` / `.mf-cardbox` | zentrierte Standalone-Karte (Login/Rechtsseiten) |
|
||||
| `.mf-container`, `.mf-page-head`, `.mf-back` | einfache Inhaltsseiten + Zurück-Link |
|
||||
| `.mf-footer` | globale Fußzeile |
|
||||
| `.mf-theme` / `.mf-theme-float` | Dark/Light-Umschalter (Logik in `theme.js`) |
|
||||
|
||||
---
|
||||
|
||||
## Pro-Spiel-CSS
|
||||
|
||||
Jedes Spiel hat eine eigene CSS-Datei mit eigenem **Klassen-Prefix**, die **ausschließlich
|
||||
Theme-Tokens** verwendet (keine eigenen Farbwerte):
|
||||
|
||||
| Spiel | Datei | Prefix |
|
||||
|---|---|---|
|
||||
| MindMatch | `mindmatch.css` | `mm-` |
|
||||
| TechTacular | `techtacular.css` | `tt-` |
|
||||
| Syntax Saga | `syntaxsaga.css` | `ss-` |
|
||||
| FinalFlux | `finalflux.css` | `finalflux-` |
|
||||
|
||||
Neues Spiel → neue `<spiel>.css` mit eigenem Prefix anlegen, **nur** `var(--…)`-Tokens nutzen,
|
||||
Feedback-Konvention (Blau/Rot) einhalten.
|
||||
|
||||
---
|
||||
|
||||
## Do / Don't
|
||||
|
||||
**Do**
|
||||
- Farben, Schatten und Effekte ausschließlich über Tokens. Für Effekte die
|
||||
Primitives `--g-text`, `--g-icon`, `--g-strong`, `--g-amb`, `--g-card`,
|
||||
`--g-card-hover`, `--g-overlay` und `--g-focus` verwenden; keine direkten
|
||||
Glow-Geometrien in Komponenten schreiben.
|
||||
- Auf Akzentflächen Text in `--accent-ink`.
|
||||
- Erfolg = Blau, Fehler = Rot (nie Grün/Orange für „richtig/falsch").
|
||||
- Neue Komponenten an den `.mf-*`-Bausteinen orientieren.
|
||||
|
||||
**Don't**
|
||||
- Keine hart kodierten Hex-/RGBA-Werte in Templates oder Spiele-CSS.
|
||||
- Keine direkten `0 0 … var(--glow-*)`-, `text-shadow`- oder `drop-shadow`-Werte
|
||||
in Komponenten. Maßgeblich ist `theme.css`.
|
||||
|
||||
---
|
||||
|
||||
*Stand: 2026-06-30 · Quelle: `src/main/resources/static/css/theme.css` (BTTF-Palette, Dark/Light).*
|
||||
+16
-4
@@ -5,16 +5,14 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
# --- Datenbank: unverändert wie im bisherigen Deploy ---
|
||||
# --- Datenbank: Passwort kommt aus Gitea-Secret (siehe Workflow), nie ins Repo ---
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mindfluxdb
|
||||
SPRING_DATASOURCE_USERNAME: mindflux
|
||||
SPRING_DATASOURCE_PASSWORD: mindflux_pass
|
||||
|
||||
SPRING_DATASOURCE_PASSWORD: "${MINDFLUX_DB_PASSWORD:?MINDFLUX_DB_PASSWORD muss als Secret gesetzt sein}"
|
||||
# --- App: lauscht intern auf 8080 (nginx -> mindflux:8080) ---
|
||||
SERVER_PORT: "8080"
|
||||
MINDFLUX_PUBLIC_URL: "https://mindflux.sysnaptik.de"
|
||||
MINDFLUX_COOKIE_SECURE: "true"
|
||||
|
||||
# --- Bootstrap-Admins: Passwörter kommen aus Gitea-Secrets (siehe Workflow) ---
|
||||
MINDFLUX_ADMIN_USERNAME: "AliceDarkest"
|
||||
MINDFLUX_ADMIN_EMAIL: "AliceDarkest@gmail.com"
|
||||
@@ -22,6 +20,20 @@ services:
|
||||
MINDFLUX_ADMIN2_USERNAME: "KevCipher"
|
||||
MINDFLUX_ADMIN2_EMAIL: "kevcipher@proton.me"
|
||||
MINDFLUX_ADMIN2_PASSWORD: "${MINDFLUX_ADMIN2_PASSWORD}"
|
||||
# --- Mailversand ---
|
||||
# Der Zertifikatsname des SMTP-Servers; der interne Docker-Alias "mailserver"
|
||||
# würde die STARTTLS-Hostname-Prüfung scheitern lassen.
|
||||
MAIL_HOST: mail.sysnaptik.de
|
||||
MAIL_PORT: "587"
|
||||
MAIL_USERNAME: "noreply@sysnaptik.de"
|
||||
MAIL_PASSWORD: "${MAIL_PASSWORD}"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "http://localhost:8080/actuator/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
- mindflux_uploads:/app/uploads
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
# Mindflux – Detailpläne für die 4 geplanten Spiele + Verbesserungsplan
|
||||
|
||||
## Kontext
|
||||
|
||||
Das Dashboard von Mindflux (`mindflux2`, Spring Boot 3.4.4 / Java 21 / PostgreSQL + Flyway / Thymeleaf / Vanilla-JS) zeigt 9 Spiele. Vier Spiele waren bei Erstellung dieses Plans Platzhalter-Karten. **Alle vier – SystemEscape, BugHunter, NetWorker und ServiceClash – sind inzwischen umgesetzt.**
|
||||
|
||||
1. **SystemEscape** – „Löse IT-Rätsel, sammle Zugriffsfragmente und entkomme dem System."
|
||||
2. **Service Clash** – „Der chaotische Admin-Alltag als Spiel – meistere Anfragen & halte Systeme am Laufen!"
|
||||
3. **Bug Hunter** – „Finde Fehler im Code, denke wie ein Debugger und löse knifflige Bugs."
|
||||
4. **Netzwerker – Das Router-Rätselspiel** – „Verbinde Netzwerke, konfiguriere IPs und löse Routing-Herausforderungen."
|
||||
|
||||
Die Spiel-Keys `codeescape`, `serviceclash`, `bughunter`, `networker` stehen in der Score-Allowlist (`GameScoreController.KNOWN_GAMES`) und in `ProgressService.GAMES`; die Dashboard-Icons (`lock`, `sword`, `bug`, `network`) existieren in `dashboard.html`. Dieser Plan dokumentiert den erreichten Stand und bleibt Referenz für spätere Content- und Visual-Vertiefungen.
|
||||
|
||||
**Zielgruppe & inhaltlicher Anspruch: die GESAMTE Fachinformatiker-Ausbildung.** Nicht nur AP1 – abgedeckt werden muss die komplette Ausbildung bis einschließlich **AP2** (beide Fachrichtungen: Anwendungsentwicklung/FIAE und Systemintegration/FISI). Konsequenzen für alle Spiele und Inhalte:
|
||||
|
||||
- **Schwierigkeitsstufen = Ausbildungsprogression.** Die in allen vier Designs vorhandenen Stufen (Junior/Senior/Principal, leicht/mittel/schwer, Welten 1–3, Schicht 1–3) werden inhaltlich gemappt: untere Stufen ≈ 1. Lehrjahr/AP1-Stoff, obere Stufen ≈ AP2-Niveau. Kein Schema-Umbau nötig – die `difficulty`-Felder existieren in allen Datenmodellen bereits; es ist eine **Content-Kuratierungs-Regel**.
|
||||
- **AP2-Content pro Spiel (Startrichtung):** SystemEscape → Szenarien zu Verschlüsselung/Zertifikaten, Virtualisierung/Container, Backup-Strategien; BugHunter → Principal-Fälle auf FIAE-AP2-Niveau (Nebenläufigkeit/Threads, Transaktions-/SQL-Fehler, Design-Pattern-Missbrauch, OWASP-Lücken); ServiceClash → Schichten mit Change-Management, Monitoring/Eskalationsketten, Security-Incidents (FISI-AP2); NetWorker → Welten zu VLAN, statischem Routing über mehrere Hops, Firewall-Regeln und (Ausbau) IPv6/Subnetting-Transfer.
|
||||
- **Content-Tagging:** Neue Parent-Inhalte erhalten von Anfang an ein persistiertes Feld `exam_scope VARCHAR(20) NOT NULL DEFAULT 'AP1'` mit der Allowlist `AP1 | AP2_FIAE | AP2_FISI | CROSS`. Seeder und Admin validieren dieses Feld; Katalog-DTOs geben es aus. Titelpräfixe sind nur Anzeige, nie Datenquelle. Lobby-Filter können dadurch später ohne Migration ergänzt werden.
|
||||
- **FinalFlux erweitern:** Das bestehende Lernmodul ist rein AP1-fokussiert (28 Seiten + FluxChecks). Die Struktur (statische Themenseiten + Frage-JSONs) trägt problemlos einen **AP2-Ast** – als eigener Sidebar-Block „AP2 Anwendungsentwicklung" / „AP2 Systemintegration" (in Teil 2 als Punkt S10 aufgenommen).
|
||||
- Die vorhandenen FinalFlux-Inhalte (IP-Berechnung, VLAN, OSI, Wirtschaft) bleiben die AP1-Basis, auf der die Spiele aufsetzen.
|
||||
|
||||
**Deliverable:** Dieser Plan liegt als `mindflux2/mindfluxfuture.md` im Repo (neben `STYLEGUIDE.md`/`CHANGELOG.md`).
|
||||
|
||||
---
|
||||
|
||||
## Namensschema: einheitlich, englisch, zusammengeschrieben
|
||||
|
||||
> ✅ **Umgesetzt am 2026-07-12**: Anzeigenamen und Spiel-Keys sind auf das Schema umgestellt (Dashboard-`GAMES`-Array, `KNOWN_GAMES`, `ProgressService`).
|
||||
|
||||
Vorgabe: Alle Spielnamen folgen dem MindMatch-Muster – **ein Wort, englisch, CamelCase**. „Netzwerker – Das Router-Rätselspiel" passt nicht ins Schema und wird umbenannt. Neue Namenstabelle (Beschreibungen bleiben deutsch):
|
||||
|
||||
| Bisher | Neu (Anzeigename) | Route | Spiel-Key (DB/API) |
|
||||
|--------|-------------------|-------|--------------------|
|
||||
| MindMatch | **MindMatch** ✓ | `/mindmatch` | `mindmatch` |
|
||||
| TechTacular | **TechTacular** ✓ | `/techtacular` | `techtacular` |
|
||||
| Dev Lab | **DevLab** | `/devlab` | `devlab` |
|
||||
| Syntax Saga | **SyntaxSaga** | `/syntaxsaga` | `syntax-saga` (Key behalten – es existieren bereits Scores) |
|
||||
| FinalFlux | **FinalFlux** ✓ | `/finalflux` | – |
|
||||
| SystemEscape | **SystemEscape** | `/codeescape` | `codeescape` |
|
||||
| Service Clash | **ServiceClash** | `/serviceclash` | `serviceclash` |
|
||||
| Bug Hunter | **BugHunter** | `/bughunter` | `bughunter` |
|
||||
| Netzwerker – Das Router-Rätselspiel | **NetWorker** | `/networker` | `networker` |
|
||||
|
||||
Begründung Keys: Die vorhandenen Routen sind alle klein und zusammengeschrieben (`/mindmatch`, `/syntaxsaga` …) – die neuen Routen folgen dem. Die historischen Plan-Aliasse `escape-room`, `service-clash`, `bug-hunter` und `netzwerker` hatten noch keine Scores und wurden deshalb vor der Implementierung auf `codeescape`, `serviceclash`, `bughunter` und `networker` vereinheitlicht. Nur `syntax-saga` behält seinen Key wegen möglicher Bestandsdaten. Die Tabelle und der Abschnitt „Gemeinsames Architektur-Rezept" sind die verbindliche Quelle; verbliebene Altbegriffe in erklärenden Designpassagen sind keine Routen- oder Schema-Vorgaben.
|
||||
|
||||
---
|
||||
|
||||
## Visuelle Identität: neun Lernwelten
|
||||
|
||||
> ◐ **Stand 2026-07-13:** Die Dark-/Light-Akzent-Tokens für alle vier Spiele sind in
|
||||
> `theme.css` angelegt. SystemEscape, BugHunter und NetWorker haben eigene Spiel-Layouts;
|
||||
> BugHunter nutzt Fallakten-/Aktenschrank-Optik, NetWorker ein sichtbares Blaupausenraster.
|
||||
> ServiceClash ist nun als eigenständiger cyanfarbener Leitstand umgesetzt; optionale
|
||||
> Echtzeit-SLA-Animationen bleiben ein möglicher Ausbau.
|
||||
|
||||
Vorgabe: Die Spiele sollen **grafisch stark ausgeprägt** sein und sich **sehr deutlich** voneinander unterscheiden – nicht nur mechanisch, sondern auf den ersten Blick. Dazu zwei Maßnahmen:
|
||||
|
||||
**1. Kontrollierte Styleguide-Erweiterung – Spiel-Akzente:** `theme.css` bekommt pro Spiel ein Akzent-Token-Paar (z. B. `--esc-accent`, `--bh-accent`, `--sc-accent`, `--nw-accent`, je mit Dark-/Light-Wert). Jedes Spiel-CSS überschreibt im Seiten-Root lokal `--accent`/`--glow-*` mit seinem Spiel-Akzent. Die Grundregeln bleiben: Erfolg = blaues ✓, Fehler = rotes ✗, nie grün als Erfolgsfarbe; beide Themes müssen funktionieren.
|
||||
|
||||
**2. Eigenes visuelles Vokabular pro Spiel** (Layout, Illustration, Animation, Typografie-Akzente):
|
||||
|
||||
| Spiel | Welt-Metapher | Farbstimmung | Prägende visuelle Elemente |
|
||||
|-------|---------------|--------------|---------------------------|
|
||||
| **SystemEscape** | Dunkle Facility bei Nacht | Bernstein/Orange auf Tiefschwarz | CRT-Terminals mit Scanline-Overlay, physisches Keypad mit Tasten-Feedback, Raum-„Bühnenbild" als SVG-Illustration (Racks, Türen, flackernde Neonröhre), Fragment-Chips wie Hardware-Token, Alarm-Puls bei Zeitablauf, Tür-Öffnungs-Animation zwischen Räumen |
|
||||
| **BugHunter** | Noir-Detektivbüro | Entsättigt + Crimson-Akzente | Fallakten-Optik (Aktendeckel, Büroklammer, „VERTRAULICH"-Stempel), IDE-Look mit Zeilennummern-Gutter und Tab-Reitern, roter Ermittlungsfaden zum markierten Bug, „GELÖST"-Stempel-Animation im Debriefing, Schreibmaschinen-Typo für Briefings |
|
||||
| **ServiceClash** | Hektischer Leitstand/Cockpit | Kühles Cyan + Warnrot | Kanban-artige Ticket-Queue mit einfliegenden Karten, große SLA-Ring-Gauges und Gesundheits-Meter, laufender Event-Ticker unten, Wellen-Countdown-Banner, komisch überzeichnete Ticket-Icons (brennender Drucker 🔥), Screen-Shake bei Major Incidents |
|
||||
| **NetWorker** | Technische Blaupause | Blueprint-Blau auf Millimeterpapier | Raster-Hintergrund wie Konstruktionszeichnung, Geräte als Schematic-Glyphen, Gummiband-Kabelzug, leuchtende Paket-Punkte, die den echten Trace entlangfliegen und am Fehler-Hop rot zerplatzen, Diagnose-Callouts wie Plan-Anmerkungen |
|
||||
|
||||
Dazu je Spiel ein eigenes Karten-Design auf dem Dashboard (Hover-Animation im jeweiligen Spiel-Akzent), damit die Unterscheidbarkeit schon in der Übersicht beginnt. Die Interaktionsformen sind ohnehin grundverschieden: Modal-Rätsel + Keypad (SystemEscape), klickbarer Code-Viewer (BugHunter), Echtzeit-Queue (ServiceClash), frei bespielbare SVG-Canvas (NetWorker).
|
||||
|
||||
### Eigenständige Skins für die Bestandsmodule ✅ umgesetzt
|
||||
|
||||
Die gemeinsame Mindflux-Grundlage bleibt erhalten: Typografie, Fokuszustände, Buttons,
|
||||
Fehler-/Erfolgsfarben und das Accessibility-Widget werden nicht pro Spiel neu erfunden.
|
||||
Jede Lernwelt erhält aber eigene Akzent-Tokens, Hintergrundstruktur und eine passende
|
||||
Oberflächenmetapher – im Dark- wie im Light-Theme.
|
||||
|
||||
| Modul | Skin | Akzent & Metapher | Umsetzungshinweis |
|
||||
|---|---|---|---|
|
||||
| **MindMatch** | Neural Archive | Violett/Mint, Synapsenlinien und schwebende Gedächtniskarten | Paare bleiben klar lesbar; Verbindungen sind dekorativ, nicht alleiniger Status. |
|
||||
| **TechTacular** | Cyber-Quizshow | Magenta/Blau, LED-Raster und Punktetafel | Das Jeopardy-Board wird zur Show-Bühne, ohne die Kachelkontraste zu verlieren. |
|
||||
| **DevLab** | Werkstatt & Build-Pipeline | Petrol/Orange, Werkbank- und Schaltkreisstruktur | **Der schwebende Kolben samt \`dlFloat\`-Animation bleibt unverändert erhalten.** Nur Farben, Umgebung und Panels werden nachgeschärft. |
|
||||
| **SyntaxSaga** | Textadventure-Terminal | Phosphorgrün/Bernstein, dezente CRT-Scanlines und Log-Charakter | Grün dient ausschließlich der Atmosphäre; richtig bleibt Plattform-Blau, falsch bleibt Rot. |
|
||||
| **FinalFlux** | Prüfungszentrale | Gold/Indigo, ruhige Sternkarten-/Konstellationsstruktur | Bewusst weniger spielig: die Lernzentrale verbindet alle Welten und priorisiert Lesbarkeit. |
|
||||
|
||||
**Technische Leitplanken:** zentrale Token in \`theme.css\`, lokale Überschreibung nur am
|
||||
jeweiligen Seiten-Root (\`mm-\`, \`tt-\`, \`dl-\`/\`dlg-\`, \`ss-\`, \`finalflux-\`), keine
|
||||
Änderung der Spielmechanik; Cache-Busting bei jeder CSS-Auslieferung. Das
|
||||
Accessibility-Widget soll anschließend jeweils den lokalen Akzent übernehmen.
|
||||
|
||||
> ✅ **Stand 2026-07-13:** Die fünf Skins sind als lokale Token-Überschreibungen mit
|
||||
> Cache-Busting umgesetzt. MindMatch, TechTacular, DevLab samt Aufgabenraum, SyntaxSaga
|
||||
> und FinalFlux haben jeweils eigene Hintergründe und Akzente; das Accessibility-Widget
|
||||
> übernimmt die neue Farbe. Die bestehende Kolben-/dlFloat-Animation in DevLab blieb
|
||||
> unverändert.
|
||||
|
||||
---
|
||||
|
||||
## Plattform-Rebrand: SysnaptiK Neural ⏳ in Umsetzung
|
||||
|
||||
> ✅ **Stand 2026-07-14:** Die sichtbare Plattformhülle ist auf SysnaptiK Neural
|
||||
> umgestellt: zentrale Neural-Schicht, Wortmarke, Login/Registrierung, Dashboard,
|
||||
> Plattformseiten und FinalProtocol. Die Spielwurzeln sind explizit vom Plattform-Scope
|
||||
> ausgeschlossen und behalten ihre jeweiligen internen Skins. NeuralPause ersetzt die
|
||||
> sichtbare FluxTimer-/Zeitreise-Copy. Domain-/DNS-Umzug und technische Paket-/Tabellennamen
|
||||
> bleiben bewusst separate Schritte. Der Dashboard-Status ist als echter Lernnetz-Status
|
||||
> umgesetzt: Er prüft den Fortschrittsspeicher statt eine feste Verfügbarkeit vorzutäuschen.
|
||||
> Die Plattformdiagramme und Accessibility-Modi berücksichtigen die Neural-Tokens auch
|
||||
> dann, wenn einzelne Seiten oder Spiele eigene lokale Akzente setzen.
|
||||
> Die Hülle ist zudem renderblockierend und mit früher HTML-Klasse eingebunden, damit beim
|
||||
> Navigationwechsel kein früheres Plattformfarbschema aufblitzt.
|
||||
|
||||
### Ziel und Abgrenzung
|
||||
|
||||
Mindflux wird vor dem öffentlichen Launch als **SysnaptiK Neural** positioniert. Die Plattform
|
||||
wechselt vom bisherigen Zeitreise-/Back-to-the-Future-Anklang zu einer eigenständigen
|
||||
neuronalen Netzwerk-Metapher. Das Lernmodul **FinalFlux** heißt künftig **FinalProtocol**.
|
||||
|
||||
Der Rebrand betrifft ausschließlich die Plattformhülle: Startseite, Login/Registrierung,
|
||||
Dashboard, Profil, Fortschritt, Erfolge, Einstellungen, Knowledge Core, FinalProtocol,
|
||||
Admin- und Rechtsseiten, Seitentitel sowie öffentliche Metadaten. Die eigenständigen
|
||||
Spielwelten und ihre internen Farben bleiben unverändert.
|
||||
|
||||
### Visuelles System
|
||||
|
||||
- **Grundwelt:** tiefes Petrol/Schwarz, leise Netzwerklinien, Knoten, weiche Glows und
|
||||
klare technische Panels statt Zeitreise-, DeLorean-, Fluxkompensator- oder BTTF-Verweise.
|
||||
- **Marke:** CSS-/SVG-Wortmarke „SYSNAPTIK NEURAL“ mit abstraktem neuronalen Knoten statt
|
||||
einer abgeleiteten Popkultur-Ästhetik.
|
||||
- **Statusfarben:** Erfolg bleibt Plattform-Blau, Fehler Rot; Knotenstatus wird zusätzlich
|
||||
über Symbol, Text und Form vermittelt.
|
||||
- **Knotenfarben auf der Plattform:** MindMatch Magenta/Violett, TechTacular Gelb,
|
||||
DevLab Mint, SyntaxSaga Violett, SystemEscape Bernstein, ServiceClash Türkis,
|
||||
BugHunter Crimson, NetWorker Blau und FinalProtocol Orange. Diese Zuordnung gilt für
|
||||
Dashboard, Lernnetz, Fortschritt und Erfolge – nicht als Änderung der Spieloberflächen.
|
||||
|
||||
### Umsetzungsphasen
|
||||
|
||||
1. **Fundament:** zentrale Neural-Tokens und Markenbausteine in theme.css, BTTF-/Zeitreise-
|
||||
Copy aus Plattform-Tokens und FluxTimer entfernen; Accessibility auf die neutrale
|
||||
Plattformfarbe ausrichten.
|
||||
2. **Referenzoberflächen:** Start/Authentifizierung und Dashboard als neuraler Leitstand:
|
||||
neue Sidebar, Spielknoten-Karten, Systemstatus, echter Fortschritt und ein
|
||||
Lernnetz statt dekorativer Platzhalter.
|
||||
3. **Plattformseiten:** Profil, Fortschritt, Erfolge, Einstellungen, Knowledge Core,
|
||||
Fehler-, Rechts- und Admin-Seiten auf das gemeinsame Layout und die Wortmarke ziehen.
|
||||
4. **FinalProtocol:** Anzeigename, Hub, Lernseiten, Navigation, Titel und Verweise von
|
||||
FinalFlux auf FinalProtocol umstellen; URLs bleiben zunächst kompatibel bei
|
||||
/finalflux, um bestehende Links nicht zu brechen.
|
||||
5. **Öffentliche Identität & Rollout:** Dokumentation, Titel/Meta-/OpenGraph-Texte,
|
||||
Datenschutz/Impressum und Konfiguration auf SysnaptiK Neural umstellen. Ein möglicher
|
||||
Domainwechsel auf neural.sysnaptik.de erfolgt separat zusammen mit DNS, nginx,
|
||||
Cookie-Domain und einer Weiterleitung von mindflux.sysnaptik.de.
|
||||
6. **Abnahme:** Dark/Light, Mobilansicht, Tastatur-/Lesemodus, Kontraste und alle
|
||||
Spielrouten prüfen. Technische Paketnamen, Datenbanktabellen, Score-Keys und Secrets
|
||||
bleiben im ersten Schritt bewusst stabil; eine spätere technische Umbenennung ist ein
|
||||
separates Migrationsprojekt.
|
||||
|
||||
---
|
||||
|
||||
## Das gemeinsame Architektur-Rezept (gilt für alle 4 Spiele)
|
||||
|
||||
Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: **Dev Lab**, `DevLabService.java` + `devlabgame.js`):
|
||||
|
||||
1. **Flyway-Migration** ab `db/migration/V11__…` (V9/V10 sind durch User-/Datenintegrität belegt; `ddl-auto=validate` ⇒ Migration ist Pflicht). Umgesetzt: V11 SystemEscape, V12 BugHunter, V13 BugHunter-Fortschritt, V14 NetWorker und V15 ServiceClash. SQL-Stil: `BIGINT GENERATED BY DEFAULT AS IDENTITY`, `slug VARCHAR UNIQUE`, `exam_scope VARCHAR(20)`, `sort_order INT DEFAULT 0`, `active BOOLEAN DEFAULT TRUE`, `FK … ON DELETE CASCADE`, Indizes.
|
||||
2. **Entities** in `model/` (Parent mit `@OneToMany(cascade=ALL, orphanRemoval=true)`, Lösungs-Logik nur serverseitig auf der Entity).
|
||||
3. **Repository** in `repository/` (`findAllByActiveTrueOrderBySortOrderAscIdAsc()`, `findBySlug`, `countBy…Id`).
|
||||
4. **Service** in `service/` mit Konstante `GAME = "<key>"`, injiziert `GameScoreService`; `@Transactional(readOnly=true)` für Build/Check, `@Transactional` für `finish`. **Score wird ausschließlich serverseitig berechnet** (`gameScoreService.recordScore(user, GAME, deck, score, moves, seconds)`); Client sendet nur Roh-Antworten.
|
||||
5. **Punkte-Konventionen der Plattform:** 1. Versuch volle Punkte, 2. Versuch ×0.6, danach ×0.3, aufgedeckte Lösung = 0; Sterne: Quote ≥0.8 → 3, ≥0.5 → 2, sonst 1; `MAX_ATTEMPTS = 3`.
|
||||
6. **DTOs** in `dto/`: lösungsfreie View-DTOs, Request-DTOs, `*Result` (score, maxScore, solved, total, stars, highscore, newHighscore, plays).
|
||||
7. **Controller:** `@Controller` (`GET /<spiel>` → Thymeleaf-View) + `@RestController @RequestMapping("/api/<spiel>")` mit `GET /session|board`, `POST /check` (mit `reveal`-Flag), `POST /finish`; `Principal` für den Usernamen.
|
||||
8. **Seeder** (`CommandLineRunner` in `config/`, Guard `count() > 0`, liest `resources/<spiel>/content.json`).
|
||||
9. **Admin-CRUD** `@Controller @RequestMapping("/admin/<spiel>")` mit Redirect-after-Post + Karte in `templates/admin/dashboard.html`.
|
||||
10. **Frontend:** Template in `templates/`, eigenes CSS mit eindeutigem Präfix (nur `theme.css`-Tokens, Erfolg = blaues ✓, Fehler = rotes ✗, **nie grün**), JS als IIFE in `static/js/`, Highscore via `GET /api/games/<key>/stats`, Cache-Busting `?v=N`.
|
||||
11. **Integration:** Route in `SecurityConfig`-CSRF-Ignore-Liste aufnehmen, Dashboard-Karte in `dashboard.html` von `/comingsoon` auf die echte Route umstellen. (`KNOWN_GAMES` + `ProgressService` enthalten die Keys bereits.)
|
||||
|
||||
### Verbindlicher Sicherheitsvertrag für neue Spiele (S3)
|
||||
|
||||
1. Session-/Katalogantworten enthalten nur opaque IDs und lösungsfreie DTOs; Lösung, Reveal-Status und Scoring-Metadaten bleiben serverseitig.
|
||||
2. `finish` akzeptiert nur IDs aus der angeforderten Runde und verwirft fremde, doppelte oder nicht zum Parent gehörende IDs.
|
||||
3. Punkte, Sterne, Highscore, gelöste Anzahl und Boni werden ausschließlich serverseitig aus kanonischen Daten neu berechnet.
|
||||
4. `durationSeconds` muss plausibel sein (`>= 1` und höchstens das konfigurierte Limit plus dokumentierte Toleranz); Listen und Zähler erhalten fachliche Obergrenzen.
|
||||
5. Reveal-/Hint-Nutzung wird serverseitig protokolliert oder beim Finish erneut validiert und kann nicht durch Weglassen im Request verborgen werden.
|
||||
6. Deterministische Seeds werden spätestens bei ServiceClash mit einem serverseitigen HMAC signiert; der Client darf keinen frei wählbaren Seed zur Wertung einreichen.
|
||||
7. Für jedes Spiel entstehen Negativtests: fremde IDs, Duplikate, überlange Listen, unmögliche Zeiten, manipulierte gesperrte Felder und Reveal-vor-Finish.
|
||||
|
||||
---
|
||||
|
||||
## Teil 1: Spieldesigns
|
||||
|
||||
### Plattformweite Barrierefreiheit ✅ Grundwidget umgesetzt
|
||||
|
||||
- Globales, per Tastatur erreichbares Accessibility-Panel über einen schwebenden Rollstuhl-Button.
|
||||
- Schriftgröße in vier nachvollziehbaren Stufen (100 %, 115 %, 130 %, 150 %), lokal gespeichert
|
||||
und auf Dashboard, Lernseiten sowie allen Spielen identisch angewendet.
|
||||
- Enthalten: erhöhter Kontrast, farbenblindfreundliches Statusfarbprofil, Monochrommodus,
|
||||
Lesemodus für Dyslexie mit Verdana und angepassten Abständen, unterstrichene Links, reduzierte Bewegungen, großer
|
||||
Cursor, gut sichtbare Fokusmarkierungen und zentraler Reset.
|
||||
- Noch offen bleibt die systematische Prüfung aller Spielmechaniken auf vollständige
|
||||
Tastaturbedienbarkeit sowie ein WCAG-Audit mit Kontrastmessung und Screenreader-Tests.
|
||||
- Layouts dürfen bei Textvergrößerung weder Inhalte abschneiden noch horizontales Scrollen der
|
||||
gesamten Seite erzwingen; Codebereiche dürfen weiterhin gezielt horizontal scrollen.
|
||||
- Das Panel ergänzt semantisches HTML und WCAG-Prüfungen, ersetzt diese aber nicht.
|
||||
|
||||
### Spiel 1: SystemEscape (interner Key: `codeescape`, Migration V11 + Datenfix V11.1) ✅ MVP inklusive Spielführung umgesetzt
|
||||
|
||||
**Konzept:** Der Spieler ist in einer digitalen Umgebung eingesperrt (Serverraum, Admin-Büro, Rechenzentrum). Ein **Szenario** besitzt einen Pool aus 8 Räumen; jeder Run zieht daraus 4 Räume in zufälliger Reihenfolge. Jeder Raum enthält 2–3 IT-Rätsel plus ein **Tür-Keypad**. Jedes gelöste Rätsel liefert ein **Code-Fragment**; die Fragmente ergeben zusammen den Türcode zum nächsten Raum. Countdown erzeugt Zeitdruck; ein 2-stufiges Hinweis-System tauscht Punkte gegen Hilfe.
|
||||
|
||||
**Lernziele (AP1 → AP2):** Zahlensysteme (Bin/Hex/Dez), Ports & Protokolle, Subnetting, TCP-Handshake/Boot-Reihenfolgen, RAID, Hardware, SQL/Shell-Grundlagen – als Rätsel verpackt statt als Quiz. Schwere Szenarien tragen AP2-Stoff (Verschlüsselung/Zertifikate, Virtualisierung/Container, Backup-Strategien).
|
||||
|
||||
**Spielablauf:**
|
||||
1. **Lobby:** Szenario-Karten (Titel, Thema, Schwierigkeit, Zeitlimit, persönliche Bestzeit).
|
||||
2. **Raum-Ansicht:** Rätsel-Karten im Terminal-Look, Fragment-Inventar, Tür-Keypad. Rätsel öffnen sich im Modal.
|
||||
3. **Rätsel-Versuch:** `MAX_ATTEMPTS = 3` wie Dev Lab; danach „Auflösen"-Button. Bei richtig **oder** aufgedeckt liefert der Server das Code-Fragment – Aufdecken blockiert nie den Fortschritt, es nullt nur die Punkte (Plattform-Konvention).
|
||||
4. **Tür:** Spieler tippt den zusammengesetzten Code; Server validiert (`POST /door`). Falsch → rotes Blinken, keine Punktstrafe (die laufende Uhr ist die Strafe).
|
||||
5. **Flucht/Ergebnis:** `POST /finish` rechnet alles serverseitig nach; Ergebnis-Screen mit Score-Aufschlüsselung (Rätselpunkte, Türboni, Hinweis-Abzug, Zeitbonus), Sternen, Highscore. „Aufgeben"-Button beendet früher (kein Zeitbonus, `escaped=false`).
|
||||
|
||||
**Punkteformel (serverseitig in `/finish` nachgerechnet):**
|
||||
- Pro Rätsel: `punkte × versuchsfaktor × hinweisfaktor` (Versuch: 1.0/0.6/0.3, aufgedeckt 0; Hinweis: keiner ×1.0, Stufe 1 ×0.8, Stufe 2 ×0.6). Hinweis-Nutzung wird **serverseitig in der HttpSession** protokolliert (der `/hint`-Endpoint merkt sich die höchste gelieferte Stufe pro Rätsel) – der Client kann sie nicht unterschlagen. _(Einzige bewusste Abweichung vom sonst zustandslosen Muster.)_
|
||||
- Pro geöffneter Tür: pauschal **+50**.
|
||||
- Sterne aus `baseScore/maxScore` (≥0.8→3, ≥0.5→2). **Zeitbonus danach**, nur bei Flucht: `max(0, round(500 × (zeitlimit − sekunden) / zeitlimit))` – die MindMatch-Idee, auf 15-Minuten-Läufe skaliert. Timer auf 0 bricht nicht ab: HUD wechselt in „ALARM"-Zustand (pulsierendes `var(--danger)`), der Bonus ist nur weg.
|
||||
- `recordScore(user, "codeescape", szenarioSlug, baseScore + zeitbonus, gelösteRätsel, sekunden)`.
|
||||
|
||||
**Datenmodell (`V11__codeescape.sql`):** 3 Tabellen im V8-Stil:
|
||||
- `escape_scenarios` (id, slug UNIQUE, title, difficulty, description, time_limit_seconds INT DEFAULT 900, sort_order, active)
|
||||
- `escape_rooms` (id, scenario_id FK CASCADE, title, intro, door_prompt, **door_code VARCHAR(50)**, success_text, sort_order)
|
||||
- `escape_puzzles` (id, room_id FK CASCADE, puzzle_type VARCHAR(30), prompt, points INT DEFAULT 100, **code_fragment VARCHAR(20)**, sort_order, payload VARCHAR(8000))
|
||||
- Indizes auf beide FKs. `code_fragment`/`door_code` als Spalten (nicht im Payload), damit Check/Door sie ohne JSON-Parsing haben und der Admin sie auf einen Blick sieht. Hinweise liegen im Payload als `"hints": [...]`.
|
||||
|
||||
**Backend:** Entities `EscapeScenario`/`EscapeRoom`/`EscapePuzzle` (Struktur wie `DevLabGame`/`DevLabTask`, LAZY, `addRoom`/`addPuzzle`-Helper). Repositories mit den Standard-Derived-Queries. `EscapeRoomService` mit `KNOWN_TYPES = Set.of("code","choice","order","match")`:
|
||||
- `buildCatalog()` / `buildSession(slug)` – Szenario mit **allen** Räumen, Rätsel per `sanitize()` lösungsfrei (keine Lösungen, Fragmente, Hints, Türcodes). Raum-Sperre ist Client-Gate, abgesichert durch Server-Türvalidierung.
|
||||
- `check(puzzleId, reveal, submission)` – Typ `code`: neuer trivialer Checker via `anyEquals(payload.solutions, answer, caseSensitive)`; **`choice`/`order`/`match`: Checker 1:1 aus `DevLabService` kopieren.** Bei `correct || reveal` kommt `"fragment"` in die Antwort.
|
||||
- `openDoor(roomId, code)`, `hint(session, puzzleId, level)`, `finish(...)` wie oben; plus Admin-Methoden nach Dev-Lab-Vorbild.
|
||||
|
||||
**API (`/api/codeescape`):** `GET /scenarios`, `GET /session/{slug}`, `POST /check` (`{puzzleId, reveal, submission:{answer}}` → `{correct, fragment, successText}`), `POST /door` (`{roomId, code}` → `{open, successText}`), `POST /hint` (`{puzzleId, level}` → `{text, level}`), `POST /finish` (`{slug, durationSeconds, escaped, items:[{puzzleId, submission, attempts, revealed}], doors:[{roomId, code}]}` → `{score, baseScore, maxScore, timeBonus, solvedPuzzles, roomsOpened, escaped, stars, highscore, newHighscore, plays}`).
|
||||
|
||||
**Frontend:** `templates/escape-room.html`, `static/js/escaperoom.js` (IIFE-State-Machine `LOBBY → INTRO → ROOM(i) → PUZZLE_MODAL → DOOR_TRY → TRANSITION → … → RESULT`), `static/css/escaperoom.css` mit Präfix `esc-`. **Die `choice`/`order`/`match`-Renderer aus `devlabgame.js` (Vertrag `{render, collect, lock, mark}`) portieren**, nur ein `code`-Renderer (Terminal-Artefakt + Textfeld) kommt neu dazu. Layout: HUD oben (Raumtitel, Raum-Punkte ● ● ○, Timer-Chip), Mitte Rätselkarten, unten Fragment-Chips + Keypad-Button. Nur Theme-Tokens, Erfolg blau ✓, Fehler rot ✗.
|
||||
|
||||
**Content & Seeder:** `EscapeRoomSeeder` liest `resources/escaperoom/content.json`. Startumfang: **3 Szenarien** (leicht/mittel/schwer) × 3 Räume × 2–3 Rätsel ≈ 22–25 Rätsel. Themen: „Der verlassene Serverraum" (Netzwerk/Zahlensysteme), „Lockdown im Rechenzentrum" (Hardware/RAID/USV), „Das gehackte Büro" (Security/Passwörter/Verschlüsselung). Beispiel-Rätsel (ausgearbeitet im Design): PIN = Dezimalwert von `0b101001` → „41", SSH-Port-Choice, Dienst↔Port-Matching (Türcode „427"), TCP-Handshake-Reihenfolge, nutzbare Hosts im /26-Netz → „62" (Tresorcode „3E62").
|
||||
|
||||
**Admin:** `AdminEscapeRoomController` (`/admin/escape-room`), 3 Drilldown-Templates (Szenario-Liste → Szenario mit Räumen → Raum mit Rätseln; Rätsel-Payload als JSON-Textarea wie Dev Lab).
|
||||
|
||||
**Aufwand:** MVP ≈ 5,5 PT (Migration/Entities 0,5 · Service 1,5 · API 0,5 · Frontend 2 · Content 0,5 · Admin+Integration 0,5). Ausbau 1 (+1,5 PT): Bestzeiten in der Lobby, „Perfekter Lauf"-Badge. Ausbau 2 (+1–2 PT): Rätseltypen `regex`/`cloze` (Checker existieren wortgleich in `DevLabService`), 2 weitere Szenarien.
|
||||
|
||||
---
|
||||
|
||||
### Spiel 2: BugHunter (Key: `bughunter`, Migration V12) ✅ umgesetzt und ausgebaut
|
||||
|
||||
> ✅ **Stand 2026-07-13:** Das serverautoritative Spiel, Seeder, Admin-CRUD und Dashboard-Route
|
||||
> sind umgesetzt. Der Katalog umfasst jetzt **12 Fallakten**, nach Junior/Senior/Principal
|
||||
> gruppiert. Die Akten liegen im Aktenschrank; nach einer perfekten Lösung erscheint ein
|
||||
> „GELÖST“-Stempel. Ein Reset startet eine inhaltlich passende Fallvariante.
|
||||
|
||||
**Konzept:** Der Spieler ist Debugging-Detektiv und arbeitet **Fallakten** ab. Ein Fall („Hunt") ist ein Mini-Projekt aus 2–4 Dateien, das sich falsch verhält; Beweismittel sind simulierte Konsolen-Ausgabe / Stacktrace / Ticket-Text. Pro Bug eine Drei-Phasen-Schleife: **ORTEN** (fehlerhafte Zeile im Tab-Code-Viewer anklicken) → **KLASSIFIZIEREN** (Syntax / Logik / Laufzeit / Sicherheit) → **FIXEN** (korrigierte Zeile aus 4 Kandidaten wählen). Saubere Bugs bauen eine **Combo-Serie** auf. Stufen: Junior / Senior / Principal.
|
||||
|
||||
**Abgrenzung zu Dev Lab:** Dev Labs `spot`-Typ ist „finde die eine falsche Zeile". Bug Hunter ist bewusst reicher: Multi-Datei-Navigation, beweisgetriebenes Denken (Stacktrace lesen → Zeile zuordnen), explizite Fehler-Taxonomie (Prüfungsthema!) und Fix-Auswahl – das vollständige Debugging-Denkmodell. Junior-Fälle decken AP1-Stoff ab; Senior/Principal-Fälle gehen auf FIAE-AP2-Niveau (Nebenläufigkeit/Threads, Transaktions-/SQL-Fehler, Design-Pattern-Missbrauch, OWASP-Sicherheitslücken).
|
||||
|
||||
**Punkteformel (serverseitig in `/finish`):**
|
||||
- Phasengewichte pro Bug: Orten 40 %, Klassifizieren 20 %, Fixen 40 %. Phasen-Award = `gewicht × punkte × versuchsfaktor` (1.0/0.6/0.3, aufgedeckt 0).
|
||||
- Sterne aus `baseScore/maxScore` (Genauigkeitsmaß, ohne Combo).
|
||||
- **Combo:** Ein Bug ist _sauber_, wenn alle 3 Phasen im 1. Versuch ohne Aufdecken gelingen. Serienposition n bringt `(min(n,4) − 1) × 10 % × punkte` Bonus (2. saubere in Folge +10 %, … 4.+ +30 %); Bonus wird **nach** der Sternberechnung addiert. Kein Timer – Bug Hunter belohnt Präzision, Zeitdruck gehört dem Escape Room.
|
||||
- Basispunkte pro Bug nach Stufe: Junior 100, Senior 150, Principal 200. `recordScore(user, "bughunter", huntSlug, baseScore + comboBonus, gelösteBugs, sekunden)`.
|
||||
|
||||
**Datenmodell (`V12__bughunter.sql`):**
|
||||
- `bughunter_hunts` (id, slug UNIQUE, title, code_language, difficulty DEFAULT 'junior', exam_scope, description, briefing VARCHAR(2000), console_output VARCHAR(4000), sort_order, active)
|
||||
- `bughunter_files` (id, hunt_id FK CASCADE, filename, content VARCHAR(8000), sort_order) – eigene Tabelle, damit der Code-Viewer-Inhalt von allen Bugs geteilt wird und im Admin als Textarea editierbar ist
|
||||
- `bughunter_bugs` (id, hunt_id FK CASCADE, points DEFAULT 100, sort_order, payload VARCHAR(8000)) – Payload: `{"file","line","category","evidence","fixOptions":[4],"fixIndex","explanation"}`
|
||||
- Indizes auf beide FKs.
|
||||
|
||||
**Backend:** Entities/Repos nach Dev-Lab-Muster. `BugHunterService` mit `CATEGORIES = Set.of("syntax","logik","laufzeit","sicherheit")`, Gewichten `0.4/0.2/0.4`:
|
||||
- `buildSession(slug)`: Briefing + `consoleOutput` + Dateien als `[{filename, lines:[…]}]` (**serverseitig in Zeilen gesplittet**, damit Zeilenindizes kanonisch sind) + Bugs sanitisiert: nur `{bugId, points, evidence}` – **keine Zeile, keine Kategorie, keine Fix-Optionen**.
|
||||
- `checkPhase(bugId, phase, reveal, submission)`: `locate` vergleicht file+line, liefert bei Erfolg/Aufdecken `location` zurück; `classify` vergleicht die Kategorie und liefert bei Auflösung erst die **gemischten `fixOptions`** (Phasen-Gate: der Fix-Text würde sonst die Position verraten); `fix` prüft die Wahl (Logik = Dev Labs `checkChoice`) und liefert `fixedLine` + `explanation` fürs Debriefing.
|
||||
- `finish(...)`: prüft alle Phasen aus den Roh-Antworten neu, Gewichte + Versuchsfaktoren, Streak/Combo, Sterne, `recordScore`.
|
||||
|
||||
**API (`/api/bughunter`):** `GET /hunts`, `GET /session/{slug}`, `POST /check` (`{bugId, phase, reveal, submission}`), `POST /finish` (`{slug, durationSeconds, items:[{bugId, locate:{file,line,attempts,revealed}, classify:{category,…}, fix:{choice,…}}]}` → `{score, baseScore, maxScore, comboBonus, maxCombo, bugsSolved, totalBugs, stars, highscore, newHighscore, plays}`).
|
||||
|
||||
**Frontend:** `templates/bughunter.html`, `static/js/bughunter.js` (State-Machine `LOBBY → BRIEFING → HUNT {pro Bug: LOCATE → CLASSIFY → FIX → DEBRIEF} → RESULT`), `static/css/bughunter.css` Präfix `bh-`. Layout: **links 60 %** Datei-Tabs + Monospace-Code-Viewer mit Zeilennummern-Gutter (Zeilenklick nur in LOCATE aktiv); **rechts 40 %** Beweis-Karte (Terminal-Style `consoleOutput`, Briefing, Bug-Hinweis), Aktions-Panel (wechselt pro Phase; Kategorie-/Fix-Buttons nutzen das `choice`-Renderer-Muster aus `devlabgame.js`), Combo-Chip + Fortschritt („Bug 2/3"). Debriefing tauscht die Bug-Zeile sichtbar gegen `fixedLine` (blaues Diff-Highlight).
|
||||
|
||||
**Content & Seeder:** `BugHunterSeeder` liest `resources/bughunter/content.json`; der komplette Bug-Knoten wird wie bei `DevLabSeeder` per `toString()` zum Payload. Der Katalog enthält **12 Fälle** in den Stufen Junior, Senior und Principal mit mehreren Dateien und mehreren Bugs pro Akte. Neue Seed-Akten werden idempotent auch in eine bereits bestehende Datenbank nachgeladen.
|
||||
|
||||
**Admin:** `AdminBugHunterController` (`/admin/bughunter`), 2 Templates (Fall-Liste + Fall-Detail mit Datei-Textareas und Bug-Payload-JSON). Service validiert beim Speichern, dass `file` im Fall existiert und `line` im Zeilenbereich liegt.
|
||||
|
||||
**Aufwand:** MVP ≈ 4,5 PT. Ausbau 1 (+1 PT): Freitext-Fix statt Optionen (`anyEquals` mit Whitespace-Normalisierung wie Dev-Lab-`cloze`), „Sauber gelöst"-Badge. Ausbau 2 (+1 PT): „Blitzjagd"-Tagesmodus (3 zufällige Junior-Bugs, 90-s-Soft-Timer).
|
||||
|
||||
---
|
||||
|
||||
### Spiel 3: ServiceClash (Key: `serviceclash`, Migration V15) ✅ umgesetzt
|
||||
|
||||
> ✅ **Stand 2026-07-13:** V15-Datenmodell, Entities, Repository, Seeder, API und ein
|
||||
> serverautoritatives Drei-Schritte-Scoring sind umgesetzt. Das Spiel enthält drei
|
||||
> Schichten mit je acht Tickets (24 insgesamt), eine einsteigerfreundliche Briefing-Ansicht,
|
||||
> Queue-/Wellen-Leitstand, Debriefs, Score-/Highscore-Speicherung und die Achievements
|
||||
> „Leitstand-Einsatz“ sowie „Incident Commander“. Die technische Echtzeit-SLA-Simulation,
|
||||
> zufällige Seed-Pläne und Admin-CRUD bleiben bewusste Ausbauschritte.
|
||||
|
||||
**Konzept: „Deine Schicht am Service Desk."** Der Spieler ist Junior-Admin im First-Level-Support. Tickets treffen auf einer Zeitleiste ein; jedes muss **eingeordnet** (Incident vs. Service Request), **priorisiert** (Hoch/Mittel/Niedrig anhand von Impact-×-Dringlichkeit-Hinweisen im Tickettext) und **gelöst** werden (richtige Erstmaßnahme aus 3–4 Optionen) – bevor sein SLA-Timer abläuft. Unbehandelte Tickets beschädigen die „Systemgesundheit"; bei 0 endet die Schicht vorzeitig.
|
||||
|
||||
**Lernziele (AP1 → AP2):** Incident-/Service-Request-Klassifikation (ITIL-light, explizites IHK-Thema), Prioritätsbewertung, korrekte Erstmaßnahmen (Troubleshooting-Reihenfolge, Eskalationsdisziplin, Phishing erkennen statt „lösen"). Höhere Schichten bringen FISI-AP2-Stoff: Change Management, Monitoring-/Eskalationsketten, Security-Incident-Behandlung, Backup-/Restore-Entscheidungen.
|
||||
|
||||
**Spielablauf:** Eine **Schicht** = ein Level mit 3 Wellen (Vormittag/Mittag/Nachmittag), je 60–90 Echtzeit-Sekunden mit steigender Spawn-Rate; Gesamtspielzeit 3–5 Minuten, zwischen Wellen 5 s Verschnaufpause. Pro Ticket im Detail-Panel:
|
||||
1. **Einordnung** [Incident]/[Service Request] – Single-Shot, falsch = 0 Punkte für diesen Anteil, richtige Antwort wird sofort gezeigt (Lerneffekt). 20 % der Ticket-Basispunkte.
|
||||
2. **Priorität** [Hoch]/[Mittel]/[Niedrig] – Single-Shot. 30 %.
|
||||
3. **Lösung** – 3–4 Aktionen (servergemischt), Plattform-Versuchsleiter 1.0/0.6/0.3; nach dem 3. Fehlgriff Auflösung + Erklärung, Ticket zählt als „eskaliert" (0 für diesen Anteil). 50 %.
|
||||
|
||||
**SLA & Gesundheit:** `slaSeconds` je Priorität. In SLA gelöst → ×1.0, verspätet → ×0.5, nie gelöst → 0 + Gesundheitsschaden. Start 100; SLA-Bruch −10 (Hoch: −20), ungelöst am Schichtende −15. Endbonus: `gesundheit × 2` (max. 200).
|
||||
|
||||
**Punkteformel (nur serverseitig):**
|
||||
```
|
||||
ticketPunkte = B × (0.2·einordnungOk + 0.3·prioOk + 0.5·lösungsLeiter) × slaFaktor
|
||||
score = Σ ticketPunkte + gesundheit×2 (B: Routine 60, Standard 100, Major Incident 150)
|
||||
maxScore = Σ B + 200 ; Sterne nach Plattform-Konvention
|
||||
```
|
||||
|
||||
**Der Server-autoritäts-Trick für Echtzeit (Kernidee):** Runden bleiben zustandslos –
|
||||
- `GET /session/{slug}`: Server zieht einen **Seed**, generiert daraus deterministisch den Schichtplan (Ticket-Auswahl aus dem Pool, Spawn-Zeitpunkte, Options-Mischung; Payloads sanitisiert ohne Lösungsfelder) und liefert Plan + Seed.
|
||||
- Jeder Klick geht an `POST /check` (sofortiges ✓/✗-Feedback; Lösungen verlassen den Server nie).
|
||||
- `POST /finish` sendet `{slug, seed, decisions[], durationSeconds}`; der Server **regeneriert die Schicht aus dem Seed und re-simuliert das Entscheidungslog** (Plausibilitätschecks: Zeitstempel ≥ Spawn-Offset, ≤ Schichtdauer, monoton pro Ticket) und berechnet Gesundheit, SLA-Faktoren und Score neu. Härtungs-Ausbau: HMAC-signierter Seed.
|
||||
|
||||
**Datenmodell (`V15__serviceclash.sql`, umgesetzt):**
|
||||
- `service_clash_shifts` (id, slug UNIQUE, title, description, difficulty, exam_scope,
|
||||
sort_order, active)
|
||||
- `service_clash_tickets` (id, shift_id FK CASCADE, title, wave, points, sort_order,
|
||||
payload VARCHAR(8000)) + FK-Index
|
||||
- Ticket-Payload enthält Antragsteller, Sachverhalt, Optionen und die ausschließlich
|
||||
serverseitig ausgewerteten Felder `category`, `priority`, `solution`, `explanation`.
|
||||
|
||||
**Backend (umgesetzt):** Entities `ServiceClashShift`/`ServiceClashTicket` und
|
||||
`ServiceClashShiftRepository`; `ServiceClashService` baut den Katalog und eine lösungsfreie
|
||||
Session, prüft `classify|priority|solve` ausschließlich gegen den serverseitig gehaltenen
|
||||
Payload und berechnet beim Abschluss Score, Sterne und Highscore mit
|
||||
`recordScore(user, "serviceclash", slug, score, gelöste, sekunden)`.
|
||||
|
||||
**API (`/api/serviceclash`, umgesetzt):** `GET /catalog`, `GET /session/{slug}` (lösungsfreie
|
||||
Ticket-Instanzen mit Welle und Auswahlmöglichkeiten), `POST /check` sowie `POST /finish` →
|
||||
`{score, maxScore, stars, resolved, total, health, highscore, newHighscore, plays}`.
|
||||
|
||||
**Frontend (umgesetzt):** `templates/serviceclash.html`, `static/js/serviceclash.js` und
|
||||
`static/css/serviceclash.css` (Präfix `sc-`). Die Zustände Katalog → Briefing → Leitstand →
|
||||
Debrief zeigen links Queue und Wellenfortschritt, in der Mitte den dreistufigen Ticket-Flow
|
||||
und am Ende eine klare Score-Auswertung. Eine Echtzeit-SLA-Uhr ist als späterer Ausbau notiert.
|
||||
|
||||
**Schwierigkeitskurve (umgesetzt):** „Frühschicht: Stabiler Start“ vermittelt Routinefälle,
|
||||
„Tagesschicht: Prioritäten unter Druck“ ergänzt Phishing und Monitoring, „Bereitschaft:
|
||||
Kritische Lage“ behandelt Ransomware, Changes, Backups, Zertifikate und DNS. Jede Schicht
|
||||
enthält acht Tickets in drei Wellen; ein Reset startet die Wertung neu.
|
||||
|
||||
**Content & Seeder (umgesetzt):** `ServiceClashSeeder` versorgt beim ersten Start drei
|
||||
Schichten mit jeweils acht kuratierten Tickets (**24 insgesamt**). Enthalten sind u. a.
|
||||
Drucker-Queue, Azubi-Onboarding, ERP-Totalausfall, Phishing, Change-Management,
|
||||
Backup-/Zertifikats- und DNS-Störungen.
|
||||
|
||||
**Admin:** Die Admin-Pflege bleibt ein Ausbau; der produktive Startkatalog wird aktuell über
|
||||
den idempotenten Seeder versorgt.
|
||||
|
||||
**Aufwand:** MVP ≈ 7,5 PT (Service inkl. Seed-Plan + Re-Simulation 2 · Frontend 3 · Content 1 · Admin 1 · Rest 0,5+0,5). Ausbau (+3 PT): Major-Incident-Folgeketten, Combo-Serien, HMAC-Seed, Endlosmodus „Überstunden".
|
||||
|
||||
---
|
||||
|
||||
### Spiel 4: NetWorker (Key: `networker`, Migration V14) ✅ umgesetzt
|
||||
|
||||
> ✅ **Stand 2026-07-13:** V14-Datenmodell, Entities, Repositories, Seeder, API,
|
||||
> serverautoritativer `NetSimulator`, Ergebnis-/Score-Integration und 15 Aufgaben in drei Welten
|
||||
> sind umgesetzt. Das UI nutzt eine SVG-Topologie mit blauem Blaupausenraster. Erweiterungen wie
|
||||
> VLAN, DHCP, Sandbox und visueller Editor bleiben bewusst als Ausbau markiert.
|
||||
|
||||
**Konzept:** Netzwerk-Puzzle in der Tradition von Schaltkreis-Spielen. Jedes Level zeigt eine **SVG-Topologie** (PCs, Switch, Router, Server, Cloud). Geräte haben fehlende oder kaputte Konfiguration (IP, Maske, Gateway, Kabel, statische Routen). Ziel: alle **Ping-Ziele** erfüllen („PC1 muss SRV1 erreichen", später auch negativ: „Gast-PC darf den Server NICHT erreichen"). „Pings testen" ruft einen **serverseitigen Netzwerk-Simulator**, der jeden Ping Hop-für-Hop auflöst und Pass/Fail plus didaktische Diagnose liefert („Das Paket verlässt PC1 nicht: Laut Maske liegt das Ziel im eigenen Netz – stimmt die Maske?").
|
||||
|
||||
**Lernziele (AP1-Kernstoff → AP2, passt zu vorhandenem FinalFlux-Content IP-Berechnung/VLAN):** IPv4-Adressierung, Subnetzmasken/CIDR, Netz-/Broadcast-Adressen, Gateway-Logik, doppelte IPs, statische Routen; auf AP2-Niveau (FISI): VLAN-Segmentierung mit Negativ-Zielen, Routing über mehrere Hops, Firewall-Regeln, im Ausbau IPv6.
|
||||
|
||||
**Spielablauf:** Session = eine **Welt** aus 4–6 Leveln in Folge (Dev-Lab-Session-Form). Fünf Level-Typen bei einheitlichem Datenmodell: `assign` (leere IP/Maske/Gateway-Felder ausfüllen), `cable` (fehlende Verbindungen ziehen), `fix` (1–3 eingebaute Fehler finden: falsche Maske, Gateway außerhalb des Subnetzes, doppelte IP, Host auf Netzadresse), `route` (statische Routen-Zeilen: Zielnetz-CIDR + Next Hop), `vlan` (Ausbau, mit Negativ-Zielen).
|
||||
|
||||
**Versuche & Punkte:** Jeder „Pings testen"-Klick mit mindestens einem roten Ziel verbraucht einen Versuch (1.0/0.6/0.3, `MAX_ATTEMPTS=3`, danach „Lösung anzeigen" = 0 Punkte). **Bewertet wird ausschließlich per Simulation, nie per Exact-Match** – viele Konfigurationen sind gültig (jede freie Host-IP funktioniert); die Musterlösung dient nur dem Aufdecken. Levelpunkte 100–200; Sterne am Weltende nach Konvention; `/finish` re-simuliert die final eingereichten Konfigurationen serverseitig. `recordScore(user, "networker", weltSlug, score, gelöste, sekunden)`.
|
||||
|
||||
**Datenmodell (`V14__networker.sql`):**
|
||||
- `network_worlds` (id, slug UNIQUE, title, description, difficulty INT, sort_order, active)
|
||||
- `network_levels` (id, world_id FK CASCADE, title, level_type VARCHAR(30) /* assign|cable|fix|route|vlan */, points INT DEFAULT 100, sort_order, payload VARCHAR(8000) /* JSON-Topologie inkl. Musterlösung */) + FK-Index
|
||||
|
||||
**Der Simulator – `service/NetSimulator.java` (Herzstück, pures Java ohne Spring, voll unit-testbar):**
|
||||
- Records: `Net(nodes, links, routes, vlans)`, `NodeCfg`, `IfaceCfg(ipLong, prefix, gatewayLong)`, `Route(destNet, prefix, nextHop)`.
|
||||
- `ping(net, from, to)`: (1) Validierung: unparsebare IP/Maske, Host-Teil 0/alles-1 → `network_address`/`broadcast_address`, doppelte IP im L2-Segment → `duplicate_ip`. (2) Gleiches Subnetz aus Quellsicht (`(ip & maske) == (ziel & maske)`) → L2-BFS über Links durch Switches (nie durch Router; VLAN-gefiltert). (3) Anderes Subnetz → Gateway nötig: gesetzt? im Quell-Subnetz? L2-erreichbar? Dann leitet der Router weiter: Ziel in direkt verbundenem Subnetz → zustellen; sonst statische Routen (Longest-Prefix-Match) → Next Hop → Rekursion mit TTL 8 (`routing_loop`-Schutz). Kein Treffer → `no_route`.
|
||||
- Fehlergründe werden im Service auf didaktische deutsche Meldungen gemappt.
|
||||
|
||||
**`NetworkerService`:** `buildCatalog()`, `buildSession(slug)` (Level per `sanitizeLevel()`: Werte editierbarer Felder, zu ziehende Links, Routen-Lösungszeilen und `solution`-Block entfernt; gesperrte Werte, Positionen, Ziele, Hints gehen mit), `check(levelId, reveal, config)` (`mergeSubmission()` legt eingereichte Werte **nur über editierbare Felder** – Manipulation gesperrter Felder wird ignoriert; alle Ziele durch `NetSimulator.ping`; Antwort mit Trace/Diagnose pro Ziel), `finish(...)`.
|
||||
|
||||
**API (`/api/networker`):** `GET /catalog`, `GET /session/{slug}`, `POST /check` (`{levelId, reveal, config:{ifaces[], links[], routes[], vlans[]}}` → `{allPassed, validation[], goals:[{id, passed, trace[] | failAt, reason, message}]}`; Masken-Eingabe akzeptiert `24`, `/24` oder `255.255.255.0` – Server normalisiert), `POST /finish` → Dev-Lab-Result-Form.
|
||||
|
||||
**Frontend:** `templates/networker.html`, `static/js/networker.js` (State-Machine `WORLD_SELECT → LEVEL_INTRO → EDITING ⇄ CHECKING → LEVEL_DONE → SUMMARY`), `static/css/networker.css` Präfix `nw-`. **SVG-Ansatz:** ein `<svg viewBox="0 0 1000 620">`, Geräte-Glyphen als `<symbol>`-Defs im Strichstil der Dashboard-Icons, Positionen aus dem Payload; Links als `<path>`, Ports als Kreise an der Gerätekante. Die aktuelle Umsetzung zeigt die Topologie mit sichtbarem Raster, Ping-Prüfung und Diagnose-Panel; Paket-Animation, Kabelmodus und Hinweis-Toggle bleiben Ausbau. Layout: Canvas links/Mitte, rechts Ziel-Liste (mit Diagnosetexten unter roten Zielen) + Konfigurations-Panel des gewählten Geräts, Footer mit „Pings testen" / „Lösung anzeigen".
|
||||
|
||||
**Schwierigkeitskurve (Welten):** 1. „Ein Netz für alle" (Same-Subnet-Basics), 2. „Über den Router" (Gateways, zwei Subnetze, erstes Negativ-Ziel), 3. „Routen & Fehlersuche" (zwei Router, statische Routen, Duplikat-IP-Jagd), 4. (Ausbau) „VLAN-Werkstatt".
|
||||
|
||||
**Content & Seeder:** `NetworkerSeeder` liest `resources/networker/content.json`. Startumfang: **3 Welten × 5 Level = 15 Level.** Drei Beispiel-Level voll ausgearbeitet: „Erste Verbindung" (assign: PC1 passend zu 192.168.10.20/24 konfigurieren), „Die falsche Maske" (fix: /25-vs-/24-Konflikt, .200 liegt außerhalb des ersten /25-Teilnetzes), „Tor zum Serverraum" (assign: IP+Maske+Gateway über Router zwischen 192.168.1.0/24 und 192.168.2.0/24).
|
||||
|
||||
**Admin:** Admin-Pflege ist als Ausbau vorgesehen; der produktive Spielkatalog wird aktuell über den Seeder versorgt.
|
||||
|
||||
**Aufwand:** MVP ≈ 9,5 PT (**NetSimulator + Unit-Tests zuerst: 2,5** · Migration/Entities/Service/API 1,5 · SVG-Frontend 3,5 · Content 1 · Admin 1). Ausbau (+4,5 PT): VLAN-Welt, DHCP-Modus, Sandbox, visueller Level-Editor.
|
||||
|
||||
---
|
||||
|
||||
### Empfohlene Baureihenfolge & Gesamtaufwand
|
||||
|
||||
0. **Vorarbeiten ✅:** Namensschema, Dashboard-Namen und Keys, `devlab` in `ProgressService`, K2,
|
||||
W8, V11–V14-Migrationsreservierung, `exam_scope`-Konvention, S3-Sicherheitsvertrag und
|
||||
Spiel-Akzent-Tokens in `theme.css` sind umgesetzt.
|
||||
1. **FluxTimer ✅** – risikoarmer, rein lokaler Ausbau; validiert den globalen UI-Bootstrap und belegt die Glocke vor NeuralNet. ≈ 1,5–2 PT
|
||||
2. **SystemEscape (V11) ✅** – serverautoritatives Spiel mit 3 Szenarien, 24 Räumen, 69 Rätseln
|
||||
und vier zufälligen Räumen pro Run; Admin-CRUD, `exam_scope`, festgeschriebene Run-Auswahl,
|
||||
dynamische Wertung und Anti-Cheat-Negativtests umgesetzt.
|
||||
3. **BugHunter (V12) ✅** – Fallakten, serverautoritatives Drei-Phasen-Debugging, Combo-Wertung, Admin-CRUD und zwölf Fälle mit Reset-Varianten umgesetzt.
|
||||
4. **NetWorker (V14) ✅** – testgetriebener Simulator, SVG-Frontend, drei Welten mit 15 Aufgaben, Score-/Achievement-Integration und Blaupausenraster umgesetzt.
|
||||
5. **ServiceClash (V15) ✅** – drei Schichten, 24 Tickets, serverautoritatives Triage-Scoring,
|
||||
Score-/Achievement-Integration und cyanfarbener Leitstand sind umgesetzt. Echtzeit-SLA,
|
||||
Seed-Mischung und Admin-Pflege bleiben als Ausbau markiert.
|
||||
|
||||
**Gesamt-MVP aller vier Spiele: ≈ 28 Personentage.** Der Navigations-Umbau (Abschnitt 2.3) und die restlichen Teil-2-Punkte können parallel oder dazwischen erfolgen; empfohlen ist der Sidebar-/Tab-Umbau **vor** dem vierten Spiel, damit alle neuen Seiten die Fragmente nutzen.
|
||||
|
||||
---
|
||||
|
||||
## Teil 2: Verbesserungen & Optimierungen am Bestand
|
||||
|
||||
> ✅ **Umgesetzt am 2026-07-12/13** (K1–K3, W1–W8, N1–N6, S1–S8, S10 – Details im CHANGELOG). Offen bleiben nur S9/i18n (bewusst zurückgestellt) sowie die schrittweise Fragment-Migration der restlichen Spiel-/Admin-Templates. **S3 ist für SystemEscape, BugHunter, ServiceClash und NetWorker umgesetzt:** lösungsfreie Sessions, serverseitige Checks, Reveal-/Versuchsprotokollierung und serverseitige Abschlusswertung sind integriert. **K1 vollständig abgeschlossen:** Gitea-Secret angelegt, Produktionspasswort rotiert und Runner-Abgleich erfolgreich deployt (Workflow 43, Commit `4f5042d`). Das Verfahren und der manuelle Notfallweg stehen in [`pw_anleitung.md`](pw_anleitung.md).
|
||||
|
||||
> ✅ **Ergänzung am 2026-07-13:** Dashboard-Wochenwerte, Verlauf und Top-Spiele sind aus
|
||||
> `game_scores` abgeleitet; die Statistik zeigt ausschließlich gemessene Spielzeit.
|
||||
> Spielbezogene Achievements für die vorhandenen Spiele sowie Spezialerfolge für SystemEscape,
|
||||
> BugHunter und NetWorker sind verdrahtet. Der Knowledge Core ist als datenbasierte Lernzentrale
|
||||
> mit AP1-, Netzwerk- und Debugging-/Security-Pfaden umgesetzt. Das Accessibility-Widget nutzt
|
||||
> in SystemEscape, BugHunter und NetWorker jeweils den passenden Spielakzent.
|
||||
|
||||
### 2.1 Kritisch (vor/mit dem nächsten Release beheben)
|
||||
|
||||
| # | Problem | Ort | Maßnahme |
|
||||
|---|---------|-----|----------|
|
||||
| K1 ✅ | **DB-Passwort im Repo** (`SPRING_DATASOURCE_PASSWORD: mindflux_pass`) | `docker-compose.yml:11` | In Gitea-Secret auslagern (wie die Admin-Passwörter bereits); Passwort rotieren. |
|
||||
| K2 ✅ | **Unsicherer Legacy-Endpunkt `/users/save`**: speichert User ohne BCrypt-Hashing, ohne Validierung, mit frei wählbarer Rolle (Mass Assignment) – für jeden eingeloggten Nutzer erreichbar | `UserViewController.java:25–29` | `UserViewController` + `user-form.html` + `user-success.html` ersatzlos löschen (toter Legacy-Code). |
|
||||
| K3 ✅ | **Keine Tests** – `src/test` existiert nicht, obwohl `spring-boot-starter-test` deklariert ist | `pom.xml:88–93` | Mindestens: Unit-Tests für Score-Berechnungen (`GameScoreService`, `SyntaxSagaService`, `DevLabService.check`-Dispatcher) und einen `@WebMvcTest` für die Security-Regeln (`/admin/**` nur ADMIN). Die Netzwerk-Simulation des neuen Netzwerker-Spiels unbedingt testgetrieben bauen. |
|
||||
|
||||
### 2.2 Wichtig (zeitnah)
|
||||
|
||||
| # | Problem | Ort | Maßnahme |
|
||||
|---|---------|-----|----------|
|
||||
| W1 ✅ | **Dev Lab fehlt auf der Statistik-Seite** – Scores werden geschrieben, aber nie angezeigt | `ProgressService.GAMES` (Zeile 18–27) | `GAMES.put("devlab", "Dev Lab");` ergänzen (Reihenfolge wie Dashboard). |
|
||||
| W2 ✅ | **XP/Level/Streak sind Dummy-Daten** (`getOverallProgress` hardcoded 432/2400/Level 5) | `ProgressService.java:35–44` | Echte Aggregation aus `game_scores`: XP = Summe aller Scores (oder gedeckelt pro Spiel), Level per Schwellen-Tabelle, richtigen Antworten aus `moves` ableiten. Gleiches Fundament später für Achievements nutzen. |
|
||||
| W3 ✅ | **Achievements sind Dummy-Daten**, nicht mit `game_scores` verdrahtet | `AchievementServiceImpl` | Nach `recordScore()` einen Achievement-Check auslösen (z. B. „Erster Sieg", „3 Sterne in Syntax Saga", „5 Spiele an einem Tag"). Ereignisgesteuert im `GameScoreService` (ein Hook-Punkt statt in jedem Spiel). |
|
||||
| W4 ✅ | **Kein globales Error-Handling** – Admin-Controller werfen `IllegalArgumentException` → Whitelabel-500; APIs liefern leere DTOs statt 404 | alle Controller | Ein `@ControllerAdvice` (HTML: eigene `error.html`) + `@RestControllerAdvice` (JSON: `{error, message}` mit korrekten Statuscodes). `ResponseStatusException` in Services nutzen. |
|
||||
| W5 ✅ | **Keine Bean Validation** – kein `spring-boot-starter-validation`, keine `@Valid`/`@NotBlank`/`@Size`; keine Passwort-Mindestlänge, kein E-Mail-Formatcheck | `pom.xml`, `RegisterController.java:34–42`, `User.java` | Starter ergänzen; DTO-Validierung für Registrierung (Passwort ≥ 8 Zeichen, E-Mail-Format) und die neuen Spiel-APIs. |
|
||||
| W6 ✅ | **Registrierungs-Race + tote Templates**: App prüft Username case-insensitiv, DB-UNIQUE ist case-sensitiv → Race mit unbehandeltem 500; `register.html` (ohne `confirmPassword`-Feld) und `login.html` sind tot/kaputt | `RegisterController.java:40`, `V1__initial_schema.sql` | Funktionalen Index `UNIQUE (lower(username))` per Migration; `DataIntegrityViolationException` abfangen → Fehlermeldung; tote Templates `register.html`/`login.html` löschen. |
|
||||
| W7 ✅ | **`System.out.println` / `printStackTrace` statt Logging** (~20 Stellen, u. a. `ProfileController.java:144`) | Seeder, `AdminSetup`, `ProfileController`, `AppStartCheck` | Auf SLF4J (`LoggerFactory.getLogger`) umstellen; `e.printStackTrace()` durch `log.error(..., e)` ersetzen. |
|
||||
| W8 ✅ | **N+1-Queries**: `TechtacularService.buildBoard` lädt Fragen+Antworten lazy pro Kategorie; `categoryRows` macht ein `count` pro Kategorie | `TechtacularService.java:46–73, 133–137` | `@EntityGraph`/`JOIN FETCH` für Board-Aufbau; Zeilen-Counts als eine `GROUP BY`-Query. Bei Seed-Datenmengen unkritisch, aber das Muster wird sonst in die 4 neuen Spiele kopiert – **daher vor deren Bau beheben bzw. in den neuen Services von Anfang an richtig machen.** |
|
||||
|
||||
### 2.3 Navigation & Profil-Bereich (vom Nutzer gemeldet – bestätigt)
|
||||
|
||||
Der Verdacht „Profilbutton fehlt / Einstellungen nicht im Profil / irgendwas ist schief" trifft zu. Befund:
|
||||
|
||||
| # | Befund | Ort |
|
||||
|---|--------|-----|
|
||||
| N1 ✅ | **Kein „Profil"-Eintrag in der Dashboard-Sidebar-Navigation.** Das Profil ist nur über den kleinen User-Block unten in der Sidebar erreichbar (`<a class="mf-profile" href="/profile">`), der nicht als Button erkennbar ist – und dessen Chevron-Pfeil (▼) ein Dropdown suggeriert, das nicht existiert. | `dashboard.html:72–82` |
|
||||
| N2 ✅ | **Zwei konkurrierende Navigationssysteme:** Die Sidebar führt zu Knowledge Core / Statistiken / Achievements / Einstellungen; die Profilseite hat eine **eigene Tab-Leiste** (Profil / Fortschritt / Erfolge / Soziales / Einstellungen). Benennungen inkonsistent („Statistiken" vs. „Fortschritt", „Achievements" vs. „Erfolge"), Mengen inkonsistent (Knowledge Core fehlt in den Tabs, Soziales fehlt in der Sidebar). | `dashboard.html:45–70` vs. `profile.html:35–41` |
|
||||
| N3 ✅ | **Die Tab-Leiste existiert nur auf `profile.html`.** Klickt man einen „Tab" (z. B. Einstellungen), landet man auf einer Seite **ohne** Tabs – die Tab-Metapher bricht sofort. `settings.html` hat nur „⟵ Zurück zum Profil", auch wenn man von der Dashboard-Sidebar kam (asymmetrische Navigation). | Grep `mf-tab-nav`: nur `profile.html` (+ eine Admin-Seite) |
|
||||
| N4 ✅ | **`/profile/settings` gehört nicht zum Profil-Code:** Die URL suggeriert Profil-Zugehörigkeit, wird aber von einem separaten `SettingsController` bedient; die Passwort-Änderung dagegen liegt im `ProfileController` (`/profile/change-password`) und redirectet nach `settings`. Zuständigkeiten sind verschränkt. | `SettingsController.java`, `ProfileController.java:154–177` |
|
||||
| N5 ✅ | **`settings.html` ist zur Hälfte Attrappe:** Theme-Select (Hinweistext verweist auf den echten Umschalter), Sprach-Select und Benachrichtigungs-Checkboxen haben **keine Funktion/kein Formular**. Real sind nur Passwort ändern und Profil löschen. Zudem hartkodierte Farbe `#c0392b` statt `var(--danger)` (Token-Regel-Verstoß). | `settings.html:40–70, 37` |
|
||||
| N6 ✅ | **Sidebar-Widgets sind Dummy-Daten:** LEVEL 12 / 3250/5000 XP / „STREAK 7 Tage" sind hart ins Template geschrieben – deckt sich mit W2 (XP/Level-Dummies im `ProgressService`). | `dashboard.html:84–91` |
|
||||
|
||||
**Lösungsansatz (ein zusammenhängender Umbau, verzahnt mit S1/Layout-Fragmenten):**
|
||||
1. **Ein Navigationsmodell:** Die Sidebar wird als Thymeleaf-Fragment extrahiert und um einen expliziten Eintrag **„Profil" → `/profile`** ergänzt; der User-Block bleibt als Shortcut, verliert aber den irreführenden Chevron (oder bekommt ein echtes Dropdown mit Profil/Einstellungen/Logout).
|
||||
2. **Profil-Bereich konsolidieren:** Die Tab-Leiste wird ebenfalls Fragment und erscheint **auf allen** Profil-Unterseiten (Profil, Statistiken, Erfolge, Soziales, Einstellungen) mit einheitlichen Namen (Empfehlung: „Statistiken" und „Erfolge" überall). Knowledge Core entweder in die Tabs aufnehmen oder bewusst nur in der Sidebar führen – einmal entscheiden.
|
||||
3. **Controller aufräumen:** `SettingsController` in den `ProfileController` mergen (oder Settings vollständig auslagern inkl. Passwort-Endpunkt) – eine Zuständigkeit pro Bereich.
|
||||
4. **Attrappen entfernen oder verdrahten:** Theme-Select an `theme.js` anbinden (Auswahl persistiert in `localStorage`, ersetzt den Hinweistext); Sprach-Select und Benachrichtigungen **ausbauen**, bis es die Funktionen gibt. `#c0392b` → `var(--danger)`.
|
||||
5. N6 wird durch W2 (echte XP/Level aus `game_scores`) gelöst; bis dahin Widgets ausblenden statt Fantasiewerte zeigen.
|
||||
|
||||
### 2.4 Sinnvoll (mittelfristig)
|
||||
|
||||
| # | Thema | Maßnahme |
|
||||
|---|-------|----------|
|
||||
| S1 ✅ teilweise | **Template-Duplikation** – 33 Templates ohne ein einziges `th:fragment`; jede Seite dupliziert `<head>`, Sidebar, Footer | `templates/fragments/layout.html` mit `head`, `sidebar`, `footer`-Fragmenten einführen; schrittweise umstellen (zuerst bei den 4 neuen Spielen verwenden, dann Bestand migrieren). |
|
||||
| S2 ✅ | **4 parallele User-Anlage-Pfade** mit inkonsistenten Regeln (`RegisterController`, REST-`UserController`, `AdminUserController`, `UserViewController`) | Nach Löschung von K2: Restlogik in einen `UserService` konsolidieren (ein Ort für Encoding, Rollen, Uniqueness). |
|
||||
| S3 ✅ | **Score-Manipulation durch eigene Nutzer möglich** (Roh-Werte fälschbar, Reveal-vor-Finish) | Der verbindliche Sicherheitsvertrag ist für SystemEscape, BugHunter, ServiceClash und NetWorker umgesetzt: lösungsfreie Sessions, serverseitige Checks sowie serverseitige Score-/Reveal-Auswertung. Weiterführende Negativtests werden bei künftigen Spieltypen ergänzt. |
|
||||
| S4 ✅ | **Infrastruktur**: kein Actuator/Healthcheck, Dockerfile läuft als root, kein Dependency-Layer-Caching, CI ohne Tests, Java-Versionskonflikt (pom: 17, Dockerfile: 21) | `spring-boot-starter-actuator` (+ `HEALTHCHECK` in compose), `USER`-Directive, Dockerfile zweistufig mit `dependency:go-offline`-Layer, Test-Stage in `.gitea/workflows/mindflux.yml`, Java-Version angleichen (empfohlen: beide auf 21). |
|
||||
| S5 ✅ | **Datenmodell**: `game_scores`/`achievements` referenzieren `username` ohne FK; `email` ohne UNIQUE | FK auf `users(username)` + `UNIQUE(email)` per Migration ergänzen (vorher Dubletten bereinigen). |
|
||||
| S6 ✅ | **Logout per GET** (`SecurityConfig.java:36`) – kleiner Logout-CSRF-Vektor | Auf POST-Logout mit CSRF-Token umstellen (kleines Formular im Sidebar-Footer). |
|
||||
| S7 ✅ | **Kein Brute-Force-Schutz** auf `/login` | Einfacher In-Memory-Rate-Limiter (z. B. Bucket pro IP/Username) oder Verzögerung nach Fehlversuchen. |
|
||||
| S8 ✅ | **Toter Code**: `AppStartCheck`, `ProfileController.getFileExtension`, Legacy-CSS (`auth.css`, …) | Aufräumen (niedrige Priorität, aber billig). |
|
||||
| S9 ☐ | **i18n**: alles hart deutsch | Nur angehen, falls Mehrsprachigkeit je geplant ist – sonst bewusst so lassen (Zielgruppe ist deutsch). |
|
||||
| S10 ✅ | **FinalFlux auf die gesamte Ausbildung erweitern (AP2)** – aktuell rein AP1-fokussiert | Eigener AP2-Ast mit Sidebar-Blöcken „AP2 Anwendungsentwicklung" und „AP2 Systemintegration"; gleiche Struktur wie bisher (statische Themenseiten unter `static/finalflux/` + FluxCheck-Frage-JSONs). Startthemen FIAE: Entwurfsmuster, Testverfahren, SQL-Vertiefung, Softwarequalität; FISI: Netzwerksicherheit, Monitoring, Hochverfügbarkeit, Automatisierung. |
|
||||
|
||||
---
|
||||
|
||||
## Teil 3: NeuralNet – private Lernverbindungen & Nachrichten ⏳ in Umsetzung
|
||||
|
||||
> ✅ **MVP-Stand 2026-07-14:** Opt-in, Kontaktanfragen (annehmen, ablehnen,
|
||||
> zurückziehen) und private Nachrichten nach bestätigter Verbindung sind umgesetzt.
|
||||
> Leere Erfolgsantworten des API-Backends werden im Client robust verarbeitet.
|
||||
> Der Gesprächsbereich öffnet unmittelbar beim Auswählen einer bestätigten Verbindung.
|
||||
> Die dynamisch gerenderten Kontakt- und Chat-Aktionen werden direkt gebunden.
|
||||
> Der Chatbereich hält Kopf, Verlauf und Eingabe als zusammenhängende Spalte.
|
||||
> Die Dashboard-Sprechblase zeigt im 30-Sekunden-Takt echte ungelesene Nachrichten.
|
||||
> Die zuletzt geöffnete bestätigte Verbindung wird pro Konto lokal wiederhergestellt.
|
||||
> Der Plattform-Lightmode und der Hochkontrastmodus nutzen die Neural-Palette.
|
||||
> Blockieren, Melden/Moderation, Duelle und Lernlobby bleiben die nächsten Ausbauschritte.
|
||||
|
||||
> Ersetzt den `/soziales`-Platzhalter („Straßen? Wo wir hingehen, brauchen wir keine Straßen!"). Ziel: User können sich befreunden, intern miteinander schreiben, gemeinsam lernen und Achievements vergleichen – **vollständig selbst gehostet, ohne einen einzigen externen Dienst**, und DSGVO-konform von Grund auf.
|
||||
|
||||
### 3.1 Funktionsumfang
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|----------|--------------|
|
||||
| **Kontaktanfragen** | Anfrage per Username-Suche senden → Gegenseite nimmt ausdrücklich an oder lehnt ab. Nur bestätigte Verbindungen sehen einander im NeuralNet. Gesendete Anfragen lassen sich zurückziehen. |
|
||||
| **Privatnachrichten** | 1:1-Direktnachrichten ausschließlich zwischen bestätigten Verbindungen. Ungelesen-Badge am Glocken-Icon der Topbar. Kein Gruppenchat in Phase 1. |
|
||||
| **Blockieren** | Blockiert = keine Anfragen, keine Nachrichten, keine Profileinsicht – in beide Richtungen serverseitig geprüft. Bestehende Freundschaft wird getrennt. Der Blockierte erhält **keine** Benachrichtigung (still). Entblocken jederzeit über die Einstellungen. |
|
||||
| **Achievements vergleichen** | Freundesprofil zeigt den Achievement-Katalog nebeneinander (meine/seine freigeschalteten) und die Spiel-Highscores im Vergleich – nur, was die Privacy-Einstellungen des anderen erlauben. |
|
||||
| **Gemeinsam lernen** | Phase 2: **Lern-Duell** – einen Freund zu Spiel + Deck herausfordern, beide spielen binnen Frist, der Server vergleicht die echten `game_scores`-Ergebnisse. Phase 3: gemeinsame Lernräume (identisches Board für beide über die Seed-Mechanik aus dem ServiceClash-Design). |
|
||||
|
||||
### 3.2 Datenmodell (nächste freie Flyway-Migrationen)
|
||||
|
||||
Alle Tabellen mit FK auf `users(username)` und `ON DELETE CASCADE ON UPDATE CASCADE` (Muster aus V10) – damit greift das Löschkonzept automatisch.
|
||||
|
||||
```sql
|
||||
CREATE TABLE privacy_settings (
|
||||
username VARCHAR(100) PRIMARY KEY REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
community_enabled BOOLEAN NOT NULL DEFAULT FALSE, -- DSGVO: Opt-in!
|
||||
profile_visibility VARCHAR(20) NOT NULL DEFAULT 'FRIENDS', -- FRIENDS | ALL
|
||||
show_achievements BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
show_stats BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
allow_requests BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE TABLE friendships (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
requester VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
addressee VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING | ACCEPTED
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
responded_at TIMESTAMP,
|
||||
CONSTRAINT uq_friend_pair UNIQUE (requester, addressee),
|
||||
CONSTRAINT chk_no_self CHECK (requester <> addressee)
|
||||
);
|
||||
|
||||
CREATE TABLE user_blocks (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
blocker VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
blocked VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
CONSTRAINT uq_block_pair UNIQUE (blocker, blocked)
|
||||
);
|
||||
|
||||
CREATE TABLE messages (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
sender VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
recipient VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
body VARCHAR(2000) NOT NULL,
|
||||
sent_at TIMESTAMP NOT NULL,
|
||||
read_at TIMESTAMP,
|
||||
reported BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE INDEX idx_messages_unread ON messages (recipient, read_at);
|
||||
CREATE INDEX idx_messages_pair ON messages (sender, recipient, sent_at);
|
||||
|
||||
CREATE TABLE challenges (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
challenger VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
challenged VARCHAR(100) NOT NULL REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
game VARCHAR(50) NOT NULL,
|
||||
deck VARCHAR(100),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'OPEN', -- OPEN | DONE | EXPIRED | DECLINED
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Eine Freundschaft zwischen A und B liegt genau einmal vor (Anfrage-Richtung = requester); Abfragen prüfen beide Richtungen (`(requester=?1 AND addressee=?2) OR (requester=?2 AND addressee=?1)`).
|
||||
|
||||
### 3.3 Backend (bewährtes Schichtmuster)
|
||||
|
||||
- **Services:** `FriendshipService` (anfragen/annehmen/ablehnen/entfernen), `MessageService` (senden, Verlauf paginiert laden, als gelesen markieren, eigene Nachricht löschen, melden), `BlockService` (blocken/entblocken, `isBlockedEitherWay(a, b)`), `PrivacyService` (Opt-in, Sichtbarkeiten).
|
||||
- **Zentrale Guard-Methode**, die JEDE soziale Aktion durchläuft: Community aktiviert (beide Seiten)? Block in irgendeiner Richtung? Freundschaft bestätigt (für Nachrichten/Profile)? → sonst 403/404, ohne dem Angreifer zu verraten, ob der Zielnutzer existiert oder blockt.
|
||||
- **Controller:** `SozialesController` (View `/soziales`, Freundesprofil `/soziales/profil/{username}`) + `@RestController /api/social/**` (Endpunkte: `GET /friends`, `POST /friends/request|accept|decline|remove`, `GET /messages/{friend}?after=`, `POST /messages`, `POST /messages/{id}/report`, `DELETE /messages/{id}`, `GET /unread-count`, `POST /block`, `DELETE /block/{username}`, `GET/PUT /privacy`). Route in die CSRF-Ignore-Liste der `SecurityConfig` aufnehmen (Muster der Spiel-APIs).
|
||||
- **Moderation:** gemeldete Nachrichten landen in einer Admin-Ansicht (`/admin/fluxnet`) – der Admin sieht **ausschließlich gemeldete** Nachrichten (Verhältnismäßigkeit), kann löschen und Nutzer verwarnen/sperren.
|
||||
|
||||
### 3.4 Messenger-Transport: voll intern, keine externen Dienste
|
||||
|
||||
- **Phase 1 – Polling:** offener Chat pollt alle 5 s `GET /api/social/messages/{friend}?after=<letzteId>`, das Ungelesen-Badge alle 30 s `GET /api/social/unread-count`. Passt exakt zur zustandslosen Vanilla-JS-Architektur, braucht null neue Infrastruktur und skaliert für die Zielgruppe locker.
|
||||
- **Phase 2 – Server-Sent Events:** `GET /api/social/stream` mit Spring `SseEmitter` (im `MessageService` wird bei `send()` an offene Emitter des Empfängers publiziert). Weiterhin 100 % selbst gehostet. nginx: `proxy_buffering off;` + langes `proxy_read_timeout` für den Stream-Pfad.
|
||||
- **Bewusst NICHT:** WebSocket/STOMP (Overkill für 1:1-Chat) und Web-Push/FCM/Service-Worker-Push – Letzteres liefe zwingend über die Push-Infrastruktur der Browserhersteller (Google/Mozilla/Apple) und wäre damit ein externer Dienst.
|
||||
|
||||
### 3.5 DSGVO-Konzept
|
||||
|
||||
| Prinzip | Umsetzung |
|
||||
|---------|-----------|
|
||||
| **Opt-in (Art. 6, 25)** | `community_enabled = FALSE` als Default. Beim ersten Besuch von `/soziales`: Aktivierungsdialog mit klarem Hinweistext (welche Daten sichtbar werden, an wen). Ohne Aktivierung ist der Nutzer für andere unsichtbar und unauffindbar. Deaktivierung jederzeit in den Einstellungen (versteckt Profil, pausiert Eingang von Anfragen/Nachrichten). |
|
||||
| **Datenminimierung (Art. 5)** | Nur Username-Referenzen und Nachrichtentexte – keine neuen Pflicht-Personendaten, keine Telefonnummern, keine Kontakte-Uploads. |
|
||||
| **Privacy by Default (Art. 25)** | Sichtbarkeit ab Werk „nur Freunde"; Nachrichten nur zwischen bestätigten Freunden (verhindert nebenbei Spam). |
|
||||
| **Löschung (Art. 17)** | Konto-Löschung kaskadiert per FK automatisch über Freundschaften, Blocks, Nachrichten und Challenges – nichts bleibt zurück. Einzelne eigene Nachrichten kann der Nutzer selbst löschen. |
|
||||
| **Auskunft/Portabilität (Art. 15/20)** | Ausbau Phase 3: „Meine Daten exportieren"-Button (JSON-Dump der eigenen Profil-, Freundes-, Nachrichten- und Score-Daten). |
|
||||
| **Vertraulichkeit (Art. 32)** | Transport via TLS (nginx). Speicherung serverseitig im Klartext in der eigenen Postgres – bewusste, dokumentierte Entscheidung (Moderationsfähigkeit); keine Weitergabe an Dritte, kein externer Dienst, keine Analytik. |
|
||||
| **Transparenz (Art. 13)** | `datenschutz.html` um den Abschnitt NeuralNet erweitern (Zwecke, Speicherdauer, Empfängerkreis „nur befreundete Nutzer", Moderationsregel); `nutzungsbedingungen.html` um Verhaltensregeln + Melde-/Sperrprozess. |
|
||||
| **Betroffenenschutz** | Blockieren ist still (kein „X hat dich blockiert"); Melden einer Nachricht legt nur DIESE Nachricht dem Admin vor. |
|
||||
|
||||
### 3.6 Frontend
|
||||
|
||||
- `soziales.html` wird zum **NeuralNet-Hub** (der Profil-Tab „Soziales" existiert schon): links Freundesliste (mit Online-Reihenfolge nach letzter Aktivität) + offene Anfragen + Username-Suche; rechts Chat-Panel des gewählten Freundes (Verlauf, Eingabefeld, „Nachricht melden/löschen" im Kontextmenü).
|
||||
- Freundesprofil: Avatar, Level, Streak, Achievement-Vergleich (zwei Spalten, gemeinsame freigeschaltete hervorgehoben), Highscore-Tabelle je Spiel, Buttons „Herausfordern" / „Blockieren".
|
||||
- Neue Dateien: `static/js/neuralnet.js` (IIFE, Polling-Logik, Render-Funktionen), `static/css/neuralnet.css` (Präfix `fn-`, nur Theme-Tokens, Erfolg blau ✓ / Fehler rot ✗). Ungelesen-Badge am Glocken-Icon der Dashboard-Topbar (zusammen mit dem FluxTimer-Popover aus Teil 4).
|
||||
|
||||
### 3.7 Phasen & Aufwand
|
||||
|
||||
1. **Phase 1 (≈ 7 PT):** Migrationen, Privacy-Opt-in, Freundschaften, Blockieren, Polling-Messenger, Ungelesen-Badge, Rechtstexte, Tests (Guard-Logik!).
|
||||
2. **Phase 2 (≈ 4 PT):** SSE-Stream, Lern-Duelle, Freundesprofil mit Achievement-/Statistik-Vergleich, Melden + Admin-Moderationsansicht.
|
||||
3. **Phase 3 (≈ 4 PT):** gemeinsame Lernräume (Seed-Boards), Datenexport (Art. 15/20), Feinschliff (Emoji-Reaktionen, Streak-Anstupsen).
|
||||
|
||||
---
|
||||
|
||||
## Teil 4: FluxTimer – Der Pausen-Lerntimer
|
||||
|
||||
> ✅ **MVP umgesetzt:** lokaler Zustand, globaler Countdown, Einstellungen, eigener Uhr-mit-Blitz-
|
||||
> Schnellzugriff in der Topbar, In-App-Overlay, Snooze, Probelauf sowie optionale Browser-
|
||||
> Benachrichtigung und Web-Audio-Signal. Die Glocke bleibt für NeuralNet-/Systembenachrichtigungen
|
||||
> reserviert. Eine serverseitige Synchronisation bleibt bewusst optional.
|
||||
|
||||
> Opt-in-Erinnerungstimer im Look der Zeitschaltkreise des DeLorean: Nach frei wählbarer Zeit erinnert Mindflux ans Aufstehen, Trinken, Essen – oder was immer man einträgt. Standardmäßig **deaktiviert**, jederzeit abschaltbar, komplett frei einstellbar.
|
||||
|
||||
### 4.1 Funktionsumfang
|
||||
|
||||
- **Aktivieren/Deaktivieren ✅** per Toggle in den Einstellungen und über das eigene
|
||||
Uhr-mit-Blitz-Icon der Dashboard-Topbar.
|
||||
- **Intervall frei wählbar:** freie Minuteneingabe, sinnvolle Grenzen 5–240 Minuten (Default-Vorschlag 45).
|
||||
- **Erinnerungsarten wählbar** (Mehrfachauswahl, Rotation): Aufstehen 🧍 · Trinken 💧 · Essen 🍎 · Augenpause 👀 · **eigener Text** (Freitextfeld).
|
||||
- **Snooze** (+5 Minuten) und „Pause erledigt“ (startet das Intervall neu).
|
||||
- **Probelauf-Button** in den Einstellungen (löst die Erinnerung sofort testweise aus).
|
||||
- Optional zuschaltbar: Browser-Benachrichtigung (mit Permission-Abfrage) und dezenter Sound.
|
||||
|
||||
### 4.2 Technik: rein clientseitig, kein externer Dienst, DSGVO-minimal
|
||||
|
||||
- **`static/js/fluxtimer.js`** (IIFE), über das Layout-Fragment auf allen eingeloggten Seiten eingebunden.
|
||||
- **Zustand in `localStorage`:** `fluxtimer = { enabled, intervalMinutes, reminders: ["aufstehen","trinken",…], customText, sound, notify, nextFireAt, lastFiredAt }`. Beim Seitenwechsel/Reload wird die Restzeit aus `nextFireAt` weitergeführt – der Timer „überlebt“ Navigation, ohne dass der Server irgendetwas weiß. **Kein Personenbezug, keine Serverdaten, nichts einwilligungspflichtig** (localStorage-Nutzung wird in der Datenschutzerklärung als technisch notwendig/funktional erwähnt).
|
||||
- **Auslösung dreistufig:** (1) immer das In-App-Overlay (siehe 4.3); (2) optional lokale Browser-`Notification` – wird **lokal im Tab erzeugt**, kein Push-Server beteiligt; (3) optional kurzer Sound (Web Audio, eingebettet, kein CDN).
|
||||
- **Bewusst kein Service-Worker-Web-Push:** Echte Push-Nachrichten bei geschlossenem Tab liefen zwingend über die Push-Gateways der Browserhersteller – das wäre ein externer Dienst und für einen Pausen-Timer unnötig (wenn kein Mindflux-Tab offen ist, lernt man gerade nicht in Mindflux).
|
||||
- **Ausbau (optional, +0,5 PT):** `user_settings`-Tabelle für geräteübergreifende Einstellungen; localStorage bleibt der schnelle Cache.
|
||||
|
||||
### 4.3 BTTF-Optik („Zeitschaltkreise“)
|
||||
|
||||
- **Panel im Zeitschaltkreis-Look:** drei Anzeigezeilen in Segmentanzeigen-Typografie (Monospace + Glow):
|
||||
- `NÄCHSTE PAUSE` – rot/bernsteinfarbene Anzeige (Countdown hh:mm:ss)
|
||||
- `JETZT` – Akzentfarbe des Themes (laufende Uhrzeit)
|
||||
- `LETZTE PAUSE` – gedimmt (wann zuletzt pausiert wurde)
|
||||
- **Beim Auslösen:** kurzer Flux-Blitz (CSS-Animation über dem Overlay), 88-mph-Anspielung und Sprüche im BTTF-Ton, rotierend je Erinnerungsart – z. B. „Großer Scott! Zeit, etwas zu trinken.“ · „88 mph erreicht – kurz aufstehen, McFly!“ · „Dein Fluxkompensator braucht einen Snack.“
|
||||
- **Styleguide-konform:** alle Farben über Theme-Tokens (die Rot-/Bernsteintöne als neue `--fluxtimer-*`-Tokens in `theme.css`, Dark/Light getestet); Grün bleibt tabu als Erfolgsfarbe. CSS in `static/css/fluxtimer.css` (Präfix `ft-`), Cache-Busting `?v=1`.
|
||||
|
||||
### 4.4 UI-Einstiegspunkte & Dateien
|
||||
|
||||
| Ort | Änderung |
|
||||
|-----|----------|
|
||||
| `settings.html` ✅ | Sektion „FluxTimer“: Toggle, Minuten-Eingabe, Erinnerungs-Checkboxen, Freitext, Benachrichtigung/Sound-Optionen, Probelauf-Button |
|
||||
| `dashboard.html` (Topbar) ✅ | Eigenes Uhr-mit-Blitz-Icon öffnet das Popover mit Status, Countdown, Schnell-Toggle und Link zu den Einstellungen; Glocke bleibt frei |
|
||||
| `theme.js` ✅ | Zentraler, idempotenter Asset-Bootstrap, damit der Timer über Seitennavigationen weiterläuft |
|
||||
| `static/js/fluxtimer.js`, `static/css/fluxtimer.css` ✅ | Timer-Logik + Zeitschaltkreis-Optik |
|
||||
|
||||
### 4.5 Aufwand
|
||||
|
||||
≈ 1,5–2 PT (JS-Logik + Settings-UI + Popover + CSS/Animationen), +0,5 PT für die optionale serverseitige Persistenz.
|
||||
|
||||
---
|
||||
|
||||
## Schlüsseldateien als Bauplan-Referenz
|
||||
|
||||
| Datei | Rolle |
|
||||
|-------|------|
|
||||
| `src/main/java/com/example/mindflux/service/DevLabService.java` | Struktureller Bauplan für alle vier Services: `sanitize`/`check*`/`finish`/`awardedPoints`/`stars` + Text-Normalisierungs-Helfer (Checker für `choice`/`order`/`match`/`regex`/`cloze` teils 1:1 kopierbar) |
|
||||
| `src/main/resources/static/js/devlabgame.js` | Renderer-Vertrag `{render, collect, lock, mark}`, Versuchsleiter-Frontend-Muster |
|
||||
| `src/main/resources/db/migration/V8__devlab.sql` | SQL-Stilvorlage für V9–V12 |
|
||||
| `src/main/java/com/example/mindflux/config/SecurityConfig.java` (Zeile 43) | CSRF-Ignore-Liste: `/api/codeescape/**`, `/api/bughunter/**`, `/api/serviceclash/**`, `/api/networker/**` ergänzen |
|
||||
| `src/main/resources/templates/dashboard.html` (Zeilen 186–196) | `GAMES`-Array: Namen aufs neue Schema umstellen (DevLab, SyntaxSaga, SystemEscape, ServiceClash, BugHunter, NetWorker) und die verbliebenen `'/comingsoon'`-Hrefs gegen echte Routen tauschen (Icons `lock`/`sword`/`bug`/`network` existieren bereits) |
|
||||
| `GameScoreController.KNOWN_GAMES` + `ProgressService.GAMES` | Vier reservierte Keys aufs neue Schema umbenennen (`codeescape`, `serviceclash`, `bughunter`, `networker`), `devlab` in `ProgressService` ergänzen (W1) |
|
||||
| `src/main/java/com/example/mindflux/service/GameScoreService.java` | `recordScore(...)`-Anschlusspunkt aller Spiele |
|
||||
| `STYLEGUIDE.md`, `CHANGELOG.md` | Design-Regeln (blau ✓ / rot ✗, nie grün; Theme-Tokens; `?v=N`) und Pflege der Historie |
|
||||
|
||||
## Verifikation
|
||||
|
||||
**Pro Spiel (nach Implementierung):**
|
||||
1. `mvn test` – insbesondere `NetSimulatorTest` (Same-Subnet, Gateway, Longest-Prefix-Routen, Loop-Guard, Netz-/Broadcast-Adresse, Duplikat-IP) und `ServiceClashServiceTest` (Seed-Determinismus: gleicher Seed ⇒ identischer Plan; Re-Simulations-Scoring).
|
||||
2. App starten (`docker-compose` oder lokal, Port 8282), einloggen und den vollen Spielfluss durchspielen: Lobby → Runde → absichtlich falsche Antworten (Versuchsleiter 1.0/0.6/0.3 prüfen) → Aufdecken (0 Punkte) → Abschluss → Score/Sterne/Highscore-Anzeige.
|
||||
3. Server-Autorität prüfen: `POST /finish` mit manipulierten Daten (fremde IDs, unmögliche Zeitstempel) darf keinen überhöhten Score ergeben; View-DTOs im Netzwerk-Tab dürfen keine Lösungsfelder enthalten.
|
||||
4. Highscore-Chip via `GET /api/games/<key>/stats`; Eintrag erscheint auf `/progress`.
|
||||
5. Admin-CRUD: Anlegen/Ändern/Löschen inkl. Payload-Validierung (Netzwerker: Musterlösungs-Gate; Bug Hunter: `file`/`line`-Konsistenz; Service Clash: `correctIndex` in `options`).
|
||||
6. Dark/Light-Theme umschalten und mobile Breite prüfen (Theme-Tokens, kein Grün).
|
||||
|
||||
**Namensschema & visuelle Differenzierung:** Dashboard zeigt alle neun Spiele mit einheitlichen CamelCase-Namen; jede der vier neuen Spielseiten hat ihren eigenen Akzent und ihr eigenes visuelles Vokabular – nebeneinander geöffnet müssen sie sofort unterscheidbar sein (Dark und Light prüfen).
|
||||
|
||||
**Navigation (Abschnitt 2.3):** Von jeder Profil-Unterseite aus sind alle anderen per Tab-Leiste erreichbar; „Profil" ist als Sidebar-Eintrag vorhanden; `settings.html` enthält keine funktionslosen Bedienelemente mehr; kein irreführender Chevron ohne Dropdown.
|
||||
|
||||
**Für die Verbesserungen (Teil 2):** Nach K1/K2: Neustart via docker-compose mit Secret-basiertem Passwort; `/users/save` liefert 404. Nach W1: DevLab erscheint auf `/progress`. Nach W4: bewusst kaputte Requests → saubere Fehlerseite/JSON-Fehler statt Whitelabel-500. Nach W6: doppelte Registrierung (Groß-/Kleinschreibung) liefert Fehlermeldung statt 500.
|
||||
@@ -0,0 +1,190 @@
|
||||
# Mindflux / SysnaptiK Neural — Gesamt-Audit & Umsetzungsplan
|
||||
|
||||
## Kontext
|
||||
|
||||
Vollständiges Audit der Plattform (Spring Boot 3.4.4, Java 21, Thymeleaf, PostgreSQL, Flyway) mit Fokus auf: Light Mode, Barrierefreiheits-Widget, Header-Icons, Spielanleitungen/Hints, AP2-Inhalte in FinalProtocol, Umlaute, DSGVO und Best Practices. Ziel: ein Light Mode, der wie ein „Geschwister" des Dark Mode wirkt (gleiche Farbidentität, aber für hell entworfen statt invertiert), konsistente UI, Hilfen in jedem Spiel, prüfungstauglicher AP2-Umfang für **beide** Fachrichtungen, saubere Umlaute, DSGVO-Konformität.
|
||||
|
||||
**Entscheidungen:** ServiceClash/BugHunter/Networker bekommen **echte Light-Varianten** (nicht dauerhaft dunkel). AP2: **5 Themen je Fachrichtung** à 20 Fragen. Impressum bleibt **strukturierter Platzhalter** (Daten werden vor Launch geliefert).
|
||||
|
||||
**Audit-Kurzantworten:**
|
||||
- **A11y-Widget:** Gut (9 Funktionen, ARIA, Escape, Fokus-Rückgabe), aber nicht perfekt: kein Focus-Trap, falsche `aria-pressed`-Semantik am 4-stufigen Font-Button, toter Legacy-CSS-Code, spielfremde Regeln in `accessibility-placement.css`.
|
||||
- **Spiele ohne Hilfe:** MindMatch + TechTacular haben **weder Anleitung noch Hints**. SyntaxSaga, DevLab, CodeEscape haben Hints, aber keine Anleitung. BugHunter nur Auflösen-Button. Networker ist Vorbild (Guide-Panel + Lexikon).
|
||||
- **AP2:** Nur je 1 Thema / 6 Fragen pro Richtung (vs. ~742 Fragen AP1/Wirtschaft). Der Ordner `systemintegration/` ist AP1-Stoff („Systeme & Netzwerke"), nicht AP2.
|
||||
- **Umlaute:** `static/finalflux/questions/usv.json` und `devlab/content.json` systematisch ohne Umlaute; Einzelfälle in `zahlensysteme.html:78`, `techtacular/content.json:4250`; Mojibake (`für`) in `style.css`-Kommentaren (Z. 19/29/220).
|
||||
- **DSGVO:** Chart.js von jsDelivr-CDN (`progress.html:9`) = einziges externes Asset, widerspricht eigener Datenschutzerklärung. Impressum = Platzhalter. NeuralNet-Messaging fehlt in der Datenschutzerklärung. Kein Datenexport (Art. 20). Footer mit Rechtslinks fehlt auf allen Spielseiten + Error/Auth-Seiten. Demo-User `NeuralTest` wird per Flyway (V17) in Prod eingespielt. Positiv: kein Tracking, keine Cookies außer Session, Secrets sauber, Löschkonzept (Art. 17) vorhanden.
|
||||
- **Best Practices:** `@Valid`/Bean-Validation nirgends genutzt (obwohl Starter + Handler verdrahtet), keine CSP/HSTS, Spring Boot 3.4.4 veraltet, Avatar-Logik im Controller statt Service, Footer 26× dupliziert statt Fragment, `DummyController`/`hello.txt`-Reste. Positiv: kein IDOR, saubere Schichten sonst, 51 Tests, Flyway, Brute-Force-Schutz.
|
||||
|
||||
Basis-Pfad: `RES` = `src\main\resources`
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungsstatus (Stand 2026-07-21, auf `main`)
|
||||
|
||||
- ✅ **AP 0** Quick Wins — vollständig (inkl. V21-Reseed-Migration für Alt-DBs)
|
||||
- ✅ **AP 1** Light Mode — vollständig, alle Kontraste WCAG-AA rechnerisch geprüft. ⚠️ **Nachtrag (2026-07-21):** Glow-Effekte wirken im Light Mode störend → Alternative nötig, s. AP 1e.
|
||||
- ✅ **AP 2** Header-Icons — vollständig
|
||||
- ✅ **AP 3** Barrierefreiheits-Widget — vollständig
|
||||
- ✅ **AP 4** Spielanleitungen & Hints — vollständig (AP 4c TechTacular-Joker war bereits erledigt)
|
||||
- ✅ **AP 5** AP2-Ausbau — vollständig (10 Themen × 20 = 200 Fragen)
|
||||
- ✅ **AP 6** DSGVO — vollständig (Footer auf 35 Seiten, Datenschutz/Impressum, Export, V22)
|
||||
- 🟡 **AP 7** Best Practices — **teilweise**: Security-Header/CSP, AvatarService-Extraktion und Bean-Validation-Start erledigt. **Offen:** Validierung auf alle Request-DTOs + NeuralNet-`Map`→DTO, Spring-Boot-Patch-Bump, Cookie-basiertes CSRF, Web-Layer-Test für `/profile/export`.
|
||||
|
||||
Automatisiert verifiziert: `mvn test` 54/54 grün, Build grün, Umlaut-QA sauber, alle 200 AP2-Fragen strukturell validiert. **Noch nicht durchgeführt (braucht laufende App + DB + Browser):** visuelle Light-Mode-Prüfung aller Spiele hell/dunkel, A11y-Widget-Handtest, Guide-Auto-Open, Export-Download- und DSGVO-Netzwerk-Check; die 8 Vollbild-Spielseiten haben nun ebenfalls einen Footer (ggf. schlanke CSS-Anpassung). Vor Launch offen: echte Impressumsdaten, Support-/Feedback-Zieladresse.
|
||||
|
||||
---
|
||||
|
||||
## AP 0 — Quick Wins (zuerst, alles klein)
|
||||
|
||||
1. **Chart.js lokal vendoren**: nach `RES\static\js\vendor\chart.umd.min.js` laden, `RES\templates\progress.html:9` auf lokalen Pfad umstellen. Schließt das einzige DSGVO-Datenleck.
|
||||
2. **Scaffolding löschen**: `controller/DummyController.java`, `templates/dummy.html`, `RES\static\hello.txt`.
|
||||
3. **Umlaute fixen**:
|
||||
- `RES\static\finalflux\questions\usv.json` — kompletter Durchgang (fuer→für, ueberbrueckt→überbrückt, heisst→heißt, …).
|
||||
- `RES\devlab\content.json` — ~30 Stellen (hints/instructions/successText). **Achtung:** DevLab wird per `DevLabSeeder` in die DB geseedet (existenzgeprüft = idempotent) → prüfen, ob Fixes im JSON bestehende DB-Zeilen erreichen; falls nicht: Seeder auf Upsert umstellen **oder** Flyway-Migration `V21__fix_devlab_umlauts.sql`.
|
||||
- `RES\static\finalflux\ap1\zahlensysteme.html:78` (`fuer`), `RES\techtacular\content.json:4250` (`zusaetzlich`, gleiche Seeder-Prüfung), `RES\static\css\style.css` Z. 19/29/220 (Mojibake-Kommentare).
|
||||
- QA-Grep danach: `fuer|ueber|heisst|koennen|moechte|zurueck|loeschen|zusaetzlich` über resources → nur URLs/Slugs/IDs dürfen übrig bleiben.
|
||||
4. **FinalProtocol-Titel**: `RES\templates\finalflux.html:5` „FinalProtocol AP1" → „FinalProtocol", Hero (Z. 75–83) auf AP1 **und** AP2 umformulieren.
|
||||
|
||||
---
|
||||
|
||||
## AP 1 — Light Mode (Kern des Redesigns)
|
||||
|
||||
Architektur bleibt: Tokens via `data-theme` auf `<html>`, zwei Ebenen (`theme.css` Amber-Palette, `platform-neural.css` Grün-Override für Plattform-Shell). Alle Werte unten sind arithmetisch auf WCAG AA geprüft (≥4,5:1 Text, ≥3:1 UI).
|
||||
|
||||
### 1a. `RES\static\css\theme.css` — Light-Block (Z. 33–54) nachschärfen
|
||||
Kein Token-Split (`--accent-text`), stattdessen Light-Akzente abdunkeln, bis ein Token Text + Grafik + weiße Ink trägt:
|
||||
|
||||
| Token | Alt | Neu | Kontrast |
|
||||
|---|---|---|---|
|
||||
| `--accent` | #d65116 | **#c2410c** | 5,18 (auch weiße Ink darauf: 5,18) |
|
||||
| `--accent-2` | #e4801d | **#b45309** | 5,02 (heilt `.login-box h2`) |
|
||||
| `--muted` | #8a7d6c | **#6b5f4c** | 6,24 / 5,79 auf #fbf6ec |
|
||||
| `--secondary` (+`--success`) | #1f86b8 | **#146b96** | 5,88 / 5,46 |
|
||||
| `--secondary-2` | #2d9cce | **#0f5a80** | passt |
|
||||
| NEU `--warn` | — | hell **#8a6a06** / dunkel **#edcb4c** | 5,07 |
|
||||
|
||||
Spiel-Tokens im selben Block, nur die Durchfaller: `--esc-accent`→#a85408, `--sc-accent`→#0e7c86, `--dl-accent`→#9c4d0f, `--ss-secondary`→#8a5710, `--ff-accent`→#8c5e10. Zugehörige `*-glow`-rgba nachziehen.
|
||||
|
||||
### 1b. `RES\static\css\platform-neural.css` — Light-Kartenakzente (der Hauptbug)
|
||||
Z. 41–49 leaken Neon-Akzente auf fast-weiße Karten. Neuer Block `html[data-theme="light"].sn-platform body .mf-card-<name>` mit farbton-treu abgedunkelten Akzenten (alle ≥4,65:1 auf Kartengrund):
|
||||
mindmatch **#a62c74**, techtacular **#67700a**, devlab **#167a52**, syntaxsaga **#6742d6**, codeescape **#8a6a06**, serviceclash **#0e7c86**, bughunter **#c22736**, networker **#1668a8**, finalprotocol **#b5490f** — je mit `--glow-s: rgba(<hex>,.22)` und hellerem `--accent-2` für Grafik-Deko. Dazu Light-Variante des finalflux-Shell-Overrides (Z. 57–60).
|
||||
|
||||
### 1c. Hardcodes
|
||||
`RES\templates\dashboard.html:19–20`: `#edcb4c` → `var(--warn)` + `color-mix(in srgb, var(--warn) 48%, transparent)`.
|
||||
|
||||
### 1d. Echte Light-Varianten für die 3 hardgecodeten Spiele
|
||||
Vorgehen je Spiel: alle Roh-Hexwerte in scoped Custom Properties überführen (`.sc-body{--sc-*}` usw. auf `<body>`-Klasse, damit A11y-Skin-Variablen sie sehen), Dark-Defaults = heutige Werte, Light-Block `[data-theme="light"] .xx-body{…}` mit handentworfener Palette gleicher Farbidentität:
|
||||
|
||||
- **ServiceClash** (`serviceclash.css`, heute bg #071015 / accent #2bb6d7): Light = kühles NOC-Hell: bg **#eef6f8**, surface #ffffff, surface-2 #e2eff2, text **#10333b**, muted #52707a (4,6:1), accent **#0e7c86**, border #cfe3e8. Zusätzlich: `RES\templates\serviceclash.html` bekommt den fehlenden `.mf-theme`-Toggle in den Header (`.sc-nav`).
|
||||
- **Networker** (`networker.css`, dunkelblauer Gradient + fälschlich `var(--text)`): Light = helles Netz-Blau: bg **#eef4fb**, surface #ffffff, text **#16324a**, muted #54708c, accent **#1668a8**, border #d3e2f0. Alle `var(--text)`/`var(--muted)`-Nutzungen auf `--nw-text`/`--nw-muted` umstellen.
|
||||
- **BugHunter** (`bughunter.css`, Holz-Braun #231b18/#3a2d28): Light = Papier/Archiv-Look: bg **#f7f1e8**, surface #fffaf2, Schubladen helles Holz #e8d9c4/#d9c5a8, text **#33261d**, muted #6f5f4e, accent **#c22736**, border #e2d3bd. Die BugHunter/Networker-Regeln aus `accessibility-placement.css:21–23` in die jeweiligen Spiel-CSS verschieben (ohne `!important`).
|
||||
|
||||
**Check:** jeder Spiel-Header, dessen Spiel jetzt theme-fähig ist, braucht den `.mf-theme`-Toggle (theme.js verdrahtet ihn automatisch); mindestens serviceclash.html fehlt er.
|
||||
|
||||
### 1e. Nachtrag: Glow im Light Mode überarbeiten (offen, 2026-07-21)
|
||||
**Befund:** Die aus dem Dark-Mode-Neon-Look übernommenen Glow-Effekte wirken im Light Mode störend (auf fast-weißem Grund lesen sie sich als Schmutz-/Haze-Rand statt als Akzent). Betrifft die durchgereichten Glow-Tokens der Kartenakzente (`--glow-s: rgba(<hex>,.22)`, s. 1b), die spielspezifischen `*-glow`-rgba (s. 1a) und den Glow am `.mf-theme`-Toggle (s. AP 2, `theme.css:120–121`).
|
||||
**Wir brauchen eine Alternative** für den hellen Modus — noch zu entscheiden, z. B. Glow-Alpha im Light-Block deutlich reduzieren/auf 0 setzen, oder den Neon-Glow durch ein dezentes Mittel ersetzen (weicher Schatten / abgesetzter Border / leichter Farbverlauf), das die Erkennungsfunktion behält, ohne den hellen Grund zu trüben. Dark Mode bleibt unverändert.
|
||||
|
||||
---
|
||||
|
||||
## AP 2 — Header-Icons vereinheitlichen
|
||||
|
||||
Eine Spezifikation für alle: **42×42 px, border-radius 10px, Glyphe 20×20, Outline-Stil** (Surface-Fill, 1px Border, Akzent nur über `color`/`border-color`):
|
||||
1. `theme.css:112` `.mf-iconbtn` → 42×42; `:114` svg → 20×20.
|
||||
2. `theme.css:120–121` `.mf-theme` → 42×42, radius 10px (Ellipse weg), svg 20×20; Akzentfarbe/Glow bleibt als Erkennungsmerkmal.
|
||||
3. `accessibility-placement.css:2–3` Trigger → 42×42, svg 20×20; Z. 16: gefüllten Gradient entfernen → `background:var(--surface); color:var(--mf-a11y-accent); border-color:color-mix(…)`.
|
||||
4. `accessibility.css:26` Legacy-56px-Kreis-Trigger löschen, saubere Basisregel im `.mf-iconbtn`-Look (Fokus-Outline behalten).
|
||||
5. `fluxtimer.css:3` `!important` entfernen; `.ft-trigger-icon` 20×20, `stroke-width:1.5`.
|
||||
6. Sichtprüfung: `.nn-unread`-Badge-Position bei 42px, `.mf-search`-Höhe angleichen. `STYLEGUIDE.md` aktualisieren.
|
||||
|
||||
---
|
||||
|
||||
## AP 3 — Barrierefreiheits-Widget perfektionieren
|
||||
|
||||
In `RES\static\js\accessibility.js` / `accessibility.css`:
|
||||
1. **Focus-Trap** im Panel (Tab/Shift+Tab zyklisch über Close + 9 Options-Buttons), `aria-modal="true"`, Klick-außerhalb schließt. Escape + Fokus-Rückgabe existieren schon.
|
||||
2. **Font-Button**: boolean `aria-pressed` ersetzen durch dynamisches `aria-label` („Textgröße ändern – aktuell 115 %") + `.is-active`-Klasse ab >100 %; `aria-pressed` vom Reset-Button entfernen (Aktion, kein Toggle).
|
||||
3. **Toten Dock-CSS löschen** (`accessibility.css:25` body-padding-72px, `.mf-a11y-dock`, Fixed-Trigger) und die dadurch obsoleten Resets in `accessibility-placement.css` einkürzen.
|
||||
4. **Hochkontrast bleibt bewusst dunkel** auch im Light Mode (maximaler Kontrast, deterministisch) — im Button-Untertitel kenntlich machen („immer dunkel") + CSS-Kommentar.
|
||||
5. Cache-Buster bumpen (theme.js: accessibility.css v6, placement v10, accessibility.js v9, fluxtimer.css v5; theme.css v4 in allen Templates).
|
||||
|
||||
---
|
||||
|
||||
## AP 4 — Spielanleitungen & Hints
|
||||
|
||||
### 4a. Gemeinsames Guide-Modul „So funktioniert's"
|
||||
Neu: `RES\static\js\game-guide.js` + `RES\static\css\game-guide.css`, nach Vorbild `networker-guide.js` (IIFE, deutsches Inline-Registry als Template-Literals, kein Fetch). Injektion über `theme.js` nur auf Game-Routen (Liste existiert dort in `isGameRoute`, Z. 15) — null Template-Edits, wie FluxTimer.
|
||||
- „?"-Button (`aria-label="Spielanleitung öffnen"`) in die jeweilige Header-Tools-Leiste; Erstbesuch öffnet Overlay automatisch (localStorage `mf-guide-seen:<slug>`), danach per „?" reopenbar; Escape/Backdrop schließt.
|
||||
- API `window.MindfluxGuide = {open, close, registerHintProvider({label, remaining, onHint})}` für einheitliche Tipp-Buttons.
|
||||
- Rollout: MindMatch, TechTacular, SyntaxSaga, DevLab-Game, CodeEscape, BugHunter, ServiceClash (kondensiert, Intro-Screen bleibt). **Networker bleibt unangetastet** (Inline-Guide ist stärker).
|
||||
|
||||
### 4b. MindMatch-Hint „Paar-Blitz"
|
||||
Button „Tipp (3)" in `.mm-bar-group` (`mindmatch.html` ~Z. 35): deckt ein ungematchtes Paar ~1 s auf (wenn `firstCard` liegt: dessen Partner), kostet **+1 Zug** → Score/Sterne degradieren über die bestehende Formel, **null Backend-Änderungen** (Server rechnet aus `moves`). Hooks in `mindmatch.js`: State Z. 27–35, `newGame` Z. 241–265 (Reset auf 3), `finish`/`cancelGame` verstecken. CSS `.mm-card.hinted`-Glow.
|
||||
|
||||
### 4c. TechTacular „50:50-Joker" ✅ umgesetzt (2026-07-19)
|
||||
`GET /api/techtacular/hint?questionId=` liefert ausschließlich bis zu zwei falsche Antwort-IDs; die korrekte ID verlässt den Service nie. Der Joker ist serverseitig pro Runde auf einen Einsatz begrenzt, markiert die konkrete Frage in der Session und wird beim Laden eines neuen Boards bzw. nach dem Abschluss zurückgesetzt. Im Antwort-Overlay werden die entfernten Optionen deaktiviert und durchgestrichen. `hintUsed` ist Teil der Answer-/Finish-DTOs; die Halbierung wird jedoch nur gewertet, wenn die Session den Joker-Einsatz für genau diese Frage bestätigt. Ein Endpoint-Test sichert JSON-Form und Weiterleitung ab.
|
||||
|
||||
### 4d. Optional (Phase 2): BugHunter progressive Hinweise (2 Stufen vor „Auflösen", nach CodeEscape-Vorbild).
|
||||
|
||||
---
|
||||
|
||||
## AP 5 — FinalProtocol: AP2-Ausbau (5 Themen je Richtung, je 20 Fragen)
|
||||
|
||||
Skelett exakt von `RES\static\finalflux\ap2-fisi\hochverfuegbarkeit.html` übernehmen (Lernfokus mit 3 Zielen, 2–4 Inhaltssektionen mit `key-list` + Merksätzen, Prüfungsmodus-Block verbatim, `finalflux-test.js` mit `data-questions-src`). JSON-Schema wie bestehende Dateien: `{category, hint, text, answers[3], correct (rotierend), explanation (nennt auch die Distraktor-Fallen)}`, 4–6 Kategorien pro Datei, „du"-Ansprache, **echte Umlaute (UTF-8 ohne BOM)**.
|
||||
|
||||
**AP2 Systemintegration** (`RES\static\finalflux\ap2-fisi\` + `questions\`):
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| netzwerkplanung | Subnetting/VLSM, IPv6, WLAN-Planung, Routing |
|
||||
| it-sicherheit | Firewall/DMZ, VPN (IPsec/TLS), sym./asym. Verschlüsselung, Zertifikate, CIA, 2FA |
|
||||
| virtualisierung | Typ-1/2-Hypervisor, Container vs. VM, Snapshots, IaaS/PaaS/SaaS |
|
||||
| monitoring-backup | SNMP/Schwellwerte, 3-2-1-Regel, Generationenprinzip, Wiederherstellungs-Szenarien, Serverdimensionierung |
|
||||
| skripting | Shell/PowerShell-Konstrukte, Pseudocode, Cron/Aufgabenplanung, Exit-Codes |
|
||||
|
||||
**AP2 Anwendungsentwicklung** (`ap2-fiae\`): **uml** (Klassen-/Sequenz-/Aktivitätsdiagramm, Multiplizitäten), **sql** (Joins, GROUP BY/HAVING, DDL/DML, Normalisierung), **oop** (Kapselung, Vererbung vs. Komposition, Polymorphie, Interfaces), **algorithmen** (Pseudocode/Struktogramm, Sortieren/Suchen, O-Notation, Rekursion), **testverfahren** (Blackbox/Whitebox, Äquivalenzklassen/Grenzwerte, Testebenen; + Kategorie REST/JSON/XML).
|
||||
|
||||
→ je Richtung ~106 Fragen inkl. Bestand. Navigation: `RES\templates\finalflux.html` Z. 62–70 um je 5 Links erweitern; optional „AP2-Kompass"-Panel neben dem AP1-Kompass. `systemintegration/`-Sektion bleibt als AP1 „Systeme & Netzwerke" unverändert.
|
||||
|
||||
---
|
||||
|
||||
## AP 6 — DSGVO
|
||||
|
||||
1. **Footer-Fragment rollout**: `fragments/layout.html:87–94` (`th:fragment="footer"`) existiert, wird 0× genutzt → alle ~26 Inline-Footer durch `th:replace="~{fragments/layout :: footer}"` ersetzen **und** auf die 7 Spielseiten + `error.html` + `forgot-password.html`/`reset-password.html` bringen (Spielseiten ggf. schlanke CSS-Anpassung, damit der Look nicht bricht). Rechtslinks damit von jeder Seite erreichbar.
|
||||
2. **Datenschutzerklärung vervollständigen** (`templates/legal/datenschutz.html`): NeuralNet-Messaging als Verarbeitungstätigkeit (Inhalte, Sender/Empfänger, Zeitstempel, Lesebestätigungen, Löschung via Kaskade), E-Mail-Token (SHA-256, 24 h TTL) + SMTP-Versand über `mail.sysnaptik.de` als Empfänger, konkrete Speicherfristen (Konto, Logs, Login-Versuche, Nachrichten), Drittland-Aussage nach CDN-Entfernung wieder korrekt.
|
||||
3. **Impressum strukturieren** (`templates/impressum.html`): saubere Struktur mit klar markierten Pflichtfeldern (Name, Anschrift, E-Mail, Vertretung) als auffällige Platzhalter — werden vor Launch ausgefüllt.
|
||||
4. **Support/Feedback-Links** (`fragments/layout.html:88–89`, `href="#"`): auf `mailto:`-Platzhalter konsistent zum Impressum umstellen oder bis dahin entfernen (Betroffenenrechte brauchen einen Kanal).
|
||||
5. **Datenexport Art. 20**: `GET /profile/export` in `ProfileController` → Service sammelt User-Stammdaten, Scores, Achievements, Fortschritt, NeuralNet-Daten → JSON-Download. Test ergänzen.
|
||||
6. **Demo-User `NeuralTest` entschärfen**: neue Migration `V22__remove_neuraltest_demo_user.sql` (Löschen, Kaskaden räumen auf); falls für Dev nötig, als profile-gebundenen Runner (`@Profile("dev")`) neu anlegen.
|
||||
|
||||
---
|
||||
|
||||
## AP 7 — Best Practices
|
||||
|
||||
1. **Bean Validation aktivieren**: `@Valid` + `@NotBlank/@Size/@Email` auf Request-DTOs (`ScoreSubmission`, BugHunter/Techtacular-Requests); die rohen `Map<String,String>`-Bodies in `NeuralNetApiController` durch typisierte DTOs ersetzen. Handler (`ApiExceptionHandler:45–51`) ist schon verdrahtet.
|
||||
2. **Security-Header** (`SecurityConfig.java`): `.headers()`-Block mit CSP (nach Chart.js-Vendoring machbar; pragmatisch starten mit `default-src 'self'` + `style-src 'self' 'unsafe-inline'` wegen Inline-Styles in Templates), HSTS (nur Prod), Referrer-Policy.
|
||||
3. **Spring Boot bumpen**: 3.4.4 → aktuellstes 3.4.x-Patch (`pom.xml:11`), Testlauf.
|
||||
4. **Avatar-Logik aus `ProfileController` (Z. 115–172) in einen `AvatarService`** extrahieren (Bild-Dekodierung/Skalierung/Datei-I/O, Pfad-Logik heute 3× dupliziert).
|
||||
5. **CSRF** (niedrige Prio): Blanket-Exemption für `/api/**` (SecurityConfig:42–46) durch Cookie-basierten CSRF-Token für JSON ersetzen.
|
||||
6. **Tests**: Web-Layer-Tests für neue Endpoints (Techtacular-Hint, Profil-Export).
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
1. AP 0 Quick Wins → 2. AP 1 Light Mode (Tokens → Kartenakzente → 3 Spiele) → 3. AP 2 Icons → 4. AP 3 Widget → 5. AP 4 Guides/Hints → 6. AP 5 AP2-Content (parallelisierbar zu 2–5) → 7. AP 6 DSGVO → 8. AP 7 Best Practices → 9. Gesamtverifikation.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- **Build/Run**: `mvn spring-boot:run` in `mindflux2` (oder `docker-compose up`); `mvn test` muss grün bleiben.
|
||||
- **Light Mode visuell**: Screenshots hell **und** dunkel: Dashboard (Kartengrid + `.mf-play`-Hover), Login, Settings, Progress, Soziales und **alle 9 Spiele** (insb. ServiceClash/Networker/BugHunter in beiden Themes); Hochkontrast- und Farbenblind-Modus je einmal gegenprüfen.
|
||||
- **Icons**: Topbar Dashboard + ein Spiel-Header, beide Themes; Badge-Positionen.
|
||||
- **Widget manuell**: Tab-Zyklus im Panel, Escape, Fokus-Rückgabe, Screenreader-Label des Font-Buttons.
|
||||
- **Guides**: `localStorage` `mf-guide-seen:*` löschen → jedes Spiel: Auto-Open einmalig, „?" reopenbar, `/networker` unverändert. MindMatch: Tipp zählt Zug hoch, Server-Score stimmt. TechTacular: Joker eliminiert Falsche, halbe Punkte bei `hintUsed`, `/hint` leakt nie die richtige ID.
|
||||
- **AP2**: alle 10 neuen Links erreichbar, je ein kompletter Quiz-Durchlauf (antworten, überspringen, neu starten, Kategorien-Summary), JSON-Validierung (`python -m json.tool`), `0 <= correct < answers.length`.
|
||||
- **Umlaut-QA**: Grep-Muster (s. AP 0) ohne Treffer in sichtbaren Texten; Browser-Darstellung stichprobenartig.
|
||||
- **DSGVO**: Netzwerk-Tab auf `/progress` → keine externen Requests; Footer-Links auf jeder Seite; Export-Download enthält alle Datenkategorien; `NeuralTest` nach Migration weg.
|
||||
|
||||
## Offene Punkte (vor Launch)
|
||||
|
||||
- Impressumsdaten (Name, Anschrift, E-Mail) liefern → Impressum + §1 Datenschutzerklärung.
|
||||
- Support-/Feedback-Zieladresse bestätigen.
|
||||
- Gesamtumfang ist groß (v. a. AP 5 Content-Arbeit) — bei Bedarf über mehrere Sitzungen umsetzen; die Arbeitspakete sind unabhängig genug.
|
||||
@@ -0,0 +1,314 @@
|
||||
# Mindflux — Refine 2: Light-Mode-Konzept, Scroll-Verhalten, Restpunkte
|
||||
|
||||
## Kontext
|
||||
|
||||
Folgeplan zu `mindfluxrefine.md`. Die Code-Verifikation (2026-07-21) bestätigt: **alle „vollständig"-Claims aus Refine 1 halten der Prüfung stand**; offen sind AP 1e (Glow im Light Mode), der AP-7-Rest sowie einige Zusatzfunde (s. AP R5). Dieser Plan liefert das, was Refine 1 offen ließ: ein **echtes Light-Mode-Konzept**, das alle Probleme an der Wurzel behebt (statt Glow-Alphas weiter zu dimmen), plus das neue Thema **Scroll-Verhalten** (Ergebnisanzeigen u. ä. müssen oben starten, nicht unten).
|
||||
|
||||
**Audit-Kurzbefunde (2026-07-21, drei unabhängige Durchgänge):**
|
||||
|
||||
- **Light-Mode-Strukturfehler:** Der Light-Block in `theme.css` tauscht nur *Farbwerte/Alphas* der Glow-Tokens. Die *Geometrie* der Effekte (`box-shadow:0 0 20px …`, `text-shadow:0 0 12px …`, `drop-shadow(0 0 8px …)`) steht hart in ~70 Komponentenregeln quer durch `theme.css`, alle Spiele-CSS und Template-`<style>`-Blöcke. Ein Halo mit reduziertem Alpha bleibt ein Halo — auf Fast-Weiß liest er sich als Schmutz/Haze. Einzige strukturell adaptierte Stelle heute: `--g-soft` (`0 0 18px` dark → `0 6px 18px` light, `theme.css:31/53`). Genau **eine** Regel neutralisiert einen Glow explizit im Light Mode (`auth.css:127–128`).
|
||||
- **5 im Light Mode kaputte Stellen:** Login-Overlay weiß-auf-weiß (auth.css:123–127 × platform-neural.css:94), SyntaxSaga-Bühne bleibt schwarz (`syntaxsaga.css:250–251`), CodeEscape-Eingaben dunkel-auf-dunkel (`codeescape.css` `.esc-input` `#080604`), NetWorker-Guide-Panel dunkelnavy mit Light-`--muted` (`networker-guide.css`), Farbenblind-Palette × Light = Hellgelb/Hellblau auf Weiß (`accessibility.css:15–23`, `!important` schlägt die Light-Kartenakzente).
|
||||
- **Theme-Bootstrap-Lücken:** `color-scheme` fehlt komplett (native Scrollbars/Form-Controls folgen dem UA-Default), kein `theme-color`-Meta, kein `prefers-color-scheme`-Fallback (Erstbesucher mit OS-Light starten dunkel), alle Templates hardcoden `data-theme="dark"`.
|
||||
- **Scroll:** Im gesamten Projekt existiert **kein einziger** `scrollTo`/`scrollIntoView`/`scrollRestoration`-Aufruf (außer Chat-Autoscroll `neuralnet.js:84`, der korrekt ist). Die vier SPA-Spiele (BugHunter, CodeEscape, NetWorker, ServiceClash) ersetzen Views per `innerHTML` ohne Reset; der FinalProtocol-Quiz kollabiert am Ende einer langen Lernseite (Browser clampt ans neue Seitenende); SyntaxSaga/DevLab fokussieren Inputs ohne `preventScroll`.
|
||||
- **CSS-Leichen:** `achievements.css`, `progress.css`, `settings.css`, `soziales.css` werden nirgends mehr referenziert (repo-weiter Grep leer); `style.css` wird nur noch von `finalflux.html` geladen, ~90 % davon ist toter Dark-Code.
|
||||
|
||||
Basis-Pfad: `RES` = `src\main\resources`
|
||||
|
||||
---
|
||||
|
||||
## Das Konzept: „Licht statt Leuchten"
|
||||
|
||||
**Prinzip:** Dark Mode und Light Mode sind Geschwister mit derselben Farbidentität, aber unterschiedlicher *Physik*:
|
||||
|
||||
| | Dark Mode | Light Mode |
|
||||
|---|---|---|
|
||||
| Metapher | Neon in der Nacht — Elemente **emittieren** Licht | Druck/Material am Tag — Elemente **werfen Schatten** |
|
||||
| Betonung | farbiger Glow (Halo) | **Elevation** (weicher, warm getönter neutraler Schatten) |
|
||||
| Identität/Wiedererkennung | Glow-Farbe | **Akzentfarbe direkt am Element** (Border, Text, Icon) — ist durch `--border-accent`, Icon-Farben und Kartenakzente bereits überall vorhanden |
|
||||
| Text-Halos | ja (`text-shadow`) | **nie** (liest sich als Unschärfe) |
|
||||
| Fokus-Ringe | farbig, weich | farbig, **crisp** (Ring statt Blur — funktional, bleibt farbig) |
|
||||
| Scrims/Overlays | dunkel | dunkel (funktional), aber leicht entschärft |
|
||||
|
||||
**Mechanik:** Effekte werden zu **Primitives** — komplette `box-shadow`/`filter`/`text-shadow`-*Werte* als Tokens, nicht nur Farben. Komponenten schreiben nie mehr `0 0 20px var(--glow-s)`, sondern `var(--g-strong)`. Der Light-Block tauscht dann die *Kategorie* des Effekts (Glow → Elevation), nicht nur die Farbe. Dark bleibt visuell gleich (Blur-Radien werden auf die Primitive normalisiert; ±4 px sind nicht unterscheidbar). `--glow-s`/`--glow-w` bleiben als Farb-Tokens bestehen — die Spiele-CSS remappen sie bereits per `color-mix` auf ihre Akzente, und die Primitives referenzieren sie per `var()`, sodass Spiel-Glows im Dark Mode automatisch die Spielfarbe behalten.
|
||||
|
||||
---
|
||||
|
||||
## AP R0 — Aufräumen (erledigt 2026-07-22)
|
||||
|
||||
1. **Erledigt:** Verwaiste CSS gelöscht: `achievements.css`, `progress.css`, `settings.css`, `soziales.css`. Die Referenzprüfung ergab 0 Treffer; STYLEGUIDE bereinigt.
|
||||
2. **Erledigt:** `style.css` auf eine vierzeilige Basis für die 40 statischen
|
||||
FinalProtocol-Lernseiten reduziert; FinalProtocol-Hub lädt sie nicht mehr.
|
||||
3. **Erledigt:** Nicht eingebundenes `finalflux-check.js` entfernt. `fluxtimer.css` bleibt separat offen, da die Datei in diesem Paket nicht geändert wurde.
|
||||
|
||||
---
|
||||
|
||||
## AP R1 — Effekt-Primitives (Kern des Light-Konzepts)
|
||||
|
||||
### R1a. Tokens in `RES\static\css\theme.css` — erledigt 2026-07-22
|
||||
|
||||
In den Dark-Block (Z. 11–32) und Light-Block (Z. 33–54) ergänzen. Schattenfarbe Light ist **warmes Braun** `rgb(67,48,26)` (zur Creme-Palette passend, kein totes Grau/Schwarz):
|
||||
|
||||
```css
|
||||
:root, [data-theme="dark"] {
|
||||
/* … bestehende Tokens … */
|
||||
color-scheme:dark;
|
||||
--g-text:0 0 12px var(--glow-s); /* Text-Halo: Logo, Headline-Akzent, *-brand b */
|
||||
--g-icon:drop-shadow(0 0 8px var(--glow-s)); /* leuchtende Glyphen/Embleme */
|
||||
--g-strong:0 0 20px var(--glow-s); /* starke Betonung: aktive Nav/Tabs, Primär-Btn */
|
||||
--g-amb:0 0 14px var(--glow-w); /* dezenter Schein: Icon-Hover, Toggle, Burger */
|
||||
--g-card:drop-shadow(0 0 7px var(--glow-w)); /* Spielkarten-Grundzustand */
|
||||
--g-card-hover:drop-shadow(0 0 16px var(--glow-s));
|
||||
--g-overlay:0 24px 60px rgba(0,0,0,.5); /* schwebende Panels: Guide, Drawer, A11y */
|
||||
--g-focus:0 0 0 3px var(--glow-w); /* Fokus-Ring (bleibt in beiden Themes farbig) */
|
||||
--scroll-thumb:var(--line);
|
||||
}
|
||||
[data-theme="light"] {
|
||||
/* … bestehende Tokens … */
|
||||
color-scheme:light;
|
||||
--bg-glow: radial-gradient(900px 420px at 60% -10%, rgba(214,81,22,.05), transparent 60%); /* statt .10 — Haze halbieren */
|
||||
--g-text:none;
|
||||
--g-icon:none;
|
||||
--g-strong:0 2px 10px rgba(67,48,26,.18), 0 1px 3px rgba(67,48,26,.10);
|
||||
--g-amb:0 1px 5px rgba(67,48,26,.10);
|
||||
--g-card:drop-shadow(0 2px 5px rgba(67,48,26,.10));
|
||||
--g-card-hover:drop-shadow(0 8px 18px rgba(67,48,26,.16));
|
||||
--g-overlay:0 18px 44px rgba(67,48,26,.20);
|
||||
--g-focus:0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
|
||||
--scroll-thumb:#d9cbb2; /* --line #f1e8d8 ist auf #fbf6ec unsichtbar */
|
||||
}
|
||||
```
|
||||
|
||||
Scrollbar-Regel `theme.css:64` auf `background:var(--scroll-thumb)` umstellen.
|
||||
|
||||
### R1b. Mechanische Migration in `theme.css`
|
||||
|
||||
| Zeile | Selektor | Alt | Neu |
|
||||
|---|---|---|---|
|
||||
| 79 | `.mf-logo .mark` | `drop-shadow(0 0 8px var(--glow-s))` | `filter:var(--g-icon)` |
|
||||
| 80 | `.mf-logo .txt b` | `text-shadow:0 0 12px var(--glow-s)` | `text-shadow:var(--g-text)` |
|
||||
| 87 | `.mf-nav a.active` | `0 0 20px var(--glow-s)` | `box-shadow:var(--g-strong)` |
|
||||
| 94 | `.mf-avatar` | `0 0 14px var(--glow-s)` | `box-shadow:var(--g-strong)` (gleiche Farbstärke-Familie wie heute) |
|
||||
| 102/104 | `.mf-streak .fire` / `.mf-brain` | drop-shadows | `filter:var(--g-icon)` |
|
||||
| 109 | `.mf-hello h1 .nm` | `text-shadow:0 0 16px var(--glow-s)` | `text-shadow:var(--g-text)` |
|
||||
| 113/120/202 | iconbtn-Hover / `.mf-theme` / `.mf-burger` | `0 0 14px var(--glow-w)` | `box-shadow:var(--g-amb)` |
|
||||
| 128/130 | `.mf-card` / `:hover` | drop-shadow 7/16px | `filter:var(--g-card)` / `var(--g-card-hover)` |
|
||||
| 131 | `.mf-card .ico` | drop-shadow | `filter:var(--g-icon)` |
|
||||
| 136 | `.mf-card:hover .mf-play` | `0 0 16px var(--glow-s)` | `box-shadow:var(--g-strong)` |
|
||||
| 146 | `.mf-panel.accent` | `0 0 20px var(--glow-w)` | `box-shadow:var(--g-amb)` |
|
||||
| 151 | `.mf-bar > i` | `0 0 10px var(--glow-s)` | eigenes Primitive `--g-bar` (dark `0 0 10px var(--glow-s)` / light `none`) — der Balken trägt seine Farbe selbst, im Light braucht er keinen Hof |
|
||||
| 154 | `.mf-btn` | `0 0 18px var(--glow-s)` | `box-shadow:var(--g-strong)` |
|
||||
| 168 | Input-Fokus | `0 0 0 3px var(--glow-w)` | `box-shadow:var(--g-focus)` |
|
||||
| 182 | `.mf-cardbox` | `0 0 30px var(--glow-w)` | Hero-Aura: Zweizeiler — Basis `box-shadow:var(--g-strong)`, plus `[data-theme="dark"] .mf-cardbox{box-shadow:0 0 30px var(--glow-w)}` |
|
||||
| 223 | Mobile `.mf-sidebar` | `0 18px 50px rgba(0,0,0,.55)` | `box-shadow:var(--g-overlay)` |
|
||||
| 238 | `.mf-tab.active` | `0 0 18px var(--glow-s)` | `box-shadow:var(--g-strong)` |
|
||||
|
||||
### R1c. `platform-neural.css`
|
||||
|
||||
Hat als einzige Datei schon gute Struktur-Overrides (Z. 91 ersetzt Schwarz- durch Grünschatten — Vorbild). Nachziehen: Z. 87 Logo-Drop-Shadow im Light auf `filter:none`; Z. 90 äußeren `0 0 13px`-Glow der aktiven Nav im Light streichen (Inset-Ring reicht). Die `--glow-s`-Kartenwerte (Z. 41–49/99–107) bleiben — sie speisen nach R1b nur noch Dark-Effekte.
|
||||
|
||||
### R1d. Spiele-CSS + Guide/Widget-CSS migrieren
|
||||
|
||||
Gleiches Muster in allen Dateien; die Spiel-`--glow-s`-Remaps (z. B. `mindmatch.css:364`) bleiben unverändert und speisen die Primitives automatisch mit der Spielfarbe. Betroffene Stellen (Audit-Inventar):
|
||||
|
||||
- **Text-Halos** → `text-shadow:var(--g-text)`: `syntaxsaga.css:40`, `mindmatch.css:53`, `techtacular.css:48`, `devlab.css` (`.dl-tube span`), `devlabgame.css` (Header-Titel), `codeescape.css` (`.esc-brand b`, `.esc-result .score`), `bughunter.css` (`.bh-brand b`, Stamp), `networker.css` (`.nw-brand b`, `.nw-final-score`), `serviceclash.css` (`.sc-brand b`, `.sc-score`), `auth.css:74` (`.auth-head b`), `style.css:89` (falls nach R0 noch vorhanden), Template-`<style>`: `progress.html:14` (Prozentzahl).
|
||||
- **Box-Glows** → `var(--g-strong)`/`var(--g-amb)` nach Gewicht: `syntaxsaga.css:37/70/100/125/136/200/229/233`, `mindmatch.css:40/106/226/237/280/287/299/324`, `techtacular.css:39/75/174/207/299`, `devlab.css:102/158/173`, `devlabgame.css:10/12/23/49/61/70/78/106/108`, `codeescape.css` (Buttons/Dialog/Hero — Hero-`0 0 50px` als Dark-only-Zweizeiler wie `.mf-cardbox`), `codeescape-guide.css` (`.esc-how .active`, `.esc-door.ready`, `.esc-input.bad`), `bughunter.css`, `networker.css`, `serviceclash.css` (`.sc-meter i`), `fluxtimer.css:6/7`, `accessibility.css:29`, `accessibility-placement.css:2/16`, `auth.css:14/34/88/98` (`.neon-btn` inkl.: den `::before`-Conic-Blur-Rotor Z. 95/99 im Light per `display:none`).
|
||||
- **Fokus-Ringe** → `var(--g-focus)`: `syntaxsaga.css:160`, `devlabgame.css:35/39`, CodeEscape-Inputs.
|
||||
- **Schwarz-Schatten** → `var(--g-overlay)` bzw. warm getönt: `game-guide.css:72`, `accessibility.css:30`, `accessibility-placement.css:17`, `fluxtimer.css:8` (+ Z. 14 `inset`-Schwarz auf hellem Display im Light entfernen), `serviceclash.css` (`.sc-card`), `bughunter.css` (Drawer, dort auch die im Light unsichtbaren `rgba(255,255,255,.09–.16)`-Inset-Highlights auf Light-Varianten umstellen).
|
||||
- **Scrims** (funktional, bleiben dunkel, aber vereinheitlichen): `mindmatch.css:309`, `techtacular.css:192`, `syntaxsaga.css:216`, `devlabgame.css:104` — im Light auf das Muster `game-guide.css:138` (`rgba(15,23,42,.5)`) wechseln, damit sich nicht zusätzlich der warme `--bg-glow`-Radial über den Scrim legt (`var(--bg-glow)` aus den Scrim-Deklarationen entfernen).
|
||||
- **Hint-Glow** `game-guide.css:155/160–161` (`#ffd54a`): Light-Override `[data-theme="light"] .mm-card.hinted { box-shadow:0 0 0 2px #8a6a06, 0 4px 12px rgba(67,48,26,.18); filter:none; }` (Warn-Ton aus dem Light-Block).
|
||||
|
||||
### R1e. Chart/Dashboard-`<style>`-Blöcke
|
||||
|
||||
`dashboard.html:14/16/18/20/29/31/43/46/171`, `achievements.html:12–16`, `admin/dashboard.html:17–19`, `impressum.html:12`, `profile.html:11`: alle `text-shadow`/`drop-shadow`/`0 0 Npx`-Vorkommen auf die Primitives umstellen (Suchmuster: `grep -n "glow\|text-shadow\|drop-shadow" RES\templates -r`).
|
||||
|
||||
---
|
||||
|
||||
## AP R2 — Kaputte Light-Stellen (kritische Punkte erledigt 2026-07-22)
|
||||
|
||||
### R2a. Login/Registrierung: Overlay weiß-auf-weiß (kritisch)
|
||||
|
||||
Ursache: `auth.css:123–127` setzt für Light `#fff`-Text/weiße Ghost-Border (Annahme: kräftiges Orange-Panel), aber auf der Plattform übersteuert `platform-neural.css:94` den Overlay-Grund mit Fast-Weiß. Fix in `platform-neural.css` (Light-Sektion, höhere Spezifität durch `.sn-platform` gewinnt gegen auth.css):
|
||||
|
||||
```css
|
||||
html[data-theme="light"].sn-platform body .overlay-panel h3 { color:#10291f; text-shadow:none; }
|
||||
html[data-theme="light"].sn-platform body .overlay-panel p { color:#3c554a; }
|
||||
html[data-theme="light"].sn-platform body .ghost-btn { color:#167a52; border-color:rgba(22,122,82,.55); }
|
||||
html[data-theme="light"].sn-platform body .ghost-btn:hover { background:rgba(22,122,82,.10); }
|
||||
```
|
||||
|
||||
### R2b. SyntaxSaga: Bühne bleibt schwarz (kritisch)
|
||||
|
||||
`syntaxsaga.css:250–251` hardcodet `#07100b` + grüne Scanlines. Light-Block ergänzen (Palette: helles Bühnen-Grün zur Light-`--ss-accent` `#277d49`):
|
||||
|
||||
```css
|
||||
[data-theme="light"] body.ss-body {
|
||||
background:radial-gradient(1100px 500px at 50% -10%, rgba(39,125,73,.08), transparent 60%), #eef6f0;
|
||||
}
|
||||
[data-theme="light"] body.ss-body::before {
|
||||
background:repeating-linear-gradient(0deg, rgba(16,64,40,.05) 0 1px, transparent 1px 4px);
|
||||
}
|
||||
```
|
||||
|
||||
### R2c. CodeEscape: Terminal-Eingaben & Dark-Inks (kritisch)
|
||||
|
||||
- `.esc-input, .esc-select` (`#080604`): Light-Override `[data-theme="light"] .esc-input, [data-theme="light"] .esc-select { background:#fffaf0; color:var(--text); border-color:var(--border); }` — Dark behält den Terminal-Look.
|
||||
- Hardcodierte Dark-Inks auf Akzentflächen durch das Token ersetzen (funktioniert in beiden Themes): `.esc-btn:hover`/`.primary` `#160d00` → `var(--accent-ink)`; `codeescape-guide.css` `.esc-fragment small` `#160d00` → `var(--accent-ink)`; `.esc-how .done b` `#06120a` → `var(--accent-ink)`.
|
||||
- Tote Scanlines `rgba(255,255,255,.018)` (`codeescape.css`, `theme.css:138`): im Light-Override auf `rgba(67,48,26,.04)` drehen oder streichen.
|
||||
|
||||
### R2d. NetWorker-Guide: dunkles Panel (kritisch)
|
||||
|
||||
`networker-guide.css` (`.nw-guide` `color-mix(#0b2540 74%, var(--surface))`): Light-Block ergänzen:
|
||||
|
||||
```css
|
||||
[data-theme="light"] .nw-guide { background:var(--surface); border-color:#d3e2f0; box-shadow:var(--g-soft); }
|
||||
```
|
||||
|
||||
(Textfarben laufen dort über `var(--muted)`/`var(--text)` und stimmen dann automatisch.)
|
||||
|
||||
### R2e. Farbenblind-Palette × Light (kritisch)
|
||||
|
||||
`accessibility.css:15–23` erzwingt helle Okabe-Ito-Akzente per `!important` — auf weißen Karten < 1.5:1. Neuen Satz `html[data-theme="light"][data-mf-color="safe"]` mit **abgedunkelten** Okabe-Ito-Entsprechungen ergänzen (gleiche Zuordnung Spiel→Farbfamilie wie im Dark-Satz, Werte rechnerisch ≥ 4.5:1 auf Weiß, beim Umsetzen mit Kontrast-Tool gegenprüfen):
|
||||
|
||||
| Okabe-Ito (dark) | Light-safe |
|
||||
|---|---|
|
||||
| `#e69f00` Orange | `#8a5f00` |
|
||||
| `#56b4e9` Himmelblau | `#19607f` |
|
||||
| `#009e73` Grün | `#007054` |
|
||||
| `#f0e442` Gelb | `#7a6a00` |
|
||||
| `#0072b2` Blau | `#0072b2` (bleibt) |
|
||||
| `#d55e00` Vermillion | `#a94500` |
|
||||
| `#cc79a7` Purpur | `#9c4f79` |
|
||||
|
||||
Die zugehörigen Glow-Alphas entfallen im Light automatisch über die Primitives (R1).
|
||||
|
||||
### R2f. Kleinere Light-Fixes
|
||||
|
||||
1. **Cancel-Hover** `mindmatch.css:128–129`, `techtacular.css:94` (`#fff` auf Blassrosa): Light-Override `[data-theme="light"] .mm-cancel:hover, …` → `background:var(--danger); color:#fff;` (Light-Danger `#be123c`, 7:1). Dark bleibt unverändert.
|
||||
2. **A11y-Widget-Inks:** `accessibility-placement.css:7–15` hardcodet dunkle `--mf-a11y-ink`-Werte — im Light sind die Akzente selbst dunkel. Eine Regel ergänzt: `html[data-theme="light"] body { --mf-a11y-ink:#fff; }` (gewinnt per Spezifität gegen die Skin-Zuweisungen). Panel-Kopf optional angleichen: `[data-theme="light"] .mf-a11y-head { background:var(--surface-2); color:var(--heading); }`.
|
||||
3. **finalflux-Altlast:** `finalflux.css:345` (`#0b0d1b`-Hardcode) funktioniert nur über die fragile `background:transparent`-Kaskade aus `platform-neural.css:61`. Hardcode durch `var(--bg)`-basierte Deklaration ersetzen, damit die Seite auch ohne `sn-platform` beide Themes überlebt.
|
||||
|
||||
**Erledigt:** Login-Overlay, SyntaxSaga-Bühne, CodeEscape-Eingaben und Akzent-Inks,
|
||||
NetWorker-Guide, Farbenblind-Palette für Light Mode, A11y-Widget-Inks sowie der
|
||||
FinalProtocol-Hintergrund. Die noch offenen kleineren Cancel-Hover- und Detaileffekte
|
||||
werden zusammen mit der vollständigen CSS-Migration aus R1 abgeschlossen.
|
||||
|
||||
---
|
||||
|
||||
## AP R3 — Theme-Bootstrap (erledigt 2026-07-22)
|
||||
|
||||
In `RES\static\js\theme.js`:
|
||||
|
||||
1. **Erledigt:** `prefers-color-scheme` wird verwendet, wenn noch keine lokale Auswahl gespeichert ist.
|
||||
2. **Erledigt:** `theme-color` wird beim initialen Anwenden und Umschalten auf `#0a0806` bzw. `#fbf6ec` aktualisiert.
|
||||
3. **Erledigt:** `color-scheme` wird im Dark-/Light-Tokenblock gesetzt.
|
||||
4. **Teilweise erledigt:** geänderte FinalProtocol-Assets tragen neue Versionen; die übrigen Template-Referenzen werden bei ihrer jeweiligen Migration angehoben.
|
||||
|
||||
---
|
||||
|
||||
## AP R4 — Scroll: Ergebnisanzeigen & Ansichten starten oben (weitgehend erledigt 2026-07-22)
|
||||
|
||||
Es gibt projektweit keinen Scroll-Reset. Drei Schichten (zentraler Helper + gezielte Aufrufe), Chat-Autoscroll `neuralnet.js:84` bleibt unangetastet:
|
||||
|
||||
### R4a. Helper in `theme.js` (läuft auf jeder Seite)
|
||||
|
||||
```js
|
||||
window.MindfluxUX = {
|
||||
toTop: function () { window.scrollTo(0, 0); },
|
||||
focusQuiet: function (el) { if (el) el.focus({ preventScroll: true }); }
|
||||
};
|
||||
```
|
||||
|
||||
**Umsetzungshinweis:** Der zentrale Helper ist vorhanden. Die vier SPA-Spiele setzen
|
||||
`history.scrollRestoration` nur auf Spielrouten auf `manual`; ein zentraler,
|
||||
ereignisbasierter Übergangs-Reset deckt Ansichtswechsel ab und lässt Antwort- bzw.
|
||||
Zeilenfeedback unverändert. FinalProtocol, SyntaxSaga, DevLab, MindMatch und
|
||||
TechTacular haben die zusätzlich beschriebenen gezielten Resets/Fokusoptionen.
|
||||
**Offen für die visuelle E2E-Prüfung:** die asynchron gerenderten Ergebniswechsel von
|
||||
CodeEscape, NetWorker und BugHunter müssen im Browser gegen die Scroll-Checkliste
|
||||
validiert und bei Bedarf direkt in den jeweiligen Render-Funktionen präzisiert werden.
|
||||
|
||||
### R4b. SPA-Spiele — Reset bei jedem Ansichtswechsel
|
||||
|
||||
Muster pro Spiel: Mini-Wrapper `function setView(html){ view.innerHTML = html; window.scrollTo(0,0); }`, alle Render-Funktionen darüber. Zusätzlich **nur in diesen vier Skripten** beim Init: `if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; window.scrollTo(0,0);` — verhindert, dass der Browser nach Reload/Back die alte, untere Position auf den neu aufgebauten Katalog anwendet. (Nicht global in `theme.js`: auf Lernseiten/Dashboard ist die Back-Position erwünscht.)
|
||||
|
||||
| Spiel | Funktionen |
|
||||
|---|---|
|
||||
| BugHunter (`bughunter.js`) | `renderBriefing` :29, `renderHunt` :36 (nur bei Fallwechsel, nicht bei Zeilen-Re-Render), `showDebrief` :82, `renderResult` :88 |
|
||||
| CodeEscape (`codeescape.js`) | `renderLobby` :6, `renderRoom` :8, `renderResult` :17 |
|
||||
| NetWorker (`networker.js`) | `renderCatalog` :3, `renderIntro` :5, `renderLevel` :6, `finish` :11 |
|
||||
| ServiceClash (`serviceclash.js`) | `renderCatalog` :2, `renderRun` :4 (**nur bei Ticket-/Indexwechsel**, nicht beim Feedback-Re-Render), `finish` :9 |
|
||||
|
||||
### R4c. FinalProtocol-Quiz (`finalflux-test.js`)
|
||||
|
||||
- `finishIfDone()` Z. 101–104: nach `testResult.hidden = false` → `elements.testResult.scrollIntoView({ block: 'start' });` (Auswertung beginnt oben im Blick, statt dass der Viewport ans geclampte Seitenende fällt).
|
||||
- `restartTest()` Z. 151–158: nach dem Wiedereinblenden → `elements.testRoot.scrollIntoView({ block: 'start' });`
|
||||
|
||||
### R4d. Fokus ohne Scroll-Sprung
|
||||
|
||||
- `syntaxsaga.js`: `renderMission()` — `window.scrollTo(0,0)` vor Z. 209, dann Z. 210 `first.focus({preventScroll:true})`. (`nextBtn.focus()` Z. 297 bleibt — Nutzeraktion am selben Ort.)
|
||||
- `devlabgame.js`: `renderTask()` Z. 243 — `window.scrollTo(0,0)` am Ende; Z. 83 und Z. 233 `focus({preventScroll:true})`.
|
||||
- `codeescape.js:8/15` bleiben (Fokus am Türterminal ist dort korrekt).
|
||||
|
||||
### R4e. Niedrige Prio (nur Mobile relevant)
|
||||
|
||||
`mindmatch.js` `newGame()` :332 und `techtacular.js` `newGame()` :265: `window.scrollTo(0,0)` — Desktop-Bodys sind `overflow:hidden`, mobil scrollt die Seite.
|
||||
|
||||
---
|
||||
|
||||
## AP R5 — Carry-over: Rest aus Refine 1 + Zusatzfunde der Verifikation
|
||||
|
||||
Aus Refine 1 (dort AP 7 / offen):
|
||||
1. **Bean Validation:** `@Valid` + Constraints auf alle Request-DTOs — `BugHunterApiController:17–18`, `DevLabApiController:37/43`, `SyntaxSagaApiController:32/38`, `TechtacularApiController:50/75`, `CodeEscapeApiController` (4 DTOs), `NetworkerApiController` (2), `ServiceClashApiController` (2); `NeuralNetApiController:14/19` `Map<String,String>` → typisierte DTOs.
|
||||
2. **Spring Boot** 3.4.4 → aktuelles 3.4.x-Patch (`pom.xml:11`), Testlauf.
|
||||
3. **CSRF:** Blanket-Exemption `/api/**` (`SecurityConfig:46`) durch Cookie-basierte Tokens ersetzen.
|
||||
4. **Web-Layer-Test** für `GET /profile/export`.
|
||||
|
||||
Zusatzfunde (neu):
|
||||
5. **`UserController:25/45`** bindet die **Entity `User`** direkt als `@RequestBody` ohne Validierung → auf DTO + `@Valid` umstellen (gehört in die Runde von Punkt 1).
|
||||
6. **5 Admin-Templates ohne Footer:** `admin\bughunter.html`, `admin\bughunter-hunt.html`, `admin\codeescape.html`, `admin\codeescape-room.html`, `admin\codeescape-scenario.html` → `th:replace="~{fragments/layout :: footer}"` ergänzen.
|
||||
7. **HSTS** explizit für Prod konfigurieren statt Spring-Default (`SecurityConfig:56–57`).
|
||||
8. Optional (unverändert Phase 2): BugHunter progressive Hinweise (Refine 1, AP 4d).
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
1. **AP R0** Aufräumen → 2. **AP R1** Primitives + Migration (theme.css zuerst, dann platform-neural, dann Spiele-CSS, dann Templates) → 3. **AP R2** kaputte Light-Stellen → 4. **AP R3** Bootstrap → 5. **AP R4** Scroll (unabhängig von 1–4, parallelisierbar) → 6. **AP R5** Carry-over → 7. Gesamtverifikation. STYLEGUIDE.md (Abschnitt „Glow, Schatten & Radien" → Primitives-Tabelle) und CHANGELOG nach R1–R3 nachziehen.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- **Build:** `mvn test` bleibt grün (54/54); App-Boot gegen Wegwerf-Postgres wie gehabt.
|
||||
- **Grep-Gates nach R1:** `grep -rnE "0 0 [0-9]+px var\(--" RES\static\css` → Treffer nur noch in den Primitive-Definitionen von `theme.css`; `grep -rn "text-shadow:0 0" RES\static\css RES\templates` → nur `--g-text`-Definition; `grep -rn "rgba(0,0,0,.5" RES\static\css` → nur Scrims/`--g-overlay`-Definition.
|
||||
- **Visuell (hell UND dunkel):** Dashboard (Karten + Hover), Login/Registrierung (insb. Overlay-Panel!), Settings, Progress, Soziales, NeuralNet, alle 9 Spiele (insb. SyntaxSaga-Bühne, CodeEscape-Eingaben, NetWorker-Guide), FinalProtocol-Lernseite + Quiz, A11y-Panel. Dark Mode muss vor/nach R1 visuell deckungsgleich sein (Stichprobe per Screenshot-Vergleich).
|
||||
- **A11y-Kombinationen:** Light × Hochkontrast (bleibt bewusst dunkel), Light × Farbenblind (neue dunkle Okabe-Ito-Werte sichtbar, Kontrast-Tool ≥ 4.5:1), Light × Mono.
|
||||
- **Native Controls:** Light Mode → helle Scrollbars und helle `<select>`-Dropdowns (Chrome + Firefox); mobile Adressleiste folgt `theme-color`.
|
||||
- **Erstbesuch:** localStorage leeren, OS auf Light stellen → Seite startet hell; OS Dark → dunkel; Umschalter persistiert weiter.
|
||||
- **Scroll:** je SPA-Spiel: langes Board nach unten scrollen → Aktion abschließen → Ergebnis-/Folge-View beginnt **oben**; ServiceClash-Antwort-Feedback und BugHunter-Zeilenauswahl dürfen **nicht** nach oben springen; FinalProtocol: langen Lernartikel bis zum Quiz scrollen, Quiz abschließen → Auswertung oben im Viewport; Reload mitten im Spiel → Katalog startet oben; SyntaxSaga/DevLab: Missions-/Taskwechsel startet oben, Fokus springt nicht; NeuralNet-Chat scrollt weiterhin ans Ende.
|
||||
|
||||
## Offene Punkte (unverändert aus Refine 1, vor Launch)
|
||||
|
||||
- Echte Impressumsdaten (`impressum.html:44–52`) + §1 Datenschutzerklärung.
|
||||
- Support-/Feedback-Adressen bestätigen (`fragments/layout.html:88–89`).
|
||||
|
||||
## Nachtrag 2026-07-22 — E-Mail-Diagnose (erledigt)
|
||||
|
||||
- Der Versand von Bestätigungs- und Reset-Links protokolliert nun korrelierbare
|
||||
Ereignisse: Token-Ausstellung, SMTP-Übergabe begonnen/angenommen/fehlgeschlagen,
|
||||
erneute Anforderung und erfolgreiche Verifikation. Logs enthalten User-/Token-IDs
|
||||
sowie einen gekürzten E-Mail-Fingerprint, niemals den Link oder das Roh-Token.
|
||||
- Wichtig für die Analyse: `SMTP_SUBMISSION_ACCEPTED` bedeutet nur, dass der
|
||||
konfigurierte SMTP-Server die Nachricht angenommen hat. Spamfilter, Bounces und
|
||||
die finale Postfachzustellung müssen zusätzlich im Mailserver bzw. über DSN geprüft
|
||||
werden.
|
||||
- Der Registrierungsabschluss meldet keinen Versand-Erfolg mehr, falls der gerade
|
||||
angelegte Benutzer beim Versand nicht geladen werden kann oder bereits als bestätigt
|
||||
vorliegt; der Grund erscheint als `VERIFICATION_SKIPPED` im Betriebslog.
|
||||
|
||||
## Nachtrag 2026-07-22 — Nutzerfeedback (erledigt)
|
||||
|
||||
- **TechTacular:** Unbeantwortete Fragen lassen sich per ×, Klick auf den Hintergrund
|
||||
oder Escape schließen und später erneut öffnen; sie werden dabei weder gewertet noch
|
||||
als beantwortet markiert.
|
||||
- **Produktverständnis:** Vor dem Login erklärt eine sichtbare Einordnung Zielgruppe,
|
||||
Lernformen und Nutzen der Plattform. Das Dashboard wiederholt den Zweck mit einem
|
||||
kompakten „Was du hier machst“-Abschnitt.
|
||||
@@ -19,7 +19,8 @@
|
||||
<description>Mindflux – Lernplattform zur Prüfungsvorbereitung</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<!-- Angeglichen an die Dockerfile-Laufzeit (Temurin 21) -->
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -53,10 +54,22 @@
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Passwort-Verschlüsselung (BCrypt) -->
|
||||
<!-- Bean Validation (@Valid, @NotBlank, @Email ...) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SMTP-Versand für Bestätigungs- und Passwort-Reset-Mails -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Health/Monitoring (Container-Healthcheck auf /actuator/health) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL Datenbanktreiber -->
|
||||
@@ -91,6 +104,12 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Anleitung: DB-Passwort rotieren & Gitea-Secret anlegen
|
||||
|
||||
> **Status 2026-07-12:** In Produktion abgeschlossen. Das Secret ist angelegt, die DB-Rolle
|
||||
> wurde rotiert und Workflow 43 erfolgreich ausgeführt. Diese Anleitung bleibt als Betriebs-
|
||||
> und Notfalldokumentation erhalten.
|
||||
|
||||
**Warum?** Das alte Datenbank-Passwort `mindflux_pass` stand im Klartext in `docker-compose.yml` – und bleibt über die Git-Historie für immer einsehbar. Es gilt daher als **kompromittiert** und muss ersetzt werden.
|
||||
|
||||
Die Technik ist bereits vorbereitet:
|
||||
- `docker-compose.yml` liest das Passwort jetzt aus `${MINDFLUX_DB_PASSWORD}` (kein Klartext mehr im Repo).
|
||||
- Der Gitea-Workflow (`.gitea/workflows/mindflux.yml`) reicht das Secret `MINDFLUX_DB_PASSWORD` beim Deploy durch – genau wie die Admin-Passwörter.
|
||||
|
||||
**Wichtig:** Bis das Secret angelegt ist, bricht das nächste Deployment mit der Meldung
|
||||
`MINDFLUX_DB_PASSWORD muss als Secret gesetzt sein` ab. So wird kein App-Container mit
|
||||
einem leeren Datenbank-Passwort gestartet. Die folgenden 5 Schritte dauern ca. 5 Minuten.
|
||||
|
||||
---
|
||||
|
||||
## Schritt 1: Starkes Passwort erzeugen
|
||||
|
||||
PowerShell (Windows):
|
||||
|
||||
```powershell
|
||||
-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
|
||||
```
|
||||
|
||||
Oder auf dem Server (Linux):
|
||||
|
||||
```bash
|
||||
openssl rand -base64 24
|
||||
```
|
||||
|
||||
Das Ergebnis kopieren – es wird in Schritt 2 **und** Schritt 3 gebraucht. Nirgendwo einchecken, nicht in Dateien im Repo speichern.
|
||||
|
||||
## Schritt 2: Gitea-Secret anlegen
|
||||
|
||||
1. Gitea öffnen → das **mindflux-Repository** aufrufen.
|
||||
2. **Einstellungen** → **Actions** → **Secrets** (derselbe Ort, an dem schon `MINDFLUX_ADMIN_PASSWORD` und `MINDFLUX_ADMIN2_PASSWORD` liegen).
|
||||
3. **„Neues Secret hinzufügen"**:
|
||||
- **Name:** `MINDFLUX_DB_PASSWORD` (exakt so – der Workflow referenziert diesen Namen)
|
||||
- **Wert:** das neue Passwort aus Schritt 1
|
||||
4. Speichern.
|
||||
|
||||
## Schritt 3: Passwort in der Produktions-Datenbank ändern
|
||||
|
||||
Der Gitea-Workflow gleicht das Passwort vor jedem Deployment automatisch mit dem Secret ab.
|
||||
Beim ersten Deployment nach der Rotation führt der Runner das entsprechende `ALTER USER` aus;
|
||||
ein separater SSH-Zugang ist deshalb nicht erforderlich.
|
||||
|
||||
Der folgende Befehl ist nur noch ein manueller Notfallweg, falls der Runner-Schritt einmal
|
||||
nicht verfügbar sein sollte:
|
||||
|
||||
Auf dem Server (dort, wo der Postgres-Container läuft – er heißt im Backend-Netz `postgres`):
|
||||
|
||||
```bash
|
||||
docker exec -it postgres psql -U mindflux -d mindfluxdb -c "ALTER USER mindflux WITH PASSWORD 'HIER_DAS_NEUE_PASSWORT';"
|
||||
```
|
||||
|
||||
> Falls der Container anders heißt: mit `docker ps` nachsehen. DB-User ist `mindflux`, Datenbank `mindfluxdb` (siehe `docker-compose.yml`).
|
||||
>
|
||||
> Achtung bei Sonderzeichen: Das Passwort steht in einfachen Anführungszeichen im SQL – ein `'` im Passwort müsste verdoppelt werden (`''`). Am einfachsten: Passwort ohne `'` erzeugen (die Generatoren aus Schritt 1 tun das).
|
||||
|
||||
## Schritt 4: Deployment auslösen
|
||||
|
||||
Einfach einen Push auf `main` machen (oder den Workflow in Gitea manuell erneut ausführen). Der Deploy-Schritt exportiert das Secret und startet den Container neu:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml pull
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
> Zwischen Schritt 3 und dem Neustart gibt es ein kurzes Fenster, in dem der noch laufende alte Container seine bestehenden DB-Verbindungen behält – das ist unkritisch. Sollte die App nach dem Neustart nicht hochkommen, prüfen ob Secret-Wert (Schritt 2) und DB-Passwort (Schritt 3) wirklich identisch sind, dann erneut `docker compose up -d`.
|
||||
|
||||
## Schritt 5: Verifizieren
|
||||
|
||||
```bash
|
||||
docker ps # Container "mindflux" muss nach ~40 s "healthy" zeigen (Actuator-Healthcheck)
|
||||
docker logs mindflux --tail 30 # keine "password authentication failed"-Fehler
|
||||
```
|
||||
|
||||
Danach im Browser einloggen – wenn Login und Dashboard funktionieren, ist die Rotation abgeschlossen. ✅
|
||||
|
||||
---
|
||||
|
||||
## Hinweise
|
||||
|
||||
- **Lokale Entwicklung:** lokal wird das Passwort über die Umgebungsvariable `MINDFLUX_DB_PASSWORD` bzw. eine (nicht eingecheckte) `.env`-Datei gesetzt (`application.properties` importiert sie automatisch). Die lokale Datenbank kann ein eigenes, anderes Passwort haben.
|
||||
- **Warum die Historie nicht bereinigen?** Das alte Passwort ließe sich nur per Force-Push/History-Rewrite aus Git entfernen – aufwendig und riskant. Rotation ist der richtige Weg: Das alte Passwort darf ruhig bekannt sein, wenn es nirgends mehr gilt.
|
||||
- Die Postgres-Instanz ist nur im internen `backend_network` erreichbar (kein öffentlicher Port) – die Rotation ist trotzdem Pflicht (Defense in Depth).
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.example.mindflux;
|
||||
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AppStartCheck implements CommandLineRunner {
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
System.out.println("✅ App läuft. Sicherheitsmodul sollte aktiv sein.");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -11,6 +13,8 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
@Configuration
|
||||
public class AdminSetup {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AdminSetup.class);
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_USERNAME:admin}")
|
||||
private String adminUsername;
|
||||
|
||||
@@ -44,7 +48,7 @@ public class AdminSetup {
|
||||
return; // Admin-Slot nicht konfiguriert -> still überspringen.
|
||||
}
|
||||
if (password == null || password.isBlank()) {
|
||||
System.out.println("Kein Bootstrap-Admin erstellt: Passwort für '" + username + "' ist nicht gesetzt.");
|
||||
log.warn("Kein Bootstrap-Admin erstellt: Passwort für '{}' ist nicht gesetzt.", username);
|
||||
return;
|
||||
}
|
||||
if (userRepo.findByUsername(username).isEmpty()) {
|
||||
@@ -54,7 +58,7 @@ public class AdminSetup {
|
||||
admin.setPassword(passwordEncoder.encode(password));
|
||||
admin.setRole("ADMIN");
|
||||
userRepo.save(admin);
|
||||
System.out.println("Bootstrap-Admin erstellt: " + username);
|
||||
log.info("Bootstrap-Admin erstellt: {}", username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.service.LoginAttemptService;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Füttert den Login-Rate-Limiter mit den Auth-Ereignissen von Spring Security. */
|
||||
@Component
|
||||
public class AuthenticationEventListener {
|
||||
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
|
||||
public AuthenticationEventListener(LoginAttemptService loginAttemptService) {
|
||||
this.loginAttemptService = loginAttemptService;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onFailure(AuthenticationFailureBadCredentialsEvent event) {
|
||||
loginAttemptService.loginFailed(event.getAuthentication().getName());
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onSuccess(AuthenticationSuccessEvent event) {
|
||||
loginAttemptService.loginSucceeded(event.getAuthentication().getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.*;
|
||||
import com.example.mindflux.repository.BugHunterHuntRepository;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import org.slf4j.*;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Erstbefüllung der Fallakten. Der vollständige Bug-Knoten bleibt als serverseitiger Payload erhalten. */
|
||||
@Component
|
||||
public class BugHunterSeeder implements CommandLineRunner {
|
||||
private static final Logger log = LoggerFactory.getLogger(BugHunterSeeder.class);
|
||||
private final BugHunterHuntRepository hunts; private final ObjectMapper mapper;
|
||||
public BugHunterSeeder(BugHunterHuntRepository hunts, ObjectMapper mapper) { this.hunts = hunts; this.mapper = mapper; }
|
||||
@Override @Transactional public void run(String... args) {
|
||||
try {
|
||||
JsonNode root = mapper.readTree(new ClassPathResource("bughunter/content.json").getInputStream()); int order = (int) hunts.count(), count = 0, added = 0;
|
||||
for (JsonNode node : root.path("hunts")) {
|
||||
if (hunts.existsBySlug(node.path("slug").asText())) continue;
|
||||
BugHunterHunt hunt = new BugHunterHunt(node.path("slug").asText(), node.path("title").asText(), node.path("language").asText("Java"), node.path("difficulty").asText("junior"), node.path("examScope").asText("AP1"), node.path("description").asText(""), node.path("briefing").asText(""), node.path("consoleOutput").asText(""), order++);
|
||||
int fileOrder = 0; for (JsonNode file : node.path("files")) hunt.addFile(new BugHunterFile(file.path("filename").asText(), file.path("content").asText(), fileOrder++));
|
||||
int bugOrder = 0; for (JsonNode bug : node.path("bugs")) { String payload = mapper.writeValueAsString(bug); if (payload.length() <= 8000) { hunt.addBug(new BugHunterBug(bug.path("points").asInt(100), bugOrder++, payload)); count++; } }
|
||||
if (!hunt.getFiles().isEmpty() && !hunt.getBugs().isEmpty()) { hunts.save(hunt); added++; }
|
||||
}
|
||||
log.info("BugHunter: {} neue Fallakten / {} Bugs geladen.", added, count);
|
||||
} catch (Exception ex) { log.error("BugHunter-Seed fehlgeschlagen", ex); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.config; import com.example.mindflux.model.*;import com.example.mindflux.repository.EscapeScenarioRepository;import com.fasterxml.jackson.databind.*;import org.slf4j.*;import org.springframework.boot.CommandLineRunner;import org.springframework.core.io.ClassPathResource;import org.springframework.stereotype.Component;import org.springframework.transaction.annotation.Transactional;
|
||||
@Component public class CodeEscapeSeeder implements CommandLineRunner {private static final Logger log=LoggerFactory.getLogger(CodeEscapeSeeder.class);private final EscapeScenarioRepository repo;private final ObjectMapper mapper;public CodeEscapeSeeder(EscapeScenarioRepository r,ObjectMapper m){repo=r;mapper=m;}@Override @Transactional public void run(String...a){if(repo.count()>0)return;try{JsonNode root=mapper.readTree(new ClassPathResource("codeescape/content.json").getInputStream());int si=0,pc=0;for(JsonNode sn:root.path("scenarios")){EscapeScenario s=new EscapeScenario(sn.path("slug").asText(),sn.path("title").asText(),sn.path("difficulty").asText(),sn.path("examScope").asText("AP1"),sn.path("description").asText(),sn.path("timeLimitSeconds").asInt(900),si++);int ri=0;for(JsonNode rn:sn.path("rooms")){EscapeRoom r=new EscapeRoom(rn.path("title").asText(),rn.path("intro").asText(),rn.path("doorPrompt").asText(),rn.path("doorCode").asText(),rn.path("successText").asText(),ri++);int pi=0;for(JsonNode pn:rn.path("puzzles")){r.addPuzzle(new EscapePuzzle(pn.path("type").asText(),pn.path("prompt").asText(),pn.path("points").asInt(100),pn.path("fragment").asText(),pi++,mapper.writeValueAsString(pn.path("payload"))));pc++;}s.addRoom(r);}repo.save(s);}log.info("CodeEscape: {} Szenarien / {} Rätsel geladen.",si,pc);}catch(Exception e){log.error("CodeEscape-Seed fehlgeschlagen",e);}}}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.DevLabGame;
|
||||
import com.example.mindflux.model.DevLabTask;
|
||||
import com.example.mindflux.repository.DevLabGameRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Befüllt das Dev Lab einmalig (wenn leer) aus devlab/content.json. Jede Aufgabe wird mit
|
||||
* ihrem Typ + vollständigem JSON-Payload gespeichert; der Service liest daraus.
|
||||
*/
|
||||
@Component
|
||||
public class DevLabSeeder implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DevLabSeeder.class);
|
||||
|
||||
private static final int MAX_PAYLOAD = 8000;
|
||||
|
||||
private final DevLabGameRepository gameRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public DevLabSeeder(DevLabGameRepository gameRepository, ObjectMapper objectMapper) {
|
||||
this.gameRepository = gameRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (gameRepository.count() > 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(new ClassPathResource("devlab/content.json").getInputStream());
|
||||
JsonNode games = root.get("games");
|
||||
if (games == null || !games.isArray()) {
|
||||
log.warn("Dev-Lab-Seed: content.json ohne 'games'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> used = new HashSet<>();
|
||||
int order = 1;
|
||||
int gameCount = 0;
|
||||
int taskCount = 0;
|
||||
int skipped = 0;
|
||||
|
||||
for (JsonNode gNode : games) {
|
||||
String title = gNode.path("title").asText("");
|
||||
JsonNode tasks = gNode.get("tasks");
|
||||
if (title.isBlank() || tasks == null || !tasks.isArray()) {
|
||||
continue;
|
||||
}
|
||||
String base = gNode.hasNonNull("slug") ? gNode.get("slug").asText() : title;
|
||||
DevLabGame game = new DevLabGame(
|
||||
uniqueSlug(base, used),
|
||||
trim(title, 200),
|
||||
trim(textOrNull(gNode, "language"), 50),
|
||||
trim(textOrNull(gNode, "path"), 50),
|
||||
trim(textOrNull(gNode, "description"), 1000),
|
||||
order, true);
|
||||
|
||||
int sort = 0;
|
||||
for (JsonNode t : tasks) {
|
||||
String type = t.path("type").asText("");
|
||||
String prompt = t.path("prompt").asText("");
|
||||
if (type.isBlank() || prompt.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String payload = t.toString();
|
||||
if (payload.length() > MAX_PAYLOAD) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
game.addTask(new DevLabTask(type, trim(prompt, 300), t.path("points").asInt(100), sort++, payload));
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
if (!game.getTasks().isEmpty()) {
|
||||
gameRepository.save(game);
|
||||
order++;
|
||||
gameCount++;
|
||||
}
|
||||
}
|
||||
log.info("DevLab: {} Spiele / {} Aufgaben geladen{}.", gameCount, taskCount,
|
||||
skipped > 0 ? " (" + skipped + " zu groß, übersprungen)" : "");
|
||||
} catch (Exception e) {
|
||||
log.warn("Dev-Lab-Seed übersprungen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String uniqueSlug(String base, Set<String> used) {
|
||||
String slug = base.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (slug.isBlank()) {
|
||||
slug = "game";
|
||||
}
|
||||
String candidate = slug;
|
||||
int counter = 2;
|
||||
while (used.contains(candidate) || gameRepository.existsBySlug(candidate)) {
|
||||
candidate = slug + "-" + counter++;
|
||||
}
|
||||
used.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String textOrNull(JsonNode node, String field) {
|
||||
if (!node.hasNonNull(field)) {
|
||||
return null;
|
||||
}
|
||||
String value = node.get(field).asText("");
|
||||
return value.isBlank() ? null : value;
|
||||
}
|
||||
|
||||
private String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.*;
|
||||
import com.example.mindflux.repository.NetworkWorldRepository;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import org.slf4j.*;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Component public class NetworkerSeeder implements CommandLineRunner {
|
||||
private static final Logger log=LoggerFactory.getLogger(NetworkerSeeder.class); private final NetworkWorldRepository worlds; private final ObjectMapper mapper;
|
||||
public NetworkerSeeder(NetworkWorldRepository worlds,ObjectMapper mapper){this.worlds=worlds;this.mapper=mapper;}
|
||||
@Override @Transactional public void run(String...args){if(worlds.count()>0)return;try{JsonNode root=mapper.readTree(new ClassPathResource("networker/content.json").getInputStream());int wi=0,li=0;for(JsonNode world:root.path("worlds")){NetworkWorld item=new NetworkWorld(world.path("slug").asText(),world.path("title").asText(),world.path("description").asText(),world.path("difficulty").asText("junior"),world.path("examScope").asText("AP1"),wi++);int order=0;for(JsonNode level:world.path("levels")){String payload=mapper.writeValueAsString(level.path("payload"));if(payload.length()<=8000){item.addLevel(new NetworkLevel(level.path("title").asText(),level.path("type").asText("assign"),level.path("points").asInt(100),order++,payload));li++;}}worlds.save(item);}log.info("NetWorker: {} Welten / {} Level geladen.",wi,li);}catch(Exception e){log.error("NetWorker-Seed fehlgeschlagen",e);}}
|
||||
}
|
||||
@@ -2,15 +2,17 @@ package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import com.example.mindflux.service.LoginAttemptService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.*;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
@@ -19,37 +21,54 @@ public class SecurityConfig {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/login", "/register", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/css/**", "/js/**", "/img/**").permitAll()
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // NEU: Adminbereich absichern
|
||||
.requestMatchers("/", "/login", "/register", "/forgot-password", "/reset-password", "/verify-email", "/verification/resend", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/favicon.ico", "/css/**", "/js/**", "/img/**").permitAll()
|
||||
.requestMatchers("/actuator/health").permitAll() // Container-Healthcheck
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.requestMatchers("/api/users/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/") // login.html liegt auf "/"
|
||||
.loginPage("/") // Login-Formular liegt auf der Startseite (index.html)
|
||||
.loginProcessingUrl("/login")
|
||||
.defaultSuccessUrl("/dashboard", true)
|
||||
.failureUrl("/?error") // zeigt Loginfehler an
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
// Logout-Links in den Templates sind GET -> GET-Logout erlauben, sonst 404/Fehler unter CSRF
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
|
||||
// Logout nur per POST (CSRF-geschützt); die Templates nutzen kleine Formulare.
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/?logout")
|
||||
.permitAll()
|
||||
)
|
||||
.csrf(csrf -> csrf
|
||||
// Spiele-APIs werden per fetch (application/json) ohne Token aufgerufen -> ausnehmen.
|
||||
// Reine JSON-Endpunkte sind nicht über klassische HTML-Formulare CSRF-ausnutzbar.
|
||||
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**")
|
||||
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**", "/api/syntaxsaga/**", "/api/devlab/**", "/api/codeescape/**", "/api/bughunter/**", "/api/serviceclash/**", "/api/networker/**", "/api/neuralnet/**")
|
||||
)
|
||||
.headers(headers -> headers
|
||||
// CSP: pragmatischer Start – alle Assets sind self-hosted (Chart.js lokal
|
||||
// gevendort). 'unsafe-inline' bleibt vorerst nötig, weil Templates Inline-
|
||||
// Styles und einzelne Inline-Skripte enthalten (später auf Nonces umstellbar).
|
||||
.contentSecurityPolicy(csp -> csp.policyDirectives(
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; "
|
||||
+ "img-src 'self' data:; font-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"))
|
||||
.referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
|
||||
// HSTS bleibt auf Spring-Default: wird nur über HTTPS gesendet, greift also
|
||||
// faktisch nur in Produktion (Dev läuft über HTTP).
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserRepository userRepo) {
|
||||
public UserDetailsService userDetailsService(UserRepository userRepo, LoginAttemptService loginAttemptService) {
|
||||
return username -> {
|
||||
User user = userRepo.findByUsername(username)
|
||||
// Brute-Force-Schutz: Nach zu vielen Fehlversuchen vorübergehend sperren.
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
throw new LockedException("Zu viele Fehlversuche – bitte später erneut versuchen.");
|
||||
}
|
||||
|
||||
User user = userRepo.findByUsernameIgnoreCase(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden"));
|
||||
|
||||
return org.springframework.security.core.userdetails.User
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.*;
|
||||
import com.example.mindflux.repository.ServiceClashShiftRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class ServiceClashSeeder implements CommandLineRunner {
|
||||
private final ServiceClashShiftRepository shifts; private final ObjectMapper mapper;
|
||||
public ServiceClashSeeder(ServiceClashShiftRepository shifts,ObjectMapper mapper){this.shifts=shifts;this.mapper=mapper;}
|
||||
@Override @Transactional public void run(String... args) {
|
||||
if(shifts.count()>0)return;
|
||||
seed("fruehschicht","Frühschicht: Stabiler Start","Lerne das Triage-Prinzip: erst Art, dann Dringlichkeit, dann die passende erste Maßnahme.","Junior",0,List.of(
|
||||
t("Druckerwarteschlange brennt","Buchhaltung","Drei Druckaufträge hängen seit dem Neustart fest; niemand kann Rechnungen drucken.","incident","medium","queue",1,100,"Störung: Die Warteschlange zurücksetzen und einen Testdruck starten.","queue","Druckwarteschlange prüfen und bereinigen","restart","Drucker ohne Prüfung neu starten","buy","Sofort ein neues Gerät bestellen"),
|
||||
t("Azubi braucht Zugang","Ausbildung","Die neue Auszubildende startet heute und braucht Zugriff auf das Lernportal.","request","low","access",1,100,"Anfrage: Berechtigung nach Freigabe über den Standardprozess einrichten.","access","Zugang nach Freigabe einrichten","security","Sicherheitsincident ausrufen","ignore","Auf nächste Woche verschieben"),
|
||||
t("WLAN im Besprechungsraum weg","Vertrieb","Seit 20 Minuten ist das Firmen-WLAN im Raum Elbe nicht sichtbar.","incident","medium","diagnose",1,110,"Störung: AP-Status und Stromversorgung prüfen, dann gezielt eskalieren.","diagnose","Access Point und Uplink prüfen","password","Alle WLAN-Passwörter zurücksetzen","replace","Das gesamte WLAN ersetzen"),
|
||||
t("Neue Verteilerliste","Personal","Für das Onboarding wird eine Mailgruppe für zehn neue Kolleg:innen benötigt.","request","low","group",1,100,"Anfrage: Gruppe anhand der genehmigten Liste anlegen und Besitzer benennen.","group","Verteilerliste nach Vorlage anlegen","broadcast","An alle Mitarbeitenden mailen","escalate","Sofort an den CISO eskalieren"),
|
||||
t("Zeiterfassung meldet Fehler","Produktion","Terminals zeigen beim Buchen nur noch „Dienst nicht erreichbar“.","incident","high","escalate",2,130,"Hohe Auswirkung: Incident dokumentieren, Dienst prüfen und an den Bereitschaftsdienst eskalieren.","escalate","Incident eröffnen und Bereitschaft informieren","wait","Bis morgen abwarten","delete","Terminals aus dem Verzeichnis löschen"),
|
||||
t("USB-Tastatur defekt","Empfang","Eine einzelne Tastatur reagiert nicht mehr; Ersatz liegt im Schrank.","request","low","swap",2,90,"Standard-Request: Ersatzgerät ausgeben und Inventar nachführen.","swap","Ersatz ausgeben und Inventar buchen","outage","Großstörung ausrufen","format","Den Empfangs-PC formatieren"),
|
||||
t("Dateiserver langsam","Konstruktion","Große CAD-Dateien öffnen seit einer Stunde extrem langsam, kleinere Dateien funktionieren.","incident","medium","monitor",2,120,"Störung: Auslastung und Netzwerkpfad messen, Befund in das Incident aufnehmen.","monitor","Last und Netzwerkpfad prüfen","reboot","Ohne Befund alle Server neu starten","close","Ticket als erledigt schließen"),
|
||||
t("Meetingraum-Display buchen","Projektbüro","Für morgen soll das Display im Raum Nord für eine Präsentation reserviert werden.","request","low","booking",3,90,"Anfrage: Ressource im Buchungssystem reservieren und bestätigen.","booking","Display im Buchungssystem reservieren","incident","Display als Störung melden","handover","An die Nachtwache delegieren")
|
||||
));
|
||||
seed("tagesschicht","Tagesschicht: Prioritäten unter Druck","Mehr parallele Tickets: Schütze zuerst Menschen, Daten und geschäftskritische Dienste.","Fortgeschritten",1,List.of(
|
||||
t("ERP nicht erreichbar","Finanzen","Alle Standorte melden beim ERP nur noch Zeitüberschreitungen. Rechnungsfreigaben stehen.","incident","high","major",1,150,"Major Incident: Lagebild eröffnen, Eigentümer informieren und Wiederherstellung koordinieren.","major","Major Incident eröffnen und eskalieren","restart","Einzelne Clients neu starten","close","Ohne Prüfung schließen"),
|
||||
t("Neue Software beantragt","Marketing","Ein Team möchte eine bereits freigegebene Bildbearbeitung auf fünf Notebooks installieren.","request","medium","deploy",1,110,"Anfrage: Freigegebenes Paket über die Softwareverteilung bereitstellen.","deploy","Paket über Softwareverteilung ausrollen","admin","Allen lokale Adminrechte geben","ignore","Anfrage ignorieren"),
|
||||
t("Verdächtige Phishing-Mail","Support","Mehrere Mitarbeitende erhielten eine Rechnung von einer ähnlich aussehenden Absenderdomain.","incident","high","contain",1,150,"Security-Incident: Nachricht sichern, Empfänger warnen und den Meldeprozess starten.","contain","Mail sichern, warnen und Security informieren","reply","Auf die Mail antworten","delete","Nur den eigenen Posteingang leeren"),
|
||||
t("VPN für Dienstreise","Vertrieb","Eine Kollegin reist nächste Woche und benötigt den Standard-VPN-Zugang.","request","medium","vpn",1,110,"Anfrage: VPN nach Richtlinie und Freigabe aktivieren, MFA erklären.","vpn","VPN mit MFA nach Richtlinie einrichten","share","Kolleg:innen-Zugang teilen","exception","MFA dauerhaft umgehen"),
|
||||
t("Monitoring meldet Platte voll","Monitoring","Das Monitoring meldet 94 % Belegung auf dem Log-Volume eines Anwendungsservers.","incident","high","capacity",2,140,"Hohe Priorität: Wachstum prüfen, sichere Bereinigung oder Erweiterung planen und Dienst beobachten.","capacity","Belegung prüfen und Kapazität sichern","rmall","Alle Logs blind löschen","silence","Alarm dauerhaft stummschalten"),
|
||||
t("Namensschild-Drucker","Office Management","Für eine Veranstaltung wird ein temporärer Druckerzugang für den Empfang benötigt.","request","low","temporary",2,90,"Anfrage: zeitlich begrenzte Berechtigung dokumentiert einrichten.","temporary","Temporären Zugang dokumentiert einrichten","global","Drucker global freigeben","incident","Als Sicherheitsincident melden"),
|
||||
t("CRM zeigt falsche Kundendaten","Vertrieb","Einige Kundensätze wirken veraltet, während Änderungen anderer Teams sichtbar sind.","incident","medium","investigate",2,120,"Störung: Reproduzierbarkeit, betroffene Datenquelle und letzte Synchronisation prüfen.","investigate","Datenquelle und Synchronisation prüfen","overwrite","Daten ohne Backup überschreiben","wait","Ticket ohne Rückmeldung parken"),
|
||||
t("Projektablage anlegen","PMO","Ein neues Projekt braucht eine Teamablage mit den Rollen Lesen, Bearbeiten und Leitung.","request","medium","roles",3,110,"Anfrage: Ablage nach Berechtigungsvorlage erstellen und Rollen testen.","roles","Ablage mit Rollenmodell erstellen","public","Ablage öffentlich freigeben","owner","Alle zu Eigentümer:innen machen")
|
||||
));
|
||||
seed("bereitschaft","Bereitschaft: Kritische Lage","Für die Abschluss-Schicht kombinierst du Security, Changes und Wiederanlauf unter klarer Kommunikation.","Profi",2,List.of(
|
||||
t("Ransomware-Verdacht","Monitoring","Ein Fileserver erzeugt ungewöhnlich viele umbenannte Dateien; EDR meldet verdächtige Prozesse.","incident","high","isolate",1,160,"Security-Incident: System isolieren, Beweise sichern und Incident-Response alarmieren.","isolate","System isolieren und Incident Response alarmieren","reboot","Server sofort neu starten","delete","Betroffene Dateien ohne Sicherung löschen"),
|
||||
t("Firewall-Change vor Wartung","Netzwerk","Für ein genehmigtes Wartungsfenster muss eine dokumentierte Regel temporär geöffnet werden.","request","high","change",1,140,"Change: Genehmigung, Zeitfenster, Rückfallplan und Protokoll vor Umsetzung prüfen.","change","Change mit Rückfallplan kontrolliert umsetzen","permanent","Regel dauerhaft und ohne Prüfung öffnen","delegate","Anfrage ohne Dokumentation weitergeben"),
|
||||
t("Backup-Job fehlgeschlagen","Backup","Der nächtliche Backup-Job für zwei Datenbanken endete mit Fehler; die Produktion läuft noch.","incident","high","restore",1,150,"Hohe Priorität: Fehlerursache und letzte erfolgreiche Sicherung prüfen, Wiederanlauf planen.","restore","Backup-Kette prüfen und Wiederanlauf planen","ignore","Bis zum nächsten Lauf warten","purge","Alte Sicherungen sofort löschen"),
|
||||
t("Notfall-Handy ausgeben","Bereitschaft","Eine neu eingeteilte Person braucht das Bereitschaftshandy inklusive Rufumleitung.","request","medium","handover",1,110,"Anfrage: Gerät, Rufumleitung und Übergabe nachvollziehbar dokumentieren.","handover","Übergabe und Rufumleitung dokumentieren","share","PIN unverschlüsselt in den Chat posten","skip","Gerät ohne Einweisung übergeben"),
|
||||
t("Zertifikat läuft heute ab","Webteam","Das TLS-Zertifikat des Kundenportals läuft laut Monitoring um 23:59 Uhr ab.","incident","high","renew",2,150,"Dringend: Zertifikatskette und Erneuerung prüfen, Rollback und Monitoring vorbereiten.","renew","Zertifikat erneuern und Ablauf testen","disable","HTTPS abschalten","wait","Erst nach Ablauf reagieren"),
|
||||
t("Neue Adminrolle","Entwicklung","Eine Entwicklerin beantragt für einen Testzeitraum erhöhte Rechte auf einem Staging-System.","request","high","least",2,130,"Anfrage: zeitlich begrenzte Least-Privilege-Rolle mit Genehmigung einrichten.","least","Befristete Minimalrechte nach Freigabe geben","root","Dauerhafte Root-Rechte vergeben","shared","Gemeinsames Admin-Konto nutzen"),
|
||||
t("DNS-Ausfall in Außenstelle","Filiale Ost","Clients können interne Namen nicht auflösen, direkte IP-Verbindungen funktionieren noch.","incident","high","dns",2,150,"Störung: DNS-Pfad und Resolver prüfen, Auswirkung kommunizieren und Eskalation steuern.","dns","Resolver und DNS-Pfad prüfen","format","Alle Clients neu installieren","close","Ticket schließen, weil IPs gehen"),
|
||||
t("Wartungsfenster verschieben","Change Management","Ein freigegebener Patch kollidiert mit einem wichtigen Kunden-Workshop und soll verschoben werden.","request","medium","reschedule",3,110,"Change-Anfrage: Auswirkungen bewerten, Beteiligte informieren und neues Fenster dokumentieren.","reschedule","Change dokumentiert neu terminieren","patch","Patch trotz Konflikt ausrollen","cancel","Alle offenen Changes löschen")
|
||||
));
|
||||
}
|
||||
private void seed(String slug,String title,String description,String difficulty,int order,List<ServiceClashTicket> tickets){ServiceClashShift shift=new ServiceClashShift(slug,title,description,difficulty,"AP2_FISI",order);tickets.forEach(shift::addTicket);shifts.save(shift);}
|
||||
private ServiceClashTicket t(String title,String requester,String text,String category,String priority,String solution,int wave,int points,String explanation,String... options){try{ObjectNode p=mapper.createObjectNode();p.put("requester",requester);p.put("text",text);p.put("category",category);p.put("priority",priority);p.put("solution",solution);p.put("explanation",explanation);var array=p.putArray("options");for(int i=0;i<options.length;i+=2){ObjectNode option=array.addObject();option.put("id",options[i]);option.put("text",options[i+1]);}return new ServiceClashTicket(title,wave,points,wave*10+options.length,mapper.writeValueAsString(p));}catch(Exception e){throw new IllegalStateException(e);}}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaGap;
|
||||
import com.example.mindflux.model.SyntaxSagaMission;
|
||||
import com.example.mindflux.model.SyntaxSagaSolution;
|
||||
import com.example.mindflux.repository.SyntaxSagaMissionRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Befüllt Syntax Saga einmalig (wenn leer) aus der kuratierten Inhaltsdatei
|
||||
* syntaxsaga/content.json. Format: Missionen mit Story, Codetemplate (Lücken als
|
||||
* {@code {{n}}}) und je Lücke einer Liste gültiger Lösungen.
|
||||
*/
|
||||
@Component
|
||||
public class SyntaxSagaSeeder implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SyntaxSagaSeeder.class);
|
||||
|
||||
private static final Set<String> DIFFICULTIES = Set.of("junior", "professional", "senior");
|
||||
|
||||
private final SyntaxSagaMissionRepository missionRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SyntaxSagaSeeder(SyntaxSagaMissionRepository missionRepository, ObjectMapper objectMapper) {
|
||||
this.missionRepository = missionRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (missionRepository.count() > 0) {
|
||||
return; // schon befüllt
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(
|
||||
new ClassPathResource("syntaxsaga/content.json").getInputStream());
|
||||
JsonNode missions = root.get("missions");
|
||||
if (missions == null || !missions.isArray()) {
|
||||
log.warn("Syntax-Saga-Seed: content.json ohne 'missions'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> usedSlugs = new HashSet<>();
|
||||
int order = 1;
|
||||
int missionCount = 0;
|
||||
int gapCount = 0;
|
||||
|
||||
for (JsonNode mNode : missions) {
|
||||
String title = mNode.path("title").asText("");
|
||||
String code = mNode.path("code").asText("");
|
||||
JsonNode gaps = mNode.get("gaps");
|
||||
if (title.isBlank() || code.isBlank() || gaps == null || !gaps.isArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SyntaxSagaMission mission = new SyntaxSagaMission(
|
||||
uniqueSlug(title, usedSlugs),
|
||||
trim(title, 200),
|
||||
normalizeDifficulty(mNode.path("difficulty").asText("junior")),
|
||||
trim(mNode.path("narrative").asText(""), 2000),
|
||||
trim(textOrNull(mNode, "dialog"), 1000),
|
||||
trim(textOrNull(mNode, "language"), 30),
|
||||
trim(code, 2000),
|
||||
trim(textOrNull(mNode, "successText"), 2000),
|
||||
order, true);
|
||||
|
||||
int pos = 1;
|
||||
for (JsonNode gNode : gaps) {
|
||||
JsonNode solutions = gNode.get("solutions");
|
||||
if (solutions == null || !solutions.isArray() || solutions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
SyntaxSagaGap gap = new SyntaxSagaGap(
|
||||
gNode.path("position").asInt(pos),
|
||||
gNode.path("points").asInt(100),
|
||||
gNode.path("caseSensitive").asBoolean(true),
|
||||
trim(textOrNull(gNode, "hint"), 500),
|
||||
trim(textOrNull(gNode, "placeholder"), 120));
|
||||
|
||||
int sort = 0;
|
||||
for (JsonNode s : solutions) {
|
||||
String st = s.asText("");
|
||||
if (!st.isBlank()) {
|
||||
gap.addSolution(new SyntaxSagaSolution(trim(st, 500), sort++));
|
||||
}
|
||||
}
|
||||
if (!gap.getSolutions().isEmpty()) {
|
||||
mission.addGap(gap);
|
||||
gapCount++;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (!mission.getGaps().isEmpty()) {
|
||||
missionRepository.save(mission);
|
||||
order++;
|
||||
missionCount++;
|
||||
}
|
||||
}
|
||||
log.info("SyntaxSaga: {} Missionen / {} Lücken geladen.", missionCount, gapCount);
|
||||
} catch (Exception e) {
|
||||
log.warn("Syntax-Saga-Seed übersprungen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDifficulty(String difficulty) {
|
||||
if (difficulty == null) {
|
||||
return "junior";
|
||||
}
|
||||
String x = difficulty.trim().toLowerCase();
|
||||
return DIFFICULTIES.contains(x) ? x : "junior";
|
||||
}
|
||||
|
||||
private String textOrNull(JsonNode node, String field) {
|
||||
if (!node.hasNonNull(field)) {
|
||||
return null;
|
||||
}
|
||||
String value = node.get(field).asText("");
|
||||
return value.isBlank() ? null : value;
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title, Set<String> used) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "mission";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (used.contains(slug) || missionRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
used.add(slug);
|
||||
return slug;
|
||||
}
|
||||
|
||||
private String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.config;
|
||||
import com.example.mindflux.model.*;import com.example.mindflux.repository.EscapeScenarioRepository;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.node.ObjectNode;import org.springframework.boot.context.event.ApplicationReadyEvent;import org.springframework.context.event.EventListener;import org.springframework.stereotype.Component;import org.springframework.transaction.annotation.Transactional;import java.util.*;
|
||||
@Component public class SystemEscapePoolSeeder{
|
||||
private final EscapeScenarioRepository repo;private final ObjectMapper mapper;public SystemEscapePoolSeeder(EscapeScenarioRepository r,ObjectMapper m){repo=r;mapper=m;}
|
||||
@EventListener(ApplicationReadyEvent.class) @Transactional public void expand()throws Exception{
|
||||
add("verlassener-serverraum","DNS-Kammer","Ein Resolver hält die nächste Schleuse gesperrt.",q("DNS-Record für IPv4-Adresse?","A","A"),q("DNS-Record für Mailserver?","MX","MX"),q("DNS-Standardport?","53","53"));
|
||||
add("verlassener-serverraum","VLAN-Brücke","Getrennte Segmente müssen korrekt verbunden werden.",q("VLAN-Tagging-Standard IEEE 802.?","1Q","1Q"),q("Gerät zwischen IP-Netzen?","Router","RTR"),q("VLAN ohne Tag am Trunk?","Native VLAN","NAT"));
|
||||
add("verlassener-serverraum","Routing-Schacht","Das Gateway erwartet drei Routing-Grundlagen.",q("Route für alle unbekannten Ziele?","Default Route","DEF"),q("IPv4-Loopback-Adresse?","127.0.0.1","127"),q("Befehl zum Weg eines Pakets?","traceroute","TRC"));
|
||||
add("verlassener-serverraum","Firewall-Gang","Nur korrekt gefilterter Verkehr öffnet den Gang.",q("Prinzip: alles verbieten, nur Nötiges erlauben?","Default Deny","DENY"),q("Firewall mit Verbindungszustand?","Stateful","STATE"),q("HTTPS-Protokoll über TCP-Port?","443","443"));
|
||||
add("verlassener-serverraum","Monitoring-Zentrale","Metriken und Ereignisse liefern die Fragmente.",q("Protokoll für Geräteüberwachung?","SNMP","SNMP"),q("Zentrales Systemprotokoll?","Syslog","LOG"),q("ICMP-Werkzeug für Erreichbarkeit?","ping","PING"));
|
||||
add("notfall-im-rechenzentrum","Identity-Vault","Der Identitätsdienst verlangt sichere Grundlagen.",q("Zweiter Faktor neben Passwort?","MFA","MFA"),q("Zentraler Verzeichnisdienst von Microsoft?","Active Directory","AD"),q("Minimal notwendige Rechte?","Least Privilege","LP"));
|
||||
add("notfall-im-rechenzentrum","Container-Dock","Isolierte Workloads blockieren den Ausgang.",q("Container teilen den Kernel des?","Hosts","HOST"),q("Deklarative Container-Zusammenstellung?","Compose","COMP"),q("Persistenter Containerspeicher?","Volume","VOL"));
|
||||
add("notfall-im-rechenzentrum","Patch-Leitstand","Ein ungepatchtes System hält die Tür verriegelt.",q("Bewertung bekannter Schwachstellen?","CVSS","CVSS"),q("Kennung einer veröffentlichten Schwachstelle?","CVE","CVE"),q("Testumgebung vor Produktion?","Staging","STG"));
|
||||
add("notfall-im-rechenzentrum","Cluster-Korridor","Hochverfügbarkeit muss wiederhergestellt werden.",q("Automatischer Rollenwechsel bei Ausfall?","Failover","FAIL"),q("Verteilung von Anfragen?","Load Balancing","LB"),q("Prüfung der Dienstverfügbarkeit?","Health Check","HC"));
|
||||
add("notfall-im-rechenzentrum","Storage-Array","Das Speichernetz fordert drei Begriffe.",q("Blockzugriff über IP?","iSCSI","ISCSI"),q("Dateifreigabe für Linux/Unix?","NFS","NFS"),q("Redundanz ist kein Ersatz für ein?","Backup","BAK"));
|
||||
add("blackout-protokoll","PKI-Tresor","Die Zertifikatskette muss rekonstruiert werden.",q("Widerrufsliste für Zertifikate?","CRL","CRL"),q("Online-Prüfung eines Zertifikatsstatus?","OCSP","OCSP"),q("Oberste Vertrauensinstanz?","Root CA","ROOT"));
|
||||
add("blackout-protokoll","API-Gateway","Die Schnittstelle akzeptiert nur sichere Antworten.",q("HTTP-Methode zum Lesen?","GET","GET"),q("Statuscode für nicht autorisiert?","401","401"),q("Datenformat mit Objekten und Arrays?","JSON","JSON"));
|
||||
add("blackout-protokoll","Git-Archiv","Die Versionshistorie enthält den nächsten Schlüssel.",q("Lokale Änderungen vorm Commit vormerken?","git add","ADD"),q("Neuen Entwicklungszweig anlegen?","git branch","BR"),q("Historie eines Repositories anzeigen?","git log","LOG"));
|
||||
add("blackout-protokoll","Datenbank-Kern","Transaktionen und Indizes sichern den Weg.",q("SQL-Befehl zum Lesen?","SELECT","SEL"),q("Index mit eindeutigen Werten?","UNIQUE","UNQ"),q("Transaktion zurücknehmen?","ROLLBACK","RB"));
|
||||
add("blackout-protokoll","Zero-Trust-Schleuse","Jeder Zugriff muss erneut bewiesen werden.",q("Zero-Trust-Grundsatz: never trust, always?","verify","VERIFY"),q("Netze in kleine Zonen teilen?","Microsegmentation","MICRO"),q("Zugriffsentscheidung anhand von Rollen?","RBAC","RBAC"));
|
||||
}
|
||||
private Q q(String p,String a,String f){return new Q(p,a,f);}
|
||||
private void add(String slug,String title,String intro,Q...qs)throws Exception{EscapeScenario s=repo.findBySlug(slug).orElse(null);if(s==null||s.getRooms().stream().anyMatch(r->r.getTitle().equals(title)))return;String code=Arrays.stream(qs).map(Q::fragment).reduce("",String::concat);EscapeRoom r=new EscapeRoom(title,intro,"Setze alle Fragmente von links nach rechts ohne Leerzeichen zusammen.",code,"Die Schleuse entriegelt und gibt den nächsten Abschnitt frei.",s.getRooms().size());int i=0;for(Q q:qs){ObjectNode p=mapper.createObjectNode();p.put("instruction",q.prompt());p.putArray("solutions").add(q.answer());p.putArray("hints").add("Ordne den gesuchten Begriff dem IT-Kontext zu.").add("Der gesuchte Wert lautet: "+q.answer()+".");p.put("explanation",q.answer()+" ist die korrekte Lösung.");r.addPuzzle(new EscapePuzzle("code",q.prompt(),120,q.fragment(),i++,mapper.writeValueAsString(p)));}s.addRoom(r);repo.save(s);}
|
||||
private record Q(String prompt,String answer,String fragment){}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import com.example.mindflux.model.TechtacularQuestion;
|
||||
import com.example.mindflux.repository.TechtacularCategoryRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -22,6 +24,8 @@ import java.util.Set;
|
||||
@Component
|
||||
public class TechtacularSeeder implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TechtacularSeeder.class);
|
||||
|
||||
private final TechtacularCategoryRepository categoryRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -40,7 +44,7 @@ public class TechtacularSeeder implements CommandLineRunner {
|
||||
JsonNode root = objectMapper.readTree(new ClassPathResource("techtacular/content.json").getInputStream());
|
||||
JsonNode categories = root.get("categories");
|
||||
if (categories == null || !categories.isArray()) {
|
||||
System.out.println("⚠️ TechTacular-Seed: content.json ohne 'categories'.");
|
||||
log.warn("TechTacular-Seed: content.json ohne 'categories'.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,9 +99,9 @@ public class TechtacularSeeder implements CommandLineRunner {
|
||||
catCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("✅ TechTacular: " + catCount + " Kategorien / " + clueCount + " Hinweise geladen.");
|
||||
log.info("TechTacular: {} Kategorien / {} Hinweise geladen.", catCount, clueCount);
|
||||
} catch (Exception e) {
|
||||
System.out.println("⚠️ TechTacular-Seed übersprungen: " + e.getMessage());
|
||||
log.warn("TechTacular-Seed übersprungen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.service.AccountMailService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class AccountRecoveryController {
|
||||
|
||||
private final AccountMailService accountMailService;
|
||||
|
||||
public AccountRecoveryController(AccountMailService accountMailService) {
|
||||
this.accountMailService = accountMailService;
|
||||
}
|
||||
|
||||
@GetMapping("/forgot-password")
|
||||
public String forgotPassword() {
|
||||
return "forgot-password";
|
||||
}
|
||||
|
||||
@PostMapping("/forgot-password")
|
||||
public String requestPasswordReset(@RequestParam String email) {
|
||||
try {
|
||||
accountMailService.requestPasswordReset(email);
|
||||
} catch (RuntimeException ignored) {
|
||||
// Die Antwort bleibt absichtlich identisch: keine E-Mail- oder Account-Informationen preisgeben.
|
||||
}
|
||||
// Einheitliche Antwort, damit nicht erkennbar ist, welche Adressen registriert sind.
|
||||
return "redirect:/forgot-password?sent";
|
||||
}
|
||||
|
||||
@PostMapping("/verification/resend")
|
||||
public String resendVerification(@RequestParam String email) {
|
||||
try {
|
||||
accountMailService.resendVerification(email);
|
||||
} catch (RuntimeException ignored) {
|
||||
// Auch bei einem SMTP-Fehler keine Information über einen möglichen Account verraten.
|
||||
}
|
||||
return "redirect:/?verificationSent";
|
||||
}
|
||||
|
||||
@GetMapping("/verify-email")
|
||||
public String verifyEmail(@RequestParam(required = false) String token) {
|
||||
return accountMailService.verifyEmail(token)
|
||||
? "redirect:/?emailVerified"
|
||||
: "redirect:/?verificationInvalid";
|
||||
}
|
||||
|
||||
@GetMapping("/reset-password")
|
||||
public String showResetPassword(@RequestParam(required = false) String token, Model model) {
|
||||
if (!accountMailService.isResetTokenValid(token)) {
|
||||
return "redirect:/?resetInvalid";
|
||||
}
|
||||
model.addAttribute("token", token);
|
||||
return "reset-password";
|
||||
}
|
||||
|
||||
@PostMapping("/reset-password")
|
||||
public String resetPassword(@RequestParam String token,
|
||||
@RequestParam String password,
|
||||
@RequestParam String confirmPassword) {
|
||||
if (password.length() < 8) {
|
||||
return "redirect:/reset-password?token=" + token + "&error=policy";
|
||||
}
|
||||
if (!password.equals(confirmPassword)) {
|
||||
return "redirect:/reset-password?token=" + token + "&error=mismatch";
|
||||
}
|
||||
return accountMailService.resetPassword(token, password, confirmPassword)
|
||||
? "redirect:/?passwordReset"
|
||||
: "redirect:/?resetInvalid";
|
||||
}
|
||||
}
|
||||
@@ -19,18 +19,8 @@ public class AchievementController {
|
||||
@GetMapping("/profile/achievements")
|
||||
public String showAchievements(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
// Kompletter Katalog: verdiente Achievements mit Datum, restliche als gesperrte Karten.
|
||||
List<Achievement> achievements = achievementService.getAchievementsForUser(username);
|
||||
|
||||
// 🔥 Dummy-Daten nur für Tests!
|
||||
if (achievements == null || achievements.isEmpty()) {
|
||||
achievements = List.of(
|
||||
new Achievement("Hangman Hero", "Gewinne 10 Runden Hangman", "hangman.png", true),
|
||||
new Achievement("Memory Meister", "Erreiche 1000 Punkte im Memory", "memory.png", false),
|
||||
new Achievement("Quizkönig", "Beantworte 50 Fragen richtig", "quiz.png", true),
|
||||
new Achievement("Early Bird", "Lerne vor 7 Uhr morgens", "earlybird.png", false)
|
||||
);
|
||||
}
|
||||
|
||||
model.addAttribute("achievements", achievements);
|
||||
|
||||
return "achievements"; // lädt achievements.html
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.service.BugHunterService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller @RequestMapping("/admin/bughunter")
|
||||
public class AdminBugHunterController {
|
||||
private final BugHunterService service;
|
||||
public AdminBugHunterController(BugHunterService service) { this.service = service; }
|
||||
@GetMapping public String list(Model model) { model.addAttribute("hunts", service.all()); return "admin/bughunter"; }
|
||||
@PostMapping("/add") public String add(@RequestParam String title, @RequestParam String language, @RequestParam String difficulty, @RequestParam String examScope, @RequestParam(required = false) String description, @RequestParam(required = false) String briefing, @RequestParam(required = false) String consoleOutput) { Long id = service.addHunt(title, language, difficulty, examScope, description, briefing, consoleOutput); return id == null ? "redirect:/admin/bughunter" : "redirect:/admin/bughunter/" + id; }
|
||||
@GetMapping("/{id}") public String detail(@PathVariable Long id, Model model) { model.addAttribute("hunt", service.hunt(id)); return "admin/bughunter-hunt"; }
|
||||
@PostMapping("/{id}/files") public String addFile(@PathVariable Long id, @RequestParam String filename, @RequestParam String content) { service.addFile(id, filename, content); return "redirect:/admin/bughunter/" + id; }
|
||||
@PostMapping("/{id}/bugs") public String addBug(@PathVariable Long id, @RequestParam(defaultValue = "100") int points, @RequestParam String payload) { service.addBug(id, points, payload); return "redirect:/admin/bughunter/" + id; }
|
||||
@PostMapping("/{id}/toggle") public String toggle(@PathVariable Long id) { service.toggle(id); return "redirect:/admin/bughunter"; }
|
||||
@PostMapping("/{id}/delete") public String delete(@PathVariable Long id) { service.deleteHunt(id); return "redirect:/admin/bughunter"; }
|
||||
@PostMapping("/files/{id}/delete") public String deleteFile(@PathVariable Long id, @RequestParam Long huntId) { service.deleteFile(id); return "redirect:/admin/bughunter/" + huntId; }
|
||||
@PostMapping("/bugs/{id}/delete") public String deleteBug(@PathVariable Long id, @RequestParam Long huntId) { service.deleteBug(id); return "redirect:/admin/bughunter/" + huntId; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.controller; import com.example.mindflux.service.CodeEscapeService;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.*;
|
||||
@Controller @RequestMapping("/admin/codeescape") public class AdminCodeEscapeController {private final CodeEscapeService service;public AdminCodeEscapeController(CodeEscapeService s){service=s;}@GetMapping public String list(Model m){m.addAttribute("scenarios",service.all());return"admin/codeescape";}@PostMapping("/add")public String add(@RequestParam String title,@RequestParam String difficulty,@RequestParam String examScope,@RequestParam(required=false)String description,@RequestParam(defaultValue="900")int timeLimitSeconds){Long id=service.addScenario(title,difficulty,examScope,description,timeLimitSeconds);return"redirect:/admin/codeescape/"+id;}@GetMapping("/{id}")public String scenario(@PathVariable Long id,Model m){m.addAttribute("scenario",service.scenario(id));return"admin/codeescape-scenario";}@PostMapping("/{id}/rooms")public String room(@PathVariable Long id,@RequestParam String title,@RequestParam String intro,@RequestParam String doorPrompt,@RequestParam String doorCode,@RequestParam String successText){service.addRoom(id,title,intro,doorPrompt,doorCode,successText);return"redirect:/admin/codeescape/"+id;}@GetMapping("/rooms/{id}")public String roomPage(@PathVariable Long id,Model m){m.addAttribute("room",service.room(id));return"admin/codeescape-room";}@PostMapping("/rooms/{id}/puzzles")public String puzzle(@PathVariable Long id,@RequestParam String type,@RequestParam String prompt,@RequestParam int points,@RequestParam String codeFragment,@RequestParam String payload){service.addPuzzle(id,type,prompt,points,codeFragment,payload);return"redirect:/admin/codeescape/rooms/"+id;}@PostMapping("/{id}/toggle")public String toggle(@PathVariable Long id){service.toggle(id);return"redirect:/admin/codeescape";}@PostMapping("/{id}/delete")public String del(@PathVariable Long id){service.deleteScenario(id);return"redirect:/admin/codeescape";}@PostMapping("/rooms/{id}/delete")public String delRoom(@PathVariable Long id){var r=service.room(id);Long sid=r.getScenario().getId();service.deleteRoom(id);return"redirect:/admin/codeescape/"+sid;}@PostMapping("/puzzles/{id}/delete")public String delPuzzle(@PathVariable Long id,@RequestParam Long roomId){service.deletePuzzle(id);return"redirect:/admin/codeescape/rooms/"+roomId;}}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.DevLabGame;
|
||||
import com.example.mindflux.service.DevLabService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** Admin-Verwaltung der Dev-Lab-Spiele und -Aufgaben (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/devlab")
|
||||
public class AdminDevLabController {
|
||||
|
||||
private final DevLabService devLabService;
|
||||
|
||||
public AdminDevLabController(DevLabService devLabService) {
|
||||
this.devLabService = devLabService;
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("games", devLabService.gameRows());
|
||||
return "admin/devlab";
|
||||
}
|
||||
|
||||
@GetMapping("/games/{gameId}")
|
||||
public String game(@PathVariable Long gameId, Model model) {
|
||||
DevLabGame game = devLabService.gameWithTasks(gameId);
|
||||
if (game == null) {
|
||||
return "redirect:/admin/devlab";
|
||||
}
|
||||
model.addAttribute("game", game);
|
||||
return "admin/devlab-game";
|
||||
}
|
||||
|
||||
@PostMapping("/games")
|
||||
public String createGame(@RequestParam String title,
|
||||
@RequestParam(required = false) String language,
|
||||
@RequestParam(required = false) String path,
|
||||
@RequestParam(required = false) String description) {
|
||||
Long id = devLabService.createGame(title, language, path, description);
|
||||
return id == null ? "redirect:/admin/devlab" : "redirect:/admin/devlab/games/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/tasks")
|
||||
public String addTask(@PathVariable Long gameId,
|
||||
@RequestParam String type,
|
||||
@RequestParam String prompt,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam String payload) {
|
||||
devLabService.addTask(gameId, type, prompt, points, payload);
|
||||
return "redirect:/admin/devlab/games/" + gameId;
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/update")
|
||||
public String updateTask(@PathVariable Long taskId,
|
||||
@RequestParam Long gameId,
|
||||
@RequestParam String prompt,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam String payload) {
|
||||
devLabService.updateTask(taskId, prompt, points, payload);
|
||||
return "redirect:/admin/devlab/games/" + gameId;
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/delete")
|
||||
public String deleteTask(@PathVariable Long taskId, @RequestParam Long gameId) {
|
||||
devLabService.deleteTask(taskId);
|
||||
return "redirect:/admin/devlab/games/" + gameId;
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/delete")
|
||||
public String deleteGame(@PathVariable Long gameId) {
|
||||
devLabService.deleteGame(gameId);
|
||||
return "redirect:/admin/devlab";
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/toggle")
|
||||
public String toggleGame(@PathVariable Long gameId) {
|
||||
devLabService.toggleGame(gameId);
|
||||
return "redirect:/admin/devlab";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaMission;
|
||||
import com.example.mindflux.service.SyntaxSagaService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** Admin-Verwaltung der Syntax-Saga-Missionen, -Lücken und -Lösungen (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/syntaxsaga")
|
||||
public class AdminSyntaxSagaController {
|
||||
|
||||
private final SyntaxSagaService syntaxSagaService;
|
||||
|
||||
public AdminSyntaxSagaController(SyntaxSagaService syntaxSagaService) {
|
||||
this.syntaxSagaService = syntaxSagaService;
|
||||
}
|
||||
|
||||
/** Übersicht aller Missionen. */
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("missions", syntaxSagaService.missionRows());
|
||||
return "admin/syntaxsaga";
|
||||
}
|
||||
|
||||
/** Detailseite einer Mission mit ihren Lücken. */
|
||||
@GetMapping("/missions/{missionId}")
|
||||
public String mission(@PathVariable Long missionId, Model model) {
|
||||
SyntaxSagaMission mission = syntaxSagaService.missionWithGaps(missionId);
|
||||
if (mission == null) {
|
||||
return "redirect:/admin/syntaxsaga";
|
||||
}
|
||||
model.addAttribute("mission", mission);
|
||||
return "admin/syntaxsaga-mission";
|
||||
}
|
||||
|
||||
@PostMapping("/missions")
|
||||
public String createMission(@RequestParam String title,
|
||||
@RequestParam(required = false) String difficulty,
|
||||
@RequestParam(required = false) String narrative,
|
||||
@RequestParam(required = false) String dialog,
|
||||
@RequestParam(required = false) String language,
|
||||
@RequestParam String codeTemplate,
|
||||
@RequestParam(required = false) String successText) {
|
||||
Long id = syntaxSagaService.createMission(title, difficulty, narrative, dialog,
|
||||
language, codeTemplate, successText);
|
||||
return id == null ? "redirect:/admin/syntaxsaga"
|
||||
: "redirect:/admin/syntaxsaga/missions/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/update")
|
||||
public String updateMission(@PathVariable Long missionId,
|
||||
@RequestParam String title,
|
||||
@RequestParam(required = false) String difficulty,
|
||||
@RequestParam(required = false) String narrative,
|
||||
@RequestParam(required = false) String dialog,
|
||||
@RequestParam(required = false) String language,
|
||||
@RequestParam String codeTemplate,
|
||||
@RequestParam(required = false) String successText) {
|
||||
syntaxSagaService.updateMission(missionId, title, difficulty, narrative, dialog,
|
||||
language, codeTemplate, successText);
|
||||
return "redirect:/admin/syntaxsaga/missions/" + missionId;
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/gaps")
|
||||
public String addGap(@PathVariable Long missionId,
|
||||
@RequestParam(defaultValue = "0") int position,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam(defaultValue = "false") boolean caseSensitive,
|
||||
@RequestParam(required = false) String hint,
|
||||
@RequestParam(required = false) String placeholder,
|
||||
@RequestParam(required = false, defaultValue = "") String solutions) {
|
||||
List<String> parsed = Arrays.stream(solutions.split("\\r?\\n"))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.toList();
|
||||
syntaxSagaService.addGap(missionId, position, points, caseSensitive, hint, placeholder, parsed);
|
||||
return "redirect:/admin/syntaxsaga/missions/" + missionId;
|
||||
}
|
||||
|
||||
@PostMapping("/gaps/{gapId}/delete")
|
||||
public String deleteGap(@PathVariable Long gapId, @RequestParam Long missionId) {
|
||||
syntaxSagaService.deleteGap(gapId);
|
||||
return "redirect:/admin/syntaxsaga/missions/" + missionId;
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/delete")
|
||||
public String deleteMission(@PathVariable Long missionId) {
|
||||
syntaxSagaService.deleteMission(missionId);
|
||||
return "redirect:/admin/syntaxsaga";
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/toggle")
|
||||
public String toggleMission(@PathVariable Long missionId) {
|
||||
syntaxSagaService.toggleMission(missionId);
|
||||
return "redirect:/admin/syntaxsaga";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.AdminUserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -12,16 +11,18 @@ import org.springframework.web.bind.annotation.*;
|
||||
@RequestMapping("/admin")
|
||||
public class AdminUserController {
|
||||
|
||||
@Autowired
|
||||
private AdminUserService userService;
|
||||
private final UserService userService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
public AdminUserController(UserService userService, PasswordEncoder passwordEncoder) {
|
||||
this.userService = userService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
// ⬅️ Neu: Dashboard-Startseite
|
||||
// ⬅️ Dashboard-Startseite
|
||||
@GetMapping("")
|
||||
public String adminDashboard() {
|
||||
return "admin/dashboard"; // du brauchst: templates/admin/dashboard.html
|
||||
return "admin/dashboard";
|
||||
}
|
||||
|
||||
// ⬅️ Benutzerverwaltung
|
||||
@@ -39,21 +40,44 @@ public class AdminUserController {
|
||||
|
||||
@PostMapping("/users/save")
|
||||
public String saveUser(@ModelAttribute("user") User user) {
|
||||
if (user.getId() != null && (user.getPassword() == null || user.getPassword().isBlank())) {
|
||||
if (user.getId() != null) {
|
||||
User existingUser = userService.findById(user.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + user.getId()));
|
||||
user.setPassword(existingUser.getPassword());
|
||||
userService.assertUsernameAvailable(user.getUsername(), user.getId());
|
||||
user.setRole(userService.validateRole(user.getRole()));
|
||||
// Passwort: leer = unverändert übernehmen, sonst neu verschlüsseln (Admin-Reset)
|
||||
if (user.getPassword() == null || user.getPassword().isBlank()) {
|
||||
user.setPassword(existingUser.getPassword());
|
||||
} else {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
}
|
||||
// Profilbild steht nicht im Formular -> bestehendes erhalten, nicht überschreiben
|
||||
user.setProfilePicture(existingUser.getProfilePicture());
|
||||
userService.save(user);
|
||||
} else {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
// Neuanlage zentral über den UserService (Validierung, Eindeutigkeit, Encoding)
|
||||
userService.createUser(user.getUsername(), user.getEmail(), user.getPassword(), user.getRole());
|
||||
}
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
/** Direktes Zurücksetzen des Passworts eines Nutzers (Admin-Recovery, ohne E-Mail). */
|
||||
@PostMapping("/users/{id}/reset-password")
|
||||
public String resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
|
||||
if (newPassword == null || newPassword.trim().length() < 8) {
|
||||
return "redirect:/admin/users?passwordPolicy";
|
||||
}
|
||||
User user = userService.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id));
|
||||
user.setPassword(passwordEncoder.encode(newPassword.trim()));
|
||||
userService.save(user);
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@GetMapping("/users/edit/{id}")
|
||||
public String editUserForm(@PathVariable Long id, Model model) {
|
||||
User user = userService.findById(id).orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id));
|
||||
User user = userService.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id));
|
||||
model.addAttribute("user", user);
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Fehlerbehandlung für alle JSON-APIs: liefert statt Whitelabel-HTML einen
|
||||
* konsistenten JSON-Körper mit korrektem Statuscode.
|
||||
*/
|
||||
@RestControllerAdvice(annotations = RestController.class)
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class ApiExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleStatus(ResponseStatusException e) {
|
||||
return body(e.getStatusCode().value(), e.getReason());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException e) {
|
||||
return body(400, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleUnreadable(HttpMessageNotReadableException e) {
|
||||
return body(400, "Ungültiger Request-Body.");
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException e) {
|
||||
String details = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(f -> f.getField() + ": " + fieldMessage(f))
|
||||
.collect(Collectors.joining("; "));
|
||||
return body(400, details.isBlank() ? "Validierung fehlgeschlagen." : details);
|
||||
}
|
||||
|
||||
/** Security-Fehler NICHT schlucken – die behandelt Spring Security selbst (403/Login). */
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public void rethrowAccessDenied(AccessDeniedException e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleServerError(Exception e) {
|
||||
log.error("Unbehandelter API-Fehler", e);
|
||||
return body(500, "Interner Fehler. Bitte später erneut versuchen.");
|
||||
}
|
||||
|
||||
private static String fieldMessage(FieldError f) {
|
||||
return f.getDefaultMessage() == null ? "ungültig" : f.getDefaultMessage();
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> body(int status, String message) {
|
||||
return ResponseEntity.status(status).body(Map.of(
|
||||
"error", true,
|
||||
"status", status,
|
||||
"message", message == null || message.isBlank() ? "Fehler" : message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.service.BugHunterService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.security.Principal;
|
||||
|
||||
@RestController @RequestMapping("/api/bughunter")
|
||||
public class BugHunterApiController {
|
||||
private final BugHunterService service;
|
||||
public BugHunterApiController(BugHunterService service) { this.service = service; }
|
||||
@GetMapping("/hunts") public JsonNode catalog(Principal principal) { return service.catalog(principal == null ? null : principal.getName()); }
|
||||
@GetMapping("/session/{slug}") public JsonNode session(@PathVariable String slug, Principal principal, HttpSession httpSession) { return service.session(slug, principal == null ? null : principal.getName(), httpSession); }
|
||||
@PostMapping("/reset/{slug}") public void reset(@PathVariable String slug, Principal principal) { service.reset(principal.getName(), slug); }
|
||||
@PostMapping("/check") public JsonNode check(@RequestBody BugHunterCheckRequest request, HttpSession httpSession) { return service.check(request.getBugId(), request.getPhase(), request.isReveal(), request.getSubmission(), httpSession); }
|
||||
@PostMapping("/finish") public BugHunterResult finish(@RequestBody BugHunterFinishRequest request, Principal principal, HttpSession httpSession) { return service.finish(principal.getName(), request, httpSession); }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.example.mindflux.controller;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@Controller public class BugHunterController { @GetMapping("/bughunter") public String page() { return "bughunter"; } }
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.controller; import com.example.mindflux.dto.*;import com.example.mindflux.service.CodeEscapeService;import com.fasterxml.jackson.databind.JsonNode;import jakarta.servlet.http.HttpSession;import org.springframework.web.bind.annotation.*;import java.security.Principal;
|
||||
@RestController @RequestMapping("/api/codeescape") public class CodeEscapeApiController {private final CodeEscapeService service;public CodeEscapeApiController(CodeEscapeService s){service=s;}@GetMapping("/scenarios")public JsonNode catalog(){return service.catalog();}@GetMapping("/session/{slug}")public JsonNode session(@PathVariable String slug,HttpSession s){return service.session(slug,s);}@PostMapping("/check")public JsonNode check(@RequestBody EscapeCheckRequest r,HttpSession s){return service.check(r.getPuzzleId(),r.isReveal(),r.getSubmission(),s);}@PostMapping("/door")public JsonNode door(@RequestBody EscapeDoorRequest r,HttpSession s){return service.door(r.getRoomId(),r.getCode(),s);}@PostMapping("/hint")public JsonNode hint(@RequestBody EscapeHintRequest r,HttpSession s){return service.hint(r.getPuzzleId(),r.getLevel(),s);}@PostMapping("/finish")public EscapeResult finish(@RequestBody EscapeFinishRequest r,Principal p,HttpSession s){return service.finish(p.getName(),r,s);}}
|
||||
@@ -0,0 +1 @@
|
||||
package com.example.mindflux.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class CodeEscapeController{@GetMapping("/codeescape")public String page(){return "codeescape";}}
|
||||
@@ -1,13 +1,35 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.service.ProgressService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import com.example.mindflux.dto.WeeklyActivity;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
private final ProgressService progressService;
|
||||
|
||||
public DashboardController(ProgressService progressService) {
|
||||
this.progressService = progressService;
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public String showDashboard() {
|
||||
return "dashboard"; // lädt dashboard.html aus templates
|
||||
public String showDashboard(Model model, Principal principal) {
|
||||
// Echte XP/Level/Streak-Werte für die Sidebar-Widgets (statt Dummy-Daten im Template).
|
||||
model.addAttribute("overall", progressService.getOverallProgress(principal.getName()));
|
||||
WeeklyActivity weekly = progressService.getCurrentWeekActivity(principal.getName());
|
||||
model.addAttribute("weekly", weekly != null ? weekly : emptyWeekly());
|
||||
var topGames = progressService.getTopGames(principal.getName(), 4);
|
||||
model.addAttribute("topGames", topGames != null ? topGames : List.of());
|
||||
return "dashboard";
|
||||
}
|
||||
|
||||
private WeeklyActivity emptyWeekly() {
|
||||
return new WeeklyActivity(List.of(0, 0, 0, 0, 0, 0, 0), List.of(0, 0, 0, 0, 0, 0, 0), 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.DevLabCheckRequest;
|
||||
import com.example.mindflux.dto.DevLabFinishRequest;
|
||||
import com.example.mindflux.dto.DevLabResult;
|
||||
import com.example.mindflux.service.DevLabService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/** JSON-API fürs Dev Lab. Server-autoritativ: Lösungen werden nie ausgeliefert. */
|
||||
@RestController
|
||||
@RequestMapping("/api/devlab")
|
||||
public class DevLabApiController {
|
||||
|
||||
private final DevLabService devLabService;
|
||||
|
||||
public DevLabApiController(DevLabService devLabService) {
|
||||
this.devLabService = devLabService;
|
||||
}
|
||||
|
||||
/** Spiele-Katalog für den Hub (nach Pfad gruppierbar). */
|
||||
@GetMapping("/catalog")
|
||||
public JsonNode catalog() {
|
||||
return devLabService.buildCatalog();
|
||||
}
|
||||
|
||||
/** Eine Spielrunde: Aufgaben einer Disziplin – ohne Lösungen. */
|
||||
@GetMapping("/games/{slug}/session")
|
||||
public JsonNode session(@PathVariable String slug) {
|
||||
return devLabService.buildSession(slug);
|
||||
}
|
||||
|
||||
/** Eine Aufgabe serverseitig prüfen; mit {@code reveal=true} auch die Auflösung. */
|
||||
@PostMapping("/check")
|
||||
public JsonNode check(@RequestBody DevLabCheckRequest request) {
|
||||
return devLabService.check(request.getTaskId(), request.isReveal(), request.getSubmission());
|
||||
}
|
||||
|
||||
/** Durchlauf abschließen: Score wird serverseitig neu berechnet und gespeichert. */
|
||||
@PostMapping("/finish")
|
||||
public DevLabResult finish(@RequestBody DevLabFinishRequest request, Principal principal) {
|
||||
return devLabService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
@Controller
|
||||
public class DevLabController {
|
||||
|
||||
@GetMapping("/devlab")
|
||||
public String showDevLab() {
|
||||
return "devlab";
|
||||
}
|
||||
|
||||
@GetMapping("/devlab/games/{slug}")
|
||||
public String showDevLabGame(@PathVariable String slug, Model model) {
|
||||
model.addAttribute("gameSlug", slug);
|
||||
return "devlab-game";
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DummyController {
|
||||
|
||||
@GetMapping("/comingsoon")
|
||||
public String comingSoon() {
|
||||
return "dummy"; // verweist auf dummy.html in src/main/resources/templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class FaviconController {
|
||||
|
||||
@GetMapping(value = "/favicon.ico", produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public ResponseEntity<Resource> favicon() {
|
||||
Resource emblem = new ClassPathResource("static/img/mindflux-emblem.png");
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(Duration.ofDays(30)).cachePublic())
|
||||
.body(emblem);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.dto.ScoreSubmission;
|
||||
import com.example.mindflux.service.GameScoreService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -21,8 +22,8 @@ public class GameScoreController {
|
||||
|
||||
/** Bekannte Spiele (entsprechen den Dashboard-Routen). */
|
||||
private static final Set<String> KNOWN_GAMES = Set.of(
|
||||
"mindmatch", "techtacular", "syntax-saga", "escape-room",
|
||||
"service-clash", "bug-hunter", "netzwerker");
|
||||
"mindmatch", "techtacular", "syntax-saga", "devlab", "codeescape",
|
||||
"serviceclash", "bughunter", "networker");
|
||||
|
||||
private final GameScoreService gameScoreService;
|
||||
|
||||
@@ -32,7 +33,7 @@ public class GameScoreController {
|
||||
|
||||
@PostMapping("/{game}/scores")
|
||||
public ResponseEntity<ScoreResult> submitScore(@PathVariable String game,
|
||||
@RequestBody ScoreSubmission submission,
|
||||
@Valid @RequestBody ScoreSubmission submission,
|
||||
Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
/**
|
||||
* Fängt Fehler aus den HTML-Controllern ab und zeigt eine gestaltete Fehlerseite
|
||||
* statt der Whitelabel-Seite. Die JSON-APIs behandelt der ApiExceptionHandler
|
||||
* (höhere Präzedenz, greift nur für @RestController).
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
/** Ungültige IDs/Parameter (z. B. Admin-Formulare) -> 400 mit lesbarer Meldung. */
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public String handleBadRequest(IllegalArgumentException e, Model model) {
|
||||
model.addAttribute("status", 400);
|
||||
model.addAttribute("friendlyMessage",
|
||||
e.getMessage() == null ? "Ungültige Anfrage." : e.getMessage());
|
||||
return "error";
|
||||
}
|
||||
|
||||
/** Unbekannte URL -> echte 404-Seite (darf nicht im Catch-all als 500 landen). */
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public String handleNotFound(NoResourceFoundException e, Model model) {
|
||||
model.addAttribute("status", 404);
|
||||
model.addAttribute("friendlyMessage", "Diese Seite existiert nicht (mehr).");
|
||||
return "error";
|
||||
}
|
||||
|
||||
/** Security-Fehler NICHT schlucken – die behandelt Spring Security selbst (403/Login). */
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public void rethrowAccessDenied(AccessDeniedException e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public String handleServerError(Exception e, Model model) {
|
||||
log.error("Unbehandelter Fehler", e);
|
||||
model.addAttribute("status", 500);
|
||||
model.addAttribute("friendlyMessage",
|
||||
"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut.");
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.mindflux.controller;
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.service.NetworkerService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.security.Principal;
|
||||
@RestController @RequestMapping("/api/networker") public class NetworkerApiController { private final NetworkerService service; public NetworkerApiController(NetworkerService service){this.service=service;} @GetMapping("/catalog") public JsonNode catalog(){return service.catalog();} @GetMapping("/session/{slug}") public JsonNode session(@PathVariable String slug,HttpSession session){return service.session(slug,session);} @PostMapping("/check") public JsonNode check(@RequestBody NetworkCheckRequest request,HttpSession session){return service.check(request,session);} @PostMapping("/finish") public NetworkResult finish(@RequestBody NetworkFinishRequest request,Principal principal,HttpSession session){return service.finish(principal.getName(),request,session);} }
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.example.mindflux.controller;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@Controller public class NetworkerController { @GetMapping("/networker") public String page(){return "networker";} }
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.controller;
|
||||
import com.example.mindflux.service.NeuralNetService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/neuralnet")
|
||||
public class NeuralNetApiController {
|
||||
private final NeuralNetService service;
|
||||
public NeuralNetApiController(NeuralNetService service) { this.service=service; }
|
||||
@GetMapping("/overview") public NeuralNetService.Overview overview(Principal p) { return service.overview(p.getName()); }
|
||||
@PostMapping("/enable") public void enable(Principal p) { service.enable(p.getName()); }
|
||||
@PostMapping("/requests") public void request(Principal p,@RequestBody Map<String,String> body) { service.request(p.getName(),body.get("username")); }
|
||||
@PostMapping("/requests/{username}/accept") public void accept(Principal p,@PathVariable String username) { service.accept(p.getName(),username); }
|
||||
@PostMapping("/requests/{username}/decline") public void decline(Principal p,@PathVariable String username) { service.decline(p.getName(),username); }
|
||||
@DeleteMapping("/requests/{username}") public void cancel(Principal p,@PathVariable String username) { service.cancel(p.getName(),username); }
|
||||
@GetMapping("/messages/{username}") public Object messages(Principal p,@PathVariable String username) { return service.conversation(p.getName(),username); }
|
||||
@PostMapping("/messages") public Object send(Principal p,@RequestBody Map<String,String> body) { return service.send(p.getName(),body.get("recipient"),body.get("body")); }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.PlatformStatus;
|
||||
import com.example.mindflux.service.PlatformStatusService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class PlatformStatusController {
|
||||
|
||||
private final PlatformStatusService platformStatusService;
|
||||
|
||||
public PlatformStatusController(PlatformStatusService platformStatusService) {
|
||||
this.platformStatusService = platformStatusService;
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/status")
|
||||
public ResponseEntity<PlatformStatus> status() {
|
||||
PlatformStatus status = platformStatusService.currentStatus();
|
||||
return ResponseEntity.status(status.ready() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE).body(status);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import com.example.mindflux.service.ProgressService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
@@ -14,28 +17,35 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.example.mindflux.dto.WeeklyActivity;
|
||||
import com.example.mindflux.service.AvatarService;
|
||||
import com.example.mindflux.service.ProfileExportService;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ProfileController.class);
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/profiles/";
|
||||
@Autowired
|
||||
private ProgressService progressService;
|
||||
|
||||
@Autowired
|
||||
private AvatarService avatarService;
|
||||
|
||||
@Autowired
|
||||
private ProfileExportService profileExportService;
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String profile(Model model, Principal principal, @RequestParam(value = "success", required = false) Boolean success) {
|
||||
@@ -57,38 +67,42 @@ public class ProfileController {
|
||||
User existingUser = userService.findByUsername(username);
|
||||
|
||||
if (existingUser != null) {
|
||||
String profilePicture = existingUser.getProfilePicture();
|
||||
if (profilePicture != null && !profilePicture.trim().isEmpty()) {
|
||||
try {
|
||||
Path picturePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", profilePicture);
|
||||
System.out.println("Versuche zu löschen: " + picturePath.toAbsolutePath());
|
||||
|
||||
if (Files.exists(picturePath)) {
|
||||
Files.delete(picturePath);
|
||||
System.out.println("Datei erfolgreich gelöscht: " + picturePath.getFileName());
|
||||
} else {
|
||||
System.out.println("Datei existiert nicht: " + picturePath.toAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Löschen der Datei: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
System.out.println("Kein Profilbild gesetzt, nichts zu löschen.");
|
||||
}
|
||||
|
||||
existingUser.setProfilePicture(null); // Wichtig: auch wenn Datei nicht existiert
|
||||
avatarService.delete(existingUser.getProfilePicture());
|
||||
existingUser.setProfilePicture(null); // auch wenn keine Datei existierte
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
/** Einstellungen gehören zum Profilbereich – eine Zuständigkeit, ein Controller. */
|
||||
@GetMapping("/profile/settings")
|
||||
public String settings() {
|
||||
return "settings";
|
||||
}
|
||||
|
||||
/** DSGVO-Datenexport (Art. 20): alle Kontodaten als JSON-Download. */
|
||||
@GetMapping("/profile/export")
|
||||
public ResponseEntity<Map<String, Object>> exportData(Principal principal) {
|
||||
Map<String, Object> data = profileExportService.export(principal.getName());
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"mindflux-datenexport.json\"")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(data);
|
||||
}
|
||||
|
||||
@GetMapping("/profile/knowledge")
|
||||
public String knowledgeCenter(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
model.addAttribute("overall", progressService.getOverallProgress(username));
|
||||
WeeklyActivity weekly = progressService.getCurrentWeekActivity(username);
|
||||
model.addAttribute("weekly", weekly != null ? weekly : new WeeklyActivity(
|
||||
List.of(0, 0, 0, 0, 0, 0, 0), List.of(0, 0, 0, 0, 0, 0, 0), 0, 0));
|
||||
var topGames = progressService.getTopGames(username, 3);
|
||||
model.addAttribute("topGames", topGames != null ? topGames : List.of());
|
||||
return "knowledge";
|
||||
}
|
||||
|
||||
@@ -102,47 +116,9 @@ public class ProfileController {
|
||||
if (existingUser != null) {
|
||||
existingUser.setEmail(email); // Email aktualisieren
|
||||
|
||||
if (file != null && !file.isEmpty()) {
|
||||
try {
|
||||
BufferedImage original = ImageIO.read(file.getInputStream());
|
||||
if (original == null) {
|
||||
// Keine lesbare Bilddatei -> Upload ignorieren, restliches Profil trotzdem speichern.
|
||||
System.out.println("Profilbild-Upload ignoriert: keine lesbare Bilddatei.");
|
||||
} else {
|
||||
// Auf max. Kantenlänge herunterskalieren (kein Hochskalieren).
|
||||
final int MAX_DIM = 400;
|
||||
int w = original.getWidth();
|
||||
int h = original.getHeight();
|
||||
double scale = Math.min(1.0, (double) MAX_DIM / Math.max(w, h));
|
||||
int nw = Math.max(1, (int) Math.round(w * scale));
|
||||
int nh = Math.max(1, (int) Math.round(h * scale));
|
||||
|
||||
BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = scaled.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g.setColor(Color.WHITE); // JPEG kennt kein Alpha -> Transparenz auf weißem Grund
|
||||
g.fillRect(0, 0, nw, nh);
|
||||
g.drawImage(original, 0, 0, nw, nh, null);
|
||||
g.dispose();
|
||||
|
||||
// Altes Profilbild löschen (falls vorhanden)
|
||||
if (existingUser.getProfilePicture() != null && !existingUser.getProfilePicture().isEmpty()) {
|
||||
Path oldFilePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", existingUser.getProfilePicture());
|
||||
Files.deleteIfExists(oldFilePath);
|
||||
}
|
||||
|
||||
// Normalisiert als JPEG speichern
|
||||
String fileName = UUID.randomUUID() + ".jpg";
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
Files.createDirectories(uploadPath);
|
||||
ImageIO.write(scaled, "jpg", uploadPath.resolve(fileName).toFile());
|
||||
|
||||
existingUser.setProfilePicture(fileName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
String newAvatar = avatarService.store(file, existingUser.getProfilePicture());
|
||||
if (newAvatar != null) {
|
||||
existingUser.setProfilePicture(newAvatar);
|
||||
}
|
||||
|
||||
userService.save(existingUser);
|
||||
@@ -183,27 +159,11 @@ public class ProfileController {
|
||||
// Profilbild-Datei von der Platte entfernen (DB-Daten löscht der Service in einer Transaktion).
|
||||
User user = userService.findByUsername(username);
|
||||
if (user != null) {
|
||||
String profilePicture = user.getProfilePicture();
|
||||
if (profilePicture != null && !profilePicture.trim().isEmpty()) {
|
||||
try {
|
||||
Path picturePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", profilePicture);
|
||||
Files.deleteIfExists(picturePath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Löschen des Profilbilds: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
avatarService.delete(user.getProfilePicture());
|
||||
}
|
||||
|
||||
userService.deleteByUsername(username);
|
||||
new SecurityContextLogoutHandler().logout(request, response, authentication);
|
||||
return "redirect:/?logout";
|
||||
}
|
||||
|
||||
// Hilfsmethode: Dateiendung extrahieren
|
||||
private String getFileExtension(String filename) {
|
||||
if (filename == null || filename.lastIndexOf(".") == -1) {
|
||||
return "";
|
||||
}
|
||||
return filename.substring(filename.lastIndexOf("."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import com.example.mindflux.dto.WeeklyActivity;
|
||||
|
||||
@Controller
|
||||
public class ProgressController {
|
||||
@@ -41,6 +42,9 @@ public class ProgressController {
|
||||
}
|
||||
|
||||
model.addAttribute("gameProgress", games != null ? games : List.of()); // leere Liste, falls null
|
||||
WeeklyActivity weekly = progressService.getCurrentWeekActivity(username);
|
||||
model.addAttribute("weekly", weekly != null ? weekly : new WeeklyActivity(
|
||||
List.of(0, 0, 0, 0, 0, 0, 0), List.of(0, 0, 0, 0, 0, 0, 0), 0, 0));
|
||||
|
||||
return "progress"; // ruft progress.html auf
|
||||
}
|
||||
|
||||
@@ -1,44 +1,42 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.RegistrationResult;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class RegisterController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
public RegisterController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/register")
|
||||
public String showRegistrationForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "register";
|
||||
public String showRegistrationForm() {
|
||||
// Das Formular lebt auf der Startseite, muss dort aber im Registrierungsmodus erscheinen.
|
||||
return "redirect:/?signup";
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public String processRegistration(@ModelAttribute("user") User user, Model model) {
|
||||
// Benutzername klein schreiben, um Doppelungen zu vermeiden
|
||||
user.setUsername(user.getUsername().toLowerCase());
|
||||
|
||||
// Prüfen, ob Benutzername schon existiert
|
||||
if (userService.findByUsername(user.getUsername()) != null) {
|
||||
// Zurück zur kombinierten Login/Signup-Seite mit geöffnetem Signup-Panel
|
||||
return "redirect:/?registerError";
|
||||
}
|
||||
|
||||
// Passwort verschlüsseln
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
user.setRole("USER"); // Standardrolle
|
||||
userService.save(user);
|
||||
|
||||
return "redirect:/?registerSuccess"; // zurück zum Login mit Erfolgsmeldung
|
||||
public String processRegistration(@RequestParam String username,
|
||||
@RequestParam String email,
|
||||
@RequestParam String password,
|
||||
@RequestParam(required = false) String confirmPassword) {
|
||||
RegistrationResult result = userService.register(username, email, password, confirmPassword);
|
||||
return switch (result) {
|
||||
// Ein SMTP-Problem darf die Nutzung eines neu angelegten Accounts nicht blockieren.
|
||||
// Die E-Mail-Adresse wird weiterhin für den Passwort-Reset gespeichert.
|
||||
case SUCCESS -> "redirect:/?registerSuccess";
|
||||
case PASSWORD_MISMATCH -> "redirect:/?passwordMismatch";
|
||||
case PASSWORD_TOO_SHORT -> "redirect:/?passwordPolicy";
|
||||
case EMAIL_INVALID -> "redirect:/?emailInvalid";
|
||||
case EMAIL_TAKEN -> "redirect:/?emailTaken";
|
||||
case USERNAME_INVALID, USERNAME_TAKEN -> "redirect:/?registerError";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.mindflux.controller;
|
||||
import com.example.mindflux.dto.*; import com.example.mindflux.service.ServiceClashService; import com.fasterxml.jackson.databind.JsonNode; import jakarta.servlet.http.HttpSession; import java.security.Principal; import org.springframework.web.bind.annotation.*;
|
||||
@RestController @RequestMapping("/api/serviceclash") public class ServiceClashApiController {private final ServiceClashService service;public ServiceClashApiController(ServiceClashService service){this.service=service;}@GetMapping("/catalog") public JsonNode catalog(){return service.catalog();}@GetMapping("/session/{slug}") public JsonNode session(@PathVariable String slug,HttpSession http){return service.session(slug,http);}@PostMapping("/check") public JsonNode check(@RequestBody ServiceClashCheckRequest request,HttpSession http){return service.check(request,http);}@PostMapping("/finish") public ServiceClashResult finish(@RequestBody ServiceClashFinishRequest request,Principal principal,HttpSession http){return service.finish(principal.getName(),request,http);}}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.mindflux.controller;
|
||||
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping;
|
||||
@Controller public class ServiceClashController { @GetMapping("/serviceclash") public String page(){return "serviceclash";} }
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.SyntaxSagaCheckRequest;
|
||||
import com.example.mindflux.dto.SyntaxSagaCheckResult;
|
||||
import com.example.mindflux.dto.SyntaxSagaFinishRequest;
|
||||
import com.example.mindflux.dto.SyntaxSagaResult;
|
||||
import com.example.mindflux.dto.SyntaxSagaStoryView;
|
||||
import com.example.mindflux.service.SyntaxSagaService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/** JSON-API für Syntax Saga. Server-autoritativ: Lösungen werden nie ausgeliefert. */
|
||||
@RestController
|
||||
@RequestMapping("/api/syntaxsaga")
|
||||
public class SyntaxSagaApiController {
|
||||
|
||||
private final SyntaxSagaService syntaxSagaService;
|
||||
|
||||
public SyntaxSagaApiController(SyntaxSagaService syntaxSagaService) {
|
||||
this.syntaxSagaService = syntaxSagaService;
|
||||
}
|
||||
|
||||
/** Missionen einer Stufe (oder stufenübergreifend) – ohne Lösungen. */
|
||||
@GetMapping("/story")
|
||||
public SyntaxSagaStoryView story(@RequestParam(required = false) String difficulty) {
|
||||
return syntaxSagaService.buildStory(difficulty);
|
||||
}
|
||||
|
||||
/** Eine Mission serverseitig prüfen; mit {@code reveal=true} auch die Auflösung. */
|
||||
@PostMapping("/check")
|
||||
public SyntaxSagaCheckResult check(@RequestBody SyntaxSagaCheckRequest request) {
|
||||
return syntaxSagaService.check(request);
|
||||
}
|
||||
|
||||
/** Durchlauf abschließen: Score wird serverseitig neu berechnet und gespeichert. */
|
||||
@PostMapping("/finish")
|
||||
public SyntaxSagaResult finish(@RequestBody SyntaxSagaFinishRequest request, Principal principal) {
|
||||
return syntaxSagaService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -4,10 +4,10 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SettingsController {
|
||||
public class SyntaxSagaController {
|
||||
|
||||
@GetMapping("/profile/settings")
|
||||
public String showSettingsPage() {
|
||||
return "settings"; // lädt templates/settings.html
|
||||
@GetMapping("/syntaxsaga")
|
||||
public String show() {
|
||||
return "syntaxsaga"; // Lädt templates/syntaxsaga.html
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
/** Spieler-API für TechTacular: Board laden, Antwort prüfen, Runde abschließen. */
|
||||
@RestController
|
||||
@RequestMapping("/api/techtacular")
|
||||
public class TechtacularApiController {
|
||||
|
||||
private static final String JOKER_USED = "techtacular.fiftyFiftyUsed";
|
||||
private static final String HINTED_QUESTION_IDS = "techtacular.hintedQuestionIds";
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public TechtacularApiController(TechtacularService techtacularService) {
|
||||
@@ -21,7 +25,10 @@ public class TechtacularApiController {
|
||||
|
||||
/** exclude = komma-separierte Frage-IDs der Vorrunde, die möglichst nicht erneut kommen sollen. */
|
||||
@GetMapping("/board")
|
||||
public TechtacularBoardView board(@RequestParam(required = false) String exclude) {
|
||||
public TechtacularBoardView board(@RequestParam(required = false) String exclude, HttpSession session) {
|
||||
// Ein neues Board beginnt immer eine neue Runde mit genau einem Joker.
|
||||
session.removeAttribute(JOKER_USED);
|
||||
session.removeAttribute(HINTED_QUESTION_IDS);
|
||||
return techtacularService.buildBoard(parseIds(exclude));
|
||||
}
|
||||
|
||||
@@ -40,12 +47,41 @@ public class TechtacularApiController {
|
||||
}
|
||||
|
||||
@PostMapping("/answer")
|
||||
public AnswerResult answer(@RequestBody AnswerRequest request) {
|
||||
return techtacularService.checkAnswer(request.getQuestionId(), request.getAnswerId());
|
||||
public AnswerResult answer(@RequestBody AnswerRequest request, HttpSession session) {
|
||||
// Das Client-Flag dient der UI und dem Finish-Payload. Ob die Punkte halbiert
|
||||
// werden, entscheidet jedoch allein der serverseitige Rundenzustand.
|
||||
boolean validHint = hintedQuestionIds(session).contains(request.getQuestionId());
|
||||
return techtacularService.checkAnswer(request.getQuestionId(), request.getAnswerId(), validHint);
|
||||
}
|
||||
|
||||
/** Liefert zwei eliminierbare, garantiert falsche Antworten ohne die Loesung offenzulegen. */
|
||||
@GetMapping("/hint")
|
||||
public FiftyFiftyHint hint(@RequestParam Long questionId, HttpSession session) {
|
||||
if (Boolean.TRUE.equals(session.getAttribute(JOKER_USED))) {
|
||||
return new FiftyFiftyHint(java.util.List.of());
|
||||
}
|
||||
|
||||
FiftyFiftyHint hint = techtacularService.fiftyFifty(questionId);
|
||||
if (!hint.removeAnswerIds().isEmpty()) {
|
||||
session.setAttribute(JOKER_USED, true);
|
||||
Set<Long> hinted = new HashSet<>();
|
||||
hinted.add(questionId);
|
||||
session.setAttribute(HINTED_QUESTION_IDS, hinted);
|
||||
}
|
||||
return hint;
|
||||
}
|
||||
|
||||
@PostMapping("/finish")
|
||||
public TechtacularResult finish(@RequestBody FinishRequest request, Principal principal) {
|
||||
return techtacularService.finish(principal.getName(), request);
|
||||
public TechtacularResult finish(@RequestBody FinishRequest request, Principal principal, HttpSession session) {
|
||||
TechtacularResult result = techtacularService.finish(principal.getName(), request, hintedQuestionIds(session));
|
||||
session.removeAttribute(JOKER_USED);
|
||||
session.removeAttribute(HINTED_QUESTION_IDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<Long> hintedQuestionIds(HttpSession session) {
|
||||
Object value = session.getAttribute(HINTED_QUESTION_IDS);
|
||||
return value instanceof Set<?> ? (Set<Long>) value : Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,72 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
private final UserService userService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
public UserController(UserService userService, PasswordEncoder passwordEncoder) {
|
||||
this.userService = userService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
// 📥 User erstellen
|
||||
// 📥 User erstellen (Validierung, Eindeutigkeit und Encoding zentral im UserService)
|
||||
@PostMapping
|
||||
public User createUser(@RequestBody User user) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
return userRepository.save(user);
|
||||
return userService.createUser(user.getUsername(), user.getEmail(), user.getPassword(), user.getRole());
|
||||
}
|
||||
|
||||
// 📋 Alle User anzeigen
|
||||
@GetMapping
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
return userService.findAll();
|
||||
}
|
||||
|
||||
// 📍 Einzelnen User anzeigen
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
||||
Optional<User> user = userRepository.findById(id);
|
||||
return user.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
return userService.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ✏️ User aktualisieren
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
|
||||
return userRepository.findById(id).map(user -> {
|
||||
user.setUsername(updatedUser.getUsername());
|
||||
return userService.findById(id).map(user -> {
|
||||
String newName = updatedUser.getUsername() == null ? "" : updatedUser.getUsername().trim();
|
||||
if (newName.isBlank()) {
|
||||
throw new IllegalArgumentException("Ungültiger Benutzername.");
|
||||
}
|
||||
userService.assertUsernameAvailable(newName, id);
|
||||
user.setUsername(newName);
|
||||
if (updatedUser.getPassword() != null && !updatedUser.getPassword().isBlank()) {
|
||||
user.setPassword(passwordEncoder.encode(updatedUser.getPassword()));
|
||||
}
|
||||
user.setEmail(updatedUser.getEmail());
|
||||
user.setRole(updatedUser.getRole());
|
||||
return ResponseEntity.ok(userRepository.save(user));
|
||||
user.setRole(userService.validateRole(updatedUser.getRole()));
|
||||
userService.save(user);
|
||||
return ResponseEntity.ok(user);
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// 🗑️ User löschen
|
||||
// 🗑️ User löschen (inkl. zugehöriger Scores/Achievements)
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
if (userRepository.existsById(id)) {
|
||||
userRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
if (userService.findById(id).isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
userService.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/users")
|
||||
public class UserViewController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
// Formular anzeigen
|
||||
@GetMapping("/form")
|
||||
public String showForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "user-form";
|
||||
}
|
||||
|
||||
// Benutzer speichern
|
||||
@PostMapping("/save")
|
||||
public String saveUser(@ModelAttribute User user) {
|
||||
userRepository.save(user);
|
||||
return "user-success"; // statt redirect
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,10 +4,15 @@ package com.example.mindflux.dto;
|
||||
public class AnswerRequest {
|
||||
private Long questionId;
|
||||
private Long answerId;
|
||||
private boolean hintUsed;
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long questionId) { this.questionId = questionId; }
|
||||
|
||||
public Long getAnswerId() { return answerId; }
|
||||
public void setAnswerId(Long answerId) { this.answerId = answerId; }
|
||||
|
||||
/** Der Client zeigt damit an, dass fuer diese Antwort der 50:50-Joker aktiv war. */
|
||||
public boolean isHintUsed() { return hintUsed; }
|
||||
public void setHintUsed(boolean hintUsed) { this.hintUsed = hintUsed; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class BugHunterCheckRequest {
|
||||
private Long bugId;
|
||||
private String phase;
|
||||
private boolean reveal;
|
||||
private JsonNode submission;
|
||||
public Long getBugId() { return bugId; } public void setBugId(Long value) { bugId = value; }
|
||||
public String getPhase() { return phase; } public void setPhase(String value) { phase = value; }
|
||||
public boolean isReveal() { return reveal; } public void setReveal(boolean value) { reveal = value; }
|
||||
public JsonNode getSubmission() { return submission; } public void setSubmission(JsonNode value) { submission = value; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.List;
|
||||
|
||||
/** Rohantworten eines BugHunter-Durchlaufs; die Wertung wird komplett serverseitig rekonstruiert. */
|
||||
public class BugHunterFinishRequest {
|
||||
private String slug;
|
||||
private int durationSeconds;
|
||||
private List<Item> items;
|
||||
public String getSlug() { return slug; } public void setSlug(String value) { slug = value; }
|
||||
public int getDurationSeconds() { return durationSeconds; } public void setDurationSeconds(int value) { durationSeconds = value; }
|
||||
public List<Item> getItems() { return items; } public void setItems(List<Item> value) { items = value; }
|
||||
public static class Item {
|
||||
private Long bugId;
|
||||
private Phase locate;
|
||||
private Phase classify;
|
||||
private Phase fix;
|
||||
public Long getBugId() { return bugId; } public void setBugId(Long value) { bugId = value; }
|
||||
public Phase getLocate() { return locate; } public void setLocate(Phase value) { locate = value; }
|
||||
public Phase getClassify() { return classify; } public void setClassify(Phase value) { classify = value; }
|
||||
public Phase getFix() { return fix; } public void setFix(Phase value) { fix = value; }
|
||||
}
|
||||
public static class Phase {
|
||||
private JsonNode submission;
|
||||
private int attempts;
|
||||
private boolean revealed;
|
||||
public JsonNode getSubmission() { return submission; } public void setSubmission(JsonNode value) { submission = value; }
|
||||
public int getAttempts() { return attempts; } public void setAttempts(int value) { attempts = value; }
|
||||
public boolean isRevealed() { return revealed; } public void setRevealed(boolean value) { revealed = value; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
public record BugHunterResult(int score, int baseScore, int maxScore, int comboBonus, int maxCombo,
|
||||
int bugsSolved, int totalBugs, int stars, int highscore,
|
||||
boolean newHighscore, long plays, boolean cleanSolve) { }
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/** Prüft eine Dev-Lab-Aufgabe. {@code submission} ist mechanikspezifisch (JSON). */
|
||||
public class DevLabCheckRequest {
|
||||
|
||||
private Long taskId;
|
||||
private boolean reveal;
|
||||
private JsonNode submission;
|
||||
|
||||
public Long getTaskId() { return taskId; }
|
||||
public void setTaskId(Long taskId) { this.taskId = taskId; }
|
||||
|
||||
public boolean isReveal() { return reveal; }
|
||||
public void setReveal(boolean reveal) { this.reveal = reveal; }
|
||||
|
||||
public JsonNode getSubmission() { return submission; }
|
||||
public void setSubmission(JsonNode submission) { this.submission = submission; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abschluss eines Dev-Lab-Durchlaufs: pro Aufgabe die letzte Eingabe + Versuche/Auflösung.
|
||||
* Der Score wird serverseitig aus den Eingaben neu berechnet.
|
||||
*/
|
||||
public class DevLabFinishRequest {
|
||||
|
||||
private String slug;
|
||||
private int durationSeconds;
|
||||
private List<Item> items;
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<Item> getItems() { return items; }
|
||||
public void setItems(List<Item> items) { this.items = items; }
|
||||
|
||||
public static class Item {
|
||||
private Long taskId;
|
||||
private JsonNode submission;
|
||||
private int attempts;
|
||||
private boolean revealed;
|
||||
|
||||
public Long getTaskId() { return taskId; }
|
||||
public void setTaskId(Long taskId) { this.taskId = taskId; }
|
||||
|
||||
public JsonNode getSubmission() { return submission; }
|
||||
public void setSubmission(JsonNode submission) { this.submission = submission; }
|
||||
|
||||
public int getAttempts() { return attempts; }
|
||||
public void setAttempts(int attempts) { this.attempts = attempts; }
|
||||
|
||||
public boolean isRevealed() { return revealed; }
|
||||
public void setRevealed(boolean revealed) { this.revealed = revealed; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der Admin-Spieleübersicht (ohne alle Aufgaben zu laden). */
|
||||
public class DevLabGameRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final String language;
|
||||
private final String path;
|
||||
private final boolean active;
|
||||
private final long taskCount;
|
||||
|
||||
public DevLabGameRow(Long id, String slug, String title, String language, String path,
|
||||
boolean active, long taskCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.language = language;
|
||||
this.path = path;
|
||||
this.active = active;
|
||||
this.taskCount = taskCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public String getLanguage() { return language; }
|
||||
public String getPath() { return path; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getTaskCount() { return taskCount; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Endergebnis eines Dev-Lab-Durchlaufs (Score serverseitig berechnet) + Highscore-Stand. */
|
||||
public class DevLabResult {
|
||||
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int solvedTasks;
|
||||
private final int totalTasks;
|
||||
private final int stars;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public DevLabResult(int score, int maxScore, int solvedTasks, int totalTasks, int stars,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.solvedTasks = solvedTasks;
|
||||
this.totalTasks = totalTasks;
|
||||
this.stars = stars;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getSolvedTasks() { return solvedTasks; }
|
||||
public int getTotalTasks() { return totalTasks; }
|
||||
public int getStars() { return stars; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.example.mindflux.dto; import com.fasterxml.jackson.databind.JsonNode; public class EscapeCheckRequest {private Long puzzleId;private boolean reveal;private JsonNode submission;public Long getPuzzleId(){return puzzleId;}public void setPuzzleId(Long v){puzzleId=v;}public boolean isReveal(){return reveal;}public void setReveal(boolean v){reveal=v;}public JsonNode getSubmission(){return submission;}public void setSubmission(JsonNode v){submission=v;}}
|
||||
@@ -0,0 +1 @@
|
||||
package com.example.mindflux.dto; public class EscapeDoorRequest {private Long roomId;private String code;public Long getRoomId(){return roomId;}public void setRoomId(Long v){roomId=v;}public String getCode(){return code;}public void setCode(String v){code=v;}}
|
||||
@@ -0,0 +1 @@
|
||||
package com.example.mindflux.dto; import com.fasterxml.jackson.databind.JsonNode; import java.util.*; public class EscapeFinishRequest {private String slug;private int durationSeconds;private boolean escaped;private List<Item> items=new ArrayList<>();private List<Door> doors=new ArrayList<>();public String getSlug(){return slug;}public void setSlug(String v){slug=v;}public int getDurationSeconds(){return durationSeconds;}public void setDurationSeconds(int v){durationSeconds=v;}public boolean isEscaped(){return escaped;}public void setEscaped(boolean v){escaped=v;}public List<Item> getItems(){return items;}public void setItems(List<Item> v){items=v;}public List<Door> getDoors(){return doors;}public void setDoors(List<Door> v){doors=v;}public static class Item{private Long puzzleId;private JsonNode submission;public Long getPuzzleId(){return puzzleId;}public void setPuzzleId(Long v){puzzleId=v;}public JsonNode getSubmission(){return submission;}public void setSubmission(JsonNode v){submission=v;}}public static class Door{private Long roomId;private String code;public Long getRoomId(){return roomId;}public void setRoomId(Long v){roomId=v;}public String getCode(){return code;}public void setCode(String v){code=v;}}}
|
||||
@@ -0,0 +1 @@
|
||||
package com.example.mindflux.dto; public class EscapeHintRequest {private Long puzzleId;private int level;public Long getPuzzleId(){return puzzleId;}public void setPuzzleId(Long v){puzzleId=v;}public int getLevel(){return level;}public void setLevel(int v){level=v;}}
|
||||
@@ -0,0 +1 @@
|
||||
package com.example.mindflux.dto; public record EscapeResult(int score,int baseScore,int maxScore,int timeBonus,int solvedPuzzles,int roomsOpened,boolean escaped,int stars,int highscore,boolean newHighscore,long plays){}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Antwort des 50:50-Jokers. Die korrekte Antwort-ID wird niemals uebertragen. */
|
||||
public record FiftyFiftyHint(List<Long> removeAnswerIds) {
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.mindflux.dto;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
public class NetworkCheckRequest { private Long levelId; private boolean reveal; private JsonNode config; public Long getLevelId(){return levelId;} public void setLevelId(Long value){levelId=value;} public boolean isReveal(){return reveal;} public void setReveal(boolean value){reveal=value;} public JsonNode getConfig(){return config;} public void setConfig(JsonNode value){config=value;} }
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.example.mindflux.dto;
|
||||
import java.util.*;
|
||||
public class NetworkFinishRequest { private String slug; private int durationSeconds; private List<Long> levelIds; public String getSlug(){return slug;} public void setSlug(String value){slug=value;} public int getDurationSeconds(){return durationSeconds;} public void setDurationSeconds(int value){durationSeconds=value;} public List<Long> getLevelIds(){return levelIds;} public void setLevelIds(List<Long> value){levelIds=value;} }
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.dto;
|
||||
public record NetworkResult(int score, int maxScore, int solved, int total, int stars, int highscore, boolean newHighscore, long plays, boolean perfect) { }
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Ein bewusst kleiner, nachvollziehbarer Plattformzustand für das Dashboard.
|
||||
* Er behauptet keine fiktive Verfügbarkeit, sondern prüft den Speicher, der
|
||||
* Spielstände und Fortschritt tatsächlich trägt.
|
||||
*/
|
||||
public record PlatformStatus(
|
||||
boolean storageConnected,
|
||||
int registeredModules,
|
||||
Instant checkedAt
|
||||
) {
|
||||
public boolean ready() {
|
||||
return storageConnected;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,15 @@ public class ProgressData {
|
||||
private int xpForNextLevel;
|
||||
private int userLevel;
|
||||
private int progressPercent;
|
||||
private int streakDays;
|
||||
|
||||
public int getStreakDays() {
|
||||
return streakDays;
|
||||
}
|
||||
|
||||
public void setStreakDays(int streakDays) {
|
||||
this.streakDays = streakDays;
|
||||
}
|
||||
|
||||
public int getCorrectAnswers() {
|
||||
return correctAnswers;
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* Was der Client nach einem abgeschlossenen Spiel sendet. Der Score wird
|
||||
* bewusst NICHT mitgeschickt, sondern serverseitig aus diesen Rohwerten
|
||||
* berechnet, damit ein manipulierter Client keinen Fantasie-Score speichern kann.
|
||||
* Die Constraints fangen offensichtlich unsinnige Eingaben schon vor der Logik ab.
|
||||
*/
|
||||
public class ScoreSubmission {
|
||||
@NotBlank @Size(max = 100)
|
||||
private String deck;
|
||||
@Min(0) @Max(1000)
|
||||
private int pairs;
|
||||
@Min(0) @Max(100000)
|
||||
private int moves;
|
||||
@Min(0) @Max(86400)
|
||||
private int durationSeconds;
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.dto;
|
||||
public class ServiceClashCheckRequest { private Long ticketId; private String step; private String value; private boolean reveal; public Long getTicketId(){return ticketId;} public void setTicketId(Long value){ticketId=value;} public String getStep(){return step;} public void setStep(String value){step=value;} public String getValue(){return value;} public void setValue(String value){this.value=value;} public boolean isReveal(){return reveal;} public void setReveal(boolean value){reveal=value;} }
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.dto;
|
||||
public class ServiceClashFinishRequest { private String slug; private int durationSeconds; public String getSlug(){return slug;} public void setSlug(String value){slug=value;} public int getDurationSeconds(){return durationSeconds;} public void setDurationSeconds(int value){durationSeconds=value;} }
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.example.mindflux.dto;
|
||||
public record ServiceClashResult(int score,int maxScore,int stars,int resolved,int total,int health,int highscore,boolean newHighscore,long plays,boolean flawless) { }
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Eine Mission prüfen: die eingetippten Lücken (+ ob die Auflösung angefordert wird). */
|
||||
public class SyntaxSagaCheckRequest {
|
||||
|
||||
private Long missionId;
|
||||
private boolean reveal;
|
||||
private List<GapAnswer> answers;
|
||||
|
||||
public Long getMissionId() { return missionId; }
|
||||
public void setMissionId(Long missionId) { this.missionId = missionId; }
|
||||
|
||||
public boolean isReveal() { return reveal; }
|
||||
public void setReveal(boolean reveal) { this.reveal = reveal; }
|
||||
|
||||
public List<GapAnswer> getAnswers() { return answers; }
|
||||
public void setAnswers(List<GapAnswer> answers) { this.answers = answers; }
|
||||
|
||||
public static class GapAnswer {
|
||||
private int position;
|
||||
private String text;
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public void setPosition(int position) { this.position = position; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Serverseitige Bewertung einer Mission (instant feedback). {@code solution} ist nur
|
||||
* gefüllt, wenn die Auflösung angefordert wurde und die Lücke nicht korrekt war.
|
||||
*/
|
||||
public class SyntaxSagaCheckResult {
|
||||
|
||||
private final boolean allCorrect;
|
||||
private final List<GapVerdict> results;
|
||||
|
||||
public SyntaxSagaCheckResult(boolean allCorrect, List<GapVerdict> results) {
|
||||
this.allCorrect = allCorrect;
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public boolean isAllCorrect() { return allCorrect; }
|
||||
public List<GapVerdict> getResults() { return results; }
|
||||
|
||||
public static class GapVerdict {
|
||||
private final int position;
|
||||
private final boolean correct;
|
||||
private final String solution;
|
||||
|
||||
public GapVerdict(int position, boolean correct, String solution) {
|
||||
this.position = position;
|
||||
this.correct = correct;
|
||||
this.solution = solution;
|
||||
}
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public boolean isCorrect() { return correct; }
|
||||
public String getSolution() { return solution; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abschluss eines Saga-Durchlaufs: alle eingetippten Lücken über alle Missionen einer
|
||||
* Stufe (+ Versuche und ob aufgelöst wurde). Der Score wird serverseitig neu berechnet.
|
||||
*/
|
||||
public class SyntaxSagaFinishRequest {
|
||||
|
||||
private String difficulty;
|
||||
private int durationSeconds;
|
||||
private List<FinishGap> gaps;
|
||||
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public void setDifficulty(String difficulty) { this.difficulty = difficulty; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<FinishGap> getGaps() { return gaps; }
|
||||
public void setGaps(List<FinishGap> gaps) { this.gaps = gaps; }
|
||||
|
||||
public static class FinishGap {
|
||||
private Long missionId;
|
||||
private int position;
|
||||
private String text;
|
||||
private int attempts;
|
||||
private boolean revealed;
|
||||
|
||||
public Long getMissionId() { return missionId; }
|
||||
public void setMissionId(Long missionId) { this.missionId = missionId; }
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public void setPosition(int position) { this.position = position; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public int getAttempts() { return attempts; }
|
||||
public void setAttempts(int attempts) { this.attempts = attempts; }
|
||||
|
||||
public boolean isRevealed() { return revealed; }
|
||||
public void setRevealed(boolean revealed) { this.revealed = revealed; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der Admin-Missionsübersicht (ohne alle Lücken zu laden). */
|
||||
public class SyntaxSagaMissionRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final String difficulty;
|
||||
private final boolean active;
|
||||
private final long gapCount;
|
||||
|
||||
public SyntaxSagaMissionRow(Long id, String slug, String title, String difficulty,
|
||||
boolean active, long gapCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.difficulty = difficulty;
|
||||
this.active = active;
|
||||
this.gapCount = gapCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getGapCount() { return gapCount; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Endergebnis eines Saga-Durchlaufs (Score serverseitig berechnet) + Highscore-Stand. */
|
||||
public class SyntaxSagaResult {
|
||||
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int solvedGaps;
|
||||
private final int totalGaps;
|
||||
private final int stars;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public SyntaxSagaResult(int score, int maxScore, int solvedGaps, int totalGaps, int stars,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.solvedGaps = solvedGaps;
|
||||
this.totalGaps = totalGaps;
|
||||
this.stars = stars;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getSolvedGaps() { return solvedGaps; }
|
||||
public int getTotalGaps() { return totalGaps; }
|
||||
public int getStars() { return stars; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Die Missionen einer Schwierigkeitsstufe fürs Frontend. Bewusst OHNE Lösungen –
|
||||
* geprüft und gewertet wird ausschließlich serverseitig.
|
||||
*/
|
||||
public class SyntaxSagaStoryView {
|
||||
|
||||
private final String difficulty;
|
||||
private final List<Mission> missions;
|
||||
private final int maxScore;
|
||||
private final int totalGaps;
|
||||
|
||||
public SyntaxSagaStoryView(String difficulty, List<Mission> missions, int maxScore, int totalGaps) {
|
||||
this.difficulty = difficulty;
|
||||
this.missions = missions;
|
||||
this.maxScore = maxScore;
|
||||
this.totalGaps = totalGaps;
|
||||
}
|
||||
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public List<Mission> getMissions() { return missions; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getTotalGaps() { return totalGaps; }
|
||||
|
||||
public static class Mission {
|
||||
private final Long missionId;
|
||||
private final String title;
|
||||
private final String difficulty;
|
||||
private final String language;
|
||||
private final String narrative;
|
||||
private final String dialog;
|
||||
private final String codeTemplate;
|
||||
private final String successText;
|
||||
private final List<Gap> gaps;
|
||||
|
||||
public Mission(Long missionId, String title, String difficulty, String language,
|
||||
String narrative, String dialog, String codeTemplate,
|
||||
String successText, List<Gap> gaps) {
|
||||
this.missionId = missionId;
|
||||
this.title = title;
|
||||
this.difficulty = difficulty;
|
||||
this.language = language;
|
||||
this.narrative = narrative;
|
||||
this.dialog = dialog;
|
||||
this.codeTemplate = codeTemplate;
|
||||
this.successText = successText;
|
||||
this.gaps = gaps;
|
||||
}
|
||||
|
||||
public Long getMissionId() { return missionId; }
|
||||
public String getTitle() { return title; }
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public String getLanguage() { return language; }
|
||||
public String getNarrative() { return narrative; }
|
||||
public String getDialog() { return dialog; }
|
||||
public String getCodeTemplate() { return codeTemplate; }
|
||||
public String getSuccessText() { return successText; }
|
||||
public List<Gap> getGaps() { return gaps; }
|
||||
}
|
||||
|
||||
public static class Gap {
|
||||
private final int position;
|
||||
private final int points;
|
||||
private final String hint;
|
||||
private final String placeholder;
|
||||
|
||||
public Gap(int position, int points, String hint, String placeholder) {
|
||||
this.position = position;
|
||||
this.points = points;
|
||||
this.hint = hint;
|
||||
this.placeholder = placeholder;
|
||||
}
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public int getPoints() { return points; }
|
||||
public String getHint() { return hint; }
|
||||
public String getPlaceholder() { return placeholder; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Tatsächlich aufgezeichnete Aktivität von Montag bis Sonntag. */
|
||||
public record WeeklyActivity(List<Integer> minutes, List<Integer> xp, int totalMinutes, int totalXp) {
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "account_email_tokens")
|
||||
public class AccountEmailToken {
|
||||
public enum Purpose { EMAIL_VERIFICATION, PASSWORD_RESET }
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
@Column(name = "token_hash", nullable = false, unique = true, length = 64)
|
||||
private String tokenHash;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 32)
|
||||
private Purpose purpose;
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private Instant expiresAt;
|
||||
|
||||
@Column(name = "consumed_at")
|
||||
private Instant consumedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public User getUser() { return user; }
|
||||
public void setUser(User user) { this.user = user; }
|
||||
public String getTokenHash() { return tokenHash; }
|
||||
public void setTokenHash(String tokenHash) { this.tokenHash = tokenHash; }
|
||||
public Purpose getPurpose() { return purpose; }
|
||||
public void setPurpose(Purpose purpose) { this.purpose = purpose; }
|
||||
public Instant getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; }
|
||||
public Instant getConsumedAt() { return consumedAt; }
|
||||
public void setConsumedAt(Instant consumedAt) { this.consumedAt = consumedAt; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity @Table(name = "bughunter_bugs")
|
||||
public class BugHunterBug {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "hunt_id", nullable = false) private BugHunterHunt hunt;
|
||||
private int points = 100;
|
||||
@Column(name = "sort_order") private int sortOrder;
|
||||
@Column(length = 8000) private String payload;
|
||||
public BugHunterBug() { }
|
||||
public BugHunterBug(int points, int sortOrder, String payload) { this.points = points; this.sortOrder = sortOrder; this.payload = payload; }
|
||||
public Long getId() { return id; } public void setId(Long value) { id = value; }
|
||||
public BugHunterHunt getHunt() { return hunt; } public void setHunt(BugHunterHunt value) { hunt = value; }
|
||||
public int getPoints() { return points; } public void setPoints(int value) { points = value; }
|
||||
public int getSortOrder() { return sortOrder; } public void setSortOrder(int value) { sortOrder = value; }
|
||||
public String getPayload() { return payload; } public void setPayload(String value) { payload = value; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity @Table(name = "bughunter_files")
|
||||
public class BugHunterFile {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "hunt_id", nullable = false) private BugHunterHunt hunt;
|
||||
private String filename;
|
||||
@Column(length = 8000) private String content;
|
||||
@Column(name = "sort_order") private int sortOrder;
|
||||
public BugHunterFile() { }
|
||||
public BugHunterFile(String filename, String content, int sortOrder) { this.filename = filename; this.content = content; this.sortOrder = sortOrder; }
|
||||
public Long getId() { return id; } public void setId(Long value) { id = value; }
|
||||
public BugHunterHunt getHunt() { return hunt; } public void setHunt(BugHunterHunt value) { hunt = value; }
|
||||
public String getFilename() { return filename; } public void setFilename(String value) { filename = value; }
|
||||
public String getContent() { return content; } public void setContent(String value) { content = value; }
|
||||
public int getSortOrder() { return sortOrder; } public void setSortOrder(int value) { sortOrder = value; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.*;
|
||||
|
||||
@Entity @Table(name = "bughunter_hunts")
|
||||
public class BugHunterHunt {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
@Column(nullable = false, unique = true) private String slug;
|
||||
@Column(nullable = false) private String title;
|
||||
@Column(name = "code_language") private String codeLanguage;
|
||||
private String difficulty;
|
||||
@Column(name = "exam_scope") private String examScope = "AP1";
|
||||
private String description;
|
||||
@Column(length = 2000) private String briefing;
|
||||
@Column(name = "console_output", length = 4000) private String consoleOutput;
|
||||
@Column(name = "sort_order") private int sortOrder;
|
||||
private boolean active = true;
|
||||
@OneToMany(mappedBy = "hunt", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC") private List<BugHunterFile> files = new ArrayList<>();
|
||||
@OneToMany(mappedBy = "hunt", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC") private List<BugHunterBug> bugs = new ArrayList<>();
|
||||
|
||||
public BugHunterHunt() { }
|
||||
public BugHunterHunt(String slug, String title, String codeLanguage, String difficulty, String examScope,
|
||||
String description, String briefing, String consoleOutput, int sortOrder) {
|
||||
this.slug = slug; this.title = title; this.codeLanguage = codeLanguage; this.difficulty = difficulty;
|
||||
this.examScope = examScope; this.description = description; this.briefing = briefing;
|
||||
this.consoleOutput = consoleOutput; this.sortOrder = sortOrder;
|
||||
}
|
||||
public void addFile(BugHunterFile file) { file.setHunt(this); files.add(file); }
|
||||
public void addBug(BugHunterBug bug) { bug.setHunt(this); bugs.add(bug); }
|
||||
public Long getId() { return id; } public void setId(Long id) { this.id = id; }
|
||||
public String getSlug() { return slug; } public void setSlug(String slug) { this.slug = slug; }
|
||||
public String getTitle() { return title; } public void setTitle(String title) { this.title = title; }
|
||||
public String getCodeLanguage() { return codeLanguage; } public void setCodeLanguage(String value) { codeLanguage = value; }
|
||||
public String getDifficulty() { return difficulty; } public void setDifficulty(String value) { difficulty = value; }
|
||||
public String getExamScope() { return examScope; } public void setExamScope(String value) { examScope = value; }
|
||||
public String getDescription() { return description; } public void setDescription(String value) { description = value; }
|
||||
public String getBriefing() { return briefing; } public void setBriefing(String value) { briefing = value; }
|
||||
public String getConsoleOutput() { return consoleOutput; } public void setConsoleOutput(String value) { consoleOutput = value; }
|
||||
public int getSortOrder() { return sortOrder; } public void setSortOrder(int value) { sortOrder = value; }
|
||||
public boolean isActive() { return active; } public void setActive(boolean value) { active = value; }
|
||||
public List<BugHunterFile> getFiles() { return files; } public List<BugHunterBug> getBugs() { return bugs; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "bughunter_hunt_progress", uniqueConstraints = @UniqueConstraint(columnNames = {"username", "hunt_slug"}))
|
||||
public class BugHunterHuntProgress {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
@Column(nullable = false) private String username;
|
||||
@Column(name = "hunt_slug", nullable = false) private String huntSlug;
|
||||
@Column(name = "solved_clean", nullable = false) private boolean solvedClean;
|
||||
@Column(name = "variant_index", nullable = false) private int variantIndex;
|
||||
@Column(name = "solved_at") private LocalDateTime solvedAt;
|
||||
|
||||
public BugHunterHuntProgress() { }
|
||||
public BugHunterHuntProgress(String username, String huntSlug) { this.username = username; this.huntSlug = huntSlug; }
|
||||
public Long getId() { return id; }
|
||||
public String getUsername() { return username; }
|
||||
public String getHuntSlug() { return huntSlug; }
|
||||
public boolean isSolvedClean() { return solvedClean; }
|
||||
public void setSolvedClean(boolean value) { solvedClean = value; }
|
||||
public int getVariantIndex() { return variantIndex; }
|
||||
public void setVariantIndex(int value) { variantIndex = value; }
|
||||
public LocalDateTime getSolvedAt() { return solvedAt; }
|
||||
public void setSolvedAt(LocalDateTime value) { solvedAt = value; }
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ein Dev-Lab-Spiel (z.B. "SQL Detective"). Bündelt themengleiche Aufgaben unterschiedlicher
|
||||
* Mechaniken. Inhalte DB-gestützt und im Admin pflegbar; server-autoritativ geprüft.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "devlab_games")
|
||||
public class DevLabGame {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
/** Anzeige-Sprache/Domäne, z.B. "SQL", "CSS", "Git". */
|
||||
@Column(name = "code_language")
|
||||
private String language;
|
||||
|
||||
/** Trainingspfad: "syntax" | "debugging" | "visual". */
|
||||
@Column(name = "lab_path")
|
||||
private String path;
|
||||
|
||||
private String description;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC")
|
||||
private List<DevLabTask> tasks = new ArrayList<>();
|
||||
|
||||
public DevLabGame() {
|
||||
}
|
||||
|
||||
public DevLabGame(String slug, String title, String language, String path,
|
||||
String description, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.language = language;
|
||||
this.path = path;
|
||||
this.description = description;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void addTask(DevLabTask task) {
|
||||
task.setGame(this);
|
||||
this.tasks.add(task);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<DevLabTask> getTasks() { return tasks; }
|
||||
public void setTasks(List<DevLabTask> tasks) { this.tasks = tasks; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
* Eine Dev-Lab-Aufgabe. Der {@code type} bestimmt die Spielmechanik (cloze, choice, order,
|
||||
* spot, match, regex); die mechanikspezifischen Daten inkl. Lösung stehen als JSON im
|
||||
* {@code payload}. Der {@code payload} verlässt nie unverändert das Backend – der Service baut
|
||||
* daraus eine bereinigte Sicht (ohne Lösung) und prüft serverseitig.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "devlab_tasks")
|
||||
public class DevLabTask {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "game_id")
|
||||
private DevLabGame game;
|
||||
|
||||
/** Mechanik: cloze | choice | order | spot | match | regex. */
|
||||
@Column(name = "task_type")
|
||||
private String type;
|
||||
|
||||
/** Kurzer Aufgabentitel (für Anzeige + Admin-Liste). */
|
||||
private String prompt;
|
||||
|
||||
private int points = 100;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
/** Vollständige Aufgabenspezifikation als JSON (inkl. Lösung). */
|
||||
private String payload;
|
||||
|
||||
public DevLabTask() {
|
||||
}
|
||||
|
||||
public DevLabTask(String type, String prompt, int points, int sortOrder, String payload) {
|
||||
this.type = type;
|
||||
this.prompt = prompt;
|
||||
this.points = points;
|
||||
this.sortOrder = sortOrder;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public DevLabGame getGame() { return game; }
|
||||
public void setGame(DevLabGame game) { this.game = game; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
|
||||
public String getPrompt() { return prompt; }
|
||||
public void setPrompt(String prompt) { this.prompt = prompt; }
|
||||
|
||||
public int getPoints() { return points; }
|
||||
public void setPoints(int points) { this.points = points; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public String getPayload() { return payload; }
|
||||
public void setPayload(String payload) { this.payload = payload; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.model;
|
||||
import jakarta.persistence.*;
|
||||
@Entity @Table(name="escape_puzzles") public class EscapePuzzle {
|
||||
@Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="room_id") private EscapeRoom room;
|
||||
@Column(name="puzzle_type") private String type; private String prompt; private int points=100; @Column(name="code_fragment") private String codeFragment; @Column(name="sort_order") private int sortOrder; private String payload;
|
||||
public EscapePuzzle(){} public EscapePuzzle(String t,String p,int pts,String f,int o,String j){type=t;prompt=p;points=pts;codeFragment=f;sortOrder=o;payload=j;} public Long getId(){return id;} public void setId(Long v){id=v;} public EscapeRoom getRoom(){return room;} public void setRoom(EscapeRoom v){room=v;} public String getType(){return type;} public void setType(String v){type=v;} public String getPrompt(){return prompt;} public void setPrompt(String v){prompt=v;} public int getPoints(){return points;} public void setPoints(int v){points=v;} public String getCodeFragment(){return codeFragment;} public void setCodeFragment(String v){codeFragment=v;} public int getSortOrder(){return sortOrder;} public void setSortOrder(int v){sortOrder=v;} public String getPayload(){return payload;} public void setPayload(String v){payload=v;}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.model;
|
||||
import jakarta.persistence.*; import java.util.*;
|
||||
@Entity @Table(name="escape_rooms")
|
||||
public class EscapeRoom {
|
||||
@Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="scenario_id") private EscapeScenario scenario;
|
||||
private String title; private String intro; @Column(name="door_prompt") private String doorPrompt; @Column(name="door_code") private String doorCode; @Column(name="success_text") private String successText; @Column(name="sort_order") private int sortOrder;
|
||||
@OneToMany(mappedBy="room",cascade=CascadeType.ALL,orphanRemoval=true) @OrderBy("sortOrder ASC, id ASC") private List<EscapePuzzle> puzzles=new ArrayList<>();
|
||||
public EscapeRoom(){} public EscapeRoom(String t,String i,String p,String c,String s,int o){title=t;intro=i;doorPrompt=p;doorCode=c;successText=s;sortOrder=o;} public void addPuzzle(EscapePuzzle p){p.setRoom(this);puzzles.add(p);} public Long getId(){return id;} public void setId(Long v){id=v;} public EscapeScenario getScenario(){return scenario;} public void setScenario(EscapeScenario v){scenario=v;} public String getTitle(){return title;} public void setTitle(String v){title=v;} public String getIntro(){return intro;} public void setIntro(String v){intro=v;} public String getDoorPrompt(){return doorPrompt;} public void setDoorPrompt(String v){doorPrompt=v;} public String getDoorCode(){return doorCode;} public void setDoorCode(String v){doorCode=v;} public String getSuccessText(){return successText;} public void setSuccessText(String v){successText=v;} public int getSortOrder(){return sortOrder;} public void setSortOrder(int v){sortOrder=v;} public List<EscapePuzzle> getPuzzles(){return puzzles;}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.model;
|
||||
import jakarta.persistence.*; import java.util.*;
|
||||
@Entity @Table(name="escape_scenarios")
|
||||
public class EscapeScenario {
|
||||
@Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id;
|
||||
@Column(unique=true,nullable=false) private String slug; private String title; private String difficulty;
|
||||
@Column(name="exam_scope") private String examScope="AP1"; private String description;
|
||||
@Column(name="time_limit_seconds") private int timeLimitSeconds=900;
|
||||
@Column(name="sort_order") private int sortOrder; private boolean active=true;
|
||||
@OneToMany(mappedBy="scenario",cascade=CascadeType.ALL,orphanRemoval=true) @OrderBy("sortOrder ASC, id ASC") private List<EscapeRoom> rooms=new ArrayList<>();
|
||||
public EscapeScenario(){} public EscapeScenario(String slug,String title,String difficulty,String examScope,String description,int limit,int order){this.slug=slug;this.title=title;this.difficulty=difficulty;this.examScope=examScope;this.description=description;this.timeLimitSeconds=limit;this.sortOrder=order;}
|
||||
public void addRoom(EscapeRoom r){r.setScenario(this);rooms.add(r);} public Long getId(){return id;} public void setId(Long v){id=v;} public String getSlug(){return slug;} public void setSlug(String v){slug=v;} public String getTitle(){return title;} public void setTitle(String v){title=v;} public String getDifficulty(){return difficulty;} public void setDifficulty(String v){difficulty=v;} public String getExamScope(){return examScope;} public void setExamScope(String v){examScope=v;} public String getDescription(){return description;} public void setDescription(String v){description=v;} public int getTimeLimitSeconds(){return timeLimitSeconds;} public void setTimeLimitSeconds(int v){timeLimitSeconds=v;} public int getSortOrder(){return sortOrder;} public void setSortOrder(int v){sortOrder=v;} public boolean isActive(){return active;} public void setActive(boolean v){active=v;} public List<EscapeRoom> getRooms(){return rooms;}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity @Table(name = "networker_levels")
|
||||
public class NetworkLevel {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "world_id", nullable = false) private NetworkWorld world;
|
||||
@Column(nullable = false) private String title;
|
||||
@Column(name = "level_type") private String levelType;
|
||||
private int points;
|
||||
@Column(name = "sort_order") private int sortOrder;
|
||||
@Column(length = 8000, nullable = false) private String payload;
|
||||
public NetworkLevel() { }
|
||||
public NetworkLevel(String title, String levelType, int points, int sortOrder, String payload) { this.title=title; this.levelType=levelType; this.points=points; this.sortOrder=sortOrder; this.payload=payload; }
|
||||
public Long getId(){return id;} public void setId(Long value){id=value;} public NetworkWorld getWorld(){return world;} public void setWorld(NetworkWorld value){world=value;} public String getTitle(){return title;} public String getLevelType(){return levelType;} public int getPoints(){return points;} public int getSortOrder(){return sortOrder;} public String getPayload(){return payload;}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.*;
|
||||
|
||||
@Entity @Table(name = "networker_worlds")
|
||||
public class NetworkWorld {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
@Column(nullable = false, unique = true) private String slug;
|
||||
@Column(nullable = false) private String title;
|
||||
private String description;
|
||||
private String difficulty;
|
||||
@Column(name = "exam_scope") private String examScope;
|
||||
@Column(name = "sort_order") private int sortOrder;
|
||||
private boolean active = true;
|
||||
@OneToMany(mappedBy = "world", cascade = CascadeType.ALL, orphanRemoval = true) @OrderBy("sortOrder ASC, id ASC") private List<NetworkLevel> levels = new ArrayList<>();
|
||||
public NetworkWorld() { }
|
||||
public NetworkWorld(String slug, String title, String description, String difficulty, String examScope, int sortOrder) { this.slug=slug; this.title=title; this.description=description; this.difficulty=difficulty; this.examScope=examScope; this.sortOrder=sortOrder; }
|
||||
public void addLevel(NetworkLevel level) { level.setWorld(this); levels.add(level); }
|
||||
public Long getId(){return id;} public void setId(Long value){id=value;} public String getSlug(){return slug;} public String getTitle(){return title;} public String getDescription(){return description;} public String getDifficulty(){return difficulty;} public String getExamScope(){return examScope;} public int getSortOrder(){return sortOrder;} public boolean isActive(){return active;} public void setActive(boolean value){active=value;} public List<NetworkLevel> getLevels(){return levels;}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "neuralnet_connections")
|
||||
public class NeuralNetConnection {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
private String requester;
|
||||
private String addressee;
|
||||
private String status;
|
||||
@Column(name = "created_at") private LocalDateTime createdAt;
|
||||
@Column(name = "responded_at") private LocalDateTime respondedAt;
|
||||
public NeuralNetConnection() {}
|
||||
public NeuralNetConnection(String requester, String addressee) { this.requester=requester; this.addressee=addressee; this.status="PENDING"; this.createdAt=LocalDateTime.now(); }
|
||||
public Long getId() { return id; } public String getRequester() { return requester; } public String getAddressee() { return addressee; }
|
||||
public String getStatus() { return status; } public void setStatus(String status) { this.status=status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getRespondedAt() { return respondedAt; }
|
||||
public void setRespondedAt(LocalDateTime respondedAt) { this.respondedAt=respondedAt; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "neuralnet_messages")
|
||||
public class NeuralNetMessage {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
private String sender;
|
||||
private String recipient;
|
||||
private String body;
|
||||
@Column(name = "sent_at") private LocalDateTime sentAt;
|
||||
@Column(name = "read_at") private LocalDateTime readAt;
|
||||
private boolean reported;
|
||||
public NeuralNetMessage() {}
|
||||
public NeuralNetMessage(String sender, String recipient, String body) { this.sender=sender; this.recipient=recipient; this.body=body; this.sentAt=LocalDateTime.now(); }
|
||||
public Long getId() { return id; } public String getSender() { return sender; } public String getRecipient() { return recipient; }
|
||||
public String getBody() { return body; } public LocalDateTime getSentAt() { return sentAt; } public LocalDateTime getReadAt() { return readAt; }
|
||||
public void setReadAt(LocalDateTime readAt) { this.readAt=readAt; } public boolean isReported() { return reported; } public void setReported(boolean reported) { this.reported=reported; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "neuralnet_privacy")
|
||||
public class NeuralNetPrivacy {
|
||||
@Id private String username;
|
||||
private boolean enabled;
|
||||
public NeuralNetPrivacy() {}
|
||||
public NeuralNetPrivacy(String username, boolean enabled) { this.username = username; this.enabled = enabled; }
|
||||
public String getUsername() { return username; }
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user