Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbe24c67eb | |||
| b4d71d7d59 | |||
| e26a0c059c | |||
| bf27c9ccdc | |||
| 0e4645541d | |||
| 477d4dc04a | |||
| 34b050503d | |||
| 83929f24ba | |||
| fc98e5aad1 | |||
| b3a0fde104 | |||
| 7cb6621d1b | |||
| 7ede78d984 | |||
| 9022961762 |
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
target
|
||||
uploads
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
.claude
|
||||
.idea
|
||||
.settings
|
||||
.classpath
|
||||
.project
|
||||
@@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
@@ -21,6 +21,9 @@ jobs:
|
||||
docker push localhost:5000/mindflux:latest
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
MINDFLUX_ADMIN_PASSWORD: ${{ secrets.MINDFLUX_ADMIN_PASSWORD }}
|
||||
MINDFLUX_ADMIN2_PASSWORD: ${{ secrets.MINDFLUX_ADMIN2_PASSWORD }}
|
||||
run: |
|
||||
docker compose -f docker-compose.yml pull
|
||||
docker compose -f docker-compose.yml up -d
|
||||
|
||||
+36
-41
@@ -1,45 +1,40 @@
|
||||
# ---> Java
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
# ---> Maven
|
||||
HELP.md
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
uploads/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.log
|
||||
.claude/
|
||||
hs_err_pid*.log
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
# Eclipse m2e generated files
|
||||
# Eclipse Core
|
||||
.project
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Changelog
|
||||
|
||||
Alle nennenswerten Änderungen an **Mindflux** 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-06-29
|
||||
|
||||
Inhaltlicher und optischer Feinschliff: echte Rechtstexte, vollständig
|
||||
überarbeitete FinalFlux-Inhalte, neue Login-Optik, eine reparierte
|
||||
Konto-Löschung sowie ein app-weiter Farb-Relaunch auf das
|
||||
„Back to the Future"-Schema.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Rechtsseiten:** Impressum-Platzhalter (Hinweis „in Entwicklung / nicht für die öffentliche Nutzung") sowie **echte** Nutzungsbedingungen/AGB, Datenschutzerklärung (DSGVO-konform, spiegelt die tatsächlich verarbeiteten Daten) und Community-/Nutzungsrichtlinien. Neuer `LegalController` (`/nutzungsbedingungen`, `/datenschutz`, `/richtlinien`) + `ImpressumController`; alle Footer-Links app-weit verdrahtet.
|
||||
- **Kombinierte Login/Registrierung:** Dual-Panel mit überschiebendem Overlay (Sign-in ↔ „Account erstellen") und Neon-Button mit Hover-Expand und rotierendem Conic-Glow; ausgelagert nach `static/css/auth.css`. Responsiver Fallback (gestapelt) auf schmalen Screens.
|
||||
- **FinalFlux – Netzwerktopologien:** fünf theme-adaptive Inline-SVG-Diagramme (Stern, Bus, Ring, Mesh, Baum) in den Erklärungen.
|
||||
|
||||
### Geändert
|
||||
- **FinalFlux-Inhalte komplett überarbeitet (28 Themen, ~742 Fragen):** die generischen Dummy-Falschantworten („nur Dekoration", „zufälliger Begriff" …) durch plausible, themennahe Distraktoren ersetzt, jede Frage mit echter Begründung und variierender Antwortposition. Jede Themenseite hat jetzt **echten Lehrtext** (Definitionen, Funktionsweise, Rechenbeispiele, Merksätze) statt des bisherigen Stubs.
|
||||
- **App-weiter Farb-Relaunch auf „Back to the Future"-Palette:** Primär **Feuer-Amber** (`#f0a81a`) + Gelb-Glow, **Flux-Blau** (`#2d9cce`) als Zweitakzent (u. a. Links); warmer Bernstein-Glow statt Matrix-Grün. Dark/Light bleiben (sehr dunkler/sehr heller Grund). Zentral über `theme.css`-Tokens; Legacy-Cyan in `style.css` auf Theme-Variablen umgestellt.
|
||||
- **Spiele-Feedback ohne Grün:** richtig = **Flux-Blau + ✓**, falsch = **Rot + ✗** in FinalFlux, MindMatch und TechTacular (löst den fehlenden Kontrast von „grün auf grün"); neue Tokens `--success`, `--danger`, `--secondary`. Das Fehler-Rot ist ein kühleres Crimson (`#ff4d6d` dunkel / `#be123c` hell), damit es sich klar vom orangen Akzent abhebt (v. a. im Light-Mode).
|
||||
- Registrierungs-Duplikat leitet auf `/?registerError` (kombinierte Seite öffnet automatisch das Signup-Panel).
|
||||
- **Vollständig responsive / mobiltauglich:** Auf schmalen Screens wird die App-Sidebar nicht mehr ausgeblendet (vorher fehlte die Navigation komplett), sondern zu einer Off-Canvas-Schublade mit Hamburger-Button + Backdrop (global via `theme.js` injiziert, Styles in `theme.css`). Topbar stapelt/umbricht (dekorativer Puls + Suche ausgeblendet), Spielkarten-Grid 3→2→1, breite Tabellen horizontal scrollbar. Spiele, FinalFlux und Login/Registrierung waren bereits responsiv.
|
||||
|
||||
### Behoben
|
||||
- **Konto-Löschung repariert:** „Profil löschen" warf einen 500er (fehlendes `@Transactional` bei einer abgeleiteten Delete-Query) und löschte nichts. Jetzt werden Konto **plus** Spielstände, Achievements und Profilbild-Datei in einer Transaktion entfernt — erfüllt die in der Datenschutzerklärung zugesicherte Löschung (Art. 17 DSGVO).
|
||||
- Impressum-„Zurück"-Link loggte eingeloggte Nutzer scheinbar aus; jetzt login-abhängig (eingeloggt → Dashboard, sonst → Startseite). Gleiche Logik auf den Rechtsseiten.
|
||||
- Login-Overlay im **Light-Mode** war weiß-auf-weiß/verwaschen; jetzt kräftiges Orange-Panel mit weißem Text (Dark-Mode unverändert).
|
||||
- **Mobile-Bedienbarkeit der Spiele:** MindMatch- und TechTacular-Boards ragten auf dem Handy aus dem Bildschirm, ohne dass man scrollen konnte (`height:100vh; overflow:hidden` + iOS-Adressleiste). Auf Touch-Breiten jetzt scrollbar, Karten/Kacheln mit fester Proportion bzw. Mindesthöhe → alle Felder erreichbar.
|
||||
- **FinalFlux-Hub auf Mobil:** Überlappung von Kopfbereich und Inhalt + unerreichbares Themen-Menü. Ursache: das generische `main {}` aus der Alt-`style.css` traf das Hub-`<main>`; jetzt in `.finalflux-main` neutralisiert und die Shell stapelt auf Mobil sauber (`display:block`, Sidebar mit voller Höhe).
|
||||
|
||||
### Sicherheit
|
||||
- Neue Rechtsseiten öffentlich erreichbar (`permitAll` für `/impressum`, `/nutzungsbedingungen`, `/datenschutz`, `/richtlinien`).
|
||||
|
||||
### Hinweise
|
||||
- Footer-Links **Support** und **Feedback** sind weiterhin Platzhalter (`href="#"`).
|
||||
- Die Rechtstexte sind inhaltlich vollständig, aber KI-erstellt — vor einem öffentlichen Launch fachlich/juristisch prüfen (insb. konkrete Anbieteranschrift im Impressum).
|
||||
- Vier ungenutzte Alt-CSS-Dateien (`progress/settings/achievements/soziales.css`, keine Referenzen) blieben vorerst liegen.
|
||||
|
||||
## [Unreleased] – 2026-06-25
|
||||
|
||||
Großer Ausbau: zwei vollständige Spiele, durchgängig DB-gestützte und
|
||||
admin-pflegbare Inhalte sowie ein kompletter Optik-Relaunch (Theme-System
|
||||
mit Dark/Light-Umschaltung).
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
#### Spiel: MindMatch (Lern-Memory)
|
||||
- Memory-Spiel, das **Begriff ↔ Bedeutung** paart (klassische Aufdeck-Mechanik mit Lernwert).
|
||||
- Deck-Auswahl, Zug-/Zeitzähler, Sternebewertung und „Spiel abbrechen".
|
||||
- **No-Scroll-Layout**: alle Karten einer Runde passen ohne Scrollen auf den Schirm (Brett skaliert die Karten).
|
||||
- Kartenrückseiten tragen das leuchtende Mindflux-Emblem (grün/magenta je Theme).
|
||||
- Inhalte **DB-gestützt und im Admin pflegbar** (`/admin/mindmatch`): Decks und Paare jederzeit erweiterbar.
|
||||
- Mitgelieferte Decks: Netzwerk, Hardware, IT-Sicherheit, SQL-Befehle sowie ein großer Fachinformatiker-Satz (15 Decks insgesamt).
|
||||
|
||||
#### Spiel: TechTacular (Jeopardy-Quiz)
|
||||
- **Echtes Jeopardy-Prinzip (umgekehrt):** angezeigt wird eine Aussage, gesucht ist der Begriff als „Was ist …?".
|
||||
- 5×5-Board (Kategorie × Punktwert 100–500), Antwortprüfung mit Erklärung.
|
||||
- **Server-autoritativ & Anti-Cheat:** das Board enthält nie die Lösung; Prüfung und Wertung passieren ausschließlich serverseitig.
|
||||
- **≥ 60 % neue Fragen pro Runde:** das Board schließt die Fragen der Vorrunde aus.
|
||||
- **Kuratierter Inhalt:** 12 Kategorien über das ganze Informatik-Prüfungsspektrum (Netzwerk, Betriebssysteme, Programmierung, Datenstrukturen & Algorithmen, Datenbanken/SQL, IT-Sicherheit & Krypto, Web, Rechnerarchitektur, Software-Engineering, Theoretische Informatik, Cloud/DevOps, IT-Recht), **384 Hinweise**.
|
||||
- Admin-Verwaltung als Drill-down (`/admin/techtacular`).
|
||||
|
||||
#### Generisches Spiel-Backend
|
||||
- Tabelle `game_scores` + `GameScoreService`/`GameScoreController` mit `POST /api/games/{game}/scores` und `GET /api/games/{game}/stats` für alle Spiele.
|
||||
- `/progress` zeigt echte Spiel-Statistiken (Highscore, Spiele, zuletzt gespielt).
|
||||
|
||||
#### Optik-Relaunch / Theme-System
|
||||
- **Zentrale Tokens** in `theme.css` + Umschalter `theme.js`, gesteuert über `<html data-theme="dark|light">` (Persistenz via `localStorage`, kein Aufblitzen).
|
||||
- **Dark = Grün-Neon, Light = heller Grund + Magenta** (bewusst invertiert).
|
||||
- Wiederverwendbare `mf-`-Komponenten (Shell/Sidebar/Topbar, HUD-Karten, Panels, Buttons, Formulare, Tabellen, Footer …).
|
||||
- **Dashboard** im „Cyber-Grid"-Look neu: linke Sidebar (Nav, Profil, Level/XP, Streak), Mitte mit HUD-Spielkarten, rechte Widget-Spalte (System-Status, Fortschritt, Top-Kategorien, Challenge), Top-Leiste mit Suche/Glocke/Theme-Schalter.
|
||||
- Original-Mindflux-Logo per CSS-Maske theme-abhängig eingefärbt (`/img/mindflux-emblem.png`).
|
||||
- Theme + Umschalter **app-weit** ausgerollt: Login/Registrierung, beide Spiele, Profil/Fortschritt/Achievements/Einstellungen/Soziales/Knowledge, alle Admin-Seiten, FinalFlux-Hub und 28 statische Lernseiten.
|
||||
- **Fußzeile** (Support · Feedback · Nutzungsbedingungen · Richtlinien · Datenschutz · Impressum) als `.mf-footer`-Komponente, einheitlich auf allen Content-Seiten.
|
||||
|
||||
### Geändert
|
||||
- `ProgressService` liefert echte DB-Daten statt der bisherigen Dummy-Spiele.
|
||||
- Admin-Bootstrap-User auf `AliceDarkest` umbenannt (vorher „Alice Darkest" mit Leerzeichen); zusätzlicher Admin `KevCipher` angelegt.
|
||||
|
||||
### Sicherheit
|
||||
- Statische JS-Ressourcen (`/js/**`) wie CSS/Bilder öffentlich (`permitAll`) — kein schützenswerter Inhalt.
|
||||
- Score-Endpunkte gegen eine Allowlist bekannter Spiele abgesichert; Punkte werden serverseitig berechnet (kein vom Client gemeldeter Score).
|
||||
|
||||
### Technisch
|
||||
- **Flyway-Migrationen V2–V6:** `game_scores` (V2), MindMatch-Decks/-Paare (V3) + Fachinformatiker-Inhalte (V4), TechTacular-Schema (V5) + kuratierte Reverse-Inhalte (V6).
|
||||
- Inhalts-Import für TechTacular über einen `CommandLineRunner`-Seeder aus `techtacular/content.json`.
|
||||
- Hinweis (Thymeleaf): Inline-`<script>` mit `[[`/`]]` benötigt `th:inline="none"`, sonst bricht die Antwort ab.
|
||||
|
||||
### Hinweise
|
||||
- Footer-Links sind aktuell Platzhalter (`href="#"`); echte Impressum-/AGB-/Datenschutz-Seiten existieren noch nicht.
|
||||
- Die fünf übrigen Dashboard-Spiele (Syntax Saga, Code Escape Room, Service Clash, Bug Hunter, Netzwerker) sind noch nicht umgesetzt und verweisen auf „coming soon".
|
||||
|
||||
---
|
||||
|
||||
_Frühere Stände (Grundgerüst der Lernplattform: Benutzer/Login, Profil, Achievements, FinalFlux, Admin) wurden vor Einführung dieses Changelogs entwickelt._
|
||||
+20
-3
@@ -5,16 +5,33 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
# --- Datenbank: unverändert wie im bisherigen Deploy ---
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mindfluxdb
|
||||
SPRING_DATASOURCE_USERNAME: mindflux
|
||||
SPRING_DATASOURCE_PASSWORD: mindflux_pass
|
||||
|
||||
# --- 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"
|
||||
MINDFLUX_ADMIN_PASSWORD: "${MINDFLUX_ADMIN_PASSWORD}"
|
||||
MINDFLUX_ADMIN2_USERNAME: "KevCipher"
|
||||
MINDFLUX_ADMIN2_EMAIL: "kevcipher@proton.me"
|
||||
MINDFLUX_ADMIN2_PASSWORD: "${MINDFLUX_ADMIN2_PASSWORD}"
|
||||
|
||||
volumes:
|
||||
- mindflux_uploads:/app/uploads
|
||||
|
||||
networks:
|
||||
- backend_network
|
||||
|
||||
ports:
|
||||
- "8081:8080"
|
||||
|
||||
networks:
|
||||
backend_network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mindflux_uploads:
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
hello ci pipeline
|
||||
versuch 2
|
||||
Versuch 3
|
||||
Versuch 4
|
||||
Versuch 5
|
||||
Versuch 6
|
||||
Versuch 7
|
||||
Versuch 8
|
||||
Versuch 9
|
||||
Versuch 10
|
||||
Versuch 11
|
||||
Versuch 12
|
||||
Versuch 13
|
||||
Versuch 14 Claude Fix
|
||||
Versuch 15 Maven Fix
|
||||
Versuch 16 Maven Fix pushed
|
||||
rollback
|
||||
@@ -1,59 +1,112 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<groupId>com.mindflux</groupId>
|
||||
<artifactId>mindflux</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.4</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.mindflux</groupId>
|
||||
<artifactId>mindflux</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>mindflux</name>
|
||||
<description>Mindflux – Lernplattform zur Prüfungsvorbereitung</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Thymeleaf für Templates -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Thymeleaf + Spring Security Integration -->
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA + Hibernate -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security komplett -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Passwort-Verschlüsselung (BCrypt) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL Datenbanktreiber -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Datenbank-Migrationen -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Hot Reload für Entwicklung -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Unit Tests -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA optional aber sinnvoll -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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
-1
@@ -1,10 +1,11 @@
|
||||
package com.mindflux;
|
||||
package com.example.mindflux;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MindfluxApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MindfluxApplication.class, args);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class AdminSetup {
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_USERNAME:admin}")
|
||||
private String adminUsername;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_EMAIL:admin@mindflux.de}")
|
||||
private String adminEmail;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_PASSWORD:}")
|
||||
private String adminPassword;
|
||||
|
||||
// Optionaler zweiter Bootstrap-Admin. Wird nur angelegt, wenn Username + Passwort gesetzt sind.
|
||||
@Value("${MINDFLUX_ADMIN2_USERNAME:}")
|
||||
private String admin2Username;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN2_EMAIL:}")
|
||||
private String admin2Email;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN2_PASSWORD:}")
|
||||
private String admin2Password;
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner createAdmin(UserRepository userRepo, BCryptPasswordEncoder passwordEncoder) {
|
||||
return args -> {
|
||||
createAdminIfAbsent(userRepo, passwordEncoder, adminUsername, adminEmail, adminPassword);
|
||||
createAdminIfAbsent(userRepo, passwordEncoder, admin2Username, admin2Email, admin2Password);
|
||||
};
|
||||
}
|
||||
|
||||
private void createAdminIfAbsent(UserRepository userRepo, BCryptPasswordEncoder passwordEncoder,
|
||||
String username, String email, String password) {
|
||||
if (username == null || username.isBlank()) {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
if (userRepo.findByUsername(username).isEmpty()) {
|
||||
User admin = new User();
|
||||
admin.setUsername(username);
|
||||
admin.setEmail(email);
|
||||
admin.setPassword(passwordEncoder.encode(password));
|
||||
admin.setRole("ADMIN");
|
||||
userRepo.save(admin);
|
||||
System.out.println("Bootstrap-Admin erstellt: " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
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;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
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("/api/users/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/") // login.html liegt auf "/"
|
||||
.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"))
|
||||
.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/**")
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserRepository userRepo) {
|
||||
return username -> {
|
||||
User user = userRepo.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden"));
|
||||
|
||||
return org.springframework.security.core.userdetails.User
|
||||
.withUsername(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.roles(user.getRole()) // Achtung: Nur ein String, z.B. "USER" oder "ADMIN"
|
||||
.build();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
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.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 TechTacular einmalig (wenn leer) aus der kuratierten Inhaltsdatei
|
||||
* techtacular/content.json. Format: Jeopardy umgekehrt – pro Hinweis eine Aussage
|
||||
* ("clue") und der gesuchte Begriff ("solution", = richtige Antwort) plus Distraktoren.
|
||||
*/
|
||||
@Component
|
||||
public class TechtacularSeeder implements CommandLineRunner {
|
||||
|
||||
private final TechtacularCategoryRepository categoryRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public TechtacularSeeder(TechtacularCategoryRepository categoryRepository, ObjectMapper objectMapper) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (categoryRepository.count() > 0) {
|
||||
return; // schon befüllt
|
||||
}
|
||||
try {
|
||||
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'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> usedSlugs = new HashSet<>();
|
||||
int order = 1;
|
||||
int catCount = 0;
|
||||
int clueCount = 0;
|
||||
|
||||
for (JsonNode catNode : categories) {
|
||||
String title = catNode.path("title").asText("");
|
||||
JsonNode clues = catNode.get("clues");
|
||||
if (title.isBlank() || clues == null || !clues.isArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TechtacularCategory category =
|
||||
new TechtacularCategory(uniqueSlug(title, usedSlugs), trim(title, 200), order, true);
|
||||
|
||||
for (JsonNode clue : clues) {
|
||||
String clueText = clue.path("clue").asText("");
|
||||
String solution = clue.path("solution").asText("");
|
||||
JsonNode distractors = clue.get("distractors");
|
||||
if (clueText.isBlank() || solution.isBlank() || distractors == null || !distractors.isArray()) {
|
||||
continue;
|
||||
}
|
||||
int points = clue.path("points").asInt(100);
|
||||
String explanation = clue.hasNonNull("explanation") ? clue.get("explanation").asText() : null;
|
||||
|
||||
TechtacularQuestion question = new TechtacularQuestion(
|
||||
trim(clueText, 1000),
|
||||
(explanation == null || explanation.isBlank()) ? null : trim(explanation, 1000),
|
||||
points);
|
||||
|
||||
int sort = 0;
|
||||
question.addAnswer(new TechtacularAnswer(trim(solution, 500), true, sort++)); // richtige Antwort
|
||||
for (JsonNode d : distractors) {
|
||||
String dt = d.asText("");
|
||||
if (!dt.isBlank()) {
|
||||
question.addAnswer(new TechtacularAnswer(trim(dt, 500), false, sort++));
|
||||
}
|
||||
}
|
||||
|
||||
if (question.getAnswers().size() >= 2) {
|
||||
category.addQuestion(question);
|
||||
clueCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!category.getQuestions().isEmpty()) {
|
||||
categoryRepository.save(category);
|
||||
order++;
|
||||
catCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("✅ TechTacular: " + catCount + " Kategorien / " + clueCount + " Hinweise geladen.");
|
||||
} catch (Exception e) {
|
||||
System.out.println("⚠️ TechTacular-Seed übersprungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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 = "kategorie";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (used.contains(slug) || categoryRepository.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,16 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry
|
||||
.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import com.example.mindflux.service.AchievementService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
public class AchievementController {
|
||||
|
||||
@Autowired
|
||||
private AchievementService achievementService;
|
||||
|
||||
@GetMapping("/profile/achievements")
|
||||
public String showAchievements(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
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,59 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.service.MindMatchService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Admin-Verwaltung der MindMatch-Decks und -Paare. Liegt unter /admin/** und ist
|
||||
* damit per SecurityConfig auf ROLE_ADMIN beschränkt.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/mindmatch")
|
||||
public class AdminMindMatchController {
|
||||
|
||||
private final MindMatchService mindMatchService;
|
||||
|
||||
public AdminMindMatchController(MindMatchService mindMatchService) {
|
||||
this.mindMatchService = mindMatchService;
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("decks", mindMatchService.allDecksWithPairs());
|
||||
return "admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks")
|
||||
public String createDeck(@RequestParam String title) {
|
||||
mindMatchService.createDeck(title);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/pairs")
|
||||
public String addPair(@PathVariable Long deckId,
|
||||
@RequestParam String term,
|
||||
@RequestParam String match) {
|
||||
mindMatchService.addPair(deckId, term, match);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/pairs/{pairId}/delete")
|
||||
public String deletePair(@PathVariable Long pairId) {
|
||||
mindMatchService.deletePair(pairId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/delete")
|
||||
public String deleteDeck(@PathVariable Long deckId) {
|
||||
mindMatchService.deleteDeck(deckId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/toggle")
|
||||
public String toggleDeck(@PathVariable Long deckId) {
|
||||
mindMatchService.toggleDeck(deckId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.service.TechtacularService;
|
||||
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 TechTacular-Kategorien, -Fragen und -Antworten (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/techtacular")
|
||||
public class AdminTechtacularController {
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public AdminTechtacularController(TechtacularService techtacularService) {
|
||||
this.techtacularService = techtacularService;
|
||||
}
|
||||
|
||||
/** Übersicht aller Kategorien. */
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("categories", techtacularService.categoryRows());
|
||||
return "admin/techtacular";
|
||||
}
|
||||
|
||||
/** Detailseite einer Kategorie mit ihren Fragen. */
|
||||
@GetMapping("/categories/{categoryId}")
|
||||
public String category(@PathVariable Long categoryId, Model model) {
|
||||
TechtacularCategory category = techtacularService.categoryWithQuestions(categoryId);
|
||||
if (category == null) {
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
model.addAttribute("category", category);
|
||||
return "admin/techtacular-category";
|
||||
}
|
||||
|
||||
@PostMapping("/categories")
|
||||
public String createCategory(@RequestParam String title) {
|
||||
techtacularService.createCategory(title);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/questions")
|
||||
public String addQuestion(@PathVariable Long categoryId,
|
||||
@RequestParam String text,
|
||||
@RequestParam(required = false) String explanation,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam(required = false, defaultValue = "") String answer1,
|
||||
@RequestParam(required = false, defaultValue = "") String answer2,
|
||||
@RequestParam(required = false, defaultValue = "") String answer3,
|
||||
@RequestParam(required = false, defaultValue = "") String answer4,
|
||||
@RequestParam(defaultValue = "0") int correct) {
|
||||
List<String> answers = Arrays.asList(answer1, answer2, answer3, answer4);
|
||||
techtacularService.addQuestion(categoryId, text, explanation, points, answers, correct);
|
||||
return "redirect:/admin/techtacular/categories/" + categoryId;
|
||||
}
|
||||
|
||||
@PostMapping("/questions/{questionId}/delete")
|
||||
public String deleteQuestion(@PathVariable Long questionId, @RequestParam Long categoryId) {
|
||||
techtacularService.deleteQuestion(questionId);
|
||||
return "redirect:/admin/techtacular/categories/" + categoryId;
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/delete")
|
||||
public String deleteCategory(@PathVariable Long categoryId) {
|
||||
techtacularService.deleteCategory(categoryId);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/toggle")
|
||||
public String toggleCategory(@PathVariable Long categoryId) {
|
||||
techtacularService.toggleCategory(categoryId);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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 org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin")
|
||||
public class AdminUserController {
|
||||
|
||||
@Autowired
|
||||
private AdminUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// ⬅️ Neu: Dashboard-Startseite
|
||||
@GetMapping("")
|
||||
public String adminDashboard() {
|
||||
return "admin/dashboard"; // du brauchst: templates/admin/dashboard.html
|
||||
}
|
||||
|
||||
// ⬅️ Benutzerverwaltung
|
||||
@GetMapping("/users")
|
||||
public String listUsers(Model model) {
|
||||
model.addAttribute("users", userService.findAll());
|
||||
return "admin/users/list";
|
||||
}
|
||||
|
||||
@GetMapping("/users/new")
|
||||
public String createUserForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/users/save")
|
||||
public String saveUser(@ModelAttribute("user") User user) {
|
||||
if (user.getId() != null && (user.getPassword() == null || user.getPassword().isBlank())) {
|
||||
User existingUser = userService.findById(user.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + user.getId()));
|
||||
user.setPassword(existingUser.getPassword());
|
||||
} else {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
}
|
||||
|
||||
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));
|
||||
model.addAttribute("user", user);
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/users/delete/{id}")
|
||||
public String deleteUser(@PathVariable Long id) {
|
||||
userService.deleteById(id);
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public String showDashboard() {
|
||||
return "dashboard"; // lädt dashboard.html aus templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class FinalfluxController {
|
||||
|
||||
@GetMapping("/finalflux")
|
||||
public String showFinalfluxOverview() {
|
||||
return "finalflux"; // Lädt templates/finalflux.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
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 org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Generisches Score-API für alle Mindflux-Spiele. Der Spielname kommt aus der URL
|
||||
* und wird gegen eine Allowlist geprüft, damit keine beliebigen Spiele-Kennungen
|
||||
* in die Datenbank wandern.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/games")
|
||||
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");
|
||||
|
||||
private final GameScoreService gameScoreService;
|
||||
|
||||
public GameScoreController(GameScoreService gameScoreService) {
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
@PostMapping("/{game}/scores")
|
||||
public ResponseEntity<ScoreResult> submitScore(@PathVariable String game,
|
||||
@RequestBody ScoreSubmission submission,
|
||||
Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(gameScoreService.record(principal.getName(), game, submission));
|
||||
}
|
||||
|
||||
@GetMapping("/{game}/stats")
|
||||
public ResponseEntity<GameStats> getStats(@PathVariable String game, Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(gameScoreService.getStats(principal.getName(), game));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class ImpressumController {
|
||||
|
||||
@GetMapping("/impressum")
|
||||
public String impressum() {
|
||||
return "impressum"; // verweist auf impressum.html in src/main/resources/templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* Liefert die statischen Rechts-/Informationsseiten aus dem Footer:
|
||||
* AGB / Nutzungsbedingungen, Datenschutzerklärung und Community-Richtlinien.
|
||||
*/
|
||||
@Controller
|
||||
public class LegalController {
|
||||
|
||||
@GetMapping("/nutzungsbedingungen")
|
||||
public String nutzungsbedingungen() {
|
||||
return "legal/nutzungsbedingungen";
|
||||
}
|
||||
|
||||
@GetMapping("/datenschutz")
|
||||
public String datenschutz() {
|
||||
return "legal/datenschutz";
|
||||
}
|
||||
|
||||
@GetMapping("/richtlinien")
|
||||
public String richtlinien() {
|
||||
return "legal/richtlinien";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.MindMatchDeckView;
|
||||
import com.example.mindflux.service.MindMatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Liefert die aktiven MindMatch-Decks (inkl. Paare) an die Spielseite. */
|
||||
@RestController
|
||||
@RequestMapping("/api/mindmatch")
|
||||
public class MindMatchApiController {
|
||||
|
||||
private final MindMatchService mindMatchService;
|
||||
|
||||
public MindMatchApiController(MindMatchService mindMatchService) {
|
||||
this.mindMatchService = mindMatchService;
|
||||
}
|
||||
|
||||
@GetMapping("/decks")
|
||||
public List<MindMatchDeckView> decks() {
|
||||
return mindMatchService.activeDecks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class MindMatchController {
|
||||
|
||||
@GetMapping("/mindmatch")
|
||||
public String showMindMatch() {
|
||||
return "mindmatch"; // lädt templates/mindmatch.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/profiles/";
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String profile(Model model, Principal principal, @RequestParam(value = "success", required = false) Boolean success) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
model.addAttribute("message", "Profil erfolgreich aktualisiert!");
|
||||
}
|
||||
|
||||
return "profile";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/delete-picture")
|
||||
public String deleteProfilePicture(Principal principal) {
|
||||
String username = principal.getName();
|
||||
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
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
@GetMapping("/profile/knowledge")
|
||||
public String knowledgeCenter(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
return "knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/update")
|
||||
public String updateProfile(@RequestParam("email") String email,
|
||||
@RequestParam(value = "profilePicture", required = false) MultipartFile file,
|
||||
Principal principal) {
|
||||
String username = principal.getName();
|
||||
User existingUser = userService.findByUsername(username);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/change-password")
|
||||
public String changePassword(@RequestParam("oldPassword") String oldPassword,
|
||||
@RequestParam("newPassword") String newPassword,
|
||||
Principal principal,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
if (user == null) {
|
||||
redirectAttributes.addFlashAttribute("error", "Benutzer nicht gefunden.");
|
||||
return "redirect:/profile/settings";
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
|
||||
redirectAttributes.addFlashAttribute("error", "Das aktuelle Passwort ist falsch.");
|
||||
return "redirect:/profile/settings";
|
||||
}
|
||||
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
userService.save(user);
|
||||
|
||||
redirectAttributes.addFlashAttribute("message", "Passwort erfolgreich geändert.");
|
||||
return "redirect:/profile/settings";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/delete")
|
||||
public String deleteProfile(Principal principal, Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
String username = principal.getName();
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.GameProgress;
|
||||
import com.example.mindflux.dto.ProgressData;
|
||||
import com.example.mindflux.service.ProgressService;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
public class ProgressController {
|
||||
|
||||
@Autowired
|
||||
private ProgressService progressService;
|
||||
|
||||
@GetMapping("/progress") // <-- hier geändert!
|
||||
public String showProgress(Model model, Principal principal) {
|
||||
String username = principal.getName(); // aktueller eingeloggter Benutzer
|
||||
|
||||
ProgressData progress = progressService.getOverallProgress(username);
|
||||
List<GameProgress> games = progressService.getGameProgress(username);
|
||||
|
||||
if (progress == null) {
|
||||
// Falls keine Fortschrittsdaten vorhanden sind, Dummy-Daten setzen
|
||||
model.addAttribute("correctAnswers", 0);
|
||||
model.addAttribute("currentXp", 0);
|
||||
model.addAttribute("xpForNextLevel", 1000);
|
||||
model.addAttribute("userLevel", 1);
|
||||
model.addAttribute("xpPercentage", 0);
|
||||
} else {
|
||||
model.addAttribute("correctAnswers", progress.getCorrectAnswers());
|
||||
model.addAttribute("currentXp", progress.getCurrentXp());
|
||||
model.addAttribute("xpForNextLevel", progress.getXpForNextLevel());
|
||||
model.addAttribute("userLevel", progress.getUserLevel());
|
||||
model.addAttribute("xpPercentage", progress.getProgressPercent());
|
||||
}
|
||||
|
||||
model.addAttribute("gameProgress", games != null ? games : List.of()); // leere Liste, falls null
|
||||
|
||||
return "progress"; // ruft progress.html auf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
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.*;
|
||||
|
||||
@Controller
|
||||
public class RegisterController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@GetMapping("/register")
|
||||
public String showRegistrationForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "register";
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SettingsController {
|
||||
|
||||
@GetMapping("/profile/settings")
|
||||
public String showSettingsPage() {
|
||||
return "settings"; // lädt templates/settings.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SozialesController {
|
||||
|
||||
@GetMapping("/soziales")
|
||||
public String soziales() {
|
||||
return "soziales"; // -> templates/soziales.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.service.TechtacularService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** Spieler-API für TechTacular: Board laden, Antwort prüfen, Runde abschließen. */
|
||||
@RestController
|
||||
@RequestMapping("/api/techtacular")
|
||||
public class TechtacularApiController {
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public TechtacularApiController(TechtacularService techtacularService) {
|
||||
this.techtacularService = techtacularService;
|
||||
}
|
||||
|
||||
/** exclude = komma-separierte Frage-IDs der Vorrunde, die möglichst nicht erneut kommen sollen. */
|
||||
@GetMapping("/board")
|
||||
public TechtacularBoardView board(@RequestParam(required = false) String exclude) {
|
||||
return techtacularService.buildBoard(parseIds(exclude));
|
||||
}
|
||||
|
||||
private Set<Long> parseIds(String csv) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (csv != null && !csv.isBlank()) {
|
||||
for (String part : csv.split(",")) {
|
||||
try {
|
||||
ids.add(Long.parseLong(part.trim()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
// ungültige IDs einfach überspringen
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@PostMapping("/answer")
|
||||
public AnswerResult answer(@RequestBody AnswerRequest request) {
|
||||
return techtacularService.checkAnswer(request.getQuestionId(), request.getAnswerId());
|
||||
}
|
||||
|
||||
@PostMapping("/finish")
|
||||
public TechtacularResult finish(@RequestBody FinishRequest request, Principal principal) {
|
||||
return techtacularService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class TechtacularController {
|
||||
|
||||
@GetMapping("/techtacular")
|
||||
public String showTechtacular() {
|
||||
return "techtacular"; // lädt templates/techtacular.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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.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;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// 📥 User erstellen
|
||||
@PostMapping
|
||||
public User createUser(@RequestBody User user) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
// 📋 Alle User anzeigen
|
||||
@GetMapping
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.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());
|
||||
}
|
||||
|
||||
// ✏️ User aktualisieren
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
|
||||
return userRepository.findById(id).map(user -> {
|
||||
user.setUsername(updatedUser.getUsername());
|
||||
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));
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// 🗑️ User löschen
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
if (userRepository.existsById(id)) {
|
||||
userRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der TechTacular-Kategorienübersicht (ohne alle Fragen zu laden). */
|
||||
public class AdminCategoryRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final boolean active;
|
||||
private final long questionCount;
|
||||
|
||||
public AdminCategoryRow(Long id, String slug, String title, boolean active, long questionCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.active = active;
|
||||
this.questionCount = questionCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getQuestionCount() { return questionCount; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine vom Spieler gewählte Antwort. */
|
||||
public class AnswerRequest {
|
||||
private Long questionId;
|
||||
private Long answerId;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Serverseitige Bewertung einer einzelnen Antwort (instant feedback). */
|
||||
public class AnswerResult {
|
||||
private final boolean correct;
|
||||
private final Long correctAnswerId;
|
||||
private final String explanation;
|
||||
private final int points;
|
||||
|
||||
public AnswerResult(boolean correct, Long correctAnswerId, String explanation, int points) {
|
||||
this.correct = correct;
|
||||
this.correctAnswerId = correctAnswerId;
|
||||
this.explanation = explanation;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public boolean isCorrect() { return correct; }
|
||||
public Long getCorrectAnswerId() { return correctAnswerId; }
|
||||
public String getExplanation() { return explanation; }
|
||||
public int getPoints() { return points; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Abschluss einer TechTacular-Runde: alle gewählten Antworten + Dauer. Score wird serverseitig recomputed. */
|
||||
public class FinishRequest {
|
||||
private int durationSeconds;
|
||||
private List<AnswerRequest> answers;
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<AnswerRequest> getAnswers() { return answers; }
|
||||
public void setAnswers(List<AnswerRequest> answers) { this.answers = answers; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class GameProgress {
|
||||
private String name;
|
||||
private int progressPercent;
|
||||
private int highscore;
|
||||
private int plays;
|
||||
private LocalDate lastPlayed;
|
||||
|
||||
// Getter
|
||||
public String getName() { return name; }
|
||||
public int getProgressPercent() { return progressPercent; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public int getPlays() { return plays; }
|
||||
public LocalDate getLastPlayed() { return lastPlayed; }
|
||||
|
||||
// Setter
|
||||
public void setName(String name) { this.name = name; }
|
||||
public void setProgressPercent(int progressPercent) { this.progressPercent = progressPercent; }
|
||||
public void setHighscore(int highscore) { this.highscore = highscore; }
|
||||
public void setPlays(int plays) { this.plays = plays; }
|
||||
public void setLastPlayed(LocalDate lastPlayed) { this.lastPlayed = lastPlayed; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** Zusammengefasste Statistik eines Spielers für ein Spiel. */
|
||||
public class GameStats {
|
||||
private final int highscore;
|
||||
private final long plays;
|
||||
private final LocalDate lastPlayed;
|
||||
|
||||
public GameStats(int highscore, long plays, LocalDate lastPlayed) {
|
||||
this.highscore = highscore;
|
||||
this.plays = plays;
|
||||
this.lastPlayed = lastPlayed;
|
||||
}
|
||||
|
||||
public int getHighscore() { return highscore; }
|
||||
public long getPlays() { return plays; }
|
||||
public LocalDate getLastPlayed() { return lastPlayed; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Ein aktives Deck mit seinen Paaren fürs Spiel-API. */
|
||||
public class MindMatchDeckView {
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final List<MindMatchPairView> pairs;
|
||||
|
||||
public MindMatchDeckView(String slug, String title, List<MindMatchPairView> pairs) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.pairs = pairs;
|
||||
}
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public List<MindMatchPairView> getPairs() { return pairs; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Ein Paar fürs Spiel-API. Feld heißt "match" (passend zur bisherigen JS-Logik). */
|
||||
public class MindMatchPairView {
|
||||
private final String term;
|
||||
private final String match;
|
||||
|
||||
public MindMatchPairView(String term, String match) {
|
||||
this.term = term;
|
||||
this.match = match;
|
||||
}
|
||||
|
||||
public String getTerm() { return term; }
|
||||
public String getMatch() { return match; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
public class ProgressData {
|
||||
private int correctAnswers;
|
||||
private int currentXp;
|
||||
private int xpForNextLevel;
|
||||
private int userLevel;
|
||||
private int progressPercent;
|
||||
|
||||
public int getCorrectAnswers() {
|
||||
return correctAnswers;
|
||||
}
|
||||
|
||||
public void setCorrectAnswers(int correctAnswers) {
|
||||
this.correctAnswers = correctAnswers;
|
||||
}
|
||||
|
||||
public int getCurrentXp() {
|
||||
return currentXp;
|
||||
}
|
||||
|
||||
public void setCurrentXp(int currentXp) {
|
||||
this.currentXp = currentXp;
|
||||
}
|
||||
|
||||
public int getXpForNextLevel() {
|
||||
return xpForNextLevel;
|
||||
}
|
||||
|
||||
public void setXpForNextLevel(int xpForNextLevel) {
|
||||
this.xpForNextLevel = xpForNextLevel;
|
||||
}
|
||||
|
||||
public int getUserLevel() {
|
||||
return userLevel;
|
||||
}
|
||||
|
||||
public void setUserLevel(int userLevel) {
|
||||
this.userLevel = userLevel;
|
||||
}
|
||||
|
||||
public int getProgressPercent() {
|
||||
return progressPercent;
|
||||
}
|
||||
|
||||
public void setProgressPercent(int progressPercent) {
|
||||
this.progressPercent = progressPercent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Antwort nach dem Speichern eines Ergebnisses. */
|
||||
public class ScoreResult {
|
||||
private final int score;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public ScoreResult(int score, int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class ScoreSubmission {
|
||||
private String deck;
|
||||
private int pairs;
|
||||
private int moves;
|
||||
private int durationSeconds;
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
public void setDeck(String deck) { this.deck = deck; }
|
||||
|
||||
public int getPairs() { return pairs; }
|
||||
public void setPairs(int pairs) { this.pairs = pairs; }
|
||||
|
||||
public int getMoves() { return moves; }
|
||||
public void setMoves(int moves) { this.moves = moves; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Das Jeopardy-Board fürs Frontend: Spalten (Kategorien) mit Kacheln (Fragen).
|
||||
* Bewusst OHNE Information darüber, welche Antwort korrekt ist – geprüft wird serverseitig.
|
||||
*/
|
||||
public class TechtacularBoardView {
|
||||
|
||||
private final List<Category> categories;
|
||||
private final int maxScore;
|
||||
|
||||
public TechtacularBoardView(List<Category> categories, int maxScore) {
|
||||
this.categories = categories;
|
||||
this.maxScore = maxScore;
|
||||
}
|
||||
|
||||
public List<Category> getCategories() { return categories; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
|
||||
public static class Category {
|
||||
private final String title;
|
||||
private final List<Tile> tiles;
|
||||
|
||||
public Category(String title, List<Tile> tiles) {
|
||||
this.title = title;
|
||||
this.tiles = tiles;
|
||||
}
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public List<Tile> getTiles() { return tiles; }
|
||||
}
|
||||
|
||||
public static class Tile {
|
||||
private final Long questionId;
|
||||
private final int points;
|
||||
private final String text;
|
||||
private final List<Option> answers;
|
||||
|
||||
public Tile(Long questionId, int points, String text, List<Option> answers) {
|
||||
this.questionId = questionId;
|
||||
this.points = points;
|
||||
this.text = text;
|
||||
this.answers = answers;
|
||||
}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public int getPoints() { return points; }
|
||||
public String getText() { return text; }
|
||||
public List<Option> getAnswers() { return answers; }
|
||||
}
|
||||
|
||||
public static class Option {
|
||||
private final Long id;
|
||||
private final String text;
|
||||
|
||||
public Option(Long id, String text) {
|
||||
this.id = id;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getText() { return text; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Ergebnis einer abgeschlossenen TechTacular-Runde. */
|
||||
public class TechtacularResult {
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int correctCount;
|
||||
private final int totalCount;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public TechtacularResult(int score, int maxScore, int correctCount, int totalCount,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.correctCount = correctCount;
|
||||
this.totalCount = totalCount;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getCorrectCount() { return correctCount; }
|
||||
public int getTotalCount() { return totalCount; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "achievements")
|
||||
public class Achievement {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
private String title;
|
||||
private String description;
|
||||
private String icon; // Dateiname des Icons (z.B. "star.png")
|
||||
private LocalDateTime dateAchieved;
|
||||
|
||||
// 🔥 Neuer Konstruktor für schnelle Dummy-Erstellung (ohne username/dateAchieved)
|
||||
public Achievement(String title, String description, String icon, boolean unlocked) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.icon = icon;
|
||||
this.dateAchieved = unlocked ? LocalDateTime.now() : null;
|
||||
}
|
||||
|
||||
// ⚡ Standard-Konstruktor (wichtig für JPA!)
|
||||
public Achievement() {
|
||||
}
|
||||
|
||||
// Getter und Setter
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public String getIcon() { return icon; }
|
||||
public void setIcon(String icon) { this.icon = icon; }
|
||||
|
||||
public LocalDateTime getDateAchieved() { return dateAchieved; }
|
||||
public void setDateAchieved(LocalDateTime dateAchieved) { this.dateAchieved = dateAchieved; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Ein einzelnes Spielergebnis. Bewusst generisch gehalten, damit alle
|
||||
* Mindflux-Spiele (MindMatch, TechTacular, ...) dieselbe Tabelle nutzen können.
|
||||
* Welches Spiel das Ergebnis erzeugt hat, steht in {@link #game}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "game_scores")
|
||||
public class GameScore {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
/** Spiel-Kennung, z.B. "mindmatch" (entspricht der Route im Dashboard). */
|
||||
private String game;
|
||||
|
||||
/** Optionales Deck / Level innerhalb des Spiels, z.B. "netzwerk". */
|
||||
private String deck;
|
||||
|
||||
private int score;
|
||||
|
||||
/** Anzahl der Zug-Versuche (kann je nach Spiel null sein). */
|
||||
private Integer moves;
|
||||
|
||||
@Column(name = "duration_seconds")
|
||||
private Integer durationSeconds;
|
||||
|
||||
@Column(name = "played_at")
|
||||
private LocalDateTime playedAt;
|
||||
|
||||
public GameScore() {
|
||||
}
|
||||
|
||||
public GameScore(String username, String game, String deck, int score,
|
||||
Integer moves, Integer durationSeconds, LocalDateTime playedAt) {
|
||||
this.username = username;
|
||||
this.game = game;
|
||||
this.deck = deck;
|
||||
this.score = score;
|
||||
this.moves = moves;
|
||||
this.durationSeconds = durationSeconds;
|
||||
this.playedAt = playedAt;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getGame() { return game; }
|
||||
public void setGame(String game) { this.game = game; }
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
public void setDeck(String deck) { this.deck = deck; }
|
||||
|
||||
public int getScore() { return score; }
|
||||
public void setScore(int score) { this.score = score; }
|
||||
|
||||
public Integer getMoves() { return moves; }
|
||||
public void setMoves(Integer moves) { this.moves = moves; }
|
||||
|
||||
public Integer getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public LocalDateTime getPlayedAt() { return playedAt; }
|
||||
public void setPlayedAt(LocalDateTime playedAt) { this.playedAt = playedAt; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Ein MindMatch-Deck (Kategorie) mit seinen Begriffspaaren. Von Admins pflegbar. */
|
||||
@Entity
|
||||
@Table(name = "mindmatch_decks")
|
||||
public class MindMatchDeck {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "deck", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("id ASC")
|
||||
private List<MindMatchPair> pairs = new ArrayList<>();
|
||||
|
||||
public MindMatchDeck() {
|
||||
}
|
||||
|
||||
public MindMatchDeck(String slug, String title, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/** Fügt ein Paar hinzu und hält die Rückreferenz konsistent. */
|
||||
public void addPair(MindMatchPair pair) {
|
||||
pair.setDeck(this);
|
||||
this.pairs.add(pair);
|
||||
}
|
||||
|
||||
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 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<MindMatchPair> getPairs() { return pairs; }
|
||||
public void setPairs(List<MindMatchPair> pairs) { this.pairs = pairs; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Ein Begriffspaar innerhalb eines {@link MindMatchDeck}: Begriff + zugehörige Bedeutung. */
|
||||
@Entity
|
||||
@Table(name = "mindmatch_pairs")
|
||||
public class MindMatchPair {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deck_id")
|
||||
private MindMatchDeck deck;
|
||||
|
||||
private String term;
|
||||
|
||||
/** Spalte heißt "match_text", da MATCH in SQL ein reserviertes Wort ist. */
|
||||
@Column(name = "match_text")
|
||||
private String matchText;
|
||||
|
||||
public MindMatchPair() {
|
||||
}
|
||||
|
||||
public MindMatchPair(String term, String matchText) {
|
||||
this.term = term;
|
||||
this.matchText = matchText;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public MindMatchDeck getDeck() { return deck; }
|
||||
public void setDeck(MindMatchDeck deck) { this.deck = deck; }
|
||||
|
||||
public String getTerm() { return term; }
|
||||
public void setTerm(String term) { this.term = term; }
|
||||
|
||||
public String getMatchText() { return matchText; }
|
||||
public void setMatchText(String matchText) { this.matchText = matchText; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Eine Antwortmöglichkeit zu einer {@link TechtacularQuestion}. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_answers")
|
||||
public class TechtacularAnswer {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "question_id")
|
||||
private TechtacularQuestion question;
|
||||
|
||||
@Column(name = "answer_text")
|
||||
private String text;
|
||||
|
||||
private boolean correct;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
public TechtacularAnswer() {
|
||||
}
|
||||
|
||||
public TechtacularAnswer(String text, boolean correct, int sortOrder) {
|
||||
this.text = text;
|
||||
this.correct = correct;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TechtacularQuestion getQuestion() { return question; }
|
||||
public void setQuestion(TechtacularQuestion question) { this.question = question; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public boolean isCorrect() { return correct; }
|
||||
public void setCorrect(boolean correct) { this.correct = correct; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Eine TechTacular-Kategorie (Spalte im Jeopardy-Board) mit ihren Fragen. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_categories")
|
||||
public class TechtacularCategory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("points ASC, id ASC")
|
||||
private List<TechtacularQuestion> questions = new ArrayList<>();
|
||||
|
||||
public TechtacularCategory() {
|
||||
}
|
||||
|
||||
public TechtacularCategory(String slug, String title, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void addQuestion(TechtacularQuestion question) {
|
||||
question.setCategory(this);
|
||||
this.questions.add(question);
|
||||
}
|
||||
|
||||
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 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<TechtacularQuestion> getQuestions() { return questions; }
|
||||
public void setQuestions(List<TechtacularQuestion> questions) { this.questions = questions; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Eine Frage mit ihren Antwortmöglichkeiten (genau eine ist korrekt) und Punktwert. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_questions")
|
||||
public class TechtacularQuestion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "category_id")
|
||||
private TechtacularCategory category;
|
||||
|
||||
@Column(name = "question_text")
|
||||
private String text;
|
||||
|
||||
private String explanation;
|
||||
|
||||
private int points;
|
||||
|
||||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC")
|
||||
private List<TechtacularAnswer> answers = new ArrayList<>();
|
||||
|
||||
public TechtacularQuestion() {
|
||||
}
|
||||
|
||||
public TechtacularQuestion(String text, String explanation, int points) {
|
||||
this.text = text;
|
||||
this.explanation = explanation;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public void addAnswer(TechtacularAnswer answer) {
|
||||
answer.setQuestion(this);
|
||||
this.answers.add(answer);
|
||||
}
|
||||
|
||||
/** Liefert die als korrekt markierte Antwort (oder null). */
|
||||
public TechtacularAnswer correctAnswer() {
|
||||
return answers.stream().filter(TechtacularAnswer::isCorrect).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TechtacularCategory getCategory() { return category; }
|
||||
public void setCategory(TechtacularCategory category) { this.category = category; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public String getExplanation() { return explanation; }
|
||||
public void setExplanation(String explanation) { this.explanation = explanation; }
|
||||
|
||||
public int getPoints() { return points; }
|
||||
public void setPoints(int points) { this.points = points; }
|
||||
|
||||
public List<TechtacularAnswer> getAnswers() { return answers; }
|
||||
public void setAnswers(List<TechtacularAnswer> answers) { this.answers = answers; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private String password;
|
||||
private String email;
|
||||
private String role;
|
||||
|
||||
@Column(name = "profile_picture")
|
||||
private String profilePicture;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String username, String password, String email, String role) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
// —————————————
|
||||
// Getter / Setter
|
||||
// —————————————
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String u) { this.username = u; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String p) { this.password = p; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String e) { this.email = e; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String r) { this.role = r; }
|
||||
|
||||
public String getProfilePicture() { return profilePicture; }
|
||||
public void setProfilePicture(String pictureUrl) { this.profilePicture = pictureUrl; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AchievementRepository extends JpaRepository<Achievement, Long> {
|
||||
List<Achievement> findByUsername(String username);
|
||||
|
||||
/** Alle Achievements eines Spielers löschen (z. B. beim Löschen des Kontos). */
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AdminUserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.GameScore;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GameScoreRepository extends JpaRepository<GameScore, Long> {
|
||||
|
||||
/** Bestes Ergebnis eines Spielers in einem Spiel (Highscore). */
|
||||
Optional<GameScore> findTopByUsernameAndGameOrderByScoreDesc(String username, String game);
|
||||
|
||||
/** Zuletzt gespieltes Ergebnis eines Spielers in einem Spiel. */
|
||||
Optional<GameScore> findTopByUsernameAndGameOrderByPlayedAtDesc(String username, String game);
|
||||
|
||||
/** Wie oft ein Spieler ein Spiel abgeschlossen hat. */
|
||||
long countByUsernameAndGame(String username, String game);
|
||||
|
||||
/** Alle Spielstände eines Spielers löschen (z. B. beim Löschen des Kontos). */
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.MindMatchDeck;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface MindMatchDeckRepository extends JpaRepository<MindMatchDeck, Long> {
|
||||
|
||||
List<MindMatchDeck> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<MindMatchDeck> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
Optional<MindMatchDeck> findBySlug(String slug);
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.MindMatchPair;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MindMatchPairRepository extends JpaRepository<MindMatchPair, Long> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TechtacularAnswerRepository extends JpaRepository<TechtacularAnswer, Long> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TechtacularCategoryRepository extends JpaRepository<TechtacularCategory, Long> {
|
||||
|
||||
List<TechtacularCategory> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<TechtacularCategory> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TechtacularQuestionRepository extends JpaRepository<TechtacularQuestion, Long> {
|
||||
|
||||
long countByCategoryId(Long categoryId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// Benutzer anhand des Benutzernamens suchen
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
// Benutzer anhand des Benutzernamens löschen
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AchievementService {
|
||||
List<Achievement> getAchievementsForUser(String username);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import com.example.mindflux.repository.AchievementRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AchievementServiceImpl implements AchievementService {
|
||||
|
||||
@Autowired
|
||||
private AchievementRepository achievementRepository;
|
||||
|
||||
@Override
|
||||
public List<Achievement> getAchievementsForUser(String username) {
|
||||
return achievementRepository.findByUsername(username);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.AdminUserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class AdminUserService {
|
||||
|
||||
@Autowired
|
||||
private AdminUserRepository userRepository;
|
||||
|
||||
public List<User> findAll() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
public Optional<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
public User save(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public void deleteById(Long id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.dto.ScoreSubmission;
|
||||
import com.example.mindflux.model.GameScore;
|
||||
import com.example.mindflux.repository.GameScoreRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Speichert Spielergebnisse und liefert Statistiken. Der Score wird hier
|
||||
* autoritativ berechnet – der Client schickt nur die Rohwerte.
|
||||
*/
|
||||
@Service
|
||||
public class GameScoreService {
|
||||
|
||||
private final GameScoreRepository repository;
|
||||
|
||||
public GameScoreService(GameScoreRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Punktezahl aus Genauigkeit und Tempo.
|
||||
* <ul>
|
||||
* <li>Genauigkeit: {@code 1000 * Paare / Züge} – 1000 Punkte bei perfektem Spiel
|
||||
* (jeder Aufdeck-Versuch ein Treffer), weniger bei vielen Fehlversuchen.</li>
|
||||
* <li>Zeitbonus: bis zu 500 Punkte, der nach 100 Sekunden auf 0 fällt.</li>
|
||||
* </ul>
|
||||
* Identisch in {@code mindmatch.js} gespiegelt, damit der Client sofort denselben
|
||||
* Wert anzeigt.
|
||||
*/
|
||||
public static int calculateScore(int pairs, int moves, int durationSeconds) {
|
||||
if (pairs <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int safeMoves = Math.max(moves, pairs); // niemals weniger Züge als Paare
|
||||
int safeSeconds = Math.max(durationSeconds, 0);
|
||||
|
||||
int accuracyScore = Math.round(1000f * pairs / safeMoves);
|
||||
int timeBonus = Math.max(0, 500 - safeSeconds * 5);
|
||||
return accuracyScore + timeBonus;
|
||||
}
|
||||
|
||||
/** Speichert ein abgeschlossenes MindMatch-Spiel (Score aus Rohwerten berechnet). */
|
||||
public ScoreResult record(String username, String game, ScoreSubmission submission) {
|
||||
int moves = Math.max(submission.getMoves(), submission.getPairs());
|
||||
int seconds = Math.max(submission.getDurationSeconds(), 0);
|
||||
int score = calculateScore(submission.getPairs(), moves, seconds);
|
||||
return recordScore(username, game, submission.getDeck(), score, moves, seconds);
|
||||
}
|
||||
|
||||
/** Speichert ein bereits berechnetes Ergebnis (z.B. TechTacular) und liefert den Highscore-Stand. */
|
||||
public ScoreResult recordScore(String username, String game, String deck,
|
||||
int score, Integer moves, Integer durationSeconds) {
|
||||
GameScore previousBest = repository
|
||||
.findTopByUsernameAndGameOrderByScoreDesc(username, game)
|
||||
.orElse(null);
|
||||
|
||||
repository.save(new GameScore(
|
||||
username, game, deck, score, moves, durationSeconds, LocalDateTime.now()));
|
||||
|
||||
boolean newHighscore = previousBest == null || score > previousBest.getScore();
|
||||
int highscore = previousBest == null ? score : Math.max(previousBest.getScore(), score);
|
||||
long plays = repository.countByUsernameAndGame(username, game);
|
||||
|
||||
return new ScoreResult(score, highscore, newHighscore, plays);
|
||||
}
|
||||
|
||||
/** Aktuelle Statistik (Highscore, Anzahl Spiele, zuletzt gespielt) für ein Spiel. */
|
||||
public GameStats getStats(String username, String game) {
|
||||
int highscore = repository.findTopByUsernameAndGameOrderByScoreDesc(username, game)
|
||||
.map(GameScore::getScore)
|
||||
.orElse(0);
|
||||
long plays = repository.countByUsernameAndGame(username, game);
|
||||
var lastPlayed = repository.findTopByUsernameAndGameOrderByPlayedAtDesc(username, game)
|
||||
.map(score -> score.getPlayedAt().toLocalDate())
|
||||
.orElse(null);
|
||||
return new GameStats(highscore, plays, lastPlayed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.MindMatchDeckView;
|
||||
import com.example.mindflux.dto.MindMatchPairView;
|
||||
import com.example.mindflux.model.MindMatchDeck;
|
||||
import com.example.mindflux.model.MindMatchPair;
|
||||
import com.example.mindflux.repository.MindMatchDeckRepository;
|
||||
import com.example.mindflux.repository.MindMatchPairRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MindMatchService {
|
||||
|
||||
private final MindMatchDeckRepository deckRepository;
|
||||
private final MindMatchPairRepository pairRepository;
|
||||
|
||||
public MindMatchService(MindMatchDeckRepository deckRepository, MindMatchPairRepository pairRepository) {
|
||||
this.deckRepository = deckRepository;
|
||||
this.pairRepository = pairRepository;
|
||||
}
|
||||
|
||||
// ---------- Spieler ----------
|
||||
|
||||
/** Aktive Decks mit Paaren fürs Spiel (open-in-view=false -> hier initialisiert). */
|
||||
@Transactional(readOnly = true)
|
||||
public List<MindMatchDeckView> activeDecks() {
|
||||
return deckRepository.findAllByActiveTrueOrderBySortOrderAscIdAsc().stream()
|
||||
.map(deck -> new MindMatchDeckView(
|
||||
deck.getSlug(),
|
||||
deck.getTitle(),
|
||||
deck.getPairs().stream()
|
||||
.map(pair -> new MindMatchPairView(pair.getTerm(), pair.getMatchText()))
|
||||
.toList()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ---------- Admin ----------
|
||||
|
||||
/** Alle Decks (auch inaktive) inkl. Paaren für die Verwaltung. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<MindMatchDeck> allDecksWithPairs() {
|
||||
List<MindMatchDeck> decks = deckRepository.findAllByOrderBySortOrderAscIdAsc();
|
||||
decks.forEach(deck -> deck.getPairs().size()); // Lazy-Collection laden
|
||||
return decks;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createDeck(String title) {
|
||||
if (title == null || title.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String clean = title.trim();
|
||||
int order = (int) deckRepository.count() + 1;
|
||||
deckRepository.save(new MindMatchDeck(uniqueSlug(clean), clean, order, true));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addPair(Long deckId, String term, String match) {
|
||||
if (term == null || term.isBlank() || match == null || match.isBlank()) {
|
||||
return;
|
||||
}
|
||||
deckRepository.findById(deckId).ifPresent(deck -> {
|
||||
deck.addPair(new MindMatchPair(term.trim(), match.trim()));
|
||||
deckRepository.save(deck);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deletePair(Long pairId) {
|
||||
pairRepository.deleteById(pairId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDeck(Long deckId) {
|
||||
deckRepository.deleteById(deckId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggleDeck(Long deckId) {
|
||||
deckRepository.findById(deckId).ifPresent(deck -> deck.setActive(!deck.isActive()));
|
||||
}
|
||||
|
||||
/** Erzeugt aus dem Titel einen URL-tauglichen, eindeutigen Slug. */
|
||||
private String uniqueSlug(String title) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "deck";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (deckRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.GameProgress;
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ProgressData;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class ProgressService {
|
||||
|
||||
/** Spiel-Kennung -> Anzeigename, in Dashboard-Reihenfolge. */
|
||||
private static final Map<String, String> GAMES = new LinkedHashMap<>();
|
||||
static {
|
||||
GAMES.put("mindmatch", "MindMatch");
|
||||
GAMES.put("techtacular", "TechTacular");
|
||||
GAMES.put("syntax-saga", "Syntax Saga");
|
||||
GAMES.put("escape-room", "Code Escape Room");
|
||||
GAMES.put("service-clash", "Service Clash");
|
||||
GAMES.put("bug-hunter", "Bug Hunter");
|
||||
GAMES.put("netzwerker", "Netzwerker");
|
||||
}
|
||||
|
||||
private final GameScoreService gameScoreService;
|
||||
|
||||
public ProgressService(GameScoreService gameScoreService) {
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
public ProgressData getOverallProgress(String username) {
|
||||
// Dummy-Daten – später aus DB berechnen
|
||||
ProgressData data = new ProgressData();
|
||||
data.setCorrectAnswers(432);
|
||||
data.setCurrentXp(2400);
|
||||
data.setXpForNextLevel(3000);
|
||||
data.setUserLevel(5);
|
||||
data.setProgressPercent(78);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Echter Spiel-Fortschritt pro Spiel aus der Datenbank (game_scores). */
|
||||
public List<GameProgress> getGameProgress(String username) {
|
||||
List<GameProgress> result = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : GAMES.entrySet()) {
|
||||
GameStats stats = gameScoreService.getStats(username, entry.getKey());
|
||||
result.add(toGameProgress(entry.getValue(), stats));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private GameProgress toGameProgress(String displayName, GameStats stats) {
|
||||
GameProgress game = new GameProgress();
|
||||
game.setName(displayName);
|
||||
game.setHighscore(stats.getHighscore());
|
||||
game.setPlays((int) stats.getPlays());
|
||||
game.setLastPlayed(stats.getLastPlayed());
|
||||
// Fortschritt auf der Score-Skala (max ~1500 Punkte -> 100 %).
|
||||
game.setProgressPercent(Math.min(100, Math.round(stats.getHighscore() / 15.0f)));
|
||||
return game;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import com.example.mindflux.repository.TechtacularCategoryRepository;
|
||||
import com.example.mindflux.repository.TechtacularQuestionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class TechtacularService {
|
||||
|
||||
private static final String GAME = "techtacular";
|
||||
private static final int BOARD_CATEGORIES = 5;
|
||||
private static final int[] POINT_TIERS = {100, 200, 300, 400, 500};
|
||||
|
||||
private final TechtacularCategoryRepository categoryRepository;
|
||||
private final TechtacularQuestionRepository questionRepository;
|
||||
private final GameScoreService gameScoreService;
|
||||
private final Random random = new Random();
|
||||
|
||||
public TechtacularService(TechtacularCategoryRepository categoryRepository,
|
||||
TechtacularQuestionRepository questionRepository,
|
||||
GameScoreService gameScoreService) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.questionRepository = questionRepository;
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
// ---------- Spieler ----------
|
||||
|
||||
/**
|
||||
* Baut ein zufälliges Board: bis zu 5 Kategorien, je eine Frage pro Punktstufe (100–500).
|
||||
* Fragen aus {@code exclude} (z.B. die Vorrunde) werden möglichst vermieden, damit jede
|
||||
* Runde mit überwiegend neuen Fragen gefüllt wird.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public TechtacularBoardView buildBoard(Set<Long> exclude) {
|
||||
Set<Long> excluded = (exclude == null) ? Set.of() : exclude;
|
||||
|
||||
List<TechtacularCategory> usable = categoryRepository.findAllByActiveTrueOrderBySortOrderAscIdAsc()
|
||||
.stream().filter(c -> !c.getQuestions().isEmpty()).collect(Collectors.toList());
|
||||
Collections.shuffle(usable, random);
|
||||
List<TechtacularCategory> chosen = usable.stream().limit(BOARD_CATEGORIES).toList();
|
||||
|
||||
List<TechtacularBoardView.Category> columns = new ArrayList<>();
|
||||
int maxScore = 0;
|
||||
|
||||
for (TechtacularCategory category : chosen) {
|
||||
Map<Integer, List<TechtacularQuestion>> byPoints = category.getQuestions().stream()
|
||||
.collect(Collectors.groupingBy(TechtacularQuestion::getPoints));
|
||||
|
||||
List<TechtacularBoardView.Tile> tiles = new ArrayList<>();
|
||||
for (int value : POINT_TIERS) {
|
||||
List<TechtacularQuestion> pool = byPoints.get(value);
|
||||
if (pool == null || pool.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// Bevorzugt Fragen, die nicht in der Vorrunde dran waren.
|
||||
List<TechtacularQuestion> fresh = pool.stream()
|
||||
.filter(q -> !excluded.contains(q.getId())).collect(Collectors.toList());
|
||||
List<TechtacularQuestion> pick = fresh.isEmpty() ? pool : fresh;
|
||||
TechtacularQuestion question = pick.get(random.nextInt(pick.size()));
|
||||
List<TechtacularBoardView.Option> options = question.getAnswers().stream()
|
||||
.map(a -> new TechtacularBoardView.Option(a.getId(), a.getText()))
|
||||
.collect(Collectors.toList());
|
||||
Collections.shuffle(options, random);
|
||||
tiles.add(new TechtacularBoardView.Tile(question.getId(), value, question.getText(), options));
|
||||
maxScore += value;
|
||||
}
|
||||
if (!tiles.isEmpty()) {
|
||||
columns.add(new TechtacularBoardView.Category(category.getTitle(), tiles));
|
||||
}
|
||||
}
|
||||
return new TechtacularBoardView(columns, maxScore);
|
||||
}
|
||||
|
||||
/** Bewertet eine einzelne Antwort serverseitig (Frontend kennt die richtige Antwort vorher nicht). */
|
||||
@Transactional(readOnly = true)
|
||||
public AnswerResult checkAnswer(Long questionId, Long answerId) {
|
||||
TechtacularQuestion question = questionRepository.findById(questionId).orElse(null);
|
||||
if (question == null) {
|
||||
return new AnswerResult(false, null, "", 0);
|
||||
}
|
||||
TechtacularAnswer correct = question.correctAnswer();
|
||||
Long correctId = correct != null ? correct.getId() : null;
|
||||
boolean isCorrect = correctId != null && correctId.equals(answerId);
|
||||
return new AnswerResult(isCorrect, correctId, question.getExplanation(), isCorrect ? question.getPoints() : 0);
|
||||
}
|
||||
|
||||
/** Schließt eine Runde ab: Score wird aus den tatsächlichen Antworten recomputed und gespeichert. */
|
||||
@Transactional
|
||||
public TechtacularResult finish(String username, FinishRequest request) {
|
||||
int score = 0;
|
||||
int maxScore = 0;
|
||||
int correctCount = 0;
|
||||
int total = 0;
|
||||
|
||||
if (request.getAnswers() != null) {
|
||||
for (AnswerRequest answer : request.getAnswers()) {
|
||||
if (answer.getQuestionId() == null) {
|
||||
continue;
|
||||
}
|
||||
TechtacularQuestion question = questionRepository.findById(answer.getQuestionId()).orElse(null);
|
||||
if (question == null) {
|
||||
continue;
|
||||
}
|
||||
total++;
|
||||
maxScore += question.getPoints();
|
||||
TechtacularAnswer correct = question.correctAnswer();
|
||||
if (correct != null && correct.getId().equals(answer.getAnswerId())) {
|
||||
score += question.getPoints();
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int seconds = Math.max(request.getDurationSeconds(), 0);
|
||||
ScoreResult saved = gameScoreService.recordScore(username, GAME, null, score, correctCount, seconds);
|
||||
return new TechtacularResult(score, maxScore, correctCount, total,
|
||||
saved.getHighscore(), saved.isNewHighscore(), saved.getPlays());
|
||||
}
|
||||
|
||||
// ---------- Admin ----------
|
||||
|
||||
/** Leichte Übersicht: Kategorien mit Fragenanzahl (lädt nicht alle Fragen). */
|
||||
@Transactional(readOnly = true)
|
||||
public List<AdminCategoryRow> categoryRows() {
|
||||
return categoryRepository.findAllByOrderBySortOrderAscIdAsc().stream()
|
||||
.map(c -> new AdminCategoryRow(c.getId(), c.getSlug(), c.getTitle(), c.isActive(),
|
||||
questionRepository.countByCategoryId(c.getId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** Eine Kategorie inkl. Fragen und Antworten (für die Detailseite). */
|
||||
@Transactional(readOnly = true)
|
||||
public TechtacularCategory categoryWithQuestions(Long id) {
|
||||
TechtacularCategory category = categoryRepository.findById(id).orElse(null);
|
||||
if (category != null) {
|
||||
category.getQuestions().forEach(q -> q.getAnswers().size()); // lazy init
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createCategory(String title) {
|
||||
if (title == null || title.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String clean = title.trim();
|
||||
int order = (int) categoryRepository.count() + 1;
|
||||
categoryRepository.save(new TechtacularCategory(uniqueSlug(clean), clean, order, true));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addQuestion(Long categoryId, String text, String explanation, int points,
|
||||
List<String> answers, int correctIndex) {
|
||||
if (text == null || text.isBlank() || answers == null) {
|
||||
return;
|
||||
}
|
||||
categoryRepository.findById(categoryId).ifPresent(category -> {
|
||||
TechtacularQuestion question = new TechtacularQuestion(
|
||||
text.trim(),
|
||||
(explanation == null || explanation.isBlank()) ? null : explanation.trim(),
|
||||
normalizePoints(points));
|
||||
|
||||
int sort = 0;
|
||||
boolean hasCorrect = false;
|
||||
for (int i = 0; i < answers.size(); i++) {
|
||||
String answerText = answers.get(i);
|
||||
if (answerText == null || answerText.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
boolean isCorrect = (i == correctIndex);
|
||||
hasCorrect = hasCorrect || isCorrect;
|
||||
question.addAnswer(new TechtacularAnswer(answerText.trim(), isCorrect, sort++));
|
||||
}
|
||||
|
||||
// Nur sinnvolle Fragen speichern: mind. 2 Antworten und genau die markierte ist korrekt.
|
||||
if (question.getAnswers().size() >= 2 && hasCorrect) {
|
||||
category.addQuestion(question);
|
||||
categoryRepository.save(category);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteQuestion(Long questionId) {
|
||||
questionRepository.deleteById(questionId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteCategory(Long categoryId) {
|
||||
categoryRepository.deleteById(categoryId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggleCategory(Long categoryId) {
|
||||
categoryRepository.findById(categoryId).ifPresent(c -> c.setActive(!c.isActive()));
|
||||
}
|
||||
|
||||
private int normalizePoints(int points) {
|
||||
int rounded = Math.round(points / 100f) * 100;
|
||||
return Math.max(100, Math.min(500, rounded));
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "kategorie";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (categoryRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
|
||||
public interface UserService {
|
||||
User findByUsername(String username);
|
||||
void save(User user);
|
||||
void deleteByUsername(String username); // <-- Nur die Methodensignatur, ohne {}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.AchievementRepository;
|
||||
import com.example.mindflux.repository.GameScoreRepository;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private GameScoreRepository gameScoreRepository;
|
||||
|
||||
@Autowired
|
||||
private AchievementRepository achievementRepository;
|
||||
|
||||
@Override
|
||||
public User findByUsername(String username) {
|
||||
return userRepository.findByUsername(username).orElse(null); // <-- Optional aufgelöst
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(User user) {
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteByUsername(String username) {
|
||||
// Zugehörige Daten mitlöschen (kein FK/Cascade -> manuell), damit keine
|
||||
// verwaisten Datensätze zurückbleiben und ein neuer gleichnamiger Account
|
||||
// keine Alt-Daten erbt.
|
||||
gameScoreRepository.deleteByUsername(username);
|
||||
achievementRepository.deleteByUsername(username);
|
||||
userRepository.deleteByUsername(username);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mindflux;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/")
|
||||
public Map<String, String> hello() {
|
||||
return Map.of(
|
||||
"app", "mindflux",
|
||||
"status", "running",
|
||||
"message", "Hello from Spring Boot 🚀, automatic deploy is working. :-)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,51 @@
|
||||
server.port=8080
|
||||
# =======================
|
||||
# Datenbankverbindung (PostgreSQL)
|
||||
# =======================
|
||||
|
||||
spring.datasource.url=jdbc:postgresql://postgres:5432/mindfluxdb
|
||||
spring.datasource.username=mindflux
|
||||
spring.datasource.password=mindflux_pass
|
||||
spring.config.import=optional:file:.env[.properties]
|
||||
spring.datasource.url=${MINDFLUX_DB_URL:jdbc:postgresql://localhost:5433/mindflux_db}
|
||||
spring.datasource.username=${MINDFLUX_DB_USER:mindflux_user}
|
||||
spring.datasource.password=${MINDFLUX_DB_PASSWORD}
|
||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
|
||||
# =======================
|
||||
# Hibernate / JPA
|
||||
# =======================
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.open-in-view=false
|
||||
|
||||
# =======================
|
||||
# Datenbank-Migrationen
|
||||
# =======================
|
||||
|
||||
spring.flyway.enabled=true
|
||||
|
||||
# =======================
|
||||
# SQL-Initialisierung
|
||||
# =======================
|
||||
|
||||
spring.sql.init.mode=never
|
||||
|
||||
# =======================
|
||||
# Server
|
||||
# =======================
|
||||
|
||||
server.port=8282
|
||||
server.forward-headers-strategy=framework
|
||||
server.servlet.session.cookie.secure=${MINDFLUX_COOKIE_SECURE:false}
|
||||
server.servlet.session.cookie.http-only=true
|
||||
server.servlet.session.cookie.same-site=lax
|
||||
spring.output.ansi.enabled=ALWAYS
|
||||
|
||||
# Öffentliche Basis-URL, z. B. für künftig erzeugte absolute Links.
|
||||
mindflux.public-url=${MINDFLUX_PUBLIC_URL:http://localhost:8282}
|
||||
|
||||
# =======================
|
||||
# Datei-Uploads (Profilbilder)
|
||||
# =======================
|
||||
# Hinweis: Der nginx-Reverse-Proxy muss client_max_body_size passend gesetzt haben,
|
||||
# sonst greift schon dort ein 413, bevor die App den Upload sieht.
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=10MB
|
||||
|
||||
@@ -16,5 +16,4 @@ CREATE TABLE achievements (
|
||||
date_achieved TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_achievements_username
|
||||
ON achievements (username);
|
||||
CREATE INDEX idx_achievements_username ON achievements (username);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Generische Spiel-Ergebnistabelle für alle Mindflux-Spiele
|
||||
-- (MindMatch ist das erste Spiel, das hier schreibt – die übrigen docken später an).
|
||||
CREATE TABLE game_scores (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
game VARCHAR(50) NOT NULL,
|
||||
deck VARCHAR(100),
|
||||
score INT NOT NULL,
|
||||
moves INT,
|
||||
duration_seconds INT,
|
||||
played_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_game_scores_user_game ON game_scores (username, game);
|
||||
@@ -0,0 +1,68 @@
|
||||
-- MindMatch-Inhalte in die DB verlagern, damit Admins Decks (Kategorien) und
|
||||
-- Paare jederzeit zur Laufzeit erweitern können (vorher statische JSON-Dateien).
|
||||
|
||||
CREATE TABLE mindmatch_decks (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||
title VARCHAR(150) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE TABLE mindmatch_pairs (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
deck_id BIGINT NOT NULL REFERENCES mindmatch_decks (id) ON DELETE CASCADE,
|
||||
term VARCHAR(255) NOT NULL,
|
||||
match_text VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mindmatch_pairs_deck ON mindmatch_pairs (deck_id);
|
||||
|
||||
-- Seed: die bisherigen 3 Decks plus das neue SQL-Befehle-Deck.
|
||||
INSERT INTO mindmatch_decks (slug, title, sort_order, active) VALUES
|
||||
('netzwerk', 'Netzwerk-Grundlagen', 1, TRUE),
|
||||
('hardware', 'Hardware & Komponenten', 2, TRUE),
|
||||
('it-sicherheit', 'IT-Sicherheit', 3, TRUE),
|
||||
('sql-befehle', 'SQL-Befehle', 4, TRUE);
|
||||
|
||||
INSERT INTO mindmatch_pairs (deck_id, term, match_text)
|
||||
SELECT d.id, v.term, v.match_text
|
||||
FROM mindmatch_decks d
|
||||
JOIN (
|
||||
VALUES
|
||||
('netzwerk', 'DNS', 'Löst Namen in IP-Adressen auf'),
|
||||
('netzwerk', 'DHCP', 'Vergibt IP-Adressen automatisch'),
|
||||
('netzwerk', 'Router', 'Verbindet verschiedene Netze'),
|
||||
('netzwerk', 'Switch', 'Verbindet Geräte im LAN'),
|
||||
('netzwerk', 'Gateway', 'Übergang in ein anderes Netz'),
|
||||
('netzwerk', 'Firewall', 'Filtert den Netzwerkverkehr'),
|
||||
('netzwerk', 'MAC-Adresse', 'Physische Hardware-Adresse'),
|
||||
('netzwerk', 'Subnetzmaske', 'Trennt Netz- und Hostanteil'),
|
||||
|
||||
('hardware', 'CPU', 'Zentrale Recheneinheit'),
|
||||
('hardware', 'RAM', 'Flüchtiger Arbeitsspeicher'),
|
||||
('hardware', 'SSD', 'Schneller Flash-Speicher'),
|
||||
('hardware', 'GPU', 'Grafikprozessor'),
|
||||
('hardware', 'Mainboard', 'Hauptplatine des Rechners'),
|
||||
('hardware', 'BIOS', 'Startet die Hardware'),
|
||||
('hardware', 'Netzteil', 'Wandelt die Netzspannung um'),
|
||||
('hardware', 'Cache', 'Schneller Zwischenspeicher der CPU'),
|
||||
|
||||
('it-sicherheit', 'Phishing', 'Täuschung zum Datenklau'),
|
||||
('it-sicherheit', 'Verschlüsselung', 'Macht Daten unlesbar'),
|
||||
('it-sicherheit', 'Backup', 'Sicherungskopie von Daten'),
|
||||
('it-sicherheit', 'VPN', 'Verschlüsselter Tunnel'),
|
||||
('it-sicherheit', '2FA', 'Zwei-Faktor-Anmeldung'),
|
||||
('it-sicherheit', 'Malware', 'Schädliche Software'),
|
||||
('it-sicherheit', 'Patch', 'Schließt Sicherheitslücken'),
|
||||
('it-sicherheit', 'Brute Force', 'Systematisches Passwortraten'),
|
||||
|
||||
('sql-befehle', 'SELECT', 'Daten abfragen'),
|
||||
('sql-befehle', 'INSERT', 'Datensatz einfügen'),
|
||||
('sql-befehle', 'UPDATE', 'Datensatz ändern'),
|
||||
('sql-befehle', 'DELETE', 'Datensatz löschen'),
|
||||
('sql-befehle', 'WHERE', 'Zeilen filtern'),
|
||||
('sql-befehle', 'JOIN', 'Tabellen verknüpfen'),
|
||||
('sql-befehle', 'GROUP BY', 'Zeilen gruppieren'),
|
||||
('sql-befehle', 'ORDER BY', 'Ergebnis sortieren')
|
||||
) AS v(slug, term, match_text) ON v.slug = d.slug;
|
||||
@@ -0,0 +1,152 @@
|
||||
-- Weitere MindMatch-Decks aus dem Dokument "Memory-Kategorien – Fachinformatiker".
|
||||
-- 5 Themen-Decks + die Oberkategorie "Abkürzungen" als 6 eigene Decks (Deck-Modell ist flach).
|
||||
|
||||
INSERT INTO mindmatch_decks (slug, title, sort_order, active) VALUES
|
||||
('betriebssysteme', 'Betriebssysteme', 5, TRUE),
|
||||
('programmierung', 'Programmierung', 6, TRUE),
|
||||
('webentwicklung', 'Webentwicklung', 7, TRUE),
|
||||
('softwareentwicklung', 'Softwareentwicklung', 8, TRUE),
|
||||
('cloud-devops', 'Cloud & DevOps', 9, TRUE),
|
||||
('abk-netzwerk', 'Abkürzungen: Netzwerk', 10, TRUE),
|
||||
('abk-hardware', 'Abkürzungen: Hardware', 11, TRUE),
|
||||
('abk-programmierung-datenformate','Abkürzungen: Programmierung & Datenformate', 12, TRUE),
|
||||
('abk-web', 'Abkürzungen: Web', 13, TRUE),
|
||||
('abk-it-sicherheit', 'Abkürzungen: IT-Sicherheit', 14, TRUE),
|
||||
('abk-betriebssysteme-devops', 'Abkürzungen: Betriebssysteme & DevOps', 15, TRUE);
|
||||
|
||||
INSERT INTO mindmatch_pairs (deck_id, term, match_text)
|
||||
SELECT d.id, v.term, v.match_text
|
||||
FROM mindmatch_decks d
|
||||
JOIN (
|
||||
VALUES
|
||||
-- Betriebssysteme
|
||||
('betriebssysteme', 'Windows', 'Microsoft'),
|
||||
('betriebssysteme', 'Linux', 'Open Source'),
|
||||
('betriebssysteme', 'Kernel', 'Betriebssystemkern'),
|
||||
('betriebssysteme', 'Prozess', 'Laufendes Programm'),
|
||||
('betriebssysteme', 'Thread', 'Ausführungsstrang'),
|
||||
('betriebssysteme', 'Dateisystem', 'Datenorganisation'),
|
||||
('betriebssysteme', 'NTFS', 'Windows-Dateisystem'),
|
||||
('betriebssysteme', 'ext4', 'Linux-Dateisystem'),
|
||||
('betriebssysteme', 'Shell', 'Kommandozeile'),
|
||||
('betriebssysteme', 'Terminal', 'Konsolenzugriff'),
|
||||
('betriebssysteme', 'Dienst', 'Hintergrundprozess'),
|
||||
('betriebssysteme', 'Rechte', 'Zugriffskontrolle'),
|
||||
|
||||
-- Programmierung
|
||||
('programmierung', 'Variable', 'Speicherplatz'),
|
||||
('programmierung', 'Schleife', 'Wiederholung'),
|
||||
('programmierung', 'Array', 'Feld'),
|
||||
('programmierung', 'Objekt', 'Instanz'),
|
||||
('programmierung', 'Klasse', 'Bauplan'),
|
||||
('programmierung', 'Methode', 'Funktion'),
|
||||
('programmierung', 'Interface', 'Vertrag'),
|
||||
('programmierung', 'Vererbung', 'OOP-Konzept'),
|
||||
('programmierung', 'Exception', 'Fehlerbehandlung'),
|
||||
('programmierung', 'Compiler', 'Übersetzer'),
|
||||
('programmierung', 'Debugger', 'Fehlersuche'),
|
||||
('programmierung', 'Git', 'Versionsverwaltung'),
|
||||
|
||||
-- Webentwicklung
|
||||
('webentwicklung', 'HTML', 'Struktur'),
|
||||
('webentwicklung', 'CSS', 'Gestaltung'),
|
||||
('webentwicklung', 'JavaScript', 'Interaktivität'),
|
||||
('webentwicklung', 'DOM', 'Dokumentenmodell'),
|
||||
('webentwicklung', 'API', 'Programmierschnittstelle'),
|
||||
('webentwicklung', 'JSON', 'Datenaustausch'),
|
||||
('webentwicklung', 'REST', 'Architekturstil'),
|
||||
('webentwicklung', 'HTTP', 'Übertragungsprotokoll'),
|
||||
('webentwicklung', 'Cookie', 'Browserdaten'),
|
||||
('webentwicklung', 'Session', 'Benutzersitzung'),
|
||||
('webentwicklung', 'Backend', 'Serverlogik'),
|
||||
('webentwicklung', 'Frontend', 'Benutzeroberfläche'),
|
||||
|
||||
-- Softwareentwicklung
|
||||
('softwareentwicklung', 'Scrum', 'Agiles Vorgehen'),
|
||||
('softwareentwicklung', 'Sprint', 'Entwicklungszyklus'),
|
||||
('softwareentwicklung', 'Product Owner', 'Produktverantwortung'),
|
||||
('softwareentwicklung', 'Kanban', 'Aufgabenfluss'),
|
||||
('softwareentwicklung', 'UML', 'Modellierung'),
|
||||
('softwareentwicklung', 'Use Case', 'Anwendungsfall'),
|
||||
('softwareentwicklung', 'ER-Modell', 'Datenmodell'),
|
||||
('softwareentwicklung', 'Testfall', 'Prüfung'),
|
||||
('softwareentwicklung', 'CI/CD', 'Automatisierte Auslieferung'),
|
||||
('softwareentwicklung', 'Refactoring', 'Codeverbesserung'),
|
||||
('softwareentwicklung', 'Repository', 'Quellcodeablage'),
|
||||
('softwareentwicklung', 'Merge', 'Zusammenführen'),
|
||||
|
||||
-- Cloud & DevOps
|
||||
('cloud-devops', 'Docker', 'Container'),
|
||||
('cloud-devops', 'Kubernetes', 'Container-Orchestrierung'),
|
||||
('cloud-devops', 'VM', 'Virtuelle Maschine'),
|
||||
('cloud-devops', 'Cloud', 'Ressourcen aus dem Internet'),
|
||||
('cloud-devops', 'Azure', 'Microsoft Cloud'),
|
||||
('cloud-devops', 'AWS', 'Amazon Web Services'),
|
||||
('cloud-devops', 'GitHub', 'Codehosting'),
|
||||
('cloud-devops', 'Jenkins', 'CI-Server'),
|
||||
('cloud-devops', 'Pipeline', 'Automatisierung'),
|
||||
('cloud-devops', 'YAML', 'Konfigurationsdatei'),
|
||||
('cloud-devops', 'Deployment', 'Bereitstellung'),
|
||||
('cloud-devops', 'Monitoring', 'Überwachung'),
|
||||
|
||||
-- Abkürzungen: Netzwerk
|
||||
('abk-netzwerk', 'DNS', 'Domain Name System'),
|
||||
('abk-netzwerk', 'DHCP', 'Dynamic Host Configuration Protocol'),
|
||||
('abk-netzwerk', 'IP', 'Internet Protocol'),
|
||||
('abk-netzwerk', 'TCP', 'Transmission Control Protocol'),
|
||||
('abk-netzwerk', 'UDP', 'User Datagram Protocol'),
|
||||
('abk-netzwerk', 'FTP', 'File Transfer Protocol'),
|
||||
('abk-netzwerk', 'SSH', 'Secure Shell'),
|
||||
('abk-netzwerk', 'VPN', 'Virtual Private Network'),
|
||||
|
||||
-- Abkürzungen: Hardware
|
||||
('abk-hardware', 'CPU', 'Central Processing Unit'),
|
||||
('abk-hardware', 'GPU', 'Graphics Processing Unit'),
|
||||
('abk-hardware', 'RAM', 'Random Access Memory'),
|
||||
('abk-hardware', 'ROM', 'Read Only Memory'),
|
||||
('abk-hardware', 'SSD', 'Solid State Drive'),
|
||||
('abk-hardware', 'HDD', 'Hard Disk Drive'),
|
||||
('abk-hardware', 'PSU', 'Power Supply Unit'),
|
||||
('abk-hardware', 'BIOS', 'Basic Input/Output System'),
|
||||
('abk-hardware', 'UEFI', 'Unified Extensible Firmware Interface'),
|
||||
('abk-hardware', 'PCIe', 'Peripheral Component Interconnect Express'),
|
||||
|
||||
-- Abkürzungen: Programmierung & Datenformate
|
||||
('abk-programmierung-datenformate', 'SQL', 'Structured Query Language'),
|
||||
('abk-programmierung-datenformate', 'IDE', 'Integrated Development Environment'),
|
||||
('abk-programmierung-datenformate', 'SDK', 'Software Development Kit'),
|
||||
('abk-programmierung-datenformate', 'OOP', 'Object Oriented Programming'),
|
||||
('abk-programmierung-datenformate', 'CRUD', 'Create Read Update Delete'),
|
||||
('abk-programmierung-datenformate', 'MVC', 'Model View Controller'),
|
||||
('abk-programmierung-datenformate', 'ORM', 'Object Relational Mapping'),
|
||||
('abk-programmierung-datenformate', 'XML', 'Extensible Markup Language'),
|
||||
|
||||
-- Abkürzungen: Web
|
||||
('abk-web', 'URL', 'Uniform Resource Locator'),
|
||||
('abk-web', 'URI', 'Uniform Resource Identifier'),
|
||||
('abk-web', 'CDN', 'Content Delivery Network'),
|
||||
('abk-web', 'SOAP', 'Simple Object Access Protocol'),
|
||||
('abk-web', 'JWT', 'JSON Web Token'),
|
||||
('abk-web', 'CORS', 'Cross Origin Resource Sharing'),
|
||||
|
||||
-- Abkürzungen: IT-Sicherheit
|
||||
('abk-it-sicherheit', 'MFA', 'Multi Factor Authentication'),
|
||||
('abk-it-sicherheit', '2FA', 'Two Factor Authentication'),
|
||||
('abk-it-sicherheit', 'TLS', 'Transport Layer Security'),
|
||||
('abk-it-sicherheit', 'AES', 'Advanced Encryption Standard'),
|
||||
('abk-it-sicherheit', 'RSA', 'Rivest Shamir Adleman'),
|
||||
('abk-it-sicherheit', 'PKI', 'Public Key Infrastructure'),
|
||||
('abk-it-sicherheit', 'IDS', 'Intrusion Detection System'),
|
||||
('abk-it-sicherheit', 'IPS', 'Intrusion Prevention System'),
|
||||
('abk-it-sicherheit', 'SIEM', 'Security Information and Event Management'),
|
||||
('abk-it-sicherheit', 'CVE', 'Common Vulnerabilities and Exposures'),
|
||||
|
||||
-- Abkürzungen: Betriebssysteme & DevOps
|
||||
('abk-betriebssysteme-devops', 'CLI', 'Command Line Interface'),
|
||||
('abk-betriebssysteme-devops', 'GUI', 'Graphical User Interface'),
|
||||
('abk-betriebssysteme-devops', 'VM', 'Virtual Machine'),
|
||||
('abk-betriebssysteme-devops', 'CI', 'Continuous Integration'),
|
||||
('abk-betriebssysteme-devops', 'CD', 'Continuous Delivery'),
|
||||
('abk-betriebssysteme-devops', 'LTS', 'Long Term Support'),
|
||||
('abk-betriebssysteme-devops', 'FQDN', 'Fully Qualified Domain Name')
|
||||
) AS v(slug, term, match_text) ON v.slug = d.slug;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- TechTacular: IT-Quizduell im Jeopardy-Stil. Inhalte DB-basiert und Admin-pflegbar.
|
||||
-- Kategorie -> Fragen -> Antworten (genau eine korrekt). Gefüllt wird per Seeder
|
||||
-- aus den vorhandenen FinalFlux-Fragenkatalogen (siehe TechtacularSeeder).
|
||||
|
||||
CREATE TABLE techtacular_categories (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slug VARCHAR(150) NOT NULL UNIQUE,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE TABLE techtacular_questions (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
category_id BIGINT NOT NULL REFERENCES techtacular_categories (id) ON DELETE CASCADE,
|
||||
question_text VARCHAR(1000) NOT NULL,
|
||||
explanation VARCHAR(1000),
|
||||
points INT NOT NULL DEFAULT 100
|
||||
);
|
||||
|
||||
CREATE TABLE techtacular_answers (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
question_id BIGINT NOT NULL REFERENCES techtacular_questions (id) ON DELETE CASCADE,
|
||||
answer_text VARCHAR(500) NOT NULL,
|
||||
correct BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sort_order INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_techtacular_questions_cat ON techtacular_questions (category_id);
|
||||
CREATE INDEX idx_techtacular_answers_q ON techtacular_answers (question_id);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Umstellung auf das echte Jeopardy-Format (umgekehrt) mit kuratierten Inhalten.
|
||||
-- Der bisherige (templatehafte) FinalFlux-Import wird geleert; der TechtacularSeeder
|
||||
-- befüllt anschließend neu aus techtacular/content.json.
|
||||
TRUNCATE TABLE techtacular_categories RESTART IDENTITY CASCADE;
|
||||
@@ -0,0 +1,165 @@
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(145deg, #1f1f2e, #2c2c3d);
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.achievements-container {
|
||||
max-width: 1400px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: #00e5ff;
|
||||
text-shadow: 0 0 5px rgba(0, 229, 255, 0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin-bottom: 2rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
/* === Zurück-Button === */
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: #00d9ff;
|
||||
color: #0d1117;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #00aacc;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Responsives Verhalten */
|
||||
@media (max-width: 768px) {
|
||||
.back-button {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.achievements-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.achievement-card {
|
||||
background-color: rgba(10, 15, 25, 0.95);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
box-shadow:
|
||||
0 0 15px rgba(0, 229, 255, 0.07),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.achievement-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 20px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
|
||||
.trophy-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.achievement-card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.achievement-card p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
font-weight: bold;
|
||||
color: #00e5ff;
|
||||
}
|
||||
|
||||
.locked {
|
||||
opacity: 0.6;
|
||||
filter: grayscale(70%);
|
||||
}
|
||||
|
||||
/* === Unlock-Effekt für freigeschaltete Achievements === */
|
||||
.achievement-card.unlocked {
|
||||
animation: unlock-glow 1s ease-out;
|
||||
}
|
||||
|
||||
/* Animation für Unlock-Glow */
|
||||
@keyframes unlock-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 0 rgba(0, 229, 255, 0);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(0, 229, 255, 0.7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 10px rgba(0, 229, 255, 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sanftes Pulsieren beim Hover über freigeschaltete Achievements */
|
||||
.achievement-card.unlocked:hover {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 10px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 10px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
}
|
||||
/* === Footer === */
|
||||
footer {
|
||||
background-color: #111122;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #66d9ef;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
@@ -0,0 +1,143 @@
|
||||
/* Auth (Login/Signup) – kombiniertes Dual-Panel mit Neon-Buttons.
|
||||
Bewusst als statische CSS-Datei: enthält @property mit '<angle>',
|
||||
was im Thymeleaf-Inline-Style als HTML-Tag fehlinterpretiert würde. */
|
||||
|
||||
/* Rotierender Winkel für den Conic-Gradient-Glow (Bild 1) */
|
||||
@property --mf-ang { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
|
||||
|
||||
.auth-stage { width:100%; display:flex; flex-direction:column; align-items:center; gap:1.4rem; }
|
||||
|
||||
/* ---- Dual-Panel-Container (Bild 2) ---- */
|
||||
.auth-container {
|
||||
position:relative; width:100%; max-width:840px; min-height:560px;
|
||||
background:var(--surface); border:1px solid var(--border-accent);
|
||||
border-radius:22px; box-shadow:0 0 44px var(--glow-w); overflow:hidden;
|
||||
}
|
||||
.form-box {
|
||||
position:absolute; top:0; height:100%; width:50%;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
padding:2.4rem 2.6rem; transition:transform .65s ease-in-out, opacity .65s ease-in-out;
|
||||
}
|
||||
.form-box .inner { width:100%; max-width:320px; }
|
||||
.form-box.login { left:0; z-index:2; }
|
||||
.form-box.signup { left:0; opacity:0; z-index:1; }
|
||||
|
||||
.auth-container.active .form-box.login { transform:translateX(100%); opacity:0; z-index:1; }
|
||||
.auth-container.active .form-box.signup { transform:translateX(100%); opacity:1; z-index:5; }
|
||||
|
||||
/* ---- Überschiebendes Gradient-Overlay ---- */
|
||||
.overlay-container {
|
||||
position:absolute; top:0; left:50%; width:50%; height:100%;
|
||||
overflow:hidden; z-index:100; transition:transform .65s ease-in-out;
|
||||
box-shadow: -1px 0 0 var(--border-accent), -26px 0 55px -30px var(--glow-s);
|
||||
}
|
||||
.auth-container.active .overlay-container { transform:translateX(-100%); }
|
||||
.overlay {
|
||||
position:relative; left:-100%; width:200%; height:100%; transform:translateX(0);
|
||||
transition:transform .65s ease-in-out;
|
||||
background:
|
||||
radial-gradient(540px 380px at 70% 22%, var(--glow-s), transparent 60%),
|
||||
radial-gradient(420px 300px at 20% 90%, var(--glow-w), transparent 65%),
|
||||
linear-gradient(150deg, var(--surface-2), var(--surface));
|
||||
}
|
||||
.auth-container.active .overlay { transform:translateX(50%); }
|
||||
.overlay-panel {
|
||||
position:absolute; top:0; height:100%; width:50%;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
text-align:center; padding:0 2.4rem; color:var(--text);
|
||||
transition:transform .65s ease-in-out;
|
||||
}
|
||||
.overlay-left { transform:translateX(-18%); }
|
||||
.auth-container.active .overlay-left { transform:translateX(0); }
|
||||
.overlay-right { right:0; transform:translateX(0); }
|
||||
.auth-container.active .overlay-right { transform:translateX(18%); }
|
||||
|
||||
.overlay-panel .o-emblem {
|
||||
width:56px; height:56px; background:var(--accent); margin-bottom:1rem;
|
||||
-webkit-mask:url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
mask:url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
filter:drop-shadow(0 0 12px var(--glow-s));
|
||||
}
|
||||
.overlay-panel h3 { color:var(--heading); font-size:1.5rem; font-weight:800; margin-bottom:.6rem; letter-spacing:.01em; text-shadow:0 0 18px var(--glow-w); }
|
||||
.overlay-panel p { color:var(--text); font-size:.9rem; line-height:1.65; margin-bottom:1.6rem; max-width:280px; }
|
||||
|
||||
/* ---- Formularkopf ---- */
|
||||
.auth-head { text-align:center; margin-bottom:1.4rem; }
|
||||
.auth-head .mark {
|
||||
width:52px; height:52px; margin:0 auto .55rem; background:var(--accent);
|
||||
-webkit-mask:url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
mask:url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
filter:drop-shadow(0 0 12px var(--glow-s));
|
||||
}
|
||||
.auth-head b { display:block; color:var(--accent); font-weight:800; letter-spacing:.16em; font-size:1.05rem; text-shadow:0 0 14px var(--glow-s); }
|
||||
.auth-head h2 { color:var(--heading); font-size:1.35rem; font-weight:800; margin-top:.5rem; }
|
||||
.auth-head .sub { color:var(--muted); font-size:.82rem; margin-top:.2rem; }
|
||||
|
||||
.mf-alert { padding:.6rem .8rem; border-radius:10px; font-size:.82rem; margin-bottom:.9rem; text-align:center; }
|
||||
.mf-alert.err { color:var(--danger); border:1px solid var(--danger); background:rgba(255,95,107,.08); }
|
||||
.mf-alert.ok { color:var(--accent); border:1px solid var(--border-accent); background:var(--surface-2); }
|
||||
|
||||
/* ---- Neon-Button mit Hover-Expand + rotierendem Conic-Glow (Bild 1) ---- */
|
||||
.neon-btn {
|
||||
position:relative; z-index:0; width:100%; margin-top:.4rem;
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:.55rem;
|
||||
padding:.85rem 1.3rem; border:0; border-radius:999px; cursor:pointer;
|
||||
font-weight:800; font-size:.82rem; letter-spacing:.08em; text-transform:uppercase;
|
||||
color:var(--accent-ink); background:var(--accent); box-shadow:0 0 18px var(--glow-s);
|
||||
transition:transform .3s ease, letter-spacing .3s ease, box-shadow .3s ease;
|
||||
}
|
||||
.neon-btn::before {
|
||||
content:""; position:absolute; inset:-3px; border-radius:inherit; z-index:-1;
|
||||
background:conic-gradient(from var(--mf-ang),
|
||||
var(--accent), var(--accent-2), var(--accent), var(--accent-2), var(--accent));
|
||||
filter:blur(9px); opacity:0; transition:opacity .35s ease;
|
||||
animation:mf-spin 4s linear infinite;
|
||||
}
|
||||
.neon-btn:hover { transform:translateY(-2px); letter-spacing:.16em; box-shadow:0 0 28px var(--glow-s); }
|
||||
.neon-btn:hover::before { opacity:.9; }
|
||||
.neon-btn:focus-visible { outline:2px solid var(--accent-2); outline-offset:3px; }
|
||||
.neon-btn .lock { font-size:.95em; }
|
||||
@keyframes mf-spin { to { --mf-ang:360deg; } }
|
||||
|
||||
/* Outline-Variante für die Overlay-Buttons (auf Gradient) */
|
||||
.ghost-btn {
|
||||
display:inline-flex; align-items:center; justify-content:center; gap:.5rem;
|
||||
padding:.75rem 1.8rem; border-radius:999px; cursor:pointer; font-weight:800;
|
||||
font-size:.8rem; letter-spacing:.1em; text-transform:uppercase;
|
||||
background:transparent; color:var(--accent); border:2px solid var(--border-accent);
|
||||
box-shadow:0 0 16px var(--glow-w);
|
||||
transition:transform .25s ease, background .25s ease, box-shadow .25s ease;
|
||||
}
|
||||
.ghost-btn:hover { transform:translateY(-2px); background:var(--surface-2); box-shadow:0 0 22px var(--glow-s); }
|
||||
|
||||
/* ---- Light-Mode: Overlay als kräftiges Orange-Panel mit weißem Text ----
|
||||
(im Dark-Mode ist das Overlay ein dunkles Panel mit Glow; im Light-Mode
|
||||
wären die Surface-Töne weiß -> Panel verschwände. Daher hier farbig.) */
|
||||
[data-theme="light"] .overlay {
|
||||
background:
|
||||
radial-gradient(600px 340px at 28% 6%, rgba(255,255,255,.18), transparent 60%),
|
||||
linear-gradient(150deg, var(--accent-2), var(--accent) 48%);
|
||||
}
|
||||
[data-theme="light"] .overlay-panel { color:#fff; }
|
||||
[data-theme="light"] .overlay-panel h3 { color:#fff; text-shadow:0 1px 12px rgba(0,0,0,.22); }
|
||||
[data-theme="light"] .overlay-panel p { color:#fff; opacity:.96; }
|
||||
[data-theme="light"] .overlay-panel .o-emblem { background:#fff; filter:drop-shadow(0 0 10px rgba(0,0,0,.25)); }
|
||||
[data-theme="light"] .ghost-btn { color:#fff; border-color:rgba(255,255,255,.9); box-shadow:none; }
|
||||
[data-theme="light"] .ghost-btn:hover { background:rgba(255,255,255,.18); box-shadow:none; }
|
||||
|
||||
.mobile-switch { display:none; text-align:center; margin-top:1.2rem; color:var(--muted); font-size:.84rem; }
|
||||
.mobile-switch a { color:var(--accent); font-weight:700; text-decoration:none; cursor:pointer; }
|
||||
|
||||
/* ---- Responsiv: gestapelt, ohne Overlay ---- */
|
||||
@media (max-width:760px) {
|
||||
.auth-container { max-width:420px; min-height:auto; }
|
||||
.overlay-container { display:none; }
|
||||
.form-box {
|
||||
position:relative; width:100%; padding:2rem 1.6rem;
|
||||
transform:none !important; opacity:1 !important; transition:none;
|
||||
}
|
||||
.form-box.signup { display:none; }
|
||||
.auth-container.active .form-box.login { display:none; }
|
||||
.auth-container.active .form-box.signup { display:flex; }
|
||||
.mobile-switch { display:block; }
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
body.finalflux-shell,
|
||||
body.finalflux-page {
|
||||
background: var(--bg-glow), var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.finalflux-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.finalflux-sidebar {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border-accent);
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.finalflux-sidebar h1,
|
||||
.finalflux-main h2,
|
||||
.finalflux-main h3,
|
||||
.finalflux-page h1,
|
||||
.finalflux-page h2,
|
||||
.finalflux-page h3 {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.finalflux-sidebar p,
|
||||
.finalflux-main p,
|
||||
.finalflux-page p,
|
||||
.finalflux-page li {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.finalflux-back,
|
||||
.finalflux-page .back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.65rem 1rem;
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.finalflux-nav section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.finalflux-nav h2 {
|
||||
color: var(--accent-2);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.finalflux-nav a {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
padding: 0.42rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.finalflux-nav a:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.finalflux-main {
|
||||
/* display/width/margin explizit, um das generische `main {}` aus style.css
|
||||
(display:flex; width:90%; margin:0 auto) zu neutralisieren -> sonst
|
||||
Überlappung/Fehllayout im Hub. */
|
||||
display: block;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.finalflux-hero,
|
||||
.finalflux-panel,
|
||||
.finalflux-page section {
|
||||
background: var(--surface);
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.finalflux-page .merksatz {
|
||||
margin: 1rem 0 0;
|
||||
padding: 0.85rem 1rem;
|
||||
background: var(--surface-2);
|
||||
border-left: 4px solid var(--accent-2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.finalflux-page .merksatz strong {
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
/* ---- Topologie-Diagramme (Inline-SVG, theme-adaptiv) ---- */
|
||||
.topo-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin: 1.1rem 0 0.2rem;
|
||||
}
|
||||
.topo-figure {
|
||||
flex: 1 1 240px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 0.9rem 0.9rem 0.7rem;
|
||||
}
|
||||
.topo-figure svg { width: 100%; max-width: 320px; height: auto; display: block; margin: 0 auto; }
|
||||
.topo-figure figcaption { color: var(--muted); font-size: 0.82rem; margin-top: 0.5rem; line-height: 1.4; }
|
||||
.topo .link { stroke: var(--border-accent); stroke-width: 2; fill: none; }
|
||||
.topo .bus { stroke: var(--accent); stroke-width: 4; stroke-linecap: round; }
|
||||
.topo .node { fill: var(--surface); stroke: var(--accent); stroke-width: 2; }
|
||||
.topo .hub { fill: var(--accent); stroke: none; }
|
||||
.topo .term { fill: var(--muted); }
|
||||
.topo .hlbl { fill: var(--accent-ink); font: 700 9px 'Segoe UI', Tahoma, sans-serif; text-anchor: middle; dominant-baseline: central; }
|
||||
|
||||
.finalflux-page table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.finalflux-page th,
|
||||
.finalflux-page td {
|
||||
text-align: left;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.finalflux-page th {
|
||||
color: var(--heading);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.finalflux-hero h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0.4rem 0 1rem;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--accent-2);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.finalflux-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.finalflux-grid article,
|
||||
.fluxcheck {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.finalflux-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.finalflux-actions a,
|
||||
.fluxcheck button {
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 0.65rem 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.finalflux-page {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.finalflux-page main {
|
||||
display: block;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.finalflux-page code {
|
||||
background: var(--surface-2);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-2);
|
||||
padding: 0.15rem 0.35rem;
|
||||
}
|
||||
|
||||
.key-list {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.fluxcheck label {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.fluxcheck-result {
|
||||
font-weight: 700;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.fluxcheck-result.correct {
|
||||
color: var(--success);
|
||||
}
|
||||
.fluxcheck-result.correct::before { content: "\2713 "; font-weight: 800; }
|
||||
|
||||
.fluxcheck-result.wrong {
|
||||
color: var(--danger);
|
||||
}
|
||||
.fluxcheck-result.wrong::before { content: "\2717 "; font-weight: 800; }
|
||||
|
||||
.finalflux-test {
|
||||
background: var(--surface);
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.test-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
justify-content: space-between;
|
||||
color: var(--accent-2);
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.category-block {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.category-block h2 {
|
||||
margin: 0.2rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
background: var(--bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.answer-options {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.answer-options label {
|
||||
align-items: flex-start;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.answer-options label:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.test-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.test-actions button,
|
||||
[data-restart-test] {
|
||||
background: var(--accent);
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
color: var(--accent-ink);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 0.65rem 1rem;
|
||||
}
|
||||
|
||||
.test-actions button.secondary {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.test-actions button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
/* Block statt Grid -> Sidebar (Themen-Menü) und Inhalt stapeln sauber,
|
||||
keine Überlappung, volle Höhe scrollbar. */
|
||||
.finalflux-shell {
|
||||
display: block;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.finalflux-sidebar {
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--border-accent);
|
||||
}
|
||||
|
||||
.finalflux-main,
|
||||
.finalflux-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
/* === MindMatch === Farben über zentrale Theme-Tokens (theme.css):
|
||||
Dark = Grün-Neon, Light = heller Grund + Magenta. Layout: feste Viewport-Höhe,
|
||||
kein Scrollen – das Spielfeld füllt den verbleibenden Platz und skaliert die Karten. */
|
||||
|
||||
/* Overlay-Einblendung (kam früher aus style.css). */
|
||||
@keyframes fadeInUp {
|
||||
0% { opacity: 0; transform: translateY(20px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* body bekommt Layout (Shell füllt 100vh über flex:1). Farben/Hintergrund liefert theme.css. */
|
||||
body.mm-body {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden; /* Seite scrollt nie */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mm-shell {
|
||||
flex: 1; /* body ist flex-column -> Shell füllt 100vh */
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.6rem 0.85rem;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
/* --- Kopfzeile (kompakt) --- */
|
||||
.mm-header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1.1rem;
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--g-soft), 0 0 8px var(--glow-w);
|
||||
}
|
||||
|
||||
.mm-header-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.mm-title {
|
||||
font-size: 1.3rem;
|
||||
color: var(--accent);
|
||||
margin: 0;
|
||||
text-shadow: 0 0 8px var(--glow-s);
|
||||
}
|
||||
|
||||
.mm-back, .mm-logout { text-decoration: none; font-weight: bold; }
|
||||
.mm-back { color: var(--accent); }
|
||||
.mm-logout { color: var(--danger); }
|
||||
|
||||
/* --- Leiste: Steuerung + Werte --- */
|
||||
.mm-bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem 0.85rem;
|
||||
}
|
||||
|
||||
.mm-bar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mm-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mm-field select {
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-accent);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.mm-btn {
|
||||
padding: 0.5rem 1.1rem;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 14px var(--glow-w);
|
||||
transition: filter 0.25s ease, transform 0.15s ease, background-color 0.25s ease;
|
||||
}
|
||||
|
||||
.mm-btn:hover { filter: brightness(1.08); transform: translateY(-2px); }
|
||||
|
||||
.mm-btn-secondary {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-accent);
|
||||
color: var(--text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Abbrechen-Button (rötlich, klar als Abbruch erkennbar) */
|
||||
.mm-btn-cancel {
|
||||
background: rgba(255, 107, 107, 0.14);
|
||||
border: 1px solid rgba(255, 107, 107, 0.55);
|
||||
color: var(--danger);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mm-btn-cancel:hover {
|
||||
background: rgba(255, 107, 107, 0.28);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Damit das hidden-Attribut den display:inline-block der .mm-btn überstimmt */
|
||||
.mm-btn[hidden] { display: none; }
|
||||
|
||||
/* --- HUD als kompakte Chips --- */
|
||||
.mm-hud { gap: 0.4rem; }
|
||||
|
||||
.mm-stat {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mm-stat b { font-size: 1.05rem; color: var(--accent); }
|
||||
|
||||
/* --- Spielfeld: füllt den Rest, kein Scrollen --- */
|
||||
.mm-stage {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mm-board {
|
||||
display: grid;
|
||||
gap: clamp(0.3rem, 1.2vmin, 0.75rem);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0 auto;
|
||||
/* grid-template-columns + max-width werden per JS gesetzt;
|
||||
gleich hohe Reihen über grid-auto-rows, damit alles in die Höhe passt. */
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
.mm-idle {
|
||||
margin: auto;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* --- Karte (füllt ihre Zelle) --- */
|
||||
.mm-card {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
perspective: 900px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mm-card-inner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.45s ease;
|
||||
}
|
||||
|
||||
.mm-card.flipped .mm-card-inner,
|
||||
.mm-card.matched .mm-card-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.mm-card-face {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0.4rem;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Rückseite (verdeckt): leuchtendes Mindflux-Emblem statt ⚡ */
|
||||
.mm-card-front {
|
||||
background: linear-gradient(145deg, var(--surface), var(--surface-2));
|
||||
border: 1px solid var(--border-accent);
|
||||
box-shadow: inset 0 0 18px var(--glow-w);
|
||||
font-size: 0; /* versteckt das alte ⚡-Zeichen */
|
||||
}
|
||||
.mm-card-front::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 56%;
|
||||
height: 56%;
|
||||
background: var(--accent);
|
||||
-webkit-mask: url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
mask: url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
filter: drop-shadow(0 0 9px var(--glow-s));
|
||||
}
|
||||
|
||||
/* Vorderseite (Inhalt) */
|
||||
.mm-card-back {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-accent);
|
||||
transform: rotateY(180deg);
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.mm-card-back.mm-term {
|
||||
color: var(--accent);
|
||||
font-size: clamp(0.72rem, 1.7vw, 1.15rem);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mm-card-back.mm-def {
|
||||
font-size: clamp(0.62rem, 1.25vw, 0.9rem);
|
||||
}
|
||||
|
||||
.mm-kind {
|
||||
font-size: clamp(0.5rem, 1vw, 0.65rem);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.mm-text {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.mm-card:not(.matched):hover .mm-card-front {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 14px var(--glow-s), inset 0 0 18px var(--glow-w);
|
||||
}
|
||||
|
||||
.mm-card.matched { cursor: default; }
|
||||
|
||||
.mm-card.matched .mm-card-back {
|
||||
border-color: var(--success);
|
||||
box-shadow: 0 0 14px var(--success);
|
||||
animation: mm-pop 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes mm-pop {
|
||||
0% { transform: rotateY(180deg) scale(1); }
|
||||
50% { transform: rotateY(180deg) scale(1.06); }
|
||||
100% { transform: rotateY(180deg) scale(1); }
|
||||
}
|
||||
|
||||
.mm-card.wrong .mm-card-back {
|
||||
border-color: var(--danger);
|
||||
box-shadow: 0 0 14px var(--danger);
|
||||
}
|
||||
|
||||
/* --- Ergebnis-Overlay --- */
|
||||
.mm-result {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-glow), rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mm-result[hidden] { display: none; }
|
||||
|
||||
.mm-result-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 18px;
|
||||
padding: 2rem 2.5rem;
|
||||
text-align: center;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
box-shadow: 0 0 30px var(--glow-w);
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.mm-result-card h2 { color: var(--accent); margin-bottom: 0.5rem; }
|
||||
.mm-stars { font-size: 1.8rem; letter-spacing: 0.2rem; margin-bottom: 0.5rem; color: var(--accent); }
|
||||
.mm-result-score { font-size: 1.3rem; margin-bottom: 0.3rem; color: var(--text); }
|
||||
.mm-result-score strong { color: var(--accent); }
|
||||
.mm-result-detail { color: var(--muted); font-size: 0.95rem; margin-bottom: 0.6rem; }
|
||||
.mm-result-highscore { color: var(--accent); font-weight: 800; margin-bottom: 1rem; }
|
||||
.mm-result-highscore[hidden] { display: none; }
|
||||
|
||||
.mm-result-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* --- Mobil --- */
|
||||
@media (max-width: 600px) {
|
||||
.mm-header { padding: 0.45rem 0.8rem; }
|
||||
.mm-title { font-size: 1.15rem; }
|
||||
.mm-field select { min-width: 150px; }
|
||||
.mm-stat { font-size: 0.65rem; padding: 0.3rem 0.55rem; }
|
||||
.mm-stat b { font-size: 0.95rem; }
|
||||
}
|
||||
|
||||
/* Auf Touch/Handy darf gescrollt werden: das "alles passt ohne Scrollen"-Layout
|
||||
bricht bei 100vh (iOS-Adressleiste) und vielen Karten -> Brett ragt raus.
|
||||
Hier: Seite scrollbar, Karten bekommen feste Proportionen (aspect-ratio). */
|
||||
@media (max-width: 820px) {
|
||||
body.mm-body { height: auto; min-height: 100vh; min-height: 100dvh; overflow-y: auto; }
|
||||
.mm-stage { overflow: visible; }
|
||||
.mm-board { height: auto; grid-auto-rows: auto; }
|
||||
.mm-card { aspect-ratio: 3 / 4; }
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
|
||||
/* === Zurück-Button === */
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: #00d9ff;
|
||||
color: #0d1117;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #00aacc;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Responsives Verhalten */
|
||||
@media (max-width: 768px) {
|
||||
.back-button {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(145deg, #1f1f2e, #2c2c3d);
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
max-width: 1400px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #00e5ff;
|
||||
text-shadow: 0 0 5px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
|
||||
/* === Gesamtfortschritt === */
|
||||
.section-overall-progress {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.progress-summary {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 2rem;
|
||||
background: rgba(0, 9, 22, 0.95);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 0 15px rgba(0, 229, 255, 0.05),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.circular-progress {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.percentage-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* XP-Leiste */
|
||||
.xp-bar {
|
||||
background-color: #1a1a2e;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.xp-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00e5ff, #4cc3dd);
|
||||
}
|
||||
|
||||
/* === Lernzeitbereich === */
|
||||
.section-learn-time {
|
||||
background: rgba(0, 9, 22, 0.9);
|
||||
padding: 1.5rem;
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 0 10px rgba(0, 229, 255, 0.05),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#learnTimeChart {
|
||||
max-width: 100%;
|
||||
height: 180px !important;
|
||||
}
|
||||
|
||||
/* === Spielkarten === */
|
||||
.section-game-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.game-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.game-card {
|
||||
background-color: rgba(10, 15, 25, 0.95);
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 0 15px rgba(0, 229, 255, 0.07),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.game-card:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.game-progress-bar {
|
||||
background-color: #1a1a2e;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
height: 10px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.game-progress-bar .fill {
|
||||
height: 100%;
|
||||
background-color: #00e5ff;
|
||||
}
|
||||
|
||||
/* === Footer === */
|
||||
footer {
|
||||
background-color: #111122;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #66d9ef;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(145deg, #1f1f2e, #2c2c3d);
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
position: relative; /* wichtig für .back-button */
|
||||
text-align: center;
|
||||
padding: 2rem 1rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: #00e5ff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
header .slogan {
|
||||
font-size: 1.2rem;
|
||||
color: #ccc;
|
||||
text-shadow: 0 0 5px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
|
||||
/* === Zurück-Button === */
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: #00d9ff;
|
||||
color: #0d1117;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #00aacc;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Responsives Verhalten */
|
||||
@media (max-width: 768px) {
|
||||
.back-button {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.back-button {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 1000px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: rgba(0, 9, 22, 0.95);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 0 15px rgba(0, 229, 255, 0.05),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: rgba(10, 15, 25, 0.95);
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 10px rgba(0, 229, 255, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.section:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 20px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: #00e5ff;
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: #888;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #00e5ff;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.settings-form input {
|
||||
padding: 0.8rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: #1a1a2e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.settings-form button {
|
||||
padding: 0.8rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #00e5ff;
|
||||
color: #00172e;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-form button:hover {
|
||||
background-color: #4cc3dd;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.danger-zone button {
|
||||
background-color: #ff4d4d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.danger-zone button:hover {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
|
||||
/* === Footer === */
|
||||
footer {
|
||||
background-color: #111122;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #66d9ef;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
body {
|
||||
background-color: #0d1117;
|
||||
color: #00d9ff;
|
||||
font-family: 'Poppins', sans-serif; /* oder deine Mindflux-Schrift */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 40px;
|
||||
border: 1px solid #00d9ff; /* feiner als dashed */
|
||||
border-radius: 20px;
|
||||
background-color: #121826;
|
||||
box-shadow: 0 0 15px rgba(0, 217, 255, 0.5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.time-machine {
|
||||
font-size: 2.5rem;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.small-hint {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 20px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: #00d9ff;
|
||||
color: #0d1117;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #00aacc;
|
||||
transform: scale(1.05); /* leichter "Hover-Zoom" */
|
||||
}
|
||||
|
||||
/* Responsives Verhalten */
|
||||
@media (max-width: 768px) {
|
||||
.back-button {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.back-button {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
/* === Reset === */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* === Animationen === */
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
/* Glanz-Effekt für Hover */
|
||||
@keyframes shine {
|
||||
0% {
|
||||
background-position: -100%;
|
||||
}
|
||||
100% {
|
||||
background-position: 200%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade-In für Container beim Laden */
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* === Body Hintergrund === */
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(145deg, var(--surface), var(--surface-2));
|
||||
color: var(--heading);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* === Header === */
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
margin: 2rem auto 1rem;
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
background-color: var(--panel);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 8px 20px var(--glow-w),
|
||||
0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
transition: all 0.3s ease-in-out;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
header:hover {
|
||||
box-shadow:
|
||||
0 10px 30px var(--glow-s),
|
||||
0 6px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-height: 350px;
|
||||
width: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header img {
|
||||
max-height: 80px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header p.slogan {
|
||||
font-style: italic;
|
||||
color: var(--accent);
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 0 5px var(--glow-s);
|
||||
}
|
||||
|
||||
/* === Main Bereich === */
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
/* === Info-Textfeld === */
|
||||
.info-box {
|
||||
flex: 1 1 400px;
|
||||
max-width: 600px;
|
||||
background: rgba(0, 9, 22, 0.9);
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 0 15px var(--glow-w),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fadeInUp 0.9s ease-out;
|
||||
}
|
||||
|
||||
/* === Login/Register Box === */
|
||||
.login-box {
|
||||
flex: 1 1 300px;
|
||||
max-width: 400px;
|
||||
background: rgba(10, 15, 25, 0.95);
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 0 15px var(--glow-w),
|
||||
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fadeInUp 1s ease-out;
|
||||
}
|
||||
|
||||
.login-box h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
.login-box form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-box input,
|
||||
.login-box button {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* === Input === */
|
||||
.login-box input {
|
||||
background-color: var(--heading);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* === Button mit Glanz- und Licht-Effekt === */
|
||||
.login-box button {
|
||||
background-color: var(--accent);
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: background-color 0.3s ease-in-out, transform 0.2s ease;
|
||||
box-shadow: 0 0 10px var(--glow-s);
|
||||
}
|
||||
|
||||
.login-box button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -75%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0.5) 50%,
|
||||
rgba(255, 255, 255, 0.2) 100%
|
||||
);
|
||||
transform: skewX(-20deg);
|
||||
animation: shine 2s infinite;
|
||||
}
|
||||
|
||||
.login-box button:hover {
|
||||
background-color: var(--accent-2);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 15px var(--glow-s), 0 0 30px var(--glow-w) inset;
|
||||
}
|
||||
|
||||
/* === Footer === */
|
||||
footer {
|
||||
background-color: var(--panel);
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--accent-2);
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Kleinere Switch-Größe */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 42px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: var(--muted);
|
||||
transition: 0.4s;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 0 6px var(--glow-s);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
font-size: 0.8rem;
|
||||
color: silver;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: var(--heading);
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Bei aktiviertem Switch */
|
||||
input:checked + .slider {
|
||||
background-color: var(--accent);
|
||||
color: var(--heading);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dashboard-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 9, 22, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
box-shadow:
|
||||
0 0 10px var(--glow-w),
|
||||
0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.dashboard-tile:hover {
|
||||
background-color: var(--glow-w);
|
||||
transform: translateY(-3px);
|
||||
box-shadow:
|
||||
0 6px 16px var(--glow-s),
|
||||
0 0 20px var(--glow-w) inset;
|
||||
}
|
||||
|
||||
.dashboard-tile span {
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 5px var(--glow-s);
|
||||
}
|
||||
|
||||
/* === Profilbereich === */
|
||||
.profile-container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
background: rgba(0, 9, 22, 0.95);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 20px var(--glow-w);
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.profile-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.profile-form label {
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.profile-form input {
|
||||
padding: 0.75rem;
|
||||
background-color: var(--surface-2);
|
||||
color: var(--heading);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.btn.save {
|
||||
background-color: var(--accent);
|
||||
color: var(--surface-2);
|
||||
}
|
||||
|
||||
.btn.save:hover {
|
||||
background-color: var(--accent-2);
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background-color: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
|
||||
/* Profilbild */
|
||||
.profile-picture img {
|
||||
max-height: 150px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
/* === Tab-Navigation oben === */
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin: 1rem auto 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-nav .tab {
|
||||
padding: 0.6rem 1.2rem;
|
||||
background-color: var(--glow-w);
|
||||
color: var(--accent);
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease-in-out;
|
||||
box-shadow: 0 2px 6px var(--glow-w);
|
||||
}
|
||||
|
||||
.tab-nav .tab:hover {
|
||||
background-color: var(--glow-s);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px var(--glow-s);
|
||||
}
|
||||
|
||||
.tab-nav .tab.active {
|
||||
background-color: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
padding: 1rem;
|
||||
margin: 1rem auto;
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 0 10px var(--glow-s);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
padding: 1rem;
|
||||
margin: 1rem auto;
|
||||
max-width: 600px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 0 10px var(--glow-s);
|
||||
opacity: 1;
|
||||
transition: opacity 1.5s ease-out;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 10px var(--glow-s);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 10px var(--glow-s);
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
font-size: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: var(--accent-2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.back-button {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
/* Admin-Button Stil */
|
||||
.admin-btn {
|
||||
background-color: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s, box-shadow 0.3s, transform 0.3s;
|
||||
box-shadow: 0 0 10px var(--glow-s);
|
||||
}
|
||||
|
||||
.admin-btn:hover {
|
||||
background-color: var(--accent-2);
|
||||
box-shadow: 0 0 15px var(--glow-s), 0 0 25px var(--glow-s);
|
||||
transform: scale(1.05);
|
||||
color: var(--surface-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
.admin-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-card {
|
||||
display: block;
|
||||
background-color: var(--glow-w);
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px var(--glow-w);
|
||||
}
|
||||
|
||||
.admin-card:hover {
|
||||
background-color: var(--glow-s);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px var(--glow-w);
|
||||
}
|
||||
|
||||
.admin-card.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
.admin-actions {
|
||||
text-align: left;
|
||||
margin: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.btn-create {
|
||||
display: inline-block;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-size: 1rem;
|
||||
background-color: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 10px var(--glow-s);
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-create:hover {
|
||||
background-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/* === TechTacular === Jeopardy-Board, Farben über zentrale Theme-Tokens (theme.css):
|
||||
Dark = Grün-Neon, Light = heller Grund + Magenta. Feste Viewport-Höhe (kein Scrollen). */
|
||||
|
||||
/* Overlay-Einblendung (kam früher aus style.css). */
|
||||
@keyframes fadeInUp {
|
||||
0% { opacity: 0; transform: translateY(20px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* body bekommt Layout (Shell füllt 100vh über flex:1). Farben/Hintergrund liefert theme.css. */
|
||||
body.tt-body {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tt-shell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.6rem 0.85rem;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
/* --- Kopf + Leiste --- */
|
||||
.tt-header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1.1rem;
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--g-soft), 0 0 8px var(--glow-w);
|
||||
}
|
||||
|
||||
.tt-header-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.tt-title { font-size: 1.3rem; color: var(--accent); margin: 0; text-shadow: 0 0 8px var(--glow-s); }
|
||||
.tt-back, .tt-logout { text-decoration: none; font-weight: bold; }
|
||||
.tt-back { color: var(--accent); }
|
||||
.tt-logout { color: var(--danger); }
|
||||
|
||||
.tt-bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem 0.85rem;
|
||||
}
|
||||
|
||||
.tt-bar-group { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
|
||||
|
||||
.tt-btn {
|
||||
padding: 0.5rem 1.1rem;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 14px var(--glow-w);
|
||||
transition: filter 0.25s ease, transform 0.15s ease, background-color 0.25s ease;
|
||||
}
|
||||
.tt-btn:hover { filter: brightness(1.08); transform: translateY(-2px); }
|
||||
.tt-btn[hidden] { display: none; }
|
||||
|
||||
.tt-btn-secondary {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-accent);
|
||||
color: var(--text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tt-btn-cancel {
|
||||
background: rgba(255, 107, 107, 0.14);
|
||||
border: 1px solid rgba(255, 107, 107, 0.55);
|
||||
color: var(--danger);
|
||||
box-shadow: none;
|
||||
}
|
||||
.tt-btn-cancel:hover { background: rgba(255, 107, 107, 0.28); color: #fff; }
|
||||
|
||||
.tt-hud { gap: 0.4rem; }
|
||||
.tt-stat {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tt-stat b { font-size: 1.05rem; color: var(--accent); }
|
||||
|
||||
/* --- Board --- */
|
||||
.tt-stage {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tt-board {
|
||||
display: grid;
|
||||
gap: clamp(0.3rem, 1vmin, 0.7rem);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
/* grid-template-columns wird per JS gesetzt (eine Spalte je Kategorie) */
|
||||
}
|
||||
|
||||
.tt-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(0.3rem, 1vmin, 0.7rem);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tt-cat-title {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0.4rem;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 8px;
|
||||
color: var(--accent);
|
||||
font-weight: 800;
|
||||
font-size: clamp(0.7rem, 1.1vw, 0.95rem);
|
||||
line-height: 1.15;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.tt-tile {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(145deg, var(--surface), var(--surface-2));
|
||||
color: var(--accent);
|
||||
font-weight: 800;
|
||||
font-size: clamp(1rem, 2.2vw, 1.8rem);
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tt-tile:hover:not(.answered) {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 14px var(--glow-s), inset 0 0 18px var(--glow-w);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tt-tile.answered {
|
||||
cursor: default;
|
||||
font-size: clamp(1.2rem, 3vw, 2rem);
|
||||
}
|
||||
.tt-tile.answered.correct { border-color: var(--success); color: var(--success); background: color-mix(in srgb, var(--success) 10%, transparent); }
|
||||
.tt-tile.answered.wrong { border-color: var(--danger); color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
|
||||
|
||||
/* --- Overlays --- */
|
||||
.tt-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-glow), rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
.tt-overlay[hidden] { display: none; }
|
||||
|
||||
.tt-q-card, .tt-result-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem 1.8rem;
|
||||
width: min(640px, 94vw);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 0 30px var(--glow-w);
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
.tt-q-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.tt-q-cat { color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.85rem; }
|
||||
.tt-q-points { color: var(--accent); font-weight: 800; font-size: 1.3rem; }
|
||||
|
||||
.tt-q-instruction { color: var(--muted); font-size: 0.85rem; font-weight: 700; margin-bottom: 0.35rem; }
|
||||
.tt-q-text {
|
||||
color: var(--heading); font-size: 1.3rem; margin-bottom: 1.1rem; line-height: 1.4;
|
||||
border-left: 3px solid var(--accent); padding-left: 0.75rem; font-style: italic;
|
||||
}
|
||||
|
||||
.tt-q-answers { display: grid; gap: 0.6rem; }
|
||||
|
||||
.tt-answer {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: 10px;
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
.tt-answer:hover:not(:disabled) { border-color: var(--accent); background: var(--surface); }
|
||||
.tt-answer:disabled { cursor: default; }
|
||||
.tt-answer.correct { border-color: var(--success); background: color-mix(in srgb, var(--success) 14%, transparent); color: var(--success); }
|
||||
.tt-answer.wrong { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }
|
||||
|
||||
.tt-q-feedback { margin-top: 0.9rem; font-weight: 700; min-height: 1.2rem; }
|
||||
.tt-q-feedback.correct { color: var(--success); }
|
||||
.tt-q-feedback.correct::before { content: "\2713 "; font-weight: 800; }
|
||||
.tt-q-feedback.wrong { color: var(--danger); }
|
||||
.tt-q-feedback.wrong::before { content: "\2717 "; font-weight: 800; }
|
||||
|
||||
.tt-q-actions { display: flex; justify-content: flex-end; margin-top: 1rem; }
|
||||
|
||||
.tt-result-card { text-align: center; width: min(440px, 94vw); }
|
||||
.tt-result-card h2 { color: var(--accent); margin-bottom: 0.6rem; }
|
||||
.tt-result-score { font-size: 1.4rem; margin-bottom: 0.4rem; color: var(--text); }
|
||||
.tt-result-score strong { color: var(--accent); }
|
||||
.tt-result-detail { color: var(--muted); margin-bottom: 0.6rem; }
|
||||
.tt-result-high { color: var(--accent); font-weight: 800; margin-bottom: 1rem; }
|
||||
.tt-result-high[hidden] { display: none; }
|
||||
.tt-result-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; margin-top: 1rem; }
|
||||
|
||||
.tt-idle { margin: auto; color: var(--muted); text-align: center; padding: 2rem 1rem; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.tt-title { font-size: 1.15rem; }
|
||||
.tt-stat { font-size: 0.62rem; padding: 0.3rem 0.5rem; }
|
||||
.tt-q-text { font-size: 1.05rem; }
|
||||
}
|
||||
|
||||
/* Auf Touch/Handy scrollbar: das "alles passt ohne Scrollen"-Layout bricht bei
|
||||
100vh (iOS-Adressleiste) und 5x5-Board -> untere Reihen (500) unerreichbar.
|
||||
Hier: Seite scrollbar, Kacheln mit fester Mindesthöhe statt flex-fill. */
|
||||
@media (max-width: 820px) {
|
||||
body.tt-body { height: auto; min-height: 100vh; min-height: 100dvh; overflow-y: auto; }
|
||||
.tt-stage { overflow: visible; }
|
||||
.tt-board { height: auto; }
|
||||
.tt-tile { flex: 0 0 auto; min-height: clamp(58px, 12vw, 86px); }
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/* ============================================================
|
||||
MINDFLUX THEME – zentrale Design-Tokens + Komponenten
|
||||
"Back to the Future"-Palette
|
||||
Dark = Feuer-Amber + Bernstein-Glow | Light = warmes Weiß + Orange
|
||||
Zweitakzent: Flux-Blau (#2D9CCE) | richtig = Blau, falsch = Rot
|
||||
Umschaltung über <html data-theme="dark|light"> (siehe theme.js)
|
||||
============================================================ */
|
||||
|
||||
:root, [data-theme="dark"] {
|
||||
--bg:#0a0806; --bg-glow: radial-gradient(900px 420px at 60% -10%, rgba(240,168,26,.10), transparent 60%);
|
||||
--panel:#13100b; --panel-2:#171209; --surface:#120f0a; --surface-2:#1b1610;
|
||||
--border:#2a2117; --line:#221b12;
|
||||
--border-accent:rgba(240,168,26,.45); --accent:#f0a81a; --accent-2:#f7dc11; --accent-ink:#1c1100;
|
||||
--secondary:#2d9cce; --secondary-2:#5cc4e8;
|
||||
--text:#ece3d2; --muted:#8c8170; --heading:#ffffff;
|
||||
--success:#2d9cce; --danger:#ff4d6d;
|
||||
--glow-s:rgba(240,168,26,.55); --glow-w:rgba(240,168,26,.16);
|
||||
--g-soft:0 0 18px var(--glow-w);
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--bg:#fbf6ec; --bg-glow: radial-gradient(900px 420px at 60% -10%, rgba(214,81,22,.10), transparent 60%);
|
||||
--panel:#ffffff; --panel-2:#fdf7ea; --surface:#ffffff; --surface-2:#fbf4e6;
|
||||
--border:#ece1cf; --line:#f1e8d8;
|
||||
--border-accent:rgba(214,81,22,.40); --accent:#d65116; --accent-2:#e4801d; --accent-ink:#ffffff;
|
||||
--secondary:#1f86b8; --secondary-2:#2d9cce;
|
||||
--text:#33271c; --muted:#8a7d6c; --heading:#241a12;
|
||||
--success:#1f86b8; --danger:#be123c;
|
||||
--glow-s:rgba(214,81,22,.40); --glow-w:rgba(214,81,22,.12);
|
||||
--g-soft:0 6px 18px var(--glow-w);
|
||||
}
|
||||
|
||||
* { box-sizing:border-box; margin:0; padding:0; }
|
||||
html,body { height:100%; }
|
||||
body {
|
||||
font-family:"Inter","Segoe UI",system-ui,-apple-system,Roboto,sans-serif;
|
||||
background:var(--bg-glow), var(--bg); color:var(--text);
|
||||
min-height:100vh; transition:background-color .35s ease,color .35s ease;
|
||||
}
|
||||
a { color:var(--secondary); }
|
||||
::-webkit-scrollbar{width:10px;height:10px} ::-webkit-scrollbar-thumb{background:var(--line);border-radius:9px}
|
||||
|
||||
/* ===== App-Shell (Sidebar | Mitte | rechte Spalte) ===== */
|
||||
.mf-shell { display:grid; grid-template-columns:264px 1fr 332px; grid-template-rows:auto 1fr; min-height:100vh; }
|
||||
.mf-shell.no-right { grid-template-columns:264px 1fr; }
|
||||
.mf-sidebar { grid-column:1; grid-row:1 / 3; background:var(--panel); border-right:1px solid var(--border); padding:1.2rem 1rem; display:flex; flex-direction:column; gap:1.1rem; }
|
||||
.mf-topbar { grid-column:2 / 4; grid-row:1; display:flex; align-items:flex-start; gap:1rem; padding:1.4rem 1.6rem 0; }
|
||||
.mf-shell.no-right .mf-topbar { grid-column:2; }
|
||||
.mf-main { grid-column:2; grid-row:2; padding:1.3rem 1.2rem 1.6rem 1.6rem; min-width:0; }
|
||||
.mf-rightbar { grid-column:3; grid-row:2; padding:1.3rem 1.6rem 1.6rem 0; display:flex; flex-direction:column; gap:1rem; }
|
||||
|
||||
/* ===== Logo ===== */
|
||||
.mf-logo { display:flex; align-items:center; gap:.6rem; padding:.2rem .3rem .4rem; text-decoration:none; }
|
||||
.mf-logo .mark { width:38px; height:38px; flex:0 0 auto; background:var(--accent);
|
||||
-webkit-mask:url("/img/mindflux-emblem.png") center/contain no-repeat; mask:url("/img/mindflux-emblem.png") center/contain no-repeat;
|
||||
filter:drop-shadow(0 0 8px var(--glow-s)); }
|
||||
.mf-logo .txt b { color:var(--accent); font-weight:800; letter-spacing:.13em; font-size:1.02rem; display:block; text-shadow:0 0 12px var(--glow-s); }
|
||||
.mf-logo .txt span { color:var(--muted); font-size:.56rem; letter-spacing:.22em; }
|
||||
|
||||
/* ===== Sidebar-Navigation ===== */
|
||||
.mf-nav { display:flex; flex-direction:column; gap:.25rem; }
|
||||
.mf-nav a { display:flex; align-items:center; gap:.75rem; padding:.7rem .85rem; border-radius:11px; color:var(--muted); text-decoration:none; font-size:.9rem; font-weight:600; border:1px solid transparent; transition:all .18s ease; }
|
||||
.mf-nav a:hover { color:var(--text); background:var(--surface-2); }
|
||||
.mf-nav a.active { color:var(--accent-ink); background:linear-gradient(100deg,var(--accent),var(--accent-2)); box-shadow:0 0 20px var(--glow-s); }
|
||||
.mf-nav a.active svg { color:var(--accent-ink); }
|
||||
.mf-nav svg { width:18px; height:18px; color:var(--accent); flex:0 0 auto; }
|
||||
|
||||
/* ===== Profil / Level / Streak ===== */
|
||||
.mf-profile { border:1px solid var(--border); border-radius:13px; padding:.7rem; background:var(--surface); }
|
||||
.mf-profile .row { display:flex; align-items:center; gap:.6rem; }
|
||||
.mf-avatar { width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,var(--accent),var(--accent-2)); display:inline-flex; align-items:center; justify-content:center; color:var(--accent-ink); font-weight:800; box-shadow:0 0 14px var(--glow-s); overflow:hidden; }
|
||||
.mf-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.mf-profile b { color:var(--heading); font-size:.86rem; display:block; }
|
||||
.mf-profile .role { color:var(--accent); font-size:.72rem; }
|
||||
.mf-meta { font-size:.74rem; }
|
||||
.mf-meta .lvl { color:var(--muted); letter-spacing:.04em; } .mf-meta .lvl b { color:var(--heading); }
|
||||
.mf-meta .xp { color:var(--muted); margin:.2rem 0 .35rem; }
|
||||
.mf-streak { display:flex; align-items:center; gap:.55rem; margin-top:.2rem; }
|
||||
.mf-streak .fire { font-size:1.1rem; filter:drop-shadow(0 0 6px var(--glow-s)); }
|
||||
.mf-streak b { color:var(--heading); font-size:.82rem; display:block; } .mf-streak small { color:var(--muted); }
|
||||
.mf-brain { margin-top:auto; opacity:.55; display:flex; justify-content:center; filter:drop-shadow(0 0 10px var(--glow-s)); }
|
||||
.mf-brain svg { width:150px; height:auto; }
|
||||
|
||||
/* ===== Topbar ===== */
|
||||
.mf-hello h1 { color:var(--heading); font-size:1.85rem; font-weight:800; letter-spacing:-.01em; }
|
||||
.mf-hello h1 .nm { color:var(--accent); text-shadow:0 0 16px var(--glow-s); }
|
||||
.mf-hello p { color:var(--muted); margin-top:.35rem; font-size:.95rem; }
|
||||
.mf-pulse { flex:1; min-width:0; display:flex; justify-content:center; align-items:center; }
|
||||
.mf-pulse svg { width:200px; max-width:100%; filter:drop-shadow(0 0 7px var(--glow-s)); }
|
||||
.mf-tools { display:flex; align-items:center; gap:.6rem; margin-left:auto; }
|
||||
.mf-iconbtn { width:38px; height:38px; border-radius:10px; border:1px solid var(--border); background:var(--surface); color:var(--muted); display:inline-flex; align-items:center; justify-content:center; cursor:pointer; text-decoration:none; transition:all .18s; }
|
||||
.mf-iconbtn:hover { color:var(--accent); border-color:var(--border-accent); box-shadow:0 0 14px var(--glow-w); }
|
||||
.mf-iconbtn svg { width:18px; height:18px; }
|
||||
.mf-search { display:flex; align-items:center; gap:.5rem; background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:.5rem .8rem; color:var(--muted); }
|
||||
.mf-search input { background:none; border:0; outline:none; color:var(--text); font-size:.86rem; width:130px; }
|
||||
.mf-search svg { width:15px; height:15px; }
|
||||
|
||||
/* ===== Theme-Umschalter ===== */
|
||||
.mf-theme { width:42px; height:38px; border-radius:50%; border:1px solid var(--border-accent); background:var(--surface); cursor:pointer; display:inline-flex; align-items:center; justify-content:center; color:var(--accent); box-shadow:0 0 14px var(--glow-w); }
|
||||
.mf-theme svg { width:18px; height:18px; }
|
||||
|
||||
/* ===== HUD-Spielkarten ===== */
|
||||
.mf-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:1.1rem; }
|
||||
.mf-card { position:relative; display:flex; flex-direction:column; padding:1.15rem 1.2rem 1.1rem; background:var(--surface);
|
||||
border:1px solid var(--border-accent); --n:18px;
|
||||
clip-path:polygon(0 0, calc(100% - var(--n)) 0, 100% var(--n), 100% 100%, 0 100%);
|
||||
text-decoration:none; color:inherit; filter:drop-shadow(0 0 7px var(--glow-w)); transition:transform .16s ease, filter .25s ease, background-color .2s; }
|
||||
.mf-card::before { content:""; position:absolute; left:10px; top:10px; width:14px; height:14px; border-left:2px solid var(--accent); border-top:2px solid var(--accent); opacity:.7; }
|
||||
.mf-card:hover { transform:translateY(-4px); background:var(--surface-2); filter:drop-shadow(0 0 16px var(--glow-s)); }
|
||||
.mf-card .ico { width:58px; height:58px; color:var(--accent); margin-bottom:.7rem; filter:drop-shadow(0 0 6px var(--glow-s)); }
|
||||
.mf-card .ico svg { width:100%; height:100%; }
|
||||
.mf-card h3 { color:var(--heading); font-size:1.06rem; font-weight:700; margin-bottom:.4rem; }
|
||||
.mf-card p { color:var(--muted); font-size:.83rem; line-height:1.5; flex:1; }
|
||||
.mf-play { margin-top:.9rem; align-self:flex-start; display:inline-flex; align-items:center; gap:.45rem; font-size:.74rem; font-weight:800; letter-spacing:.08em; color:var(--accent); border:1px solid var(--border-accent); border-radius:8px; padding:.5rem .85rem; text-decoration:none; transition:all .18s; }
|
||||
.mf-card:hover .mf-play { background:var(--accent); color:var(--accent-ink); box-shadow:0 0 16px var(--glow-s); }
|
||||
.mf-play svg { width:14px; height:14px; }
|
||||
|
||||
/* ===== Panels / Widgets ===== */
|
||||
.mf-panel { background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:1.1rem; box-shadow:var(--g-soft); }
|
||||
.mf-panel.accent { border-color:var(--border-accent); box-shadow:0 0 20px var(--glow-w); }
|
||||
.mf-panel h4 { display:flex; align-items:center; gap:.5rem; color:var(--accent); font-size:.74rem; letter-spacing:.12em; text-transform:uppercase; margin-bottom:.9rem; }
|
||||
.mf-panel h4 svg { width:15px; height:15px; }
|
||||
.mf-eyebrow { color:var(--accent); font-weight:700; text-transform:uppercase; letter-spacing:.08em; font-size:.8rem; }
|
||||
.mf-bar { height:7px; border-radius:99px; background:var(--surface-2); border:1px solid var(--border); overflow:hidden; }
|
||||
.mf-bar > i { display:block; height:100%; background:linear-gradient(90deg,var(--accent),var(--accent-2)); box-shadow:0 0 10px var(--glow-s); }
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
.mf-btn { display:inline-flex; align-items:center; justify-content:center; gap:.45rem; font-weight:800; font-size:.82rem; letter-spacing:.04em; border:0; border-radius:9px; padding:.6rem 1.1rem; cursor:pointer; text-decoration:none; background:var(--accent); color:var(--accent-ink); box-shadow:0 0 18px var(--glow-s); transition:all .18s; }
|
||||
.mf-btn:hover { filter:brightness(1.05); transform:translateY(-1px); }
|
||||
.mf-btn svg { width:15px; height:15px; }
|
||||
.mf-btn.outline { background:transparent; color:var(--accent); border:1px solid var(--border-accent); box-shadow:none; }
|
||||
.mf-btn.outline:hover { background:var(--surface-2); }
|
||||
.mf-btn.ghost { background:var(--surface-2); color:var(--text); border:1px solid var(--border); box-shadow:none; }
|
||||
.mf-btn.danger { background:var(--danger); color:#fff; box-shadow:none; }
|
||||
|
||||
/* ===== Formulare ===== */
|
||||
.mf-field { display:flex; flex-direction:column; gap:.35rem; margin-bottom:.9rem; }
|
||||
.mf-label { color:var(--accent); font-weight:700; font-size:.82rem; }
|
||||
.mf-input, .mf-select, input.mf-input, select.mf-select, textarea.mf-input {
|
||||
width:100%; padding:.65rem .8rem; border-radius:10px; border:1px solid var(--border);
|
||||
background:var(--surface-2); color:var(--text); font-size:.95rem; font-family:inherit; }
|
||||
.mf-input:focus, .mf-select:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--glow-w); }
|
||||
.mf-input::placeholder { color:var(--muted); }
|
||||
|
||||
/* ===== Tabellen ===== */
|
||||
.mf-table { width:100%; border-collapse:collapse; }
|
||||
.mf-table th, .mf-table td { text-align:left; padding:.7rem .8rem; border-bottom:1px solid var(--border); font-size:.9rem; }
|
||||
.mf-table th { color:var(--accent); text-transform:uppercase; font-size:.72rem; letter-spacing:.08em; }
|
||||
.mf-table tr:hover td { background:var(--surface-2); }
|
||||
|
||||
/* ===== Badges ===== */
|
||||
.mf-badge { font-size:.68rem; text-transform:uppercase; letter-spacing:.05em; padding:.18rem .5rem; border-radius:7px; background:color-mix(in srgb, var(--accent) 12%, transparent); color:var(--accent); border:1px solid var(--border-accent); }
|
||||
|
||||
/* ===== Standalone-Karte (Login/Auth, einzelne Inhaltsseiten) ===== */
|
||||
.mf-center { display:flex; align-items:center; justify-content:center; min-height:100vh; padding:1.5rem; }
|
||||
.mf-cardbox { width:100%; max-width:420px; background:var(--surface); border:1px solid var(--border-accent); border-radius:18px; padding:2rem; box-shadow:0 0 30px var(--glow-w); }
|
||||
.mf-container { max-width:1000px; margin:0 auto; padding:1.5rem; }
|
||||
.mf-page-head { display:flex; align-items:center; gap:1rem; margin-bottom:1.4rem; }
|
||||
.mf-back { color:var(--accent); text-decoration:none; font-weight:700; font-size:.88rem; }
|
||||
|
||||
/* schwebender Theme-Umschalter für Seiten ohne Topbar */
|
||||
.mf-theme-float { position:fixed; top:16px; right:16px; z-index:1000; }
|
||||
|
||||
/* ===== Fußzeile (Support / AGB / Impressum …) ===== */
|
||||
.mf-footer {
|
||||
display:flex; flex-wrap:wrap; justify-content:center; align-items:center;
|
||||
gap:.35rem 1rem; padding:1.1rem 1.5rem;
|
||||
border-top:1px solid var(--border); background:var(--panel);
|
||||
font-size:.82rem; color:var(--muted);
|
||||
}
|
||||
.mf-footer a { color:var(--muted); text-decoration:none; position:relative; }
|
||||
.mf-footer a:hover { color:var(--accent); }
|
||||
.mf-footer a + a::before { content:"·"; position:absolute; left:-.6rem; color:var(--border); }
|
||||
|
||||
/* ===== Mobile-Nav (Off-Canvas-Drawer; Button/Backdrop per theme.js injiziert) ===== */
|
||||
.mf-burger { display:none; position:fixed; top:.7rem; left:.7rem; z-index:1300; width:42px; height:42px; border-radius:10px; border:1px solid var(--border-accent); background:var(--surface); color:var(--accent); align-items:center; justify-content:center; cursor:pointer; box-shadow:0 0 14px var(--glow-w); }
|
||||
.mf-burger svg { width:20px; height:20px; }
|
||||
.mf-nav-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .28s ease; z-index:1100; }
|
||||
body.mf-nav-open .mf-nav-backdrop { opacity:1; pointer-events:auto; }
|
||||
|
||||
@media (max-width:1180px){ .mf-shell{grid-template-columns:240px 1fr;} .mf-rightbar{display:none;} .mf-topbar{grid-column:2;} .mf-main{grid-column:2;} }
|
||||
|
||||
@media (max-width:820px){
|
||||
/* Shell stapelt; Sidebar wird zum Off-Canvas-Drawer */
|
||||
.mf-shell, .mf-shell.no-right { display:block; }
|
||||
.mf-topbar { padding:1.05rem 1rem 0 3.7rem; flex-wrap:wrap; gap:.7rem; }
|
||||
.mf-main { padding:1.1rem 1rem 1.4rem; }
|
||||
.mf-rightbar { padding:0 1rem 1.4rem; }
|
||||
.mf-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.mf-pulse, .mf-search { display:none; }
|
||||
.mf-hello h1 { font-size:1.4rem; }
|
||||
.mf-tools { margin-left:auto; }
|
||||
|
||||
.mf-sidebar {
|
||||
position:fixed; top:0; left:0; height:100dvh; width:min(284px,82vw); z-index:1200;
|
||||
transform:translateX(-100%); transition:transform .28s ease; overflow-y:auto;
|
||||
box-shadow:0 18px 50px rgba(0,0,0,.55);
|
||||
}
|
||||
body.mf-nav-open .mf-sidebar { transform:translateX(0); }
|
||||
.mf-burger { display:inline-flex; }
|
||||
|
||||
/* breite Tabellen horizontal scrollbar statt überlaufend */
|
||||
.mf-table { display:block; overflow-x:auto; white-space:nowrap; }
|
||||
}
|
||||
|
||||
@media (max-width:560px){ .mf-grid{grid-template-columns:1fr;} }
|
||||
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Datenschutz - FinalFlux AP1</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/theme.css">
|
||||
<link rel="stylesheet" href="/css/finalflux.css">
|
||||
<script src="/js/theme.js"></script>
|
||||
</head>
|
||||
<body class="finalflux-page">
|
||||
<div class="mf-theme-float"><button class="mf-theme"></button></div>
|
||||
<main>
|
||||
<a href="/finalflux" class="back-link">Zurück zu FinalFlux</a>
|
||||
<h1>Datenschutz & Datensicherheit</h1>
|
||||
|
||||
<section>
|
||||
<h2>Prüfungsmodus</h2>
|
||||
<p>
|
||||
Dieser FluxCheck trainiert Datenschutz so, wie er in AP1-Handlungssituationen
|
||||
auftaucht: personenbezogene Daten erkennen, DSGVO-Grundsätze anwenden,
|
||||
Betroffenenrechte einordnen, Schutzmaßnahmen auswählen und Praxisfälle bewerten.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Merksatz</h2>
|
||||
<p>
|
||||
Datenschutz schützt Menschen hinter den Daten. Datensicherheit schützt die Daten
|
||||
technisch und organisatorisch. In der Prüfung musst du meistens beides zusammen denken.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Zweck von DSGVO und BDSG</h2>
|
||||
<p>
|
||||
Die Datenschutz-Grundverordnung (DSGVO) ist eine EU-Verordnung und gilt unmittelbar in allen
|
||||
Mitgliedstaaten. Sie soll natürliche Personen beim Umgang mit ihren Daten schützen und
|
||||
gleichzeitig den freien Datenverkehr im Binnenmarkt ermöglichen. Das Bundesdatenschutzgesetz
|
||||
(BDSG) ergänzt die DSGVO dort, wo sie den Mitgliedstaaten Spielraum lässt, etwa beim
|
||||
Beschäftigtendatenschutz oder bei der Benennung des Datenschutzbeauftragten.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>DSGVO: europaweit einheitliches, unmittelbar geltendes Recht.</li>
|
||||
<li>BDSG: nationale Konkretisierung und Ergänzung der DSGVO.</li>
|
||||
<li>Schutzgut: das Grundrecht auf informationelle Selbstbestimmung.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Personenbezogene Daten</h2>
|
||||
<p>
|
||||
Personenbezogene Daten sind alle Informationen, die sich auf eine identifizierte oder
|
||||
identifizierbare natürliche Person beziehen, zum Beispiel Name, Anschrift, Personalnummer,
|
||||
Kundennummer oder IP-Adresse. Nur wenn ein Personenbezug besteht, ist die DSGVO überhaupt
|
||||
anwendbar. Besondere Kategorien nach Art. 9 DSGVO (etwa Gesundheits-, Religions- oder
|
||||
biometrische Daten) sind streng geschützt.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Anonyme Daten: kein Personenbezug mehr möglich, DSGVO greift nicht.</li>
|
||||
<li>Pseudonyme Daten: Bezug nur mit Zusatzwissen herstellbar, bleiben personenbezogen.</li>
|
||||
<li>Besondere Kategorien (Art. 9): zusätzlicher Schutzbedarf.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Grundsätze der Verarbeitung (Art. 5 DSGVO)</h2>
|
||||
<p>
|
||||
Jede Verarbeitung muss sich an den Grundsätzen des Art. 5 messen lassen. Sie sind der rote
|
||||
Faden, mit dem du in Prüfungsfällen beurteilst, ob ein Vorgehen zulässig ist.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Rechtmäßigkeit, Verarbeitung nach Treu und Glauben, Transparenz.</li>
|
||||
<li>Zweckbindung: Daten nur für festgelegte, eindeutige Zwecke nutzen.</li>
|
||||
<li>Datenminimierung: nur so viele Daten wie für den Zweck nötig.</li>
|
||||
<li>Richtigkeit: Daten sachlich korrekt und aktuell halten.</li>
|
||||
<li>Speicherbegrenzung: Daten nur so lange wie nötig aufbewahren.</li>
|
||||
<li>Integrität und Vertraulichkeit: angemessene Sicherheit gewährleisten.</li>
|
||||
<li>Rechenschaftspflicht: Einhaltung nachweisen können.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Rechtsgrundlagen (Art. 6 DSGVO)</h2>
|
||||
<p>
|
||||
Personenbezogene Daten dürfen nur verarbeitet werden, wenn eine Rechtsgrundlage vorliegt.
|
||||
Fehlt sie, ist die Verarbeitung rechtswidrig, selbst wenn sie technisch einwandfrei abläuft.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Einwilligung der betroffenen Person (freiwillig, widerrufbar).</li>
|
||||
<li>Erfüllung eines Vertrags mit der betroffenen Person.</li>
|
||||
<li>Erfüllung einer rechtlichen Verpflichtung (z. B. Aufbewahrungspflichten).</li>
|
||||
<li>Berechtigtes Interesse, sofern keine schutzwürdigen Interessen überwiegen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Betroffenenrechte</h2>
|
||||
<p>
|
||||
Betroffene haben durchsetzbare Rechte gegenüber dem Verantwortlichen. Anfragen sind in der
|
||||
Regel innerhalb eines Monats zu beantworten; zuvor ist die Identität der anfragenden Person
|
||||
zu prüfen, damit keine Daten an Unbefugte gelangen.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Auskunft (Art. 15): welche Daten zu welchem Zweck verarbeitet werden.</li>
|
||||
<li>Berichtigung (Art. 16): Korrektur unrichtiger Daten.</li>
|
||||
<li>Löschung (Art. 17): Recht auf Vergessenwerden, sofern keine Pflicht entgegensteht.</li>
|
||||
<li>Einschränkung (Art. 18) und Widerspruch (Art. 21).</li>
|
||||
<li>Datenübertragbarkeit (Art. 20): Daten in maschinenlesbarem Format erhalten.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Verantwortlicher und Auftragsverarbeiter</h2>
|
||||
<p>
|
||||
Der Verantwortliche entscheidet über Zweck und Mittel der Verarbeitung. Lässt er Daten durch
|
||||
einen weisungsgebundenen Dienstleister verarbeiten (z. B. Cloud-Hosting, externe Wartung,
|
||||
Lohnabrechnung), ist dieser Auftragsverarbeiter. Dafür ist nach Art. 28 DSGVO ein
|
||||
Auftragsverarbeitungsvertrag (AV-Vertrag) zwingend erforderlich.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Verantwortlicher: bestimmt das "Warum" und "Wie" der Verarbeitung.</li>
|
||||
<li>Auftragsverarbeiter: handelt nur auf Weisung des Verantwortlichen.</li>
|
||||
<li>AV-Vertrag regelt Zweck, Dauer, TOM, Vertraulichkeit und Löschung.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Technische und organisatorische Maßnahmen (TOM)</h2>
|
||||
<p>
|
||||
Nach Art. 32 DSGVO muss ein dem Risiko angemessenes Schutzniveau sichergestellt werden. TOM
|
||||
sind die konkreten Maßnahmen dafür. Ordne sie in Prüfungsaufgaben den Schutzzielen
|
||||
Vertraulichkeit, Integrität und Verfügbarkeit zu.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Vertraulichkeit: Verschlüsselung, Zugriffs- und Rechtekonzept, Need-to-know.</li>
|
||||
<li>Integrität: Protokollierung, Hash-/Prüfverfahren, Eingabekontrolle.</li>
|
||||
<li>Verfügbarkeit: Backups, Redundanz, USV, Notfallkonzept.</li>
|
||||
<li>Organisatorisch: Schulungen, Zuständigkeiten, Bildschirmsperre, Clean Desk.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Meldepflicht bei Datenpannen</h2>
|
||||
<p>
|
||||
Eine Verletzung des Schutzes personenbezogener Daten (z. B. verlorener USB-Stick, Hackerangriff,
|
||||
Fehlversand) muss der Verantwortliche bei Risiko nach Art. 33 DSGVO grundsätzlich innerhalb von
|
||||
72 Stunden der zuständigen Aufsichtsbehörde melden. Besteht ein hohes Risiko für die Betroffenen,
|
||||
sind diese nach Art. 34 zusätzlich zu benachrichtigen. Verschweigen ist unzulässig und kann zu
|
||||
Bußgeldern führen.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li>Meldung an die Aufsichtsbehörde in der Regel binnen 72 Stunden.</li>
|
||||
<li>Benachrichtigung der Betroffenen bei hohem Risiko.</li>
|
||||
<li>Jede Panne dokumentieren, auch wenn keine Meldung nötig ist.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Merksätze für die Prüfung</h2>
|
||||
<ul class="key-list">
|
||||
<li>Erst Personenbezug prüfen, dann erst greift die DSGVO.</li>
|
||||
<li>Keine Verarbeitung ohne Rechtsgrundlage nach Art. 6.</li>
|
||||
<li>So wenig Daten wie möglich, so lange wie nötig.</li>
|
||||
<li>Privacy by Design und Privacy by Default von Anfang an mitdenken.</li>
|
||||
<li>Datenpanne mit Risiko: 72-Stunden-Meldung nicht vergessen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="finalflux-test" data-finalflux-test aria-live="polite">
|
||||
<div class="test-meta">
|
||||
<span data-question-counter>Frage 1</span>
|
||||
<span data-score-counter>0 richtig</span>
|
||||
</div>
|
||||
|
||||
<div class="category-block">
|
||||
<p class="eyebrow">Kategorie</p>
|
||||
<h2 data-category-title>Datenschutz</h2>
|
||||
<p data-category-hint>Personenbezogene Daten, Rechte und Schutzmaßnahmen.</p>
|
||||
</div>
|
||||
|
||||
<div class="question-card">
|
||||
<h3 data-question-text></h3>
|
||||
<div data-answer-options class="answer-options"></div>
|
||||
<div data-answer-feedback class="fluxcheck-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-actions">
|
||||
<button type="button" data-check-answer>Antwort prüfen</button>
|
||||
<button type="button" data-skip-question class="secondary">Überspringen</button>
|
||||
<button type="button" data-next-question class="secondary" disabled>Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-test-result class="finalflux-panel" hidden>
|
||||
<h2>Auswertung</h2>
|
||||
<p data-result-summary></p>
|
||||
<ul data-category-summary class="key-list"></ul>
|
||||
<div class="finalflux-actions">
|
||||
<button type="button" data-restart-test>Test neu starten</button>
|
||||
<a href="/finalflux">Zurück zur Übersicht</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/finalflux-test.js" data-questions-src="/finalflux/questions/datenschutz.json" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>IP-Adressberechnung - FinalFlux AP1</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/theme.css">
|
||||
<link rel="stylesheet" href="/css/finalflux.css">
|
||||
<script src="/js/theme.js"></script>
|
||||
</head>
|
||||
<body class="finalflux-page">
|
||||
<div class="mf-theme-float"><button class="mf-theme"></button></div>
|
||||
<main>
|
||||
<a href="/finalflux" class="back-link">Zurück zu FinalFlux</a>
|
||||
<h1>IP-Adressberechnung</h1>
|
||||
|
||||
<section>
|
||||
<h2>Lernfokus</h2>
|
||||
<p>Trainiere IPv4, CIDR, Netzadresse, Broadcast und Hostanzahlen.</p>
|
||||
<ul class="key-list">
|
||||
<li>CIDR und Subnetzmaske zusammenbringen.</li>
|
||||
<li>Netzadresse, Broadcast und Hostbereich bestimmen.</li>
|
||||
<li>Typische Subnetting-Fehler vermeiden.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Aufbau einer IPv4-Adresse</h2>
|
||||
<p>Eine IPv4-Adresse ist 32 Bit lang und wird in vier Oktette (je 8 Bit, also 1 Byte) unterteilt, die durch Punkte getrennt sind. Pro Oktett sind 2^8 = 256 Werte möglich, der Wertebereich reicht also von 0 bis 255 (z. B. 192.168.10.25).</p>
|
||||
<ul class="key-list">
|
||||
<li>1 Oktett = 8 Bit, 4 Oktette = 32 Bit Gesamtlänge.</li>
|
||||
<li>Jedes Oktett: 0 bis 255 (256 Werte).</li>
|
||||
<li>Bitwertigkeiten im Oktett: 128, 64, 32, 16, 8, 4, 2, 1.</li>
|
||||
<li>Beispiel: 11000000 binär = 128 + 64 = 192 dezimal.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Subnetzmaske und CIDR-Präfix</h2>
|
||||
<p>Die Subnetzmaske trennt die Adresse in Netz- und Hostanteil. Alle 1-Bits markieren den Netzanteil, alle 0-Bits den Hostanteil. Das CIDR-Präfix (z. B. /24) zählt einfach die führenden 1-Bits. So entspricht /24 der Maske 255.255.255.0 und lässt 32 - 24 = 8 Hostbits übrig.</p>
|
||||
<ul class="key-list">
|
||||
<li>/8 = 255.0.0.0, /16 = 255.255.0.0, /24 = 255.255.255.0.</li>
|
||||
<li>/26 = 255.255.255.192, /30 = 255.255.255.252.</li>
|
||||
<li>Hostbits = 32 - Präfix. Beispiel /26: 32 - 26 = 6 Hostbits.</li>
|
||||
<li>Maskenwert eines Oktetts: 128, 192, 224, 240, 248, 252, 254, 255.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Netzadresse, Broadcast und Hostbereich</h2>
|
||||
<p>Innerhalb eines Subnetzes ist die Netzadresse die erste Adresse (alle Hostbits 0), die Broadcast-Adresse die letzte (alle Hostbits 1). Beide werden nicht an Geräte vergeben. Dazwischen liegen die nutzbaren Hostadressen.</p>
|
||||
<p>Rechenbeispiel für 192.168.1.0/24: Netzadresse 192.168.1.0, Broadcast 192.168.1.255, erste Hostadresse 192.168.1.1, letzte Hostadresse 192.168.1.254.</p>
|
||||
<ul class="key-list">
|
||||
<li>Netzadresse = alle Hostbits 0 (erste Adresse).</li>
|
||||
<li>Broadcast = alle Hostbits 1 (letzte Adresse).</li>
|
||||
<li>Erste nutzbare Hostadresse = Netzadresse + 1.</li>
|
||||
<li>Letzte nutzbare Hostadresse = Broadcast - 1.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Hostanzahl berechnen</h2>
|
||||
<p>Die Zahl der nutzbaren Hosts ergibt sich aus den Hostbits: 2^Hostbits - 2. Der Abzug von 2 berücksichtigt Netz- und Broadcast-Adresse, die nicht für Geräte zur Verfügung stehen.</p>
|
||||
<ul class="key-list">
|
||||
<li>/24: 8 Hostbits, 2^8 - 2 = 256 - 2 = 254 Hosts.</li>
|
||||
<li>/26: 6 Hostbits, 2^6 - 2 = 64 - 2 = 62 Hosts.</li>
|
||||
<li>/30: 2 Hostbits, 2^2 - 2 = 4 - 2 = 2 Hosts (Punkt-zu-Punkt).</li>
|
||||
<li>Achtung: häufiger Fehler ist 2^Hostbits ohne den Abzug von 2.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Subnetting mit der Blockgröße</h2>
|
||||
<p>Wird ein Netz in mehrere Subnetze geteilt, hilft die Blockgröße (Schrittweite) im relevanten Oktett: Blockgröße = 256 - Maskenwert. Die Subnetze beginnen dann bei Vielfachen dieser Blockgröße.</p>
|
||||
<p>Beispiel 192.168.1.0/26: Maskenwert im letzten Oktett ist 192, also Blockgröße 256 - 192 = 64. Die Subnetze sind:</p>
|
||||
<ul class="key-list">
|
||||
<li>192.168.1.0/26 - Broadcast 192.168.1.63 (Hosts .1 bis .62).</li>
|
||||
<li>192.168.1.64/26 - Broadcast 192.168.1.127 (Hosts .65 bis .126).</li>
|
||||
<li>192.168.1.128/26 - Broadcast 192.168.1.191 (Hosts .129 bis .190).</li>
|
||||
<li>192.168.1.192/26 - Broadcast 192.168.1.255 (Hosts .193 bis .254).</li>
|
||||
</ul>
|
||||
<p>So liegt etwa 192.168.1.130 im Block ab .128, gehört also zum Subnetz 192.168.1.128/26.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Private Adressbereiche</h2>
|
||||
<p>Private Adressen (RFC 1918) sind nicht im Internet routbar und werden in lokalen Netzen genutzt. Sie müssen für den Internetzugang über NAT in öffentliche Adressen übersetzt werden.</p>
|
||||
<ul class="key-list">
|
||||
<li>10.0.0.0/8 (10.0.0.0 - 10.255.255.255).</li>
|
||||
<li>172.16.0.0/12 (172.16.0.0 - 172.31.255.255).</li>
|
||||
<li>192.168.0.0/16 (192.168.0.0 - 192.168.255.255).</li>
|
||||
<li>169.254.0.0/16 ist APIPA und deutet auf ein DHCP-Problem hin.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>IPv6 - die Grundidee</h2>
|
||||
<p>IPv6 wurde wegen der Adressknappheit von IPv4 eingeführt. Eine IPv6-Adresse ist 128 Bit lang und wird in acht Blöcken zu je 16 Bit hexadezimal geschrieben (z. B. 2001:0db8:0000:0000:0000:0000:0000:0001). Führende Nullen dürfen entfallen, eine zusammenhängende Nullfolge lässt sich einmal durch :: ersetzen (verkürzt: 2001:db8::1).</p>
|
||||
<ul class="key-list">
|
||||
<li>IPv4 = 32 Bit, IPv6 = 128 Bit.</li>
|
||||
<li>Schreibweise hexadezimal in 8 Blöcken zu 16 Bit.</li>
|
||||
<li>:: ersetzt einmalig eine Folge aus Nullblöcken.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Merksätze für die Prüfung</h2>
|
||||
<ul class="key-list">
|
||||
<li>Hostbits = 32 - Präfix; nutzbare Hosts = 2^Hostbits - 2.</li>
|
||||
<li>Netzadresse = erste, Broadcast = letzte Adresse des Subnetzes.</li>
|
||||
<li>Blockgröße = 256 - Maskenwert des relevanten Oktetts.</li>
|
||||
<li>Rechenweg immer notieren - das bringt Teilpunkte.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Prüfungsmodus</h2>
|
||||
<p>Dieser FluxCheck führt dich durch die wichtigsten AP1-Aspekte dieses Themas. Du kannst Antworten prüfen, Fragen überspringen und bekommst am Ende eine Auswertung nach Kategorie.</p>
|
||||
</section>
|
||||
|
||||
<section class="finalflux-test" data-finalflux-test aria-live="polite">
|
||||
<div class="test-meta">
|
||||
<span data-question-counter>Frage 1</span>
|
||||
<span data-score-counter>0 richtig</span>
|
||||
</div>
|
||||
<div class="category-block">
|
||||
<p class="eyebrow">Kategorie</p>
|
||||
<h2 data-category-title>IP-Adressberechnung</h2>
|
||||
<p data-category-hint>AP1-Training</p>
|
||||
</div>
|
||||
<div class="question-card">
|
||||
<h3 data-question-text></h3>
|
||||
<div data-answer-options class="answer-options"></div>
|
||||
<div data-answer-feedback class="fluxcheck-result"></div>
|
||||
</div>
|
||||
<div class="test-actions">
|
||||
<button type="button" data-check-answer>Antwort prüfen</button>
|
||||
<button type="button" data-skip-question class="secondary">Überspringen</button>
|
||||
<button type="button" data-next-question class="secondary" disabled>Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-test-result class="finalflux-panel" hidden>
|
||||
<h2>Auswertung</h2>
|
||||
<p data-result-summary></p>
|
||||
<ul data-category-summary class="key-list"></ul>
|
||||
<div class="finalflux-actions">
|
||||
<button type="button" data-restart-test>Test neu starten</button>
|
||||
<a href="/finalflux">Zurück zur Übersicht</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/finalflux-test.js" data-questions-src="/finalflux/questions/ap1_ip-berechnung.json" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Projektmanagement - FinalFlux AP1</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/theme.css">
|
||||
<link rel="stylesheet" href="/css/finalflux.css">
|
||||
<script src="/js/theme.js"></script>
|
||||
</head>
|
||||
<body class="finalflux-page">
|
||||
<div class="mf-theme-float"><button class="mf-theme"></button></div>
|
||||
<main>
|
||||
<a href="/finalflux" class="back-link">Zurück zu FinalFlux</a>
|
||||
<h1>Projektmanagement in der AP1</h1>
|
||||
|
||||
<section>
|
||||
<h2>Prüfungsmodus</h2>
|
||||
<p>
|
||||
Dieser FluxCheck führt dich durch prüfungsrelevante Projektmanagement-Themen:
|
||||
Auftrag, Ziele, Anforderungen, Planung, Risiken, Qualität, Kommunikation,
|
||||
Dokumentation und Abnahme. Jede Frage gehört zu einer Kategorie, die oben im
|
||||
Themenblock angezeigt wird.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Projektphasen im Überblick</h2>
|
||||
<p>
|
||||
Ein IT-Projekt durchläuft typische Phasen, die in der AP1 immer wieder
|
||||
auftauchen. Sie bauen aufeinander auf: Erst wenn eine Phase ein
|
||||
verwertbares Ergebnis liefert, beginnt sinnvoll die nächste. Wer Phasen
|
||||
überspringt, plant meist auf unsicherer Grundlage.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Initiierung / Auftragsklärung:</strong> Ziel, Auftraggeber, Rahmenbedingungen und Erfolgskriterien klären.</li>
|
||||
<li><strong>Analyse:</strong> Ist-Zustand aufnehmen, Anforderungen sammeln, Soll-Konzept beschreiben.</li>
|
||||
<li><strong>Planung:</strong> Arbeitspakete, Termine, Ressourcen, Kosten und Risiken festlegen.</li>
|
||||
<li><strong>Realisierung:</strong> Umsetzung der geplanten Arbeitspakete.</li>
|
||||
<li><strong>Test & Abnahme:</strong> Soll-Ist-Vergleich, Abnahmekriterien prüfen, Übergabe.</li>
|
||||
<li><strong>Abschluss:</strong> Dokumentation, Übergabe, Lessons Learned.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Merksatz:</strong> Eine Phase ist ein Zeitabschnitt mit Dauer; ein
|
||||
Meilenstein ist nur ein Zeitpunkt (Kontrollpunkt) ohne eigene Dauer.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Projektziele und die SMART-Regel</h2>
|
||||
<p>
|
||||
Ein gutes Projektziel ist eindeutig und überprüfbar. Vage Wünsche wie
|
||||
"Das System soll besser werden" lassen sich weder planen noch abnehmen.
|
||||
Die SMART-Regel hilft, Ziele prüfbar zu formulieren.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>S – Spezifisch:</strong> eindeutig und konkret beschrieben.</li>
|
||||
<li><strong>M – Messbar:</strong> der Erfolg lässt sich an einer Größe prüfen.</li>
|
||||
<li><strong>A – Attraktiv / Akzeptiert:</strong> von den Beteiligten getragen.</li>
|
||||
<li><strong>R – Realistisch:</strong> mit Zeit, Budget und Ressourcen erreichbar.</li>
|
||||
<li><strong>T – Terminiert:</strong> mit klarem Zeitpunkt oder Zeitraum.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Beispiel: "Bis Freitag, 16 Uhr sind 12 Arbeitsplätze eingerichtet und ihre
|
||||
Netzverbindung ist getestet" erfüllt alle fünf Kriterien.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Das magische Dreieck (Zeit, Kosten, Qualität)</h2>
|
||||
<p>
|
||||
Zeit, Kosten und Qualität stehen in Wechselwirkung. Ändert man eine der
|
||||
drei Größen, wirkt sich das fast immer auf mindestens eine andere aus.
|
||||
Wird zum Beispiel der Termin verkürzt, der Umfang bleibt aber gleich,
|
||||
steigen entweder die Kosten (mehr Personal) oder die Qualität sinkt.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Zeit:</strong> verfügbare Dauer bis zum Termin.</li>
|
||||
<li><strong>Kosten:</strong> Budget, Personal, Material, Lizenzen.</li>
|
||||
<li><strong>Qualität:</strong> Erfüllungsgrad der Anforderungen.</li>
|
||||
<li><strong>Scope (Leistungsumfang):</strong> was im Projekt enthalten ist; <em>Scope Creep</em> ist sein schleichendes, unkontrolliertes Anwachsen.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Merksatz:</strong> An einer Ecke des Dreiecks ziehen heißt immer,
|
||||
an einer anderen Ecke nachzugeben. Änderungswünsche deshalb bewerten und
|
||||
dokumentieren.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Lastenheft vs. Pflichtenheft</h2>
|
||||
<p>
|
||||
Diese beiden Dokumente werden in der Prüfung gern verwechselt. Die einfache
|
||||
Eselsbrücke: Das Lastenheft beschreibt das <em>Was</em> (vom Auftraggeber),
|
||||
das Pflichtenheft das <em>Wie</em> (vom Auftragnehmer).
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Lastenheft:</strong> erstellt vom <em>Auftraggeber</em>; beschreibt Anforderungen und Ziele aus Kundensicht.</li>
|
||||
<li><strong>Pflichtenheft:</strong> erstellt vom <em>Auftragnehmer</em>; beschreibt die konkrete technische Umsetzung der Anforderungen.</li>
|
||||
<li><strong>Funktionale Anforderung:</strong> was das System tun soll (z. B. Anmeldung mit Benutzername und Passwort).</li>
|
||||
<li><strong>Nicht-funktionale Anforderung:</strong> Qualitätsmerkmal wie Antwortzeit, Sicherheit oder Verfügbarkeit.</li>
|
||||
<li><strong>Priorisierung:</strong> Muss-, Soll- und Kann-Anforderungen ordnen die Wichtigkeit.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Planung: Arbeitspakete, Meilensteine, Gantt und Netzplan</h2>
|
||||
<p>
|
||||
Aus dem Projektziel entsteht ein Projektstrukturplan (PSP), der das Projekt
|
||||
in Teilaufgaben bzw. Arbeitspakete zerlegt. Für jedes Arbeitspaket werden
|
||||
Aufwand, Dauer und ein Verantwortlicher festgelegt.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Aufwand vs. Dauer:</strong> Aufwand ist die geleistete Arbeit (z. B. 16 Personenstunden), Dauer die verstrichene Zeit (z. B. 2 Tage) – nicht dasselbe.</li>
|
||||
<li><strong>Projektstrukturplan:</strong> hierarchische Zerlegung in Arbeitspakete (das Was).</li>
|
||||
<li><strong>Gantt-Diagramm:</strong> Balkenplan, zeigt zeitliche Lage und Dauer von Aufgaben auf einer Zeitachse.</li>
|
||||
<li><strong>Netzplan:</strong> zeigt Abhängigkeiten und Reihenfolge der Vorgänge sowie den kritischen Pfad.</li>
|
||||
<li><strong>Kritischer Pfad:</strong> die längste Vorgangskette ohne Puffer; Verzögerungen darauf verschieben das Projektende.</li>
|
||||
<li><strong>Puffer:</strong> Reserve für Verzögerungen, Lieferverzug oder Probleme.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Stakeholder und Kommunikation</h2>
|
||||
<p>
|
||||
Stakeholder sind alle Personen oder Gruppen mit Interesse oder Einfluss auf
|
||||
das Projekt: Auftraggeber, Anwender, IT, Datenschutz, Management. Wer sie
|
||||
früh einbindet, vermeidet späte Überraschungen.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Kick-off:</strong> gemeinsamer Projektstart zur Klärung von Ziel, Rollen und Vorgehen.</li>
|
||||
<li><strong>Kommunikationsplan:</strong> regelt, wer wann welche Information erhält.</li>
|
||||
<li><strong>Protokolle:</strong> halten Beschlüsse, Aufgaben, Verantwortliche und Termine fest.</li>
|
||||
<li><strong>Eskalation:</strong> Weitergabe eines Problems an eine höhere Entscheidungsebene.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Risikomanagement</h2>
|
||||
<p>
|
||||
Ein Risiko ist ein mögliches zukünftiges Ereignis mit negativer Auswirkung –
|
||||
keine sichere Tatsache. Risiken werden früh erkannt, bewertet und mit
|
||||
Maßnahmen versehen; im Projektverlauf werden sie regelmäßig überprüft.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Bewertung:</strong> nach Eintrittswahrscheinlichkeit und Schadensauswirkung (Risikomatrix).</li>
|
||||
<li><strong>Vermeidung:</strong> Vorgehen wählen, bei dem das Risiko gar nicht entsteht.</li>
|
||||
<li><strong>Minderung:</strong> Wahrscheinlichkeit oder Auswirkung senken (z. B. getestetes Backup).</li>
|
||||
<li><strong>Übertragung:</strong> Risiko abgeben, etwa per Versicherung oder Vertrag.</li>
|
||||
<li><strong>Akzeptanz / Restrisiko:</strong> das, was nach allen Maßnahmen bewusst bleibt.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Qualität, Abnahme und Projektabschluss</h2>
|
||||
<p>
|
||||
Qualität wird im Projekt an den Anforderungen gemessen, nicht am
|
||||
Geschmack. Tests und ein Soll-Ist-Vergleich zeigen vor der Übergabe, ob
|
||||
die Lösung passt. Die Abnahme stützt sich auf vorher vereinbarte
|
||||
Abnahmekriterien und wird in einem Abnahmeprotokoll dokumentiert.
|
||||
</p>
|
||||
<ul class="key-list">
|
||||
<li><strong>Testfall:</strong> Eingabe, Ablauf und erwartetes Ergebnis – wiederholbar und nachvollziehbar.</li>
|
||||
<li><strong>Pilotbetrieb:</strong> begrenzter Test mit kleiner Nutzergruppe vor dem breiten Rollout.</li>
|
||||
<li><strong>Abnahmeprotokoll:</strong> Ergebnis, offene Punkte, Datum und Freigabe.</li>
|
||||
<li><strong>Dokumentation:</strong> technische Doku für Betrieb und Wartung, Anwenderdoku für Nutzer.</li>
|
||||
<li><strong>Lessons Learned:</strong> Erkenntnisse für künftige Projekte sichern.</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Merksatz:</strong> Ein Projekt ist erst sauber abgeschlossen, wenn
|
||||
Abnahme erfolgt, Dokumentation übergeben und offene Punkte geklärt sind.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="finalflux-test" data-finalflux-test aria-live="polite">
|
||||
<div class="test-meta">
|
||||
<span data-question-counter>Frage 1</span>
|
||||
<span data-score-counter>0 richtig</span>
|
||||
</div>
|
||||
|
||||
<div class="category-block">
|
||||
<p class="eyebrow">Kategorie</p>
|
||||
<h2 data-category-title>Projektstart</h2>
|
||||
<p data-category-hint>Auftrag, Ausgangslage und Ziel verstehen.</p>
|
||||
</div>
|
||||
|
||||
<div class="question-card">
|
||||
<h3 data-question-text></h3>
|
||||
<div data-answer-options class="answer-options"></div>
|
||||
<div data-answer-feedback class="fluxcheck-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-actions">
|
||||
<button type="button" data-check-answer>Antwort prüfen</button>
|
||||
<button type="button" data-skip-question class="secondary">Überspringen</button>
|
||||
<button type="button" data-next-question class="secondary" disabled>Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-test-result class="finalflux-panel" hidden>
|
||||
<h2>Auswertung</h2>
|
||||
<p data-result-summary></p>
|
||||
<ul data-category-summary class="key-list"></ul>
|
||||
<div class="finalflux-actions">
|
||||
<button type="button" data-restart-test>Test neu starten</button>
|
||||
<a href="/finalflux">Zurück zur Übersicht</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/finalflux-test.js" data-questions-src="/finalflux/questions/projektmanagement.json" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Zahlensysteme & Speicher - FinalFlux AP1</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/theme.css">
|
||||
<link rel="stylesheet" href="/css/finalflux.css">
|
||||
<script src="/js/theme.js"></script>
|
||||
</head>
|
||||
<body class="finalflux-page">
|
||||
<div class="mf-theme-float"><button class="mf-theme"></button></div>
|
||||
<main>
|
||||
<a href="/finalflux" class="back-link">Zurück zu FinalFlux</a>
|
||||
<h1>Zahlensysteme & Speicher</h1>
|
||||
|
||||
<section>
|
||||
<h2>Worum geht es?</h2>
|
||||
<p>Ein Zahlensystem legt fest, mit welchen Ziffern und welcher Basis Zahlen geschrieben werden. In der AP1 brauchst du drei Systeme sicher: das Dezimalsystem (Basis 10, gewohnt aus dem Alltag), das Binärsystem (Basis 2, die Sprache des Computers) und das Hexadezimalsystem (Basis 16, eine kompakte Kurzschrift für Binärwerte). Dazu kommen die Speichereinheiten von Bit und Byte bis GiB.</p>
|
||||
<ul class="key-list">
|
||||
<li>Dezimal: Ziffern 0-9, Stellenwerte sind Potenzen von 10 (1, 10, 100, ...).</li>
|
||||
<li>Binär (dual): Ziffern 0 und 1, Stellenwerte sind Potenzen von 2 (1, 2, 4, 8, ...).</li>
|
||||
<li>Hexadezimal: Ziffern 0-9 und A-F, Stellenwerte sind Potenzen von 16 (1, 16, 256, ...).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Stellenwertsysteme verstehen</h2>
|
||||
<p>In jedem dieser Systeme hat jede Stelle einen festen Stellenwert: Basis hoch Position, von rechts mit Position 0 beginnend. Der Wert einer Zahl ist die Summe aus Ziffer mal Stellenwert. Genau dieses Prinzip ist der Schlüssel zu allen Umrechnungen.</p>
|
||||
<p>Beispiel binär: 1011 = 1*8 + 0*4 + 1*2 + 1*1 = <strong>11</strong> (dezimal). Beispiel hexadezimal: 2F = 2*16 + 15*1 = <strong>47</strong> (dezimal).</p>
|
||||
<p class="merksatz"><strong>Merksatz:</strong> Stellenwert = Basis hoch Stellennummer (rechts beginnt bei 0). Gesetzte Stellen mit ihrem Stellenwert multiplizieren und alles addieren.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Dezimal nach Binär umrechnen (Rechenweg)</h2>
|
||||
<p>Die zuverlässigste Methode ist die <strong>fortlaufende Division durch 2</strong>. Man teilt die Zahl wiederholt durch 2 und notiert jeden Rest. Die Reste von <em>unten nach oben</em> gelesen ergeben die Binärzahl.</p>
|
||||
<p>Beispiel 13 nach binär:</p>
|
||||
<ul class="key-list">
|
||||
<li>13 : 2 = 6 Rest <strong>1</strong></li>
|
||||
<li>6 : 2 = 3 Rest <strong>0</strong></li>
|
||||
<li>3 : 2 = 1 Rest <strong>1</strong></li>
|
||||
<li>1 : 2 = 0 Rest <strong>1</strong></li>
|
||||
</ul>
|
||||
<p>Reste von unten nach oben: <strong>1101</strong>. Probe: 8 + 4 + 0 + 1 = 13. Passt.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Hexadezimal als Kurzschrift fürs Binäre</h2>
|
||||
<p>Eine Hexstelle entspricht genau <strong>4 Bit</strong> (einem Nibble), denn 4 Bit liefern 2^4 = 16 mögliche Werte. Deshalb rechnet man zwischen Binär und Hex nibbleweise: Binärzahl von rechts in Vierergruppen teilen und jede Gruppe in eine Hexziffer übersetzen.</p>
|
||||
<ul class="key-list">
|
||||
<li>1011 1100 -> 1011 = 11 = B, 1100 = 12 = C, also <strong>BC</strong>.</li>
|
||||
<li>Umgekehrt: F = 1111, A = 1010, 0 = 0000.</li>
|
||||
<li>A-F stehen für die Dezimalwerte 10-15.</li>
|
||||
<li>Ein Byte (8 Bit) wird immer von genau zwei Hexstellen dargestellt, z. B. FF = 255.</li>
|
||||
</ul>
|
||||
<p class="merksatz"><strong>Merksatz:</strong> 1 Hexstelle = 1 Nibble = 4 Bit, 2 Hexstellen = 1 Byte. Immer in Vierergruppen denken, dann verrutscht nichts.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Bit, Nibble, Byte</h2>
|
||||
<p>Ein <strong>Bit</strong> ist die kleinste Informationseinheit mit den Zuständen 0 oder 1. Vier Bit bilden ein <strong>Nibble</strong>, acht Bit ein <strong>Byte</strong>. Mit n Bit lassen sich 2^n verschiedene Werte unterscheiden - ein Byte kann also 2^8 = 256 Werte (0 bis 255) darstellen.</p>
|
||||
<ul class="key-list">
|
||||
<li>1 Nibble = 4 Bit = eine Hexstelle (16 Werte).</li>
|
||||
<li>1 Byte = 8 Bit = 256 Werte (0-255).</li>
|
||||
<li>Achtung: kleines b meint Bit, großes B meint Byte. 1 Byte = 8 Bit.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Speichereinheiten: binär (KiB) vs. dezimal (KB)</h2>
|
||||
<p>Bei Speichergrößen werden zwei Präfix-Familien verwechselt. <strong>Dezimale Präfixe</strong> (kB, MB, GB, TB) rechnen in 1000er-Schritten, <strong>binäre Präfixe</strong> (KiB, MiB, GiB, TiB) in 1024er-Schritten. Hersteller von Festplatten werben dezimal, Betriebssysteme rechnen traditionell binär - daher zeigt eine "1 TB"-Platte im System nur rund 0,91 TiB.</p>
|
||||
<ul class="key-list">
|
||||
<li>1 kB = 1000 Byte, 1 KiB = 1024 Byte.</li>
|
||||
<li>1 MB = 10^6 = 1.000.000 Byte, 1 MiB = 1024^2 = 1.048.576 Byte.</li>
|
||||
<li>1 GB = 10^9 Byte, 1 GiB = 1024^3 = 1.073.741.824 Byte.</li>
|
||||
</ul>
|
||||
<p class="merksatz"><strong>Merksatz:</strong> Das kleine "i" (KiB, MiB, GiB) steht fuer "binär" und damit für 1024. Ohne "i" ist es dezimal (1000).</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Typische Prüfungsaufgaben</h2>
|
||||
<p>So begegnen dir diese Themen in der AP1 - und so vermeidest du die häufigsten Fehler:</p>
|
||||
<ul class="key-list">
|
||||
<li>Umrechnen zwischen Binär, Dezimal und Hex - immer den Rechenweg notieren, denn dafür gibt es Teilpunkte.</li>
|
||||
<li>Datenrate gegen Datenmenge: "Wie lange dauert ein 100-MByte-Download bei 100 Mbit/s?" Hier zuerst auf eine Einheit bringen (1 Byte = 8 Bit).</li>
|
||||
<li>Kapazitäten: gegebene Größe Schritt für Schritt mit den Faktoren 8 (Bit/Byte) und 1024 (je binärer Stufe) umrechnen.</li>
|
||||
<li>Häufigster Fehler: Bit und Byte verwechseln (Faktor 8) oder dezimale und binäre Präfixe mischen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Prüfungsmodus</h2>
|
||||
<p>Dieser FluxCheck führt dich durch die wichtigsten AP1-Aspekte dieses Themas. Du kannst Antworten prüfen, Fragen überspringen und bekommst am Ende eine Auswertung nach Kategorie.</p>
|
||||
</section>
|
||||
|
||||
<section class="finalflux-test" data-finalflux-test aria-live="polite">
|
||||
<div class="test-meta">
|
||||
<span data-question-counter>Frage 1</span>
|
||||
<span data-score-counter>0 richtig</span>
|
||||
</div>
|
||||
<div class="category-block">
|
||||
<p class="eyebrow">Kategorie</p>
|
||||
<h2 data-category-title>Zahlensysteme & Speicher</h2>
|
||||
<p data-category-hint>AP1-Training</p>
|
||||
</div>
|
||||
<div class="question-card">
|
||||
<h3 data-question-text></h3>
|
||||
<div data-answer-options class="answer-options"></div>
|
||||
<div data-answer-feedback class="fluxcheck-result"></div>
|
||||
</div>
|
||||
<div class="test-actions">
|
||||
<button type="button" data-check-answer>Antwort prüfen</button>
|
||||
<button type="button" data-skip-question class="secondary">Überspringen</button>
|
||||
<button type="button" data-next-question class="secondary" disabled>Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-test-result class="finalflux-panel" hidden>
|
||||
<h2>Auswertung</h2>
|
||||
<p data-result-summary></p>
|
||||
<ul data-category-summary class="key-list"></ul>
|
||||
<div class="finalflux-actions">
|
||||
<button type="button" data-restart-test>Test neu starten</button>
|
||||
<a href="/finalflux">Zurück zur Übersicht</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/finalflux-test.js" data-questions-src="/finalflux/questions/ap1_zahlensysteme.json" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,365 @@
|
||||
{
|
||||
"title": "IP-Adressberechnung",
|
||||
"questions": [
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Adressaufbau, Oktette und Wertebereiche verstehen.",
|
||||
"text": "Wie ist eine IPv4-Adresse grundsätzlich aufgebaut?",
|
||||
"answers": [
|
||||
"Aus 4 Oktetten zu je 8 Bit, jedes Oktett 0 bis 255.",
|
||||
"Aus 6 Oktetten zu je 8 Bit, jedes Oktett 0 bis 255.",
|
||||
"Aus 4 Oktetten zu je 16 Bit, jedes Oktett 0 bis 65535."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Eine IPv4-Adresse ist 32 Bit lang, aufgeteilt in 4 Oktette zu je 8 Bit. Pro Oktett sind 2^8 = 256 Werte (0 bis 255) möglich."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Bits, Byte und Adresslänge unterscheiden.",
|
||||
"text": "Wie viele Bit umfasst ein Oktett und wie viele eine komplette IPv4-Adresse?",
|
||||
"answers": [
|
||||
"Ein Oktett hat 4 Bit, die ganze Adresse 16 Bit.",
|
||||
"Ein Oktett hat 8 Bit, die ganze Adresse 32 Bit.",
|
||||
"Ein Oktett hat 8 Bit, die ganze Adresse 128 Bit."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Ein Oktett besteht aus 8 Bit (1 Byte). Vier Oktette ergeben 4 x 8 = 32 Bit Gesamtlänge bei IPv4. 128 Bit gilt dagegen für IPv6."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Aufgabe der Subnetzmaske erkennen.",
|
||||
"text": "Welche Funktion hat die Subnetzmaske?",
|
||||
"answers": [
|
||||
"Sie legt fest, welcher Host das Standardgateway ist.",
|
||||
"Sie verschlüsselt die übertragenen IP-Pakete.",
|
||||
"Sie trennt die IP-Adresse in Netz- und Hostanteil."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Die Subnetzmaske markiert mit den 1-Bits den Netzanteil und mit den 0-Bits den Hostanteil. Erst dadurch ist erkennbar, in welchem Netz eine Adresse liegt."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Bedeutung des CIDR-Präfixes.",
|
||||
"text": "Was gibt das CIDR-Präfix (z. B. /24) an?",
|
||||
"answers": [
|
||||
"Die Anzahl der gesetzten 1-Bits (Netzbits) in der Subnetzmaske.",
|
||||
"Die Anzahl der Hostbits in der Subnetzmaske.",
|
||||
"Die Anzahl der nutzbaren Hosts im Subnetz."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Das Präfix zählt die führenden 1-Bits der Maske, also die Netzbits. Bei /24 sind 24 Netzbits gesetzt, es verbleiben 32 - 24 = 8 Hostbits."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Präfix in Dezimalmaske umrechnen.",
|
||||
"text": "Welche Subnetzmaske in Dezimalschreibweise entspricht dem Präfix /24?",
|
||||
"answers": [
|
||||
"255.255.0.0",
|
||||
"255.255.255.0",
|
||||
"255.255.255.192"
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "/24 setzt die ersten drei Oktette komplett auf 1 (255.255.255) und lässt das letzte Oktett frei (0). 255.255.0.0 wäre /16, 255.255.255.192 wäre /26."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Rolle des Netzanteils.",
|
||||
"text": "Was kennzeichnet der Netzanteil einer IP-Adresse?",
|
||||
"answers": [
|
||||
"Er nummeriert die einzelnen Geräte innerhalb des Netzes.",
|
||||
"Er gibt die Position des Routers im Internet an.",
|
||||
"Er identifiziert das Subnetz, in dem sich die Adresse befindet."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Der Netzanteil ist bei allen Geräten desselben Subnetzes identisch und bestimmt die Netzzugehörigkeit. Die Unterscheidung einzelner Geräte übernimmt der Hostanteil."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Rolle des Hostanteils.",
|
||||
"text": "Wofür ist der Hostanteil einer IP-Adresse zuständig?",
|
||||
"answers": [
|
||||
"Er identifiziert das einzelne Gerät innerhalb eines Subnetzes.",
|
||||
"Er gibt an, zu welcher Adressklasse die Adresse gehört.",
|
||||
"Er bestimmt, welche Subnetzmaske verwendet wird."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Innerhalb eines Subnetzes ist der Netzanteil gleich; der Hostanteil unterscheidet die Geräte. Aus der Zahl der Hostbits ergibt sich auch die mögliche Hostanzahl."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Private Adressbereiche nach RFC 1918.",
|
||||
"text": "Welcher Adressbereich gehört zu den privaten IPv4-Netzen?",
|
||||
"answers": [
|
||||
"8.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"169.254.0.0/16"
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Privat sind 10.0.0.0/8, 172.16.0.0/12 und 192.168.0.0/16. 8.0.0.0/8 ist öffentlich, 169.254.0.0/16 ist der APIPA-Bereich."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "Aufgabe des Standardgateways.",
|
||||
"text": "Welche Aufgabe hat das Standardgateway?",
|
||||
"answers": [
|
||||
"Es vergibt automatisch IP-Adressen an die Clients.",
|
||||
"Es löst Hostnamen in IP-Adressen auf.",
|
||||
"Es leitet Pakete in Netze außerhalb des eigenen Subnetzes weiter."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Das Standardgateway (meist der Router) ist der Übergang in fremde Netze. Adressvergabe macht DHCP, Namensauflösung macht DNS."
|
||||
},
|
||||
{
|
||||
"category": "IPv4-Grundlagen",
|
||||
"hint": "APIPA-Adressen deuten.",
|
||||
"text": "Ein Client hat die Adresse 169.254.23.7. Worauf deutet das hin?",
|
||||
"answers": [
|
||||
"Der Client konnte keinen DHCP-Server erreichen (APIPA).",
|
||||
"Der Client hat eine gültige öffentliche Internetadresse.",
|
||||
"Der Client befindet sich in einem privaten 172er-Netz."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Der Bereich 169.254.0.0/16 ist APIPA und wird selbst vergeben, wenn kein DHCP-Server antwortet. Das ist ein typischer Hinweis auf ein DHCP-Problem."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Netzadresse berechnen.",
|
||||
"text": "Welche Aussage zur Netzadresse eines Subnetzes ist richtig?",
|
||||
"answers": [
|
||||
"Sie ist die erste Adresse des Subnetzes (alle Hostbits 0).",
|
||||
"Sie ist die letzte Adresse des Subnetzes (alle Hostbits 1).",
|
||||
"Sie ist eine frei an Hosts vergebbare Adresse."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Die Netzadresse hat alle Hostbits auf 0 und ist die erste Adresse des Blocks. Sie wird nicht an Hosts vergeben. Die letzte Adresse (Hostbits alle 1) ist der Broadcast."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Broadcast-Adresse bestimmen.",
|
||||
"text": "Was kennzeichnet die Broadcast-Adresse eines IPv4-Subnetzes?",
|
||||
"answers": [
|
||||
"Sie ist die erste Adresse des Subnetzes mit allen Hostbits 0.",
|
||||
"Sie ist die letzte Adresse des Subnetzes mit allen Hostbits 1.",
|
||||
"Sie ist immer die Adresse des Standardgateways."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Bei der Broadcast-Adresse sind alle Hostbits auf 1 gesetzt; sie ist die höchste und letzte Adresse im Subnetz und adressiert alle Hosts gleichzeitig."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Hostanzahl mit 2^Hostbits - 2 berechnen.",
|
||||
"text": "Wie berechnet man die Anzahl nutzbarer Hosts in einem normalen Subnetz?",
|
||||
"answers": [
|
||||
"2 hoch Hostbits, ohne Abzug.",
|
||||
"2 hoch Netzbits minus 2.",
|
||||
"2 hoch Hostbits minus 2."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Von allen 2^Hostbits Adressen sind Netz- und Broadcast-Adresse nicht für Hosts nutzbar, deshalb 2^Hostbits - 2. Bei 8 Hostbits also 256 - 2 = 254 Hosts."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Adressanzahl und Hosts bei /26.",
|
||||
"text": "Wie viele Gesamtadressen und wie viele nutzbare Hosts hat ein /26-Netz?",
|
||||
"answers": [
|
||||
"64 Adressen, davon 62 nutzbare Hosts.",
|
||||
"32 Adressen, davon 30 nutzbare Hosts.",
|
||||
"64 Adressen, davon 64 nutzbare Hosts."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "/26 hat 32 - 26 = 6 Hostbits, also 2^6 = 64 Adressen. Abzüglich Netz- und Broadcast-Adresse bleiben 64 - 2 = 62 nutzbare Hosts."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Einsatzzweck und Größe von /30.",
|
||||
"text": "Wofür wird ein /30-Netz typischerweise eingesetzt?",
|
||||
"answers": [
|
||||
"Für große Client-Netze mit über 250 Geräten.",
|
||||
"Für Punkt-zu-Punkt-Verbindungen mit genau 2 nutzbaren Hosts.",
|
||||
"Für Netze mit genau 4 nutzbaren Hosts."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "/30 hat 2 Hostbits, also 2^2 = 4 Adressen und 4 - 2 = 2 nutzbare Hosts. Das passt genau zu einer Punkt-zu-Punkt-Verbindung zwischen zwei Routern."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Blockgröße aus der Maske ableiten.",
|
||||
"text": "Wie ergibt sich die Blockgröße (Schrittweite) im relevanten Oktett?",
|
||||
"answers": [
|
||||
"Aus 255 geteilt durch die Anzahl der Subnetze.",
|
||||
"Aus der Anzahl der Netzbits direkt als Zahl.",
|
||||
"Aus 256 minus dem Maskenwert des relevanten Oktetts."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Die Blockgröße ist 256 minus Maskenwert im betroffenen Oktett. Bei Maske 192 ist das 256 - 192 = 64, die Subnetze beginnen also bei .0, .64, .128, .192."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Zusammenhang Hostbits und Adressanzahl.",
|
||||
"text": "Welche Aussage über die Hostbits ist korrekt?",
|
||||
"answers": [
|
||||
"Aus der Anzahl der Hostbits ergibt sich mit 2^Hostbits die Adressanzahl je Subnetz.",
|
||||
"Je mehr Hostbits, desto kleiner wird das Subnetz.",
|
||||
"Die Hostbits stehen immer im ersten Oktett der Adresse."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Die Hostbits sind die mit 0 maskierten Bits. 2^Hostbits ergibt die Adressanzahl je Subnetz; mehr Hostbits bedeuten ein größeres Subnetz."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Wirkung der Netzbits auf die Subnetzgröße.",
|
||||
"text": "Welche Auswirkung hat eine größere Anzahl Netzbits (höheres Präfix)?",
|
||||
"answers": [
|
||||
"Die Subnetze werden größer und enthalten mehr Hosts.",
|
||||
"Die Subnetze werden kleiner und enthalten weniger Hosts.",
|
||||
"Die Anzahl der Hosts pro Subnetz bleibt unverändert."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Mehr Netzbits bedeuten weniger Hostbits. Von /24 auf /26 sinkt die Hostzahl von 254 auf 62 - die Subnetze werden also kleiner."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Subnetzgrenzen prüfen.",
|
||||
"text": "In welchem Subnetz liegt die Adresse 192.168.1.130/26?",
|
||||
"answers": [
|
||||
"Im Subnetz 192.168.1.0/26.",
|
||||
"Im Subnetz 192.168.1.64/26.",
|
||||
"Im Subnetz 192.168.1.128/26."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Bei /26 ist die Blockgröße 64, die Netze starten bei .0, .64, .128, .192. Die .130 liegt im Block .128 bis .191, also im Subnetz 192.168.1.128/26."
|
||||
},
|
||||
{
|
||||
"category": "Subnetting",
|
||||
"hint": "Nicht vergebbare Adressen kennen.",
|
||||
"text": "Warum kann die Broadcast-Adresse nicht als Hostadresse vergeben werden?",
|
||||
"answers": [
|
||||
"Weil sie reserviert ist, um alle Hosts im Subnetz gleichzeitig anzusprechen.",
|
||||
"Weil sie immer dem Standardgateway gehört.",
|
||||
"Weil sie als einzige Adresse für DHCP reserviert ist."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Die Broadcast-Adresse (alle Hostbits 1) adressiert alle Hosts des Subnetzes zugleich und ist deshalb reserviert. Sie zählt - wie die Netzadresse - nicht zu den nutzbaren Hosts."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Auswirkungen einer falschen Subnetzmaske.",
|
||||
"text": "Welche Folge kann eine falsch konfigurierte Subnetzmaske haben?",
|
||||
"answers": [
|
||||
"Hosts werden scheinbar dem falschen Netz zugeordnet und sind nicht erreichbar.",
|
||||
"Die MAC-Adressen der Geräte ändern sich.",
|
||||
"Der DNS-Server wird automatisch deaktiviert."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Eine falsche Maske verschiebt die Grenze zwischen Netz- und Hostanteil. Geräte glauben dann, in einem anderen Netz zu sein, und die Kommunikation scheitert."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Symptome eines falschen Gateways.",
|
||||
"text": "Ein PC erreicht Geräte im eigenen Netz, aber keine fremden Netze. Was ist die wahrscheinlichste Ursache?",
|
||||
"answers": [
|
||||
"Die Subnetzmaske ist auf 255.255.255.255 gesetzt.",
|
||||
"Das Standardgateway ist falsch oder fehlt.",
|
||||
"Die Netzwerkkarte ist physisch defekt."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Lokale Kommunikation funktioniert ohne Gateway. Für fremde Netze ist das Gateway nötig; fehlt oder stimmt es nicht, sind nur externe Ziele unerreichbar."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "DHCP-Probleme erkennen.",
|
||||
"text": "Woran erkennt man in einer Handlungssituation ein DHCP-Problem?",
|
||||
"answers": [
|
||||
"Der Host hat eine feste, korrekt geroutete öffentliche Adresse.",
|
||||
"Der Host kann sowohl per IP als auch per Name pingen.",
|
||||
"Der Host bezieht keine gültige Adresse und nutzt eine 169.254-Adresse."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Bekommt ein Client keine Antwort vom DHCP-Server, vergibt er sich selbst eine APIPA-Adresse (169.254.x.x). Das ist ein klares Indiz für ein DHCP-Problem."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Folgen doppelt vergebener IP-Adressen.",
|
||||
"text": "Was passiert bei einer Doppelvergabe derselben IP-Adresse an zwei Hosts?",
|
||||
"answers": [
|
||||
"Es entsteht ein Adresskonflikt, beide Hosts kommunizieren fehlerhaft.",
|
||||
"Die Hosts teilen sich die Bandbreite problemlos auf.",
|
||||
"Der Router weist automatisch eine neue Maske zu."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Zwei Geräte mit identischer IP führen zu einem Adresskonflikt; Pakete werden falsch zugestellt und die Verbindungen brechen sporadisch ab."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "DNS gegen IP-Erreichbarkeit abgrenzen.",
|
||||
"text": "Ein Host ist per IP erreichbar, aber nicht per Hostnamen. Worauf deutet das hin?",
|
||||
"answers": [
|
||||
"Auf eine defekte Subnetzmaske.",
|
||||
"Auf ein Problem mit der Namensauflösung (DNS).",
|
||||
"Auf eine fehlende Broadcast-Adresse."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Funktioniert ping per IP, aber nicht per Name, ist die IP-Verbindung in Ordnung. Der Fehler liegt dann in der Namensauflösung, also beim DNS."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Strukturiertes Vorgehen bei der Fehlersuche.",
|
||||
"text": "Was sollte man bei fehlender lokaler Erreichbarkeit zuerst prüfen?",
|
||||
"answers": [
|
||||
"Die externe DNS-Konfiguration des Internetproviders.",
|
||||
"Die Routing-Tabellen aller fremden Netze.",
|
||||
"Physische Verbindung, IP-Adresse und Subnetzmaske des Hosts."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Man arbeitet von unten nach oben: erst Link/Kabel, dann die eigene IP-Konfiguration und Maske. Gateway und DNS sind erst für fremde Netze relevant."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Routingprobleme einordnen.",
|
||||
"text": "Woran erkennt man, dass ein Erreichbarkeitsproblem ein Routingproblem ist?",
|
||||
"answers": [
|
||||
"Nur fremde Netze sind unerreichbar, das lokale Netz funktioniert.",
|
||||
"Auch Geräte im eigenen Subnetz sind nicht erreichbar.",
|
||||
"Der Host bezieht eine APIPA-Adresse aus 169.254.0.0/16."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Routing betrifft nur den Weg in fremde Netze. Klappt die lokale Kommunikation, scheitert aber der Zugriff auf andere Netze, fehlt eine Route oder das Gateway."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Nutzen einer Netzdokumentation.",
|
||||
"text": "Warum ist eine saubere Adressplanung und Dokumentation wichtig?",
|
||||
"answers": [
|
||||
"Sie ersetzt die Notwendigkeit einer Subnetzmaske.",
|
||||
"Sie vermeidet Adressüberschneidungen und doppelte Vergaben.",
|
||||
"Sie erhöht automatisch die Übertragungsgeschwindigkeit."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Ein gepflegter Adressplan zeigt belegte und freie Bereiche und verhindert so Doppelvergaben sowie Überschneidungen von Subnetzen."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Sinn von DHCP-Reservierungen.",
|
||||
"text": "Warum erhalten Server und Drucker oft feste oder reservierte IP-Adressen?",
|
||||
"answers": [
|
||||
"Weil sie sonst keine Subnetzmaske benötigen.",
|
||||
"Weil dynamische Adressen für sie technisch unmöglich sind.",
|
||||
"Weil sie unter einer gleichbleibenden Adresse zuverlässig erreichbar sein müssen."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Dienste wie Server und Drucker müssen dauerhaft unter derselben Adresse erreichbar sein. Das erreicht man über statische Adressen oder DHCP-Reservierungen."
|
||||
},
|
||||
{
|
||||
"category": "Fehleranalyse IP",
|
||||
"hint": "Saubere Vorgehensweise in der Prüfung.",
|
||||
"text": "Welches Vorgehen ist bei IP-Rechenaufgaben in der AP1 sinnvoll?",
|
||||
"answers": [
|
||||
"Rechenschritte (Maske, Netz, Broadcast, Hosts) nachvollziehbar notieren und prüfen.",
|
||||
"Nur das Endergebnis ohne Rechenweg angeben.",
|
||||
"Die Subnetzmaske grundsätzlich ignorieren und schätzen."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Ein dokumentierter Rechenweg von Maske über Netz- und Broadcast-Adresse bis zur Hostzahl gibt Teilpunkte und macht Flüchtigkeitsfehler leichter auffindbar."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
{
|
||||
"title": "Zahlensysteme & Speicher",
|
||||
"questions": [
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||
"text": "Welche Aussage beschreibt das Binärsystem korrekt?",
|
||||
"answers": [
|
||||
"Es nutzt nur die Ziffern 0 und 1 und basiert auf Potenzen von 2.",
|
||||
"Es nutzt die Ziffern 0 bis 9 und basiert auf Potenzen von 10.",
|
||||
"Es nutzt die Ziffern 0 bis 7 und basiert auf Potenzen von 8."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Das Binärsystem hat die Basis 2 und kennt nur 0 und 1; die anderen Optionen beschreiben das Dezimal- bzw. das Oktalsystem."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Jede Stelle hat einen festen Stellenwert (Potenz der Basis).",
|
||||
"text": "Welche Stellenwerte haben die ersten vier Stellen einer Binärzahl (von rechts nach links)?",
|
||||
"answers": [
|
||||
"1, 10, 100, 1000",
|
||||
"1, 2, 4, 8",
|
||||
"2, 4, 6, 8"
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Im Binärsystem verdoppelt sich der Stellenwert pro Stelle: 2^0=1, 2^1=2, 2^2=4, 2^3=8. Die anderen Reihen sind Dezimal-Stellenwerte bzw. einfach geradzahlige Vielfache."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Stellenwertmethode: gesetzte Bits mit ihrer Zweierpotenz multiplizieren.",
|
||||
"text": "Wie rechnet man die Binärzahl 1011 korrekt in eine Dezimalzahl um?",
|
||||
"answers": [
|
||||
"1+0+1+1 = 3, da man einfach die gesetzten Einsen zählt.",
|
||||
"1011 wird direkt als 1011 dezimal gelesen.",
|
||||
"8 + 0 + 2 + 1 = 11, durch Addieren der Zweierpotenzen gesetzter Bits."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Man addiert die Zweierpotenzen der gesetzten Bits: 2^3+2^1+2^0 = 8+2+1 = 11. Stellen werden nicht ignoriert und Binär ist nicht gleich Dezimal."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Kleinste Einheit der Information.",
|
||||
"text": "Was ist ein Bit?",
|
||||
"answers": [
|
||||
"Die kleinste Informationseinheit mit genau zwei Zuständen (0 oder 1).",
|
||||
"Eine Einheit aus 8 Zuständen, die ein Zeichen speichert.",
|
||||
"Die Anzahl der Stellen einer Dezimalzahl."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Ein Bit (binary digit) kann nur die Werte 0 oder 1 annehmen; 8 Bit ergeben erst ein Byte, das z. B. ein Zeichen speichert."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Halbes Byte - Grundlage der Hex-Darstellung.",
|
||||
"text": "Aus wie vielen Bit besteht ein Nibble?",
|
||||
"answers": [
|
||||
"Aus 8 Bit.",
|
||||
"Aus 4 Bit.",
|
||||
"Aus 2 Bit."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Ein Nibble ist ein halbes Byte, also 4 Bit, und entspricht damit genau einer Hexadezimalstelle. 8 Bit wären ein ganzes Byte."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "8 Bit fassen zusammen.",
|
||||
"text": "Wie viele unterschiedliche Werte kann ein Byte (8 Bit) darstellen?",
|
||||
"answers": [
|
||||
"8 Werte, also einen pro Bit.",
|
||||
"255 Werte, von 1 bis 255.",
|
||||
"256 Werte, von 0 bis 255."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Mit 8 Bit gibt es 2^8 = 256 Kombinationen, also Werte von 0 bis 255. 255 ist nur der höchste Wert, nicht die Anzahl."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "2^10 ist ein wichtiger Merkwert.",
|
||||
"text": "Welchen Dezimalwert hat 2^10?",
|
||||
"answers": [
|
||||
"1024",
|
||||
"1000",
|
||||
"512"
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "2^10 = 1024 und ist die Grundlage der binären Speicherpräfixe (KiB). 1000 ist 10^3 (dezimal) und 512 ist 2^9."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Nullen ganz links vor der ersten Eins.",
|
||||
"text": "Welche Wirkung haben führende Nullen bei einer Zahl wie 0011?",
|
||||
"answers": [
|
||||
"Sie verdoppeln den Wert pro zusätzlicher Null.",
|
||||
"Sie ändern den Zahlenwert nicht, dienen aber dem Auffüllen der Darstellung.",
|
||||
"Sie machen die Zahl ungültig und müssen entfernt werden."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Führende Nullen haben den Stellenwert 0 und ändern den Wert nicht; man nutzt sie nur, um z. B. auf volle 4er- oder 8er-Gruppen aufzufüllen."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Anderes Wort für Binärzahl.",
|
||||
"text": "Was bezeichnet der Begriff Dualzahl?",
|
||||
"answers": [
|
||||
"Eine Zahl mit genau zwei Ziffern.",
|
||||
"Eine Zahl im Hexadezimalsystem.",
|
||||
"Eine Zahl im Binärsystem (Basis 2)."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Dualzahl ist das deutsche Synonym für Binärzahl, also eine Zahl zur Basis 2 - es geht nicht um die Anzahl der Ziffern."
|
||||
},
|
||||
{
|
||||
"category": "Binär & Dezimal",
|
||||
"hint": "Dezimal nach Binär per fortgesetzter Division.",
|
||||
"text": "Mit welchem Verfahren wandelt man eine Dezimalzahl in eine Binärzahl um?",
|
||||
"answers": [
|
||||
"Fortlaufende Division durch 2; die Reste von unten nach oben gelesen ergeben die Binärzahl.",
|
||||
"Fortlaufende Division durch 10; die Reste ergeben die Binärzahl.",
|
||||
"Multiplikation der Zahl mit 2, bis sie gerade wird."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Beim Divisionsrestverfahren teilt man wiederholt durch die Zielbasis 2 und liest die Reste rückwärts. Division durch 10 wäre der Weg ins Dezimalsystem."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Basis 16, kompakte Schreibweise für Bytes.",
|
||||
"text": "Welche Aussage beschreibt das Hexadezimalsystem korrekt?",
|
||||
"answers": [
|
||||
"Es nutzt die Basis 16 mit den Ziffern 0 bis 9 und den Buchstaben A bis F.",
|
||||
"Es nutzt die Basis 12 mit den Ziffern 0 bis 9 und den Buchstaben A und B.",
|
||||
"Es nutzt die Basis 16, aber nur die Buchstaben A bis F ohne Ziffern."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Das Hexadezimalsystem hat 16 Symbole: 0-9 für 0-9 und A-F für 10-15. Es gibt 16 Symbole, nicht 12, und die Ziffern 0-9 gehören dazu."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Buchstaben stehen für Werte oberhalb von 9.",
|
||||
"text": "Für welche Dezimalwerte stehen die Hex-Ziffern A bis F?",
|
||||
"answers": [
|
||||
"Für die Werte 11 bis 16.",
|
||||
"Für die Werte 10 bis 15.",
|
||||
"Für die Werte 9 bis 14."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "A=10, B=11, C=12, D=13, E=14, F=15. Die Zählung beginnt bei 10 (nach der Ziffer 9) und endet bei 15."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Eine Hexstelle deckt eine feste Anzahl Bit ab.",
|
||||
"text": "Wie vielen Bit entspricht genau eine Hexadezimalstelle?",
|
||||
"answers": [
|
||||
"8 Bit, also einem ganzen Byte.",
|
||||
"2 Bit.",
|
||||
"4 Bit, also einem Nibble."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Eine Hexstelle kann 16 Werte annehmen (2^4), entspricht also genau 4 Bit. Ein Byte (8 Bit) wird daher von zwei Hexstellen dargestellt."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Von rechts in Gruppen aufteilen.",
|
||||
"text": "Wie wandelt man eine Binärzahl am schnellsten in Hexadezimal um?",
|
||||
"answers": [
|
||||
"Man teilt sie von rechts in Vierergruppen und übersetzt jede Gruppe in eine Hexziffer.",
|
||||
"Man teilt sie von rechts in Dreiergruppen und übersetzt jede Gruppe in eine Hexziffer.",
|
||||
"Man addiert alle gesetzten Bits und schreibt die Summe als Hexziffer."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Da eine Hexstelle 4 Bit entspricht, bildet man Vierergruppen von rechts. Dreiergruppen gehören zum Oktalsystem."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Höchster Wert eines Bytes.",
|
||||
"text": "Welchen Dezimalwert hat die Hexadezimalzahl FF?",
|
||||
"answers": [
|
||||
"256",
|
||||
"255",
|
||||
"16"
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "FF = 15*16 + 15 = 240 + 15 = 255, der größte Wert eines Bytes. 256 ist bereits die Anzahl der Kombinationen, nicht der höchste Wert."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Wo begegnet einem Hex im IT-Alltag?",
|
||||
"text": "In welchem Zusammenhang wird Hexadezimal in der Praxis besonders häufig genutzt?",
|
||||
"answers": [
|
||||
"Nur in der reinen Mathematik, nicht in der Informatik.",
|
||||
"Ausschließlich zur Darstellung von Dezimalbrüchen.",
|
||||
"Bei Speicheradressen, MAC-Adressen, IPv6 und Farbcodes."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Hex stellt Bytes sehr kompakt dar und wird daher bei Speicher-/MAC-Adressen, IPv6 und RGB-Farbcodes verwendet - gerade in der Informatik."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Stellenwertsystem zur Basis 16.",
|
||||
"text": "Welchen Stellenwert hat die zweite Hexstelle von rechts?",
|
||||
"answers": [
|
||||
"16^1, also 16.",
|
||||
"2^4, also den Wert eines einzelnen Bits.",
|
||||
"10^1, also 10."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Die Stellenwerte im Hexsystem sind Potenzen von 16: 16^0=1, 16^1=16, 16^2=256. Der Stellenwert 10 gehört ins Dezimalsystem."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Stellenwert mal Ziffer, dann addieren.",
|
||||
"text": "Welchen Dezimalwert hat die Hexadezimalzahl 2F?",
|
||||
"answers": [
|
||||
"215, da 2 und F einfach nebeneinander gestellt werden.",
|
||||
"31, also 16 + 15.",
|
||||
"47, also 2*16 + 15."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "2F = 2*16 + 15 = 32 + 15 = 47. Die führende 2 hat den Stellenwert 16, nicht 1."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Jedes Nibble einzeln übersetzen.",
|
||||
"text": "Welche Hexadezimalzahl ergibt die Binärzahl 1011 1100?",
|
||||
"answers": [
|
||||
"AC, da 1011 = A und 1100 = C.",
|
||||
"BC, da 1011 = B und 1100 = C.",
|
||||
"CB, da 1100 = C und 1011 = B."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "1011 = 11 = B und 1100 = 12 = C, also BC. 1011 ist B (11), nicht A (10), und die Reihenfolge der Gruppen bleibt erhalten."
|
||||
},
|
||||
{
|
||||
"category": "Hexadezimal",
|
||||
"hint": "Gruppierung verhindert Fehler.",
|
||||
"text": "Warum bilden Profis beim Umrechnen zwischen Binär und Hex zuerst Vierergruppen?",
|
||||
"answers": [
|
||||
"Weil eine Hexstelle genau 4 Bit entspricht und so Verrutschen vermieden wird.",
|
||||
"Weil Vierergruppen die Zahl automatisch verdoppeln.",
|
||||
"Weil das Dezimalsystem ebenfalls auf Vierergruppen beruht."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Da 1 Hexstelle = 4 Bit ist, lässt sich jede Vierergruppe direkt übersetzen; das verhindert Stellenfehler. Die Gruppierung verändert den Wert nicht."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Dezimalpräfix kB vs. Binärpräfix KiB.",
|
||||
"text": "Worin unterscheiden sich kB und KiB?",
|
||||
"answers": [
|
||||
"kB steht für 1024 Byte, KiB für 1000 Byte.",
|
||||
"kB und KiB bezeichnen beide exakt 1000 Byte.",
|
||||
"kB steht für 1000 Byte (dezimal), KiB für 1024 Byte (binär)."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Das dezimale Präfix k bedeutet 1000, das binäre Präfix Ki bedeutet 1024. Die Zuordnung darf nicht vertauscht werden."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Mega dezimal gegen Mebi binär.",
|
||||
"text": "Welche Aussage zu MB und MiB ist korrekt?",
|
||||
"answers": [
|
||||
"1 MB = 1.000.000 Byte (dezimal), 1 MiB = 1.048.576 Byte (binär, 1024^2).",
|
||||
"1 MB = 1.048.576 Byte, 1 MiB = 1.000.000 Byte.",
|
||||
"MB und MiB sind nur zwei Schreibweisen für exakt denselben Wert."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "MB ist dezimal (10^6 = 1.000.000), MiB ist binär (1024^2 = 1.048.576). Die Werte sind unterschiedlich, nicht identisch."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Gibi ist 1024^3.",
|
||||
"text": "Wie viele Byte entspricht 1 GiB?",
|
||||
"answers": [
|
||||
"1.000.000.000 Byte, also 10^9.",
|
||||
"1.000.000 Byte, also 10^6.",
|
||||
"1.073.741.824 Byte, also 1024^3."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Das binäre Präfix Gi bedeutet 1024^3 = 1.073.741.824 Byte. 10^9 wäre dagegen 1 GB (dezimal)."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Grundbeziehung der kleinsten Einheiten.",
|
||||
"text": "Wie viele Bit ergeben 1 Byte?",
|
||||
"answers": [
|
||||
"8 Bit.",
|
||||
"4 Bit.",
|
||||
"1024 Bit."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "1 Byte besteht definitionsgemäß aus 8 Bit. 4 Bit wären ein Nibble, 1024 ist der Umrechnungsfaktor für binäre Präfixe."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Großes B gegen kleines b beachten.",
|
||||
"text": "Warum sind 100 Mbit/s nicht dasselbe wie 100 MByte/s?",
|
||||
"answers": [
|
||||
"Weil Mbit/s immer größer ist als MByte/s.",
|
||||
"Weil 1 Byte = 8 Bit ist, sind 100 Mbit/s nur etwa 12,5 MByte/s.",
|
||||
"Weil bit und Byte identisch sind und sich nur die Schreibweise unterscheidet."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "Übertragungsraten werden meist in Bit/s angegeben; geteilt durch 8 ergeben sich die Byte/s. 100 Mbit/s entsprechen daher rund 12,5 MByte/s."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Hersteller rechnen anders als das Betriebssystem.",
|
||||
"text": "Warum zeigt Windows bei einer als '1 TB' verkauften Festplatte weniger Kapazität an?",
|
||||
"answers": [
|
||||
"Weil ein Teil der Platte grundsätzlich defekt ist.",
|
||||
"Weil Windows die Hälfte für das Betriebssystem reserviert.",
|
||||
"Weil der Hersteller dezimal (10^12) rechnet, das System aber binär (1024^4 = TiB) anzeigt."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Hersteller nutzen das dezimale TB (10^12), Windows rechnet aber binär (TiB, 1024^4) und zeigt deshalb nur rund 0,909 TiB an - es fehlt nichts."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Wie das Betriebssystem Kapazitäten interpretiert.",
|
||||
"text": "Wie interpretieren viele Betriebssysteme das Präfix in Anzeigen wie '512 MB'?",
|
||||
"answers": [
|
||||
"Sie rechnen intern binär (1024er-Schritte), beschriften aber oft mit dezimalen Kürzeln.",
|
||||
"Sie rechnen ausschließlich dezimal mit exakt 1000er-Schritten.",
|
||||
"Sie ignorieren das Präfix und zeigen reine Bitzahlen an."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Klassisch rechnen Systeme mit 1024 (binär), verwenden aber das dezimale Kürzel MB statt MiB - daher die Verwirrung bei Kapazitätsangaben."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Nachvollziehbarkeit zählt in der Prüfung.",
|
||||
"text": "Worauf solltest du bei Rundungen in einer AP1-Speicheraufgabe achten?",
|
||||
"answers": [
|
||||
"Immer auf ganze Zahlen abrunden, egal was gefragt ist.",
|
||||
"Den Rechenweg mit Zwischenschritten und Einheit nachvollziehbar dokumentieren.",
|
||||
"Rundungen ganz weglassen, da nur das Endergebnis zählt."
|
||||
],
|
||||
"correct": 1,
|
||||
"explanation": "In der Prüfung gibt es Teilpunkte für einen nachvollziehbaren Rechenweg; Einheiten und Rundungsschritte gehören dokumentiert, nicht weggelassen."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Klassische Stolperfalle bei der Einheit.",
|
||||
"text": "Welcher Fehler ist beim Rechnen mit Speicher- und Datenraten besonders typisch?",
|
||||
"answers": [
|
||||
"Bit und Byte zu verwechseln und so einen Faktor 8 zu übersehen.",
|
||||
"Sekunden und Minuten gleichzusetzen.",
|
||||
"Hexadezimal- statt Dezimalziffern zu verwenden."
|
||||
],
|
||||
"correct": 0,
|
||||
"explanation": "Der häufigste Fehler ist die Verwechslung von Bit und Byte (Faktor 8), z. B. bei Download-Zeiten oder Übertragungsraten."
|
||||
},
|
||||
{
|
||||
"category": "Speichereinheiten",
|
||||
"hint": "Saubere Struktur statt Kopfrechnen.",
|
||||
"text": "Wie gehst du eine Speicherberechnung in der Prüfung am sichersten an?",
|
||||
"answers": [
|
||||
"Alles im Kopf rechnen und nur das Ergebnis hinschreiben.",
|
||||
"Sofort runden und Einheiten erst am Ende ergänzen.",
|
||||
"Gegebene Größen mit Einheit notieren und Schritt für Schritt mit den passenden Faktoren (8, 1024) umrechnen."
|
||||
],
|
||||
"correct": 2,
|
||||
"explanation": "Wer Werte mit Einheit notiert und schrittweise mit den richtigen Faktoren (8 Bit/Byte, 1024 je binärer Stufe) umrechnet, vermeidet Einheitenfehler und sichert Teilpunkte."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
{
|
||||
"title": "Backuparten",
|
||||
"questions": [
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Was ist das Hauptziel eines Backups?",
|
||||
"answers": ["Daten nach Verlust wiederherstellen können", "Die Lese- und Schreibgeschwindigkeit der Festplatte erhöhen", "Speicherplatz auf dem Produktivsystem dauerhaft freigeben"],
|
||||
"correct": 0,
|
||||
"explanation": "Ein Backup sichert eine zweite Kopie, um nach Verlust, Fehlern oder Ausfällen den Datenbestand wiederherzustellen. Performance oder Speicherentlastung sind nicht sein Zweck."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Was bedeutet Restore?",
|
||||
"answers": ["Das Anlegen einer neuen Sicherung", "Wiederherstellung von Daten aus einer Sicherung", "Das Verschieben der Daten in ein RAID-Verbund"],
|
||||
"correct": 1,
|
||||
"explanation": "Restore ist das Zurückspielen bereits gesicherter Daten. Das Anlegen einer Sicherung heißt dagegen Backup."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Warum sollte ein Restore regelmäßig getestet werden?",
|
||||
"answers": ["Damit sicher ist, dass die Sicherung im Ernstfall funktioniert", "Damit der benötigte Speicherplatz sinkt", "Weil ein Test das Backup automatisch verschlüsselt"],
|
||||
"correct": 0,
|
||||
"explanation": "Eine Sicherung ist nur dann wertvoll, wenn die Wiederherstellung tatsächlich klappt. Defekte oder unvollständige Backups fallen oft erst beim Restore-Test auf."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Was beschreibt der RPO (Recovery Point Objective)?",
|
||||
"answers": ["Die maximal tolerierbare Wiederherstellungszeit nach einem Ausfall", "Den maximal tolerierbaren Datenverlust, gemessen als Zeitspanne", "Die maximale Anzahl gleichzeitiger Restore-Vorgänge"],
|
||||
"correct": 1,
|
||||
"explanation": "Der RPO legt fest, wie alt die wiederhergestellten Daten höchstens sein dürfen, und bestimmt damit den Sicherungsabstand. Die zulässige Ausfalldauer beschreibt dagegen der RTO."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Was beschreibt der RTO (Recovery Time Objective)?",
|
||||
"answers": ["Den maximal tolerierbaren Datenverlust in Zeit", "Wie oft ein Vollbackup erstellt werden muss", "Die maximal tolerierbare Wiederherstellungszeit nach einem Ausfall"],
|
||||
"correct": 2,
|
||||
"explanation": "Der RTO gibt vor, wie schnell ein System nach einem Ausfall wieder verfügbar sein muss. Den zulässigen Datenverlust beschreibt dagegen der RPO."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Warum ist eine Backup-Dokumentation wichtig?",
|
||||
"answers": ["Damit Zuständigkeiten, Zeitpunkte und Wiederherstellung nachvollziehbar sind", "Weil sie regelmäßige Restore-Tests überflüssig macht", "Weil sie den Speicherbedarf der Sicherungen verringert"],
|
||||
"correct": 0,
|
||||
"explanation": "Dokumentation macht Sicherung und Restore planbar und im Notfall auch durch Vertretungen durchführbar. Restore-Tests ersetzt sie aber nicht."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Welche Daten sollten vorrangig gesichert werden?",
|
||||
"answers": ["Temporäre Dateien und der Browser-Cache", "Ausschließlich Daten, die kleiner als 1 GB sind", "Geschäftskritische Daten und notwendige Konfigurationen"],
|
||||
"correct": 2,
|
||||
"explanation": "Der Sicherungsumfang richtet sich nach Kritikalität und Wiederherstellungsbedarf, nicht nach Dateigröße. Temporäre Daten lassen sich meist neu erzeugen."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Was ist ein Backup-Fenster?",
|
||||
"answers": ["Zeitraum, in dem Sicherungen durchgeführt werden", "Die maximale Größe einer einzelnen Sicherungsdatei", "Der freie Speicherplatz auf dem Sicherungsmedium"],
|
||||
"correct": 0,
|
||||
"explanation": "Das Backup-Fenster ist die geplante Zeitspanne für die Sicherung, oft in Zeiten geringer Systemlast wie nachts. Es bezeichnet keine Datei- oder Speichergröße."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Was ist bei personenbezogenen Daten in Backups wichtig?",
|
||||
"answers": ["Sie müssen nach der DSGVO nicht gesichert werden", "Schutz, Zugriffsbeschränkung und ein Löschkonzept beachten", "Sie dürfen nur unverschlüsselt gespeichert werden"],
|
||||
"correct": 1,
|
||||
"explanation": "Auch Sicherungen können personenbezogene Daten enthalten, daher gelten Datenschutz, Zugriffsschutz und Löschpflichten. Verschlüsselung ist dabei erwünscht, nicht verboten."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Grundlagen",
|
||||
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||
"text": "Welche Aussage zu Backup-Konzepten ist korrekt?",
|
||||
"answers": ["Ein einzelnes, ungetestetes Backup genügt in jedem Fall", "Ein Backup ersetzt Firewall und Virenschutz vollständig", "Backups müssen zum Risiko und zum Wiederherstellungsbedarf passen"],
|
||||
"correct": 2,
|
||||
"explanation": "Ein gutes Konzept richtet Sicherungsart, Häufigkeit und Tests an Risiken und Anforderungen aus. Backups sind Teil der IT-Sicherheit, ersetzen aber keine Schutzmaßnahmen."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Was sichert ein Vollbackup?",
|
||||
"answers": ["Alle ausgewählten Daten", "Nur die seit dem letzten Vollbackup geänderten Daten", "Nur die seit der letzten Sicherung geänderten Daten"],
|
||||
"correct": 0,
|
||||
"explanation": "Ein Vollbackup enthält den kompletten ausgewählten Datenbestand. Nur die Änderungen sichern dagegen differentielle bzw. inkrementelle Backups."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Worauf bezieht sich ein differentielles Backup?",
|
||||
"answers": ["Auf das jeweils letzte Backup, egal welcher Art", "Auf das letzte Vollbackup und sichert alle seitdem geänderten Daten", "Es sichert stets den gesamten Datenbestand neu"],
|
||||
"correct": 1,
|
||||
"explanation": "Differentielle Backups sichern alle Änderungen seit dem letzten Vollbackup und wachsen daher bis zum nächsten Vollbackup an. Der Bezug auf das letzte beliebige Backup kennzeichnet inkrementelle Sicherungen."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Worauf bezieht sich ein inkrementelles Backup?",
|
||||
"answers": ["Auf das letzte Vollbackup", "Auf das jeweils letzte Backup, egal ob voll oder inkrementell", "Es sichert ausschließlich Betriebssystemdateien"],
|
||||
"correct": 1,
|
||||
"explanation": "Inkrementelle Backups sichern nur die Änderungen seit der letzten Sicherung beliebiger Art. Der feste Bezug auf das letzte Vollbackup kennzeichnet dagegen differentielle Sicherungen."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Welche Sicherungsart benötigt pro Durchlauf meist am meisten Speicherplatz?",
|
||||
"answers": ["Das inkrementelle Backup", "Das differentielle Backup", "Das Vollbackup"],
|
||||
"correct": 2,
|
||||
"explanation": "Ein Vollbackup sichert den gesamten ausgewählten Datenbestand und ist daher am größten. Differentielle und besonders inkrementelle Sicherungen speichern nur Änderungen."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Welche Sicherungsart spart im laufenden Betrieb meist am meisten Speicherplatz?",
|
||||
"answers": ["Das inkrementelle Backup", "Das tägliche Vollbackup", "Das differentielle Backup gegenüber inkrementell"],
|
||||
"correct": 0,
|
||||
"explanation": "Inkrementelle Backups sichern nur die Änderungen seit der letzten Sicherung und sind deshalb am kleinsten. Differentielle Sicherungen wachsen dagegen bis zum nächsten Vollbackup an."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Was wird für einen differentiellen Restore benötigt?",
|
||||
"answers": ["Sämtliche differentiellen Backups seit dem Vollbackup", "Das letzte Vollbackup und das letzte differentielle Backup", "Nur das letzte differentielle Backup"],
|
||||
"correct": 1,
|
||||
"explanation": "Da ein differentielles Backup alle Änderungen seit dem letzten Vollbackup enthält, genügen Vollbackup plus das letzte differentielle Backup. Die Zwischenstände werden nicht benötigt."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Was wird für einen inkrementellen Restore typischerweise benötigt?",
|
||||
"answers": ["Nur das letzte inkrementelle Backup", "Nur das Vollbackup ohne weitere Sicherungen", "Das Vollbackup und alle inkrementellen Backups der Kette in Reihenfolge"],
|
||||
"correct": 2,
|
||||
"explanation": "Jedes inkrementelle Backup baut auf dem vorherigen auf, daher muss die gesamte Kette ab dem Vollbackup vollständig sein. Fehlt ein Glied, scheitert der Restore."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Was ist ein Nachteil langer inkrementeller Ketten?",
|
||||
"answers": ["Der Restore dauert länger und ist anfälliger, weil jedes Kettenglied benötigt wird", "Sie belegen mehr Speicher als tägliche Vollbackups", "Sie können kein Vollbackup als Ausgangspunkt nutzen"],
|
||||
"correct": 0,
|
||||
"explanation": "Ist nur ein Glied der Kette defekt oder fehlt, kann der gesamte Restore scheitern, und das Zusammensetzen vieler Stände kostet Zeit. Im Speicherbedarf sind sie dagegen sparsam."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Welche Strategie ist in der Praxis häufig sinnvoll?",
|
||||
"answers": ["Ausschließlich täglich ein Vollbackup ohne Zwischensicherungen", "Regelmäßige Vollbackups plus tägliche inkrementelle oder differentielle Sicherungen", "Nur einmal jährlich ein einzelnes Vollbackup"],
|
||||
"correct": 1,
|
||||
"explanation": "Die Kombination aus periodischem Vollbackup und täglichen Teilsicherungen balanciert Speicherbedarf, Sicherungsdauer und Restore-Aufwand. Reine Vollbackups oder zu seltene Sicherungen sind meist unwirtschaftlich oder riskant."
|
||||
},
|
||||
{
|
||||
"category": "Voll, differentiell, inkrementell",
|
||||
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||
"text": "Welche Aussage zum Restore eines Vollbackups stimmt?",
|
||||
"answers": ["Es kann ohne ein Vollbackup wiederhergestellt werden", "Es enthält nur einen Teil des Datenbestands", "Der Restore ist oft einfacher als bei langen inkrementellen Ketten"],
|
||||
"correct": 2,
|
||||
"explanation": "Ein vollständiger Sicherungsstand kann in einem Schritt zurückgespielt werden und vereinfacht so die Wiederherstellung. Ketten aus mehreren Teilsicherungen erfordern dagegen mehrere abhängige Schritte."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Was besagt die 3-2-1-Regel?",
|
||||
"answers": ["3 Kopien der Daten, auf 2 unterschiedlichen Medientypen, davon 1 Kopie extern", "3 Vollbackups, 2 inkrementelle, 1 differentielles pro Woche", "3 Restore-Tests, 2 Verantwortliche, 1 Backup-Server"],
|
||||
"correct": 0,
|
||||
"explanation": "Die 3-2-1-Regel verlangt drei Datenkopien auf zwei Medientypen mit einer Kopie an einem externen Ort. So sinkt das Risiko, durch ein einzelnes Ereignis alle Kopien zu verlieren."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Warum ist eine externe oder offline gelagerte Kopie sinnvoll?",
|
||||
"answers": ["Weil sie den benötigten Speicherplatz halbiert", "Weil sie besser gegen Brand, Diebstahl oder Ransomware schützt", "Weil sie das Vollbackup durch ein inkrementelles ersetzt"],
|
||||
"correct": 1,
|
||||
"explanation": "Eine räumlich oder netzwerktechnisch getrennte Kopie übersteht Ereignisse, die den Standort oder das Produktivnetz treffen. Mit Speicherersparnis hat das nichts zu tun."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Was ist bei Cloud-Backups besonders zu beachten?",
|
||||
"answers": ["Datenschutz, Verschlüsselung, Vertragsbedingungen und Wiederherstellbarkeit", "Dass kein Restore-Test mehr notwendig ist", "Dass die 3-2-1-Regel dadurch entfällt"],
|
||||
"correct": 0,
|
||||
"explanation": "Cloud-Backups erfordern eine technische, rechtliche und organisatorische Bewertung, etwa zu Speicherort, Verschlüsselung und Anbietervertrag. Tests und Mehrfachkopien bleiben weiterhin nötig."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Warum sollten Backups verschlüsselt werden?",
|
||||
"answers": ["Damit sie schneller wiederhergestellt werden können", "Damit sie weniger Speicherplatz belegen", "Zum Schutz der Vertraulichkeit vor unbefugtem Zugriff"],
|
||||
"correct": 2,
|
||||
"explanation": "Verschlüsselung verhindert, dass gestohlene oder entwendete Sicherungsmedien lesbar sind. Auf Geschwindigkeit oder Speicherbedarf wirkt sie nicht positiv."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Was sollte nach der Einrichtung eines Backups dokumentiert werden?",
|
||||
"answers": ["Zeitplan, Umfang, Speicherort, Verantwortliche und Restore-Verfahren", "Nur der Hersteller des verwendeten Sicherungsmediums", "Nur das Datum des allerersten Vollbackups"],
|
||||
"correct": 0,
|
||||
"explanation": "Eine vollständige Dokumentation macht Betrieb und Notfallwiederherstellung nachvollziehbar und unabhängig von einzelnen Personen. Einzelangaben wie Hersteller oder Startdatum reichen nicht aus."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Ein Backup-Job läuft täglich, schlägt aber seit Wochen unbemerkt fehl. Was ist das Hauptproblem?",
|
||||
"answers": ["Die inkrementelle Kette ist zu lang geworden", "Es fehlt eine Überwachung samt Reaktion auf Fehlermeldungen", "Es wurde ein falscher Medientyp gewählt"],
|
||||
"correct": 1,
|
||||
"explanation": "Backups müssen überwacht werden, damit Fehlschläge sofort auffallen und behoben werden. Ohne Monitoring bleibt das Problem wochenlang unentdeckt."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Was schützt Backups besonders wirksam gegen Ransomware?",
|
||||
"answers": ["Nicht veränderbare (unveränderliche) oder vom Netz getrennte Backup-Kopien", "Ein möglichst kurzes Backup-Fenster", "Häufigere inkrementelle statt differentielle Sicherungen"],
|
||||
"correct": 0,
|
||||
"explanation": "Ransomware kann erreichbare und beschreibbare Backups mitverschlüsseln oder löschen, daher helfen unveränderliche (immutable) oder getrennte Kopien. Die Sicherungsart allein bietet keinen Schutz."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Was ist ein typischer Fehler in Backup-Konzepten?",
|
||||
"answers": ["RPO und RTO festzulegen", "Den Zugriff auf Sicherungen einzuschränken", "Die Wiederherstellung nie zu testen"],
|
||||
"correct": 2,
|
||||
"explanation": "Ungetestete Sicherungen können im Ernstfall unbrauchbar sein, ohne dass es vorher auffällt. RPO/RTO und Zugriffsschutz gehören dagegen zu guten Konzepten."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Welche Frage ist bei der Backup-Planung zentral?",
|
||||
"answers": ["Wie viel Datenverlust und Ausfallzeit sind tolerierbar?", "Welcher Medienhersteller hat das schönste Design?", "Wie viele Mitarbeiter arbeiten im Serverraum?"],
|
||||
"correct": 0,
|
||||
"explanation": "Aus den Vorgaben für tolerierbaren Datenverlust (RPO) und Ausfallzeit (RTO) ergeben sich Sicherungsart und Häufigkeit. Diese Kennzahlen steuern die gesamte Strategie."
|
||||
},
|
||||
{
|
||||
"category": "Backup-Strategie & Praxis",
|
||||
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||
"text": "Welche Aussage zu einem guten Backup-Konzept ist richtig?",
|
||||
"answers": ["Ein Backup ohne Restore-Test ist in jedem Fall ausreichend", "Sicherung, Restore-Test und Dokumentation gehören zusammen", "Die Dokumentation behindert die Wiederherstellung"],
|
||||
"correct": 1,
|
||||
"explanation": "Erst das Zusammenspiel aus Sicherung, geprüfter Wiederherstellung und nachvollziehbarer Dokumentation ergibt ein verlässliches Konzept. Ungetestete Backups bleiben ein Risiko."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
{
|
||||
"title": "Datenschutz & Datensicherheit",
|
||||
"questions": [
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Erkenne, wann sich Daten auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||
"text": "Welche Angabe ist eindeutig personenbezogen im Sinne der DSGVO?",
|
||||
"answers": ["Die Seriennummer eines Switches", "Die Auslastung der CPU in Prozent", "Name und E-Mail-Adresse eines Kunden"],
|
||||
"correct": 2,
|
||||
"explanation": "Personenbezogen sind Daten, die einer natürlichen Person zuzuordnen sind; Name und E-Mail-Adresse leisten das direkt, eine Seriennummer oder ein Messwert hingegen nicht."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Manche Daten werden erst mit Zusatzwissen zu einer Person zuordenbar.",
|
||||
"text": "Welche Daten gelten als indirekt personenbezogen?",
|
||||
"answers": ["Eine IP-Adresse in Logdateien", "Die Anzahl aktiver TCP-Verbindungen", "Die Firmware-Version eines Routers"],
|
||||
"correct": 0,
|
||||
"explanation": "Eine IP-Adresse kann über den Provider einem Anschluss und damit einer Person zugeordnet werden und ist daher personenbezogen; technische Zähl- oder Versionsangaben sind es nicht."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Auch interne Kennungen können einen Personenbezug herstellen.",
|
||||
"text": "Welche Aussage zu Personalnummern ist richtig?",
|
||||
"answers": ["Sie sind anonym, weil sie nur aus Ziffern bestehen", "Sie können Beschäftigte eindeutig identifizieren", "Sie sind nur mit Einwilligung personenbezogen"],
|
||||
"correct": 1,
|
||||
"explanation": "Eine Personalnummer ist eine Kennung, die mit dem Personalbestand einer konkreten Person zugeordnet werden kann, und ist daher personenbezogen, unabhängig von einer Einwilligung."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Unterscheide echte Anonymisierung von bloßer Verschleierung.",
|
||||
"text": "Was bedeutet Anonymisierung?",
|
||||
"answers": ["Der Personenbezug wird so entfernt, dass keine Zuordnung mehr möglich ist", "Die Daten werden mit einem Schlüssel verschlüsselt und können entschlüsselt werden", "Identifikatoren werden durch ein Pseudonym ersetzt"],
|
||||
"correct": 0,
|
||||
"explanation": "Bei der Anonymisierung ist eine Re-Identifizierung dauerhaft ausgeschlossen; Verschlüsselung und Pseudonymisierung lassen den Bezug mit Schlüssel bzw. Zusatzwissen weiterhin zu."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Pseudonymisierung ist eine Schutzmaßnahme, keine vollständige Anonymisierung.",
|
||||
"text": "Was bedeutet Pseudonymisierung?",
|
||||
"answers": ["Der Personenbezug entfällt vollständig und unwiderruflich", "Identifizierende Merkmale werden ersetzt, eine Zuordnung bleibt mit Zusatzwissen möglich", "Die Daten werden gelöscht und durch Zufallswerte ersetzt"],
|
||||
"correct": 1,
|
||||
"explanation": "Pseudonymisierte Daten bleiben personenbezogen, weil die Zuordnung über eine getrennt aufbewahrte Zuordnungstabelle wiederhergestellt werden kann; sie sind daher nicht anonym."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Bestimmte Datenkategorien genießen besonderen Schutz nach Art. 9 DSGVO.",
|
||||
"text": "Welche Information zählt zu den besonderen Kategorien personenbezogener Daten?",
|
||||
"answers": ["Die Abteilung eines Mitarbeiters", "Die dienstliche Telefonnummer", "Gesundheitsdaten eines Mitarbeiters"],
|
||||
"correct": 2,
|
||||
"explanation": "Gesundheitsdaten gehören zu den besonderen Kategorien nach Art. 9 DSGVO und unterliegen strengeren Voraussetzungen; Abteilung und Telefonnummer sind gewöhnliche personenbezogene Daten."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Logdaten sind selten von vornherein harmlos.",
|
||||
"text": "Was ist bei Logdateien mit Benutzerkennungen zu beachten?",
|
||||
"answers": ["Sie können personenbezogene Daten enthalten und unterliegen dem Datenschutz", "Sie sind technische Daten und damit grundsätzlich anonym", "Sie dürfen nur mit Einwilligung jedes Nutzers überhaupt erzeugt werden"],
|
||||
"correct": 0,
|
||||
"explanation": "Benutzerkennung, Zeitstempel und Aktion lassen Rückschlüsse auf das Verhalten einer Person zu; solche Logs sind personenbezogen, dürfen aber zu berechtigten Zwecken wie der Sicherheit verarbeitet werden."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Erst die Kombination von Merkmalen kann eine Person identifizierbar machen.",
|
||||
"text": "Welche Datensammlung ist personenbezogen?",
|
||||
"answers": ["Die Zahl belegter Switchports je Etage", "Abteilung, Name und Telefonnummer in einer Liste", "Eine Liste installierter Softwareversionen"],
|
||||
"correct": 1,
|
||||
"explanation": "Die Kombination aus Name, Abteilung und Telefonnummer ermöglicht eine eindeutige Zuordnung zu einer Person; reine Bestands- oder Versionsangaben ohne Personenbezug tun dies nicht."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Der Personenbezug ist der Auslöser für die Anwendbarkeit der DSGVO.",
|
||||
"text": "Warum ist die Prüfung des Personenbezugs in AP1-Aufgaben so wichtig?",
|
||||
"answers": ["Er entscheidet, ob die DSGVO überhaupt anwendbar ist", "Er entscheidet über die nötige Bandbreite der Verbindung", "Er ist nur bei Cloud-Diensten relevant"],
|
||||
"correct": 0,
|
||||
"explanation": "Die DSGVO greift nur bei der Verarbeitung personenbezogener Daten; liegt kein Personenbezug vor, gelten ihre Pflichten nicht."
|
||||
},
|
||||
{
|
||||
"category": "Personenbezogene Daten",
|
||||
"hint": "Personenbezogene Daten kommen in vielen Formen und Kontexten vor.",
|
||||
"text": "Welche Aussage über personenbezogene Daten ist korrekt?",
|
||||
"answers": ["Nur Passwörter und Kontodaten sind personenbezogen", "Personenbezogene Daten existieren nur in digitaler Form", "Auch Kunden- und Mitarbeiterdaten in digitaler oder analoger Form können personenbezogen sein"],
|
||||
"correct": 2,
|
||||
"explanation": "Personenbezogene Daten umfassen jede Information über eine bestimmbare Person und können sowohl digital als auch auf Papier vorliegen; sie sind nicht auf Zugangsdaten beschränkt."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Die Grundsätze des Art. 5 DSGVO steuern jede Verarbeitung.",
|
||||
"text": "Was bedeutet der Grundsatz der Datenminimierung?",
|
||||
"answers": ["Nur die für den Zweck notwendigen Daten verarbeiten", "Daten möglichst stark komprimiert speichern", "Personenbezogene Daten in kleineren Tabellen ablegen"],
|
||||
"correct": 0,
|
||||
"explanation": "Datenminimierung verlangt, dass Daten dem Zweck angemessen und auf das notwendige Maß beschränkt sind; sie betrifft die Menge der Daten, nicht die technische Dateigröße."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Der Zweck einer Verarbeitung muss vorab feststehen.",
|
||||
"text": "Was bedeutet der Grundsatz der Zweckbindung?",
|
||||
"answers": ["Der Zweck der Verarbeitung muss vor den Betroffenen geheim gehalten werden", "Daten dürfen nur für festgelegte, eindeutige Zwecke verarbeitet werden", "Daten dürfen nach Erhebung für beliebige neue Zwecke genutzt werden"],
|
||||
"correct": 1,
|
||||
"explanation": "Daten dürfen nur für die festgelegten, eindeutigen und legitimen Zwecke verwendet werden; eine spätere, damit unvereinbare Nutzung ist unzulässig."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Unrichtige Daten führen zu falschen Entscheidungen.",
|
||||
"text": "Was beschreibt der Grundsatz der Richtigkeit?",
|
||||
"answers": ["Personenbezogene Daten müssen sachlich richtig und auf dem neuesten Stand sein", "Daten müssen verschlüsselt übertragen werden", "Daten müssen vollständig protokolliert werden"],
|
||||
"correct": 0,
|
||||
"explanation": "Der Grundsatz der Richtigkeit verlangt sachlich korrekte und aktuelle Daten; Verschlüsselung und Protokollierung gehören zu Integrität und Vertraulichkeit."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Speicherdauer und Zweck hängen eng zusammen.",
|
||||
"text": "Was bedeutet der Grundsatz der Speicherbegrenzung?",
|
||||
"answers": ["Daten dürfen nur eine begrenzte Speichermenge belegen", "Daten sollen nur so lange gespeichert werden, wie es für den Zweck nötig ist", "Daten dürfen nur auf bestimmten Datenträgern liegen"],
|
||||
"correct": 1,
|
||||
"explanation": "Personenbezogene Daten sind zu löschen oder zu anonymisieren, sobald der Zweck erfüllt ist; der Grundsatz betrifft die Dauer, nicht die Menge oder den Speicherort."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Dieser Grundsatz ist die Brücke zur IT-Sicherheit.",
|
||||
"text": "Was verlangt der Grundsatz der Integrität und Vertraulichkeit?",
|
||||
"answers": ["Eine angemessene Sicherheit gegen unbefugten Zugriff, Verlust und Manipulation", "Eine möglichst lange Aufbewahrung aller Daten", "Eine Veröffentlichung der Verarbeitungszwecke"],
|
||||
"correct": 0,
|
||||
"explanation": "Dieser Grundsatz fordert geeignete technische und organisatorische Maßnahmen, um Daten zu schützen; lange Aufbewahrung widerspräche sogar der Speicherbegrenzung."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Es genügt nicht, datenschutzkonform zu sein - man muss es belegen können.",
|
||||
"text": "Was bedeutet die Rechenschaftspflicht (Accountability)?",
|
||||
"answers": ["Betroffene müssen jeder Verarbeitung schriftlich zustimmen", "Der Verantwortliche muss die Einhaltung der Grundsätze nachweisen können", "Jede Verarbeitung muss der Aufsichtsbehörde vorab gemeldet werden"],
|
||||
"correct": 1,
|
||||
"explanation": "Der Verantwortliche muss die Einhaltung der DSGVO-Grundsätze dokumentieren und belegen können; eine generelle Einwilligung oder Vorabmeldung ist dafür nicht erforderlich."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Suche die Verarbeitung, die mehrere Grundsätze verletzt.",
|
||||
"text": "Welche Verarbeitung verstößt gegen die DSGVO-Grundsätze?",
|
||||
"answers": ["Zugriffe rollenbasiert einschränken", "Kundendaten ohne Zweck zeitlich unbegrenzt aufbewahren", "Daten nach Ablauf der Aufbewahrungsfrist löschen"],
|
||||
"correct": 1,
|
||||
"explanation": "Eine zweck- und fristlose Dauerspeicherung verletzt zugleich Zweckbindung und Speicherbegrenzung; Rollenrechte und fristgerechtes Löschen sind hingegen korrekt."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Rechtmäßigkeit ist der erste Grundsatz des Art. 5.",
|
||||
"text": "Warum benötigt jede Verarbeitung personenbezogener Daten eine Rechtsgrundlage?",
|
||||
"answers": ["Weil Verarbeitung ohne Rechtsgrundlage unzulässig ist", "Weil sonst die Daten technisch nicht gespeichert werden können", "Weil nur so Backups erstellt werden dürfen"],
|
||||
"correct": 0,
|
||||
"explanation": "Die Rechtmäßigkeit verlangt für jede Verarbeitung eine Rechtsgrundlage nach Art. 6 DSGVO; fehlt sie, ist die Verarbeitung rechtswidrig."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Transparenz richtet sich an die betroffene Person.",
|
||||
"text": "Welche Aussage passt zum Grundsatz der Transparenz?",
|
||||
"answers": ["Verarbeitungen sollen vor den Betroffenen verborgen bleiben", "Betroffene sind in verständlicher und leicht zugänglicher Form zu informieren", "Es genügt, die Aufsichtsbehörde zu informieren"],
|
||||
"correct": 1,
|
||||
"explanation": "Transparenz bedeutet, dass Betroffene klar und verständlich über die Verarbeitung ihrer Daten informiert werden, etwa über Datenschutzhinweise."
|
||||
},
|
||||
{
|
||||
"category": "DSGVO-Grundsätze",
|
||||
"hint": "Achte darauf, welches Vorgehen einen Grundsatz verletzt.",
|
||||
"text": "Welches Vorgehen widerspricht dem Grundsatz der Datenminimierung?",
|
||||
"answers": ["Zugriffsrechte auf das Nötige begrenzen", "Mehr Daten erfassen, als für den Auftrag erforderlich sind", "Löschfristen für nicht mehr benötigte Daten festlegen"],
|
||||
"correct": 1,
|
||||
"explanation": "Das Erheben überflüssiger Daten verstößt gegen die Datenminimierung; eingeschränkte Rechte und Löschfristen unterstützen die Grundsätze dagegen."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Betroffene haben durchsetzbare Rechte gegenüber dem Verantwortlichen.",
|
||||
"text": "Was umfasst das Auskunftsrecht nach Art. 15 DSGVO?",
|
||||
"answers": ["Betroffene müssen ihre Daten selbst beim Verantwortlichen sichern", "Betroffene können Auskunft über die zu ihnen gespeicherten Daten und deren Zweck verlangen", "Betroffene dürfen Daten anderer Personen einsehen"],
|
||||
"correct": 1,
|
||||
"explanation": "Das Auskunftsrecht gibt der betroffenen Person Anspruch darauf zu erfahren, ob und welche ihrer Daten zu welchem Zweck verarbeitet werden, nicht jedoch Daten Dritter."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Nicht jedes Recht ist ein Löschanspruch.",
|
||||
"text": "Was bedeutet das Recht auf Berichtigung?",
|
||||
"answers": ["Unrichtige personenbezogene Daten müssen korrigiert werden", "Alle Daten müssen unverzüglich gelöscht werden", "Daten müssen in ein anderes Format überführt werden"],
|
||||
"correct": 0,
|
||||
"explanation": "Das Recht auf Berichtigung nach Art. 16 DSGVO verlangt die Korrektur unrichtiger und die Vervollständigung unvollständiger Daten; Löschung und Übertragbarkeit sind eigene Rechte."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Das Recht auf Löschung kennt Ausnahmen.",
|
||||
"text": "Was gilt für das Recht auf Löschung (Recht auf Vergessenwerden)?",
|
||||
"answers": ["Daten müssen in jedem Fall sofort und ausnahmslos gelöscht werden", "Daten können gelöscht werden, sofern keine Aufbewahrungspflicht oder kein anderer Grund entgegensteht", "Daten dürfen grundsätzlich niemals gelöscht werden"],
|
||||
"correct": 1,
|
||||
"explanation": "Die Löschung erfolgt unter den Voraussetzungen des Art. 17 DSGVO, kann aber durch gesetzliche Aufbewahrungspflichten oder andere Gründe ausgeschlossen sein."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Unterscheide Widerspruch von Einwilligungswiderruf.",
|
||||
"text": "Was bedeutet das Widerspruchsrecht nach Art. 21 DSGVO?",
|
||||
"answers": ["Betroffene können der Verarbeitung aus Gründen ihrer besonderen Situation widersprechen", "Betroffene müssen jeder Verarbeitung dauerhaft zustimmen", "Nur Administratoren dürfen einer Verarbeitung widersprechen"],
|
||||
"correct": 0,
|
||||
"explanation": "Beim Widerspruchsrecht kann die betroffene Person einer auf berechtigtem Interesse beruhenden Verarbeitung widersprechen; bei Direktwerbung gilt der Widerspruch sogar uneingeschränkt."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Es geht um den Wechsel zwischen Anbietern.",
|
||||
"text": "Was bedeutet das Recht auf Datenübertragbarkeit?",
|
||||
"answers": ["Daten ausschließlich in Papierform zu erhalten", "Die eigenen Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten", "Daten an die Aufsichtsbehörde übertragen zu lassen"],
|
||||
"correct": 1,
|
||||
"explanation": "Die Datenübertragbarkeit nach Art. 20 DSGVO erlaubt es, bereitgestellte Daten in einem maschinenlesbaren Format zu erhalten oder zu einem anderen Anbieter übertragen zu lassen."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Kläre, wer Träger der Rechte ist.",
|
||||
"text": "Wer ist die betroffene Person im Sinne der DSGVO?",
|
||||
"answers": ["Die natürliche Person, auf die sich die personenbezogenen Daten beziehen", "Der Datenschutzbeauftragte des Unternehmens", "Die zuständige Aufsichtsbehörde"],
|
||||
"correct": 0,
|
||||
"explanation": "Betroffene Person ist die identifizierte oder identifizierbare natürliche Person, deren Daten verarbeitet werden; Datenschutzbeauftragter und Behörde haben andere Rollen."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Vor der Antwort steht eine wichtige Prüfung.",
|
||||
"text": "Was sollte bei einer Auskunftsanfrage zuerst geprüft werden?",
|
||||
"answers": ["Die Identität der anfragenden Person", "Die zur Verfügung stehende Speicherkapazität", "Die Auslastung des Mailservers"],
|
||||
"correct": 0,
|
||||
"explanation": "Vor der Herausgabe ist die Identität zu prüfen, damit Daten nicht an Unberechtigte gelangen; eine fehlerhafte Auskunft wäre selbst eine Datenpanne."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Betroffenenrechte sind rechtlich verbindlich und fristgebunden.",
|
||||
"text": "Warum dürfen Betroffenenrechte nicht ignoriert werden?",
|
||||
"answers": ["Sie sind unverbindliche Empfehlungen ohne Folgen", "Sie sind durchsetzbare Ansprüche, deren Missachtung Bußgelder nach sich ziehen kann", "Sie gelten nur für öffentliche Stellen"],
|
||||
"correct": 1,
|
||||
"explanation": "Betroffenenrechte sind rechtlich verbindliche Ansprüche; werden sie nicht fristgerecht erfüllt, drohen Beschwerden bei der Aufsichtsbehörde und Bußgelder."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Manche Pflichten haben Vorrang vor dem Löschwunsch.",
|
||||
"text": "Was ist bei einer Löschanfrage besonders zu beachten?",
|
||||
"answers": ["Ob gesetzliche Aufbewahrungspflichten der Löschung entgegenstehen", "Ob der zuständige Drucker betriebsbereit ist", "Ob die Datei sinnvoll benannt ist"],
|
||||
"correct": 0,
|
||||
"explanation": "Handels- und steuerrechtliche Aufbewahrungspflichten können einer sofortigen Löschung entgegenstehen; dann sind die Daten zu sperren statt zu löschen."
|
||||
},
|
||||
{
|
||||
"category": "Betroffenenrechte",
|
||||
"hint": "Es gilt eine gesetzliche Reaktionsfrist.",
|
||||
"text": "Wie sollte mit einer Betroffenenanfrage professionell umgegangen werden?",
|
||||
"answers": ["Die Anfrage löschen, um Aufwand zu vermeiden", "Die Anfrage dokumentieren, prüfen und grundsätzlich innerhalb eines Monats beantworten", "Die Anfrage zur Klärung öffentlich im Team-Chat posten"],
|
||||
"correct": 1,
|
||||
"explanation": "Anfragen sind nachvollziehbar zu dokumentieren und in der Regel innerhalb eines Monats zu beantworten; ein öffentliches Posten wäre selbst ein Datenschutzverstoß."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "TOM ist ein zentraler Begriff aus Art. 32 DSGVO.",
|
||||
"text": "Wofür steht die Abkürzung TOM im Datenschutz?",
|
||||
"answers": ["Technische und organisatorische Maßnahmen", "Test- und Optimierungsmodus", "Temporäre Online-Migration"],
|
||||
"correct": 0,
|
||||
"explanation": "TOM steht für technische und organisatorische Maßnahmen, mit denen nach Art. 32 DSGVO ein angemessenes Schutzniveau für personenbezogene Daten sichergestellt wird."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Ordne die Maßnahme dem passenden Schutzziel zu.",
|
||||
"text": "Welches Schutzziel wird durch Verschlüsselung in erster Linie unterstützt?",
|
||||
"answers": ["Verfügbarkeit", "Vertraulichkeit", "Belastbarkeit"],
|
||||
"correct": 1,
|
||||
"explanation": "Verschlüsselung verhindert, dass Unbefugte Inhalte lesen können, und dient damit vor allem der Vertraulichkeit; Verfügbarkeit wird etwa durch Backups gesichert."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Backups zielen auf die Wiederherstellbarkeit.",
|
||||
"text": "Welches Schutzziel wird durch regelmäßige Backups besonders unterstützt?",
|
||||
"answers": ["Verfügbarkeit", "Vertraulichkeit", "Authentizität"],
|
||||
"correct": 0,
|
||||
"explanation": "Backups ermöglichen die Wiederherstellung nach Verlust oder Ausfall und dienen damit der Verfügbarkeit; Vertraulichkeit erreicht man dagegen durch Zugriffsschutz."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Integrität betrifft die Unverändertheit von Daten.",
|
||||
"text": "Was bedeutet das Schutzziel Integrität?",
|
||||
"answers": ["Daten sind nur für Berechtigte einsehbar", "Daten sind vollständig, korrekt und vor unbemerkter Veränderung geschützt", "Daten sind jederzeit abrufbar"],
|
||||
"correct": 1,
|
||||
"explanation": "Integrität bedeutet, dass Daten nicht unbemerkt verändert werden; die Einsehbarkeit betrifft die Vertraulichkeit, die Abrufbarkeit die Verfügbarkeit."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Zugriffskontrolle steuert, wer auf welche Daten zugreift.",
|
||||
"text": "Welche Maßnahme unterstützt die Zugriffskontrolle?",
|
||||
"answers": ["Ein Rollen- und Rechtekonzept mit individuellen Konten", "Ein gemeinsam genutzter Administratorzugang", "Frei zugängliche, unverschlüsselte Passwortlisten"],
|
||||
"correct": 0,
|
||||
"explanation": "Ein Rollen- und Rechtekonzept beschränkt den Zugriff auf das, was die jeweilige Aufgabe erfordert; Sammelkonten und offene Passwortlisten untergraben jede Kontrolle."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Protokolle schaffen Nachvollziehbarkeit, ersetzen aber keine anderen Maßnahmen.",
|
||||
"text": "Warum ist Protokollierung eine sinnvolle organisatorische Maßnahme?",
|
||||
"answers": ["Weil sich damit Zugriffe und Änderungen nachvollziehen lassen", "Weil dadurch alle Daten automatisch anonym werden", "Weil sie ein Backup vollständig ersetzt"],
|
||||
"correct": 0,
|
||||
"explanation": "Protokolle dokumentieren Zugriffe und Änderungen und unterstützen so Kontrolle und Fehleranalyse; sie anonymisieren keine Daten und ersetzen keine Datensicherung."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Gutes Authentifizierungsmanagement beruht auf mehreren Bausteinen.",
|
||||
"text": "Was kennzeichnet ein gutes Passwort- und Authentifizierungskonzept?",
|
||||
"answers": ["Ein gemeinsames Passwort für das ganze Team zur Vereinfachung", "Starke Passwörter, individuelle Konten und nach Möglichkeit Mehrfaktor-Authentifizierung", "Notierte Passwörter am Bildschirm für den schnellen Zugriff"],
|
||||
"correct": 1,
|
||||
"explanation": "Individuelle Konten, starke Passwörter und Mehrfaktor-Authentifizierung erschweren Missbrauch und ermöglichen die Zurechenbarkeit von Aktionen."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Organisatorische Maßnahmen betreffen Menschen und Prozesse.",
|
||||
"text": "Welche organisatorische Maßnahme stärkt den Datenschutz?",
|
||||
"answers": ["Regelmäßige Schulung und Sensibilisierung der Mitarbeitenden", "Geheimhaltung der Sicherheitsregeln vor den Mitarbeitenden", "Verzicht auf klare Zuständigkeiten"],
|
||||
"correct": 0,
|
||||
"explanation": "Schulungen reduzieren Fehlverhalten und schaffen Bewusstsein; klare Zuständigkeiten und bekannte Regeln sind Voraussetzung für ihre Einhaltung."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Das Prinzip begrenzt Zugriff auf das Notwendige.",
|
||||
"text": "Was beschreibt das Need-to-know-Prinzip?",
|
||||
"answers": ["Zugriff besteht nur, soweit er für die jeweilige Aufgabe erforderlich ist", "Jede Person erhält Zugriff auf alle Daten", "Nur externe Dienstleister erhalten Zugriff"],
|
||||
"correct": 0,
|
||||
"explanation": "Need-to-know beschränkt den Datenzugriff auf das für die Aufgabe Erforderliche und setzt damit Datenminimierung und Zugriffskontrolle praktisch um."
|
||||
},
|
||||
{
|
||||
"category": "TOMs & Schutzziele",
|
||||
"hint": "Verfügbarkeit braucht Schutz vor Ausfall und Datenverlust.",
|
||||
"text": "Welche Maßnahmenkombination dient dem Schutzziel Verfügbarkeit?",
|
||||
"answers": ["USV, regelmäßige Backups und redundante Systeme", "Verzicht auf Wartung zur Kostensenkung", "Eine zentrale, unverschlüsselte Passwortdatei"],
|
||||
"correct": 0,
|
||||
"explanation": "Unterbrechungsfreie Stromversorgung, Backups und Redundanz sichern den fortlaufenden Betrieb und die Wiederherstellbarkeit und stützen damit die Verfügbarkeit."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Eine Datenpanne unterliegt Meldepflichten mit Fristen.",
|
||||
"text": "Ein USB-Stick mit unverschlüsselten Kundendaten geht verloren. Was ist richtig?",
|
||||
"answers": ["Den Vorfall verschweigen, um Aufsehen zu vermeiden", "Den Vorfall melden, das Risiko bewerten und die Aufsichtsbehörde in der Regel binnen 72 Stunden informieren", "Den Stick lediglich ersetzen und neu beschriften"],
|
||||
"correct": 1,
|
||||
"explanation": "Eine Verletzung des Schutzes personenbezogener Daten ist nach Art. 33 DSGVO bei Risiko innerhalb von 72 Stunden der Aufsichtsbehörde zu melden; ein Verschweigen ist unzulässig."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Verantwortlicher ist eine genau definierte Rolle.",
|
||||
"text": "Wer ist der Verantwortliche im Sinne der DSGVO?",
|
||||
"answers": ["Die Stelle, die über Zweck und Mittel der Verarbeitung entscheidet", "Der Dienstleister, der die Daten weisungsgebunden verarbeitet", "Stets der Datenschutzbeauftragte"],
|
||||
"correct": 0,
|
||||
"explanation": "Verantwortlicher ist, wer über Zweck und Mittel der Verarbeitung entscheidet; ein weisungsgebundener Dienstleister ist Auftragsverarbeiter, der Datenschutzbeauftragte berät nur."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Achte auf die Rollenverteilung und den nötigen Vertrag.",
|
||||
"text": "Was kennzeichnet eine Auftragsverarbeitung?",
|
||||
"answers": ["Der Verantwortliche gibt die Daten an einen Käufer ab", "Ein Dienstleister verarbeitet personenbezogene Daten weisungsgebunden im Auftrag des Verantwortlichen", "Zwei Unternehmen entscheiden gleichberechtigt über die Zwecke"],
|
||||
"correct": 1,
|
||||
"explanation": "Bei der Auftragsverarbeitung handelt ein Dienstleister nach Weisung des Verantwortlichen; dafür ist nach Art. 28 DSGVO ein Auftragsverarbeitungsvertrag erforderlich."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Externe Zugriffe auf Kundendaten brauchen rechtliche und technische Regelungen.",
|
||||
"text": "Ein externer IT-Dienstleister wartet ein System mit Kundendaten. Was ist zu prüfen?",
|
||||
"answers": ["Nur, ob der Dienstleister termingerecht erscheint", "Ob ein Auftragsverarbeitungsvertrag besteht und Zugriff, Vertraulichkeit und Zweckbindung geregelt sind", "Ob der Dienstleister dieselbe Software wie das eigene Team nutzt"],
|
||||
"correct": 1,
|
||||
"explanation": "Wartet ein Dienstleister Systeme mit personenbezogenen Daten, ist ein AV-Vertrag erforderlich und der Zugriff muss auf das Nötige beschränkt und vertraulich behandelt werden."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Schutz vor unbefugter Einsicht gehört zur Vertraulichkeit am Arbeitsplatz.",
|
||||
"text": "Was ist an einem Bildschirmarbeitsplatz mit Kundendaten sinnvoll?",
|
||||
"answers": ["Den Bildschirm beim Verlassen sperren und bei Bedarf einen Sichtschutz nutzen", "Die Daten dauerhaft offen anzeigen, um schneller zu arbeiten", "Das Passwort gut sichtbar an den Monitor heften"],
|
||||
"correct": 0,
|
||||
"explanation": "Eine Bildschirmsperre und ein Sichtschutz verhindern, dass Unbefugte personenbezogene Daten einsehen, und setzen so die Vertraulichkeit am Arbeitsplatz um."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Normales Löschen schützt nicht vor Wiederherstellung.",
|
||||
"text": "Warum müssen ausgemusterte Datenträger sicher gelöscht oder vernichtet werden?",
|
||||
"answers": ["Damit personenbezogene Daten nicht wiederhergestellt werden können", "Damit die Datenträger leichter weiterverkauft werden können", "Damit die verbleibenden Backups schneller laufen"],
|
||||
"correct": 0,
|
||||
"explanation": "Einfaches Löschen oder Formatieren lässt Daten oft rekonstruieren; nur sicheres Überschreiben oder physische Vernichtung verhindert die Wiederherstellung zuverlässig."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Testsysteme brauchen selten echte Personendaten.",
|
||||
"text": "Was ist beim Umgang mit Testdaten in Entwicklungssystemen sinnvoll?",
|
||||
"answers": ["Produktivdaten ungefiltert in die Testumgebung kopieren", "Keine echten personenbezogenen Daten verwenden oder diese vorher anonymisieren", "Echte Kundendaten zu Testzwecken öffentlich bereitstellen"],
|
||||
"correct": 1,
|
||||
"explanation": "Aus Gründen der Datenminimierung sollten Testsysteme synthetische oder anonymisierte Daten nutzen, da Testumgebungen oft schwächer geschützt sind."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Datenschutz beginnt schon bei der Planung eines Systems.",
|
||||
"text": "Was sollte bei der Entwicklung neuer IT-Systeme früh berücksichtigt werden?",
|
||||
"answers": ["Datenschutz durch Technikgestaltung und datenschutzfreundliche Voreinstellungen", "Eine möglichst umfangreiche Sammlung aller verfügbaren Daten", "Die Verschiebung des Datenschutzes auf die Zeit nach dem Go-live"],
|
||||
"correct": 0,
|
||||
"explanation": "Privacy by Design und Privacy by Default nach Art. 25 DSGVO verlangen, Datenschutz von Anfang an einzuplanen und datensparsame Voreinstellungen zu wählen."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Ohne berechtigten Zweck kein Zugriff.",
|
||||
"text": "Ein Kollege bittet ohne dienstlichen Grund um Personaldaten anderer Beschäftigter. Wie ist zu reagieren?",
|
||||
"answers": ["Die Daten umgehend übermitteln, um kollegial zu sein", "Den Zugriff verweigern und auf die fehlende Berechtigung verweisen", "Die Daten zur Abstimmung im Team-Chat teilen"],
|
||||
"correct": 1,
|
||||
"explanation": "Ohne berechtigten dienstlichen Zweck und Befugnis darf kein Zugriff auf personenbezogene Daten erfolgen; das Need-to-know-Prinzip gilt auch unter Kollegen."
|
||||
},
|
||||
{
|
||||
"category": "Praxisfälle & Verantwortung",
|
||||
"hint": "Datenschutz und Datensicherheit greifen ineinander, sind aber nicht dasselbe.",
|
||||
"text": "Warum sind Datenschutz und IT-Sicherheit in AP1-Aufgaben oft verbunden?",
|
||||
"answers": ["Weil IT-Sicherheit den Datenschutz vollständig ersetzt", "Weil technische und organisatorische Schutzmaßnahmen personenbezogene Daten absichern", "Weil Datenschutz ausschließlich aus dem Betrieb einer Firewall besteht"],
|
||||
"correct": 1,
|
||||
"explanation": "Datenschutz legt fest, welche Daten warum verarbeitet werden dürfen, während die IT-Sicherheit diese Daten technisch und organisatorisch schützt; beides ergänzt sich, ist aber nicht identisch."
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user