From 02975ebb25ea9a45f417a6f162f8877794ea2a3f Mon Sep 17 00:00:00 2001 From: AliceDarkest Date: Sun, 12 Jul 2026 22:03:44 +0200 Subject: [PATCH] =?UTF-8?q?FluxTimer=20und=20Fundament=20f=C3=BCr=20neue?= =?UTF-8?q?=20Spiele=20umsetzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 ++ mindfluxfuture.md | 59 ++++---- src/main/resources/static/css/fluxtimer.css | 47 +++++++ src/main/resources/static/css/theme.css | 12 ++ src/main/resources/static/js/fluxtimer.js | 129 ++++++++++++++++++ src/main/resources/static/js/theme.js | 17 +++ src/main/resources/templates/dashboard.html | 2 +- .../templates/legal/datenschutz.html | 13 +- src/main/resources/templates/settings.html | 44 ++++++ 9 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/static/css/fluxtimer.css create mode 100644 src/main/resources/static/js/fluxtimer.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4c063..d8c1aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,16 @@ Docker/CI-Härtung und erste Tests. - **`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 CodeEscape, 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; die Dashboard-Glocke öffnet den + Schnellzugriff und die Einstellungen enthalten die vollständige Konfiguration. +- Datenschutzerklärung um lokale Speicherung und Browser-Benachrichtigung ergänzt. + ## [Unreleased] – 2026-06-30 · Login, Registrierung & Konten Reihe von Fixes und Verbesserungen rund um Anmeldung, Registrierung und diff --git a/mindfluxfuture.md b/mindfluxfuture.md index 9d23baa..c4c8d65 100644 --- a/mindfluxfuture.md +++ b/mindfluxfuture.md @@ -9,13 +9,13 @@ Das Dashboard von Mindflux (`mindflux2`, Spring Boot 3.4.4 / Java 17 / PostgreSQ 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 Infrastruktur wartet bereits auf sie: Die Spiel-Keys `escape-room`, `service-clash`, `bug-hunter`, `netzwerker` stehen schon in der Score-Allowlist (`GameScoreController.KNOWN_GAMES`, Zeile 23–25) und in `ProgressService.GAMES` (Zeile 23–26); die Dashboard-Icons (`lock`, `sword`, `bug`, `network`) existieren in `dashboard.html`. Ziel dieses Plans: detaillierte, umsetzungsreife Designs für alle vier Spiele nach dem etablierten Architektur-Rezept der vorhandenen Spiele (MindMatch, TechTacular, Syntax Saga, Dev Lab) sowie ein priorisierter Verbesserungs-/Optimierungskatalog für den Rest der Plattform. +Die Infrastruktur wartet bereits auf sie: 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`. Ziel dieses Plans: detaillierte, umsetzungsreife Designs für alle vier Spiele nach dem etablierten Architektur-Rezept der vorhandenen Spiele (MindMatch, TechTacular, SyntaxSaga, DevLab) sowie ein priorisierter Verbesserungs-/Optimierungskatalog für den Rest der Plattform. **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):** CodeEscape → 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:** Beim Kuratieren jedes Inhalts (Seeder-JSONs, Admin-Eingaben) wird vermerkt, ob er AP1-, AP2/FIAE- oder AP2/FISI-relevant ist – zunächst als Konvention im Titel/`description` (z. B. Präfix „[AP2]"), später optional als eigenes Feld + Filter in den Lobbys („Zeige nur AP2-Stoff"). +- **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. @@ -41,7 +41,7 @@ Vorgabe: Alle Spielnamen folgen dem MindMatch-Muster – **ein Wort, englisch, C | 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 vier reservierten Keys (`escape-room`, `service-clash`, `bug-hunter`, `netzwerker`) haben **noch keine Scores in der DB** und werden daher jetzt kostenlos auf das einheitliche Schema umbenannt (`codeescape`, `serviceclash`, `bughunter`, `networker`) – zu ändern in `GameScoreController.KNOWN_GAMES`, `ProgressService.GAMES` und den jeweiligen Service-`GAME`-Konstanten. Nur `syntax-saga` behält seinen Key (Bestandsdaten), Anzeigename wird trotzdem `SyntaxSaga`. Anzeigenamen anpassen in: `dashboard.html`-`GAMES`-Array, `ProgressService.GAMES`, Seitentiteln/Templates der neuen Spiele. _Alle nachfolgenden Abschnitte verwenden bereits die neuen Namen; wo im Text noch V9–V12-Design-Details Alt-Keys nennen, gilt die Tabelle hier._ +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. --- @@ -70,7 +70,7 @@ Dazu je Spiel ein eigenes Karten-Design auf dem Dashboard (Hover-Animation im je Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: **Dev Lab**, `DevLabService.java` + `devlabgame.js`): -1. **Flyway-Migration** `db/migration/V9__…` ff. (V8 ist die letzte; `ddl-auto=validate` ⇒ Migration ist Pflicht). SQL-Stil: `BIGINT GENERATED BY DEFAULT AS IDENTITY`, `slug VARCHAR UNIQUE`, `sort_order INT DEFAULT 0`, `active BOOLEAN DEFAULT TRUE`, `FK … ON DELETE CASCADE`, Indizes. +1. **Flyway-Migration** ab `db/migration/V11__…` (V9/V10 sind durch User-/Datenintegrität belegt; `ddl-auto=validate` ⇒ Migration ist Pflicht). Reserviert: V11 CodeEscape, V12 BugHunter, V13 NetWorker, V14 ServiceClash; danach FluxNet. 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 = ""`, 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. @@ -82,11 +82,21 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: 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//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 -### Spiel 1: Code Escape Room (Key: `escape-room`, Migration V9) +### Spiel 1: CodeEscape (Key: `codeescape`, Migration V11) **Konzept:** Der Spieler ist in einer digitalen Umgebung eingesperrt (Serverraum, Admin-Büro, Rechenzentrum). Ein **Szenario** = ein Durchlauf mit 3 Räumen in fester Reihenfolge; jeder Raum enthält 2–3 IT-Rätsel plus ein **Tür-Keypad**. Jedes gelöste Rätsel liefert ein **Code-Fragment** (Ziffern/Hex-Chunk in einer Inventar-Leiste); die Fragmente ergeben zusammen den Türcode zum nächsten Raum. Countdown erzeugt Zeitdruck; ein 2-stufiges Hinweis-System tauscht Punkte gegen Hilfe. @@ -103,9 +113,9 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: - 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, "escape-room", szenarioSlug, baseScore + zeitbonus, gelösteRätsel, sekunden)`. +- `recordScore(user, "codeescape", szenarioSlug, baseScore + zeitbonus, gelösteRätsel, sekunden)`. -**Datenmodell (`V9__escape_room.sql`):** 3 Tabellen im V8-Stil: +**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)) @@ -116,7 +126,7 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: - `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/escape-room`):** `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}`). +**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 ✗. @@ -128,7 +138,7 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: --- -### Spiel 2: Bug Hunter (Key: `bug-hunter`, Migration V10) +### Spiel 2: BugHunter (Key: `bughunter`, Migration V12) **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. @@ -138,9 +148,9 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: - 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, "bug-hunter", huntSlug, baseScore + comboBonus, gelösteBugs, sekunden)`. +- Basispunkte pro Bug nach Stufe: Junior 100, Senior 150, Principal 200. `recordScore(user, "bughunter", huntSlug, baseScore + comboBonus, gelösteBugs, sekunden)`. -**Datenmodell (`V10__bug_hunter.sql`):** +**Datenmodell (`V12__bughunter.sql`):** - `bughunter_hunts` (id, slug UNIQUE, title, code_language, difficulty DEFAULT 'junior', 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"}` @@ -151,7 +161,7 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: - `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/bug-hunter`):** `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}`). +**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/bug-hunter.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). @@ -163,7 +173,7 @@ Jedes neue Spiel folgt exakt dem Muster der vorhandenen Spiele (bester Bauplan: --- -### Spiel 3: Service Clash (Key: `service-clash`, Migration V11) +### Spiel 3: ServiceClash (Key: `serviceclash`, Migration V14) **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. @@ -188,14 +198,14 @@ maxScore = Σ B + 200 ; Sterne nach Plattform-Konvention - 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 (`V11__service_clash.sql`):** +**Datenmodell (`V14__serviceclash.sql`):** - `service_clash_shifts` (id, slug UNIQUE, title, description, difficulty INT, wave_count INT DEFAULT 3, wave_seconds INT DEFAULT 60, spawn_count INT DEFAULT 12, settings VARCHAR(4000) /* JSON: SLA-Defaults, Health-Regeln, Prio-Stufe an/aus */, sort_order, active) - `service_clash_tickets` (id, shift_id FK CASCADE, wave INT, base_points INT DEFAULT 100, sort_order, payload VARCHAR(8000)) + FK-Index - Ticket-Payload: `{title, requester, text, kind (normal|phishing), options[], correctIndex✂, category✂, priority✂, slaSeconds, explanation✂, successText}` (✂ = wird von `sanitize()` entfernt). **Backend:** Entities `ServiceClashShift`/`ServiceClashTicket`, Standard-Repos (`findAllByShiftIdOrderByWaveAscSortOrderAscIdAsc`, `countByShiftId`). `ServiceClashService`: `buildCatalog()`, `buildSession(slug)` (Seed + `generatePlan()`), `check(templateId, step, value, reveal)` mit `step ∈ classify|priority|solve` (Vergleich immer über Options-**Text**, nie Indizes – immun gegen Mischungs-Versatz), `finish(...)` (Plan aus Seed regenerieren, Log re-simulieren, `recordScore(user, "service-clash", slug, score, gelöste, sekunden)`), Admin-Methoden nach Dev-Lab-Vorbild. -**API (`/api/service-clash`):** `GET /catalog`, `GET /session/{slug}` (liefert `seed`, Wellen-Konfiguration, Ticket-Instanzen `{instanceId, templateId, wave, spawnAtMs, …}`), `POST /check`, `POST /finish` → `{score, maxScore, stars, resolved, total, slaBreaches, health, highscore, newHighscore, plays, tickets[Debrief-Aufschlüsselung]}`. +**API (`/api/serviceclash`):** `GET /catalog`, `GET /session/{slug}` (liefert `seed`, Wellen-Konfiguration, Ticket-Instanzen `{instanceId, templateId, wave, spawnAtMs, …}`), `POST /check`, `POST /finish` → `{score, maxScore, stars, resolved, total, slaBreaches, health, highscore, newHighscore, plays, tickets[Debrief-Aufschlüsselung]}`. **Frontend:** `templates/service-clash.html`, `static/js/serviceclash.js` (State-Machine `SHIFT_SELECT → BRIEFING → RUNNING ⇄ WAVE_BREAK → DEBRIEF`; **ein** `setInterval(250 ms)`-Tick: fällige Tickets spawnen, SLA-Ringe aktualisieren), `static/css/service-clash.css` Präfix `sc-`. Layout: Topbar (Schicht-Uhr, Wellen-Pips, Gesundheits-Meter, „Gelöst 4/8"), links die Ticket-Queue (Karten mit SLA-Ring, pulsierender roter Rand bei Überfälligkeit), Mitte das aktive Ticket mit 3-Stufen-Flow, unten ein Event-Log („14:02 Ticket ‚ERP down' eingegangen"). SLA-Ringe: `var(--accent)`, unter 25 % `var(--danger)`. @@ -209,7 +219,7 @@ maxScore = Σ B + 200 ; Sterne nach Plattform-Konvention --- -### Spiel 4: Netzwerker – Das Router-Rätselspiel (Key: `netzwerker`, Migration V12) +### Spiel 4: NetWorker (Key: `networker`, Migration V13) **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?"). @@ -219,7 +229,7 @@ maxScore = Σ B + 200 ; Sterne nach Plattform-Konvention **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, "netzwerker", weltSlug, score, gelöste, sekunden)`. -**Datenmodell (`V12__netzwerker.sql`):** +**Datenmodell (`V13__networker.sql`):** - `netzwerker_worlds` (id, slug UNIQUE, title, description, difficulty INT, sort_order, active) - `netzwerker_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 @@ -230,7 +240,7 @@ maxScore = Σ B + 200 ; Sterne nach Plattform-Konvention **`NetzwerkerService`:** `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(...)`, Admin-CRUD **plus Qualitäts-Gate beim Speichern: die hinterlegte Musterlösung wird durch den Simulator geschickt – Speichern wird abgelehnt, wenn sie nicht alle Ziele erfüllt.** -**API (`/api/netzwerker`):** `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. +**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/netzwerker.html`, `static/js/netzwerker.js` (State-Machine `WORLD_SELECT → LEVEL_INTRO → EDITING ⇄ CHECKING → LEVEL_DONE → SUMMARY`), `static/css/netzwerker.css` Präfix `nw-`. **SVG-Ansatz:** ein ``, Geräte-Glyphen als ``-Defs im Strichstil der Dashboard-Icons, Positionen aus dem Payload; Links als ``, Ports als Kreise an der Gerätekante. **Die Paket-Animation ist die Belohnung:** bei Erfolg wandert ein leuchtender Punkt entlang des tatsächlich vom Server gelieferten `trace`-Pfads (`requestAnimationFrame`); bei Fehlschlag stirbt er exakt am `failAt`-Hop mit rotem Burst. Kabelmodus: Port A klicken → Gummiband-Linie → Port B; gezogenes Kabel per Klick löschen. 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" / Hinweis-Toggle. @@ -247,10 +257,11 @@ maxScore = Σ B + 200 ; Sterne nach Plattform-Konvention ### Empfohlene Baureihenfolge & Gesamtaufwand 0. **Vorarbeiten:** Namensschema umsetzen (Dashboard-Namen, Keys, `devlab` in `ProgressService` = W1) · K2 (Legacy-Endpunkt löschen) und W8 (N+1-Muster) beheben, damit die neuen Services das saubere Muster übernehmen · Spiel-Akzent-Tokens in `theme.css` anlegen. ≈ 1 PT — **✅ erledigt bis auf die Spiel-Akzent-Tokens.** -1. **CodeEscape (V9)** – validiert die Wiederverwendung der DevLab-Checker/-Renderer mit minimalem Risiko. ≈ 5,5 PT -2. **BugHunter (V10)** – übernimmt Terminal-Styling und Choice-Renderer aus Schritt 1. ≈ 4,5 PT -3. **NetWorker (V12)** – **Simulator zuerst, testgetrieben** (pures Java, kein UI-Risiko), dann SVG-Frontend. ≈ 9,5 PT -4. **ServiceClash (V11)** – komplexestes Frontend (Echtzeit-Tick); profitiert von allen Vorarbeiten. ≈ 7,5 PT +1. **FluxTimer ✅** – risikoarmer, rein lokaler Ausbau; validiert den globalen UI-Bootstrap und belegt die Glocke vor FluxNet. ≈ 1,5–2 PT +2. **CodeEscape (V11)** – validiert die Wiederverwendung der DevLab-Checker/-Renderer mit minimalem Risiko. ≈ 5,5 PT +3. **BugHunter (V12)** – übernimmt Terminal-Styling und Choice-Renderer aus Schritt 2. ≈ 4,5 PT +4. **NetWorker (V13)** – **Simulator zuerst, testgetrieben** (pures Java, kein UI-Risiko), dann SVG-Frontend. ≈ 9,5 PT +5. **ServiceClash (V14)** – komplexestes Frontend (Echtzeit-Tick); profitiert von allen Vorarbeiten. ≈ 7,5 PT **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. @@ -307,7 +318,7 @@ Der Verdacht „Profilbutton fehlt / Einstellungen nicht im Profil / irgendwas i |---|-------|----------| | S1 ✅ teilweise | **Template-Duplikation** – 33 Templates ohne ein einziges `th:fragment`; jede Seite dupliziert ``, 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) | Akzeptables Risiko (nur eigener Highscore), aber bei den neuen Spielen von Anfang an minimieren: Server generiert Runden-Daten mit IDs, `finish` validiert nur bekannte IDs, Zeit-Plausibilitätscheck (z. B. `durationSeconds ≥ 1`, Obergrenzen für `pairs`/`moves`). | +| S3 ✅ Fundament | **Score-Manipulation durch eigene Nutzer möglich** (Roh-Werte fälschbar, Reveal-vor-Finish) | Verbindlicher Sicherheitsvertrag ist oben dokumentiert und gilt für jedes neue Spiel; konkrete Implementierung und Negativtests entstehen jeweils mit dem Spiel. | | 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). | @@ -433,6 +444,8 @@ Eine Freundschaft zwischen A und B liegt genau einmal vor (Anfrage-Richtung = re ## Teil 4: FluxTimer – Der Pausen-Lerntimer +> ✅ **MVP umgesetzt:** lokaler Zustand, globaler Countdown, Einstellungen, Schnellzugriff über die Glocke, In-App-Overlay, Snooze, Probelauf sowie optionale Browser-Benachrichtigung und Web-Audio-Signal. 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 diff --git a/src/main/resources/static/css/fluxtimer.css b/src/main/resources/static/css/fluxtimer.css new file mode 100644 index 0000000..16cda49 --- /dev/null +++ b/src/main/resources/static/css/fluxtimer.css @@ -0,0 +1,47 @@ +/* FluxTimer – Zeitschaltkreis-Optik, rein clientseitig. */ +.ft-bell { position:relative; } +.ft-bell-dot { position:absolute; right:5px; top:5px; width:7px; height:7px; border-radius:50%; background:var(--muted); border:1px solid var(--surface); } +.ft-bell.ft-active .ft-bell-dot { background:var(--fluxtimer-primary); box-shadow:0 0 8px var(--fluxtimer-primary); } +.ft-popover { position:fixed; z-index:1600; width:min(340px,calc(100vw - 24px)); padding:1rem; border:1px solid color-mix(in srgb,var(--fluxtimer-secondary) 55%,var(--border)); border-radius:16px; background:var(--panel); box-shadow:0 18px 55px rgba(0,0,0,.38),0 0 24px color-mix(in srgb,var(--fluxtimer-secondary) 22%,transparent); display:none; } +.ft-popover.open { display:block; animation:ft-in .18s ease-out; } +.ft-popover-head,.ft-popover-actions,.ft-settings-head,.ft-settings-actions,.ft-options { display:flex; align-items:center; gap:.7rem; } +.ft-popover-head { justify-content:space-between; margin-bottom:.75rem; } +.ft-popover-head b { color:var(--heading); letter-spacing:.08em; } +.ft-close { border:0; background:none; color:var(--muted); cursor:pointer; font-size:1.25rem; } +.ft-circuit { display:grid; gap:.55rem; padding:.8rem; border-radius:12px; background:var(--fluxtimer-display); border:1px solid color-mix(in srgb,var(--fluxtimer-primary) 45%,var(--border)); box-shadow:inset 0 0 22px rgba(0,0,0,.28); } +.ft-row { display:grid; grid-template-columns:1fr auto; align-items:center; gap:.7rem; } +.ft-row span { font-size:.65rem; letter-spacing:.12em; color:var(--muted); } +.ft-row strong,.ft-preview strong { color:var(--fluxtimer-segment); font-family:"Courier New",monospace; font-variant-numeric:tabular-nums; letter-spacing:.08em; text-shadow:0 0 9px color-mix(in srgb,var(--fluxtimer-segment) 58%,transparent); } +.ft-row.primary strong { font-size:1.3rem; } +.ft-popover-status { color:var(--muted); font-size:.8rem; margin:.7rem 0; } +.ft-popover-actions { justify-content:space-between; flex-wrap:wrap; } +.ft-mini-toggle { border:1px solid var(--border-accent); background:var(--surface-2); color:var(--text); border-radius:9px; padding:.45rem .7rem; cursor:pointer; font-weight:700; } +.ft-settings-head { justify-content:space-between; align-items:flex-start; margin-bottom:1rem; } +.ft-settings-head p { color:var(--muted); font-size:.88rem; margin-top:.25rem; } +.ft-settings-grid { display:grid; grid-template-columns:1fr 220px; gap:1rem; align-items:stretch; } +.ft-preview { display:flex; flex-direction:column; justify-content:center; align-items:center; padding:.8rem; border-radius:12px; background:var(--fluxtimer-display); border:1px solid color-mix(in srgb,var(--fluxtimer-primary) 42%,var(--border)); } +.ft-preview span { color:var(--muted); font-size:.65rem; letter-spacing:.13em; } +.ft-preview strong { font-size:1.55rem; margin-top:.25rem; } +.ft-switch { display:inline-flex; align-items:center; gap:.5rem; cursor:pointer; color:var(--text); } +.ft-switch input { position:absolute; opacity:0; } +.ft-switch span { width:44px; height:24px; border-radius:99px; background:var(--surface-2); border:1px solid var(--border); position:relative; transition:.2s; } +.ft-switch span::after { content:""; position:absolute; width:18px; height:18px; left:2px; top:2px; border-radius:50%; background:var(--muted); transition:.2s; } +.ft-switch input:checked + span { background:color-mix(in srgb,var(--fluxtimer-secondary) 34%,var(--surface)); border-color:var(--fluxtimer-secondary); } +.ft-switch input:checked + span::after { transform:translateX(20px); background:var(--fluxtimer-secondary); box-shadow:0 0 8px var(--fluxtimer-secondary); } +.ft-reminders { display:flex; flex-wrap:wrap; gap:.2rem 1rem; border:0; margin:.3rem 0 .8rem; } +.ft-reminders legend { width:100%; margin-bottom:.25rem; } +.ft-options { flex-wrap:wrap; margin:.25rem 0 .8rem; } +.ft-settings-actions { justify-content:space-between; flex-wrap:wrap; } +.ft-overlay { position:fixed; inset:0; z-index:1900; display:none; place-items:center; padding:1rem; background:rgba(5,3,2,.76); backdrop-filter:blur(7px); } +.ft-overlay.open { display:grid; } +.ft-overlay::before { content:""; position:absolute; inset:0; pointer-events:none; background:linear-gradient(115deg,transparent 35%,rgba(255,255,255,.7) 49%,transparent 55%); transform:translateX(-120%); } +.ft-overlay.open::before { animation:ft-flash .7s ease-out; } +.ft-modal { position:relative; width:min(560px,100%); text-align:center; padding:1.5rem; border-radius:20px; background:var(--panel); border:1px solid var(--fluxtimer-secondary); box-shadow:0 0 50px color-mix(in srgb,var(--fluxtimer-secondary) 35%,transparent); } +.ft-modal .ft-kicker { color:var(--fluxtimer-secondary); text-transform:uppercase; letter-spacing:.18em; font-size:.72rem; font-weight:800; } +.ft-modal h2 { color:var(--heading); font-size:1.7rem; margin:.7rem 0 .4rem; } +.ft-modal p { color:var(--muted); line-height:1.55; } +.ft-modal-actions { display:flex; justify-content:center; flex-wrap:wrap; gap:.7rem; margin-top:1.2rem; } +@keyframes ft-in { from{opacity:0;transform:translateY(-6px)} to{opacity:1;transform:none} } +@keyframes ft-flash { to{transform:translateX(120%)} } +@media (max-width:620px){ .ft-settings-grid{grid-template-columns:1fr}.ft-settings-head{align-items:stretch;flex-direction:column}.ft-popover{left:12px!important;right:12px!important;top:64px!important;width:auto} } +@media (prefers-reduced-motion:reduce){ .ft-popover.open,.ft-overlay.open::before{animation:none} } diff --git a/src/main/resources/static/css/theme.css b/src/main/resources/static/css/theme.css index 224054b..074d72f 100644 --- a/src/main/resources/static/css/theme.css +++ b/src/main/resources/static/css/theme.css @@ -14,6 +14,12 @@ --secondary:#2d9cce; --secondary-2:#5cc4e8; --text:#ece3d2; --muted:#8c8170; --heading:#ffffff; --success:#2d9cce; --danger:#ff4d6d; + --esc-accent:#f0a81a; --esc-glow:rgba(240,168,26,.48); + --bh-accent:#d94b64; --bh-glow:rgba(217,75,100,.42); + --sc-accent:#38b9d6; --sc-glow:rgba(56,185,214,.40); + --nw-accent:#4f8ff7; --nw-glow:rgba(79,143,247,.42); + --fluxtimer-primary:#ff5a5f; --fluxtimer-secondary:#f0a81a; + --fluxtimer-display:#170704; --fluxtimer-segment:#ff765f; --glow-s:rgba(240,168,26,.55); --glow-w:rgba(240,168,26,.16); --g-soft:0 0 18px var(--glow-w); } @@ -25,6 +31,12 @@ --secondary:#1f86b8; --secondary-2:#2d9cce; --text:#33271c; --muted:#8a7d6c; --heading:#241a12; --success:#1f86b8; --danger:#be123c; + --esc-accent:#c96a12; --esc-glow:rgba(201,106,18,.30); + --bh-accent:#b42342; --bh-glow:rgba(180,35,66,.26); + --sc-accent:#147f9e; --sc-glow:rgba(20,127,158,.26); + --nw-accent:#2868c7; --nw-glow:rgba(40,104,199,.26); + --fluxtimer-primary:#b42342; --fluxtimer-secondary:#c96a12; + --fluxtimer-display:#fff5ec; --fluxtimer-segment:#a93131; --glow-s:rgba(214,81,22,.40); --glow-w:rgba(214,81,22,.12); --g-soft:0 6px 18px var(--glow-w); } diff --git a/src/main/resources/static/js/fluxtimer.js b/src/main/resources/static/js/fluxtimer.js new file mode 100644 index 0000000..f5ad65b --- /dev/null +++ b/src/main/resources/static/js/fluxtimer.js @@ -0,0 +1,129 @@ +/* FluxTimer – lokaler Pausen-Timer ohne Server- oder Drittdienst. */ +(function () { + 'use strict'; + if (window.MindfluxTimer) return; + + var KEY = 'fluxtimer'; + var MIN_INTERVAL = 5; + var MAX_INTERVAL = 240; + var DEFAULTS = { enabled:false, intervalMinutes:45, reminders:['aufstehen','trinken','augenpause'], customText:'', sound:false, notify:false, nextFireAt:null, lastFiredAt:null, reminderIndex:0 }; + var LABELS = { + aufstehen:'88 mph erreicht – kurz aufstehen, McFly!', + trinken:'Großer Scott! Zeit, etwas zu trinken.', + essen:'Dein Fluxkompensator braucht einen Snack.', + augenpause:'Blick in die Zukunft: Gönn deinen Augen eine Pause.' + }; + var state = load(); + var ringing = false; + var intervalId; + + function number(value, fallback) { var n = Number(value); return Number.isFinite(n) ? n : fallback; } + function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } + function normalize(raw) { + raw = raw && typeof raw === 'object' ? raw : {}; + var reminders = Array.isArray(raw.reminders) ? raw.reminders.filter(function (x) { return Object.prototype.hasOwnProperty.call(LABELS, x); }) : DEFAULTS.reminders.slice(); + return { + enabled: raw.enabled === true, + intervalMinutes: clamp(Math.round(number(raw.intervalMinutes, 45)), MIN_INTERVAL, MAX_INTERVAL), + reminders: reminders, + customText: typeof raw.customText === 'string' ? raw.customText.trim().slice(0,120) : '', + sound: raw.sound === true, + notify: raw.notify === true, + nextFireAt: Number.isFinite(Number(raw.nextFireAt)) ? Number(raw.nextFireAt) : null, + lastFiredAt: Number.isFinite(Number(raw.lastFiredAt)) ? Number(raw.lastFiredAt) : null, + reminderIndex: Math.max(0, Math.round(number(raw.reminderIndex, 0))) + }; + } + function load() { try { return normalize(JSON.parse(localStorage.getItem(KEY) || '{}')); } catch (e) { return normalize({}); } } + function save() { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {} syncUi(); } + function schedule(from) { state.nextFireAt = (from || Date.now()) + state.intervalMinutes * 60000; ringing = false; save(); } + function setEnabled(enabled) { state.enabled = !!enabled; if (state.enabled) schedule(Date.now()); else { state.nextFireAt = null; ringing = false; closeOverlay(); save(); } } + function remainingMs(now) { return state.enabled && state.nextFireAt ? Math.max(0, state.nextFireAt - (now || Date.now())) : 0; } + function pad(n) { return String(n).padStart(2,'0'); } + function formatDuration(ms) { var seconds=Math.max(0,Math.ceil(ms/1000)); return pad(Math.floor(seconds/3600))+':'+pad(Math.floor((seconds%3600)/60))+':'+pad(seconds%60); } + function formatClock(ts) { if (!ts) return '--:--:--'; return new Date(ts).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); } + function reminderText() { + var pool = state.reminders.slice(); if (state.customText) pool.push('custom'); + if (!pool.length) return 'Zeit für eine kurze Pause.'; + var key = pool[state.reminderIndex % pool.length]; + state.reminderIndex = (state.reminderIndex + 1) % pool.length; + return key === 'custom' ? state.customText : LABELS[key]; + } + function ensureUi() { + if (!document.querySelector('.ft-popover')) { + var pop=document.createElement('section'); pop.className='ft-popover'; pop.setAttribute('aria-label','FluxTimer'); + pop.innerHTML='
FLUXTIMER
NÄCHSTE PAUSE--:--:--
JETZT--:--:--
LETZTE PAUSE--:--:--

'; + document.body.appendChild(pop); + } + if (!document.querySelector('.ft-overlay')) { + var overlay=document.createElement('div'); overlay.className='ft-overlay'; overlay.setAttribute('role','dialog'); overlay.setAttribute('aria-modal','true'); overlay.setAttribute('aria-labelledby','ft-title'); + overlay.innerHTML='
ZEITSCHALTKREISE AKTIV

Zeit für eine Pause

'; + document.body.appendChild(overlay); + } + } + function positionPopover() { + var trigger=document.querySelector('[data-fluxtimer-toggle]'), pop=document.querySelector('.ft-popover'); if (!trigger || !pop) return; + var r=trigger.getBoundingClientRect(); pop.style.top=(r.bottom+8)+'px'; pop.style.left=Math.max(12,Math.min(window.innerWidth-pop.offsetWidth-12,r.right-pop.offsetWidth))+'px'; + } + function togglePopover() { var pop=document.querySelector('.ft-popover'); if (!pop) return; pop.classList.toggle('open'); var open=pop.classList.contains('open'); var trigger=document.querySelector('[data-fluxtimer-toggle]'); if(trigger) trigger.setAttribute('aria-expanded',String(open)); if(open){positionPopover();syncUi();} } + function closePopover() { var pop=document.querySelector('.ft-popover'); if(pop) pop.classList.remove('open'); var trigger=document.querySelector('[data-fluxtimer-toggle]'); if(trigger) trigger.setAttribute('aria-expanded','false'); } + function closeOverlay() { var overlay=document.querySelector('.ft-overlay'); if(overlay) overlay.classList.remove('open'); } + function playSound() { if (!state.sound) return; try { var C=window.AudioContext||window.webkitAudioContext; if(!C)return; var ctx=new C(),o=ctx.createOscillator(),g=ctx.createGain(); o.frequency.value=523.25; g.gain.setValueAtTime(.05,ctx.currentTime); g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+.35); o.connect(g);g.connect(ctx.destination);o.start();o.stop(ctx.currentTime+.35); } catch(e){} } + function notify(text) { if (!state.notify || !('Notification' in window) || Notification.permission!=='granted') return; try { new Notification('Mindflux FluxTimer',{body:text,tag:'mindflux-fluxtimer'}); } catch(e){} } + function fire(testOnly) { ensureUi(); if(ringing&&!testOnly)return; ringing=true; var text=reminderText(); state.lastFiredAt=Date.now(); save(); var msg=document.querySelector('[data-ft-message]'); if(msg)msg.textContent=text; document.querySelector('.ft-overlay').classList.add('open'); playSound(); notify(text); } + function done() { closeOverlay(); schedule(Date.now()); } + function snooze() { closeOverlay(); state.nextFireAt=Date.now()+5*60000; ringing=false; save(); } + function tick() { var now=Date.now(); if(state.enabled&&state.nextFireAt&&now>=state.nextFireAt)fire(false); syncUi(now); } + function syncUi(now) { + now=now||Date.now(); var countdown=state.enabled?formatDuration(remainingMs(now)):'PAUSIERT'; + document.querySelectorAll('[data-ft-countdown]').forEach(function(el){el.textContent=countdown;}); + document.querySelectorAll('[data-ft-now]').forEach(function(el){el.textContent=formatClock(now);}); + document.querySelectorAll('[data-ft-last]').forEach(function(el){el.textContent=formatClock(state.lastFiredAt);}); + document.querySelectorAll('[data-ft-status]').forEach(function(el){el.textContent=state.enabled?'Aktiv · '+state.intervalMinutes+' Minuten':'Der Timer ist deaktiviert.';}); + document.querySelectorAll('[data-ft-quick-toggle]').forEach(function(el){el.textContent=state.enabled?'Timer pausieren':'Timer aktivieren';}); + document.querySelectorAll('[data-fluxtimer-toggle]').forEach(function(el){el.classList.toggle('ft-active',state.enabled);}); + var fields=document.querySelector('[data-fluxtimer-settings]'); if(fields){ + var enabled=fields.querySelector('[data-ft-field="enabled"]'); if(enabled&&document.activeElement!==enabled)enabled.checked=state.enabled; + var interval=fields.querySelector('[data-ft-field="intervalMinutes"]'); if(interval&&document.activeElement!==interval)interval.value=state.intervalMinutes; + var custom=fields.querySelector('[data-ft-field="customText"]'); if(custom&&document.activeElement!==custom)custom.value=state.customText; + ['sound','notify'].forEach(function(k){var el=fields.querySelector('[data-ft-field="'+k+'"]');if(el&&document.activeElement!==el)el.checked=state[k];}); + fields.querySelectorAll('[data-ft-reminder]').forEach(function(el){if(document.activeElement!==el)el.checked=state.reminders.indexOf(el.value)>=0;}); + } + } + function readSettings(container) { + var interval=container.querySelector('[data-ft-field="intervalMinutes"]'), previousInterval=state.intervalMinutes; state.intervalMinutes=clamp(Math.round(number(interval.value,45)),MIN_INTERVAL,MAX_INTERVAL); interval.value=state.intervalMinutes; + state.customText=container.querySelector('[data-ft-field="customText"]').value.trim().slice(0,120); + state.sound=container.querySelector('[data-ft-field="sound"]').checked; + state.notify=container.querySelector('[data-ft-field="notify"]').checked; + state.reminders=Array.prototype.map.call(container.querySelectorAll('[data-ft-reminder]:checked'),function(el){return el.value;}); + if(state.enabled&&previousInterval!==state.intervalMinutes)schedule(Date.now());else save(); + var status=container.querySelector('[data-ft-save-status]');if(status){status.textContent='Auf diesem Gerät gespeichert.';setTimeout(function(){status.textContent='Wird nur auf diesem Gerät gespeichert.';},1800);} + } + function init() { + if(location.pathname==='/'||location.pathname==='/login'||location.pathname==='/register')return; + ensureUi(); + if(state.enabled&&!state.nextFireAt)schedule(Date.now()); + document.addEventListener('click',function(e){ + if(e.target.closest('[data-fluxtimer-toggle]')){togglePopover();return;} + if(e.target.closest('[data-ft-close]')){closePopover();return;} + if(e.target.closest('[data-ft-quick-toggle]')){setEnabled(!state.enabled);return;} + if(e.target.closest('[data-ft-done]')){done();return;} + if(e.target.closest('[data-ft-snooze]')){snooze();return;} + if(e.target.closest('[data-ft-test]')){fire(true);return;} + var pop=e.target.closest('.ft-popover'); if(!pop&&!e.target.closest('[data-fluxtimer-toggle]'))closePopover(); + }); + var settings=document.querySelector('[data-fluxtimer-settings]'); if(settings){ + settings.addEventListener('change',function(e){ + if(e.target.matches('[data-ft-field="enabled"]')){setEnabled(e.target.checked);return;} + if(e.target.matches('[data-ft-field="notify"]')&&e.target.checked&&'Notification' in window&&Notification.permission==='default') Notification.requestPermission().then(function(p){if(p!=='granted'){e.target.checked=false;state.notify=false;save();}}); + readSettings(settings); + }); + settings.addEventListener('input',function(e){if(e.target.matches('[data-ft-field="customText"],[data-ft-field="intervalMinutes"]'))readSettings(settings);}); + } + window.addEventListener('storage',function(e){if(e.key===KEY){state=load();syncUi();}}); + window.addEventListener('resize',positionPopover); + intervalId=window.setInterval(tick,1000); tick(); + } + window.MindfluxTimer={getState:function(){return JSON.parse(JSON.stringify(state));},setEnabled:setEnabled,remainingMs:remainingMs,formatDuration:formatDuration,test:function(){fire(true);},reset:function(){state=normalize({});save();}}; + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init(); +})(); diff --git a/src/main/resources/static/js/theme.js b/src/main/resources/static/js/theme.js index dc6eda9..455570a 100644 --- a/src/main/resources/static/js/theme.js +++ b/src/main/resources/static/js/theme.js @@ -8,6 +8,23 @@ apply(read()); // früh -> kein Aufblitzen + // Globale, rein clientseitige Komfortfunktionen. FluxTimer bleibt auf öffentlichen + // Seiten inaktiv, läuft bei aktivierter Einstellung aber über Seitennavigationen weiter. + if (!document.querySelector('link[data-fluxtimer]')) { + var timerCss = document.createElement('link'); + timerCss.rel = 'stylesheet'; + timerCss.href = '/css/fluxtimer.css?v=1'; + timerCss.setAttribute('data-fluxtimer', ''); + document.head.appendChild(timerCss); + } + if (!document.querySelector('script[data-fluxtimer]')) { + var timerScript = document.createElement('script'); + timerScript.src = '/js/fluxtimer.js?v=1'; + timerScript.defer = true; + timerScript.setAttribute('data-fluxtimer', ''); + document.head.appendChild(timerScript); + } + var SUN = ''; var MOON = ''; diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 7ba7159..34274bd 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -48,7 +48,7 @@
- +
diff --git a/src/main/resources/templates/legal/datenschutz.html b/src/main/resources/templates/legal/datenschutz.html index 926dfb5..1ed5817 100644 --- a/src/main/resources/templates/legal/datenschutz.html +++ b/src/main/resources/templates/legal/datenschutz.html @@ -34,7 +34,7 @@

Datenschutzerklärung

-

Stand: Juni 2026

+

Stand: Juli 2026

Hinweis: Mindflux befindet sich in aktiver Entwicklung und ist derzeit nicht für die öffentliche @@ -87,6 +87,17 @@ im Rahmen von Tests zurückgesetzt werden.

+

6a. Lokale Einstellungen und FluxTimer

+

+ Theme-Auswahl und FluxTimer-Einstellungen werden ausschließlich im lokalen Browserspeicher + (localStorage) des verwendeten Geräts abgelegt. Dazu gehören Aktivierungsstatus, + Intervall, ausgewählte Erinnerungsarten, optionaler eigener Erinnerungstext sowie der nächste + und letzte Auslösezeitpunkt. Diese Angaben werden nicht an den Mindflux-Server übertragen und + können durch Löschen der Website-Daten im Browser entfernt werden. Browser-Benachrichtigungen + werden nur nach ausdrücklicher Freigabe lokal erzeugt; es gibt keinen Web-Push- oder externen + Benachrichtigungsdienst. +

+

7. Empfänger und Hosting

Die Daten werden auf einer vom Anbieter betriebenen Server-Infrastruktur verarbeitet und nicht zu diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html index 83061fb..abc73bb 100644 --- a/src/main/resources/templates/settings.html +++ b/src/main/resources/templates/settings.html @@ -52,6 +52,50 @@ Die Auswahl gilt sofort und wird auf diesem Gerät gespeichert. + +

+
+
+

⏱ FluxTimer

+

Erinnert dich während des Lernens ans Aufstehen, Trinken oder Durchatmen.

+
+ +
+
+
+ + + Zwischen 5 und 240 Minuten. +
+
+ NÄCHSTE PAUSE + --:--:-- +
+
+
+ Erinnerungen rotieren + + + + +
+
+ + +
+
+ + +
+
+ + Wird nur auf diesem Gerät gespeichert. +
+
+

🔒 Passwort ändern