Volle Mindflux-App ins Deploy-Repo übernehmen
Ersetzt das Hello-World-Gerüst durch die vollständige Anwendung (com.example.mindflux: 64 Klassen, Thymeleaf-Templates, Static-Assets, Flyway V1–V6). Deploy-Infrastruktur (.gitea, Dockerfile) bleibt erhalten. - pom: volle Abhängigkeiten (Security, Thymeleaf, JPA, Flyway), Spring Boot 3.4.4 / Java 17 - docker-compose: SPRING_DATASOURCE_* unverändert; App lauscht auf 8080 (nginx -> mindflux:8080); Uploads-Volume; Admin-Passwörter via Gitea-Secrets - Workflow: Deploy-Step injiziert MINDFLUX_ADMIN*_PASSWORD aus Secrets - .dockerignore ergänzt; Hello-World (com.mindflux) und Test-Datei 'main' entfernt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
docker push localhost:5000/mindflux:latest
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
|
env:
|
||||||
|
MINDFLUX_ADMIN_PASSWORD: ${{ secrets.MINDFLUX_ADMIN_PASSWORD }}
|
||||||
|
MINDFLUX_ADMIN2_PASSWORD: ${{ secrets.MINDFLUX_ADMIN2_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
docker compose -f docker-compose.yml pull
|
docker compose -f docker-compose.yml pull
|
||||||
docker compose -f docker-compose.yml up -d
|
docker compose -f docker-compose.yml up -d
|
||||||
|
|||||||
+36
-41
@@ -1,45 +1,40 @@
|
|||||||
# ---> Java
|
HELP.md
|
||||||
# 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
|
|
||||||
target/
|
target/
|
||||||
pom.xml.tag
|
uploads/
|
||||||
pom.xml.releaseBackup
|
.env
|
||||||
pom.xml.versionsBackup
|
.env.*
|
||||||
pom.xml.next
|
!.env.example
|
||||||
release.properties
|
*.log
|
||||||
dependency-reduced-pom.xml
|
.claude/
|
||||||
buildNumber.properties
|
hs_err_pid*.log
|
||||||
.mvn/timing.properties
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
|
!**/src/main/**/target/
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
!**/src/test/**/target/
|
||||||
|
|
||||||
# Eclipse m2e generated files
|
### STS ###
|
||||||
# Eclipse Core
|
.apt_generated
|
||||||
.project
|
|
||||||
# JDT-specific (Eclipse Java Development Tools)
|
|
||||||
.classpath
|
.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,63 @@
|
|||||||
|
# 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-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
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
# --- Datenbank: unverändert wie im bisherigen Deploy ---
|
||||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mindfluxdb
|
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mindfluxdb
|
||||||
SPRING_DATASOURCE_USERNAME: mindflux
|
SPRING_DATASOURCE_USERNAME: mindflux
|
||||||
SPRING_DATASOURCE_PASSWORD: mindflux_pass
|
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:
|
networks:
|
||||||
- backend_network
|
- backend_network
|
||||||
|
|
||||||
ports:
|
|
||||||
- "8081:8080"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
backend_network:
|
backend_network:
|
||||||
external: true
|
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,40 +1,72 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>mindflux</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.3.2</version>
|
<version>3.4.4</version>
|
||||||
|
<relativePath/>
|
||||||
</parent>
|
</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>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>17</java.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- Spring Boot Web -->
|
||||||
<!-- Web -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- PostgreSQL -->
|
<!-- Thymeleaf für Templates -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- JPA optional aber sinnvoll -->
|
<!-- Thymeleaf + Spring Security Integration -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.thymeleaf.extras</groupId>
|
||||||
|
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA + Hibernate -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-core</artifactId>
|
<artifactId>flyway-core</artifactId>
|
||||||
@@ -45,9 +77,30 @@
|
|||||||
<artifactId>flyway-database-postgresql</artifactId>
|
<artifactId>flyway-database-postgresql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
<!-- 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>
|
<build>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>false</filtering>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -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.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class MindfluxApplication {
|
public class MindfluxApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(MindfluxApplication.class, 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,65 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/", "/login", "/register", "/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
|
||||||
|
.logoutSuccessUrl("/?logout")
|
||||||
|
.permitAll()
|
||||||
|
)
|
||||||
|
.csrf(csrf -> csrf.disable()); // optional deaktivieren bei Custom-Logik/API-Nutzung
|
||||||
|
|
||||||
|
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,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,172 @@
|
|||||||
|
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 jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
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 {
|
||||||
|
// 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());
|
||||||
|
if (Files.exists(oldFilePath)) {
|
||||||
|
Files.delete(oldFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neues Profilbild speichern
|
||||||
|
String fileExtension = getFileExtension(file.getOriginalFilename());
|
||||||
|
String fileName = UUID.randomUUID() + fileExtension;
|
||||||
|
|
||||||
|
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||||
|
Files.createDirectories(uploadPath);
|
||||||
|
|
||||||
|
Path filePath = uploadPath.resolve(fileName);
|
||||||
|
file.transferTo(filePath.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,
|
||||||
|
Model model) {
|
||||||
|
String username = principal.getName();
|
||||||
|
User user = userService.findByUsername(username);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
model.addAttribute("error", "Benutzer nicht gefunden.");
|
||||||
|
return "profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
|
||||||
|
model.addAttribute("error", "Das aktuelle Passwort ist falsch.");
|
||||||
|
return "profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
|
userService.save(user);
|
||||||
|
|
||||||
|
model.addAttribute("message", "Passwort erfolgreich geändert.");
|
||||||
|
return "profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/profile/delete")
|
||||||
|
public String deleteProfile(Principal principal, Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
String username = principal.getName();
|
||||||
|
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) {
|
||||||
|
model.addAttribute("error", "Benutzername bereits vergeben.");
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,10 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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,18 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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,28 @@
|
|||||||
|
package com.example.mindflux.service;
|
||||||
|
|
||||||
|
import com.example.mindflux.model.User;
|
||||||
|
import com.example.mindflux.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@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
|
||||||
|
public void deleteByUsername(String 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,43 @@
|
|||||||
server.port=8080
|
# =======================
|
||||||
|
# Datenbankverbindung (PostgreSQL)
|
||||||
|
# =======================
|
||||||
|
|
||||||
spring.datasource.url=jdbc:postgresql://postgres:5432/mindfluxdb
|
spring.config.import=optional:file:.env[.properties]
|
||||||
spring.datasource.username=mindflux
|
spring.datasource.url=${MINDFLUX_DB_URL:jdbc:postgresql://localhost:5433/mindflux_db}
|
||||||
spring.datasource.password=mindflux_pass
|
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.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}
|
||||||
|
|||||||
@@ -16,5 +16,4 @@ CREATE TABLE achievements (
|
|||||||
date_achieved TIMESTAMP
|
date_achieved TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_achievements_username
|
CREATE INDEX idx_achievements_username ON 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,273 @@
|
|||||||
|
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 {
|
||||||
|
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-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: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fluxcheck-result.wrong {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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) {
|
||||||
|
.finalflux-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finalflux-sidebar {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--border-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finalflux-main,
|
||||||
|
.finalflux-page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/* === 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: #00ff88;
|
||||||
|
box-shadow: 0 0 14px rgba(0, 255, 136, 0.3);
|
||||||
|
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: #ff6b6b;
|
||||||
|
box-shadow: 0 0 14px rgba(255, 107, 107, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 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: #00ff88; 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; }
|
||||||
|
}
|
||||||
@@ -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, #1f1f2e, #2c2c3d);
|
||||||
|
color: #ffffff;
|
||||||
|
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: #000916;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 20px rgba(0, 229, 255, 0.15),
|
||||||
|
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 rgba(0, 229, 255, 0.25),
|
||||||
|
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: #00e5ff;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-shadow: 0 0 5px rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 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 rgba(0, 229, 255, 0.05),
|
||||||
|
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 rgba(0, 229, 255, 0.07),
|
||||||
|
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: #66d9ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #ffffff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Button mit Glanz- und Licht-Effekt === */
|
||||||
|
.login-box button {
|
||||||
|
background-color: #00e5ff;
|
||||||
|
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 rgba(0, 229, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #4cc3dd;
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 229, 255, 0.4), 0 0 30px rgba(0, 229, 255, 0.1) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #888;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 0 6px rgba(0, 229, 255, 0.3);
|
||||||
|
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: #fff;
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bei aktiviertem Switch */
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #00e5ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: #00e5ff;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px rgba(0, 229, 255, 0.1),
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tile:hover {
|
||||||
|
background-color: rgba(0, 229, 255, 0.1);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow:
|
||||||
|
0 6px 16px rgba(0, 229, 255, 0.2),
|
||||||
|
0 0 20px rgba(0, 229, 255, 0.1) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tile span {
|
||||||
|
color: #00e5ff;
|
||||||
|
text-shadow: 0 0 5px rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 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 rgba(0, 229, 255, 0.1);
|
||||||
|
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: #00e5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-form input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #1a1a2e;
|
||||||
|
color: #fff;
|
||||||
|
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: #00e5ff;
|
||||||
|
color: #00172e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.save:hover {
|
||||||
|
background-color: #4cc3dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
background-color: #ff4d4d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger:hover {
|
||||||
|
background-color: #cc0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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: rgba(0, 229, 255, 0.1);
|
||||||
|
color: #00e5ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 229, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-nav .tab:hover {
|
||||||
|
background-color: rgba(0, 229, 255, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 229, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-nav .tab.active {
|
||||||
|
background-color: #00e5ff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background-color: #00d9ff;
|
||||||
|
color: #0d1117;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
max-width: 600px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background-color: #00d9ff;
|
||||||
|
color: #0d1117;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
max-width: 600px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 217, 255, 0.5);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 1.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
background-color: #00e5ff;
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: #00e5ff;
|
||||||
|
color: #0d1117;
|
||||||
|
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 rgba(0, 229, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn:hover {
|
||||||
|
background-color: #4cc3dd;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 229, 255, 0.6), 0 0 25px rgba(0, 229, 255, 0.4);
|
||||||
|
transform: scale(1.05);
|
||||||
|
color: #00172e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
.admin-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-card {
|
||||||
|
display: block;
|
||||||
|
background-color: rgba(0, 229, 255, 0.1);
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #00e5ff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 229, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-card:hover {
|
||||||
|
background-color: rgba(0, 229, 255, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 229, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #00e5ff;
|
||||||
|
color: #0d1117;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 229, 255, 0.2);
|
||||||
|
transition: background-color 0.3s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create:hover {
|
||||||
|
background-color: #00bcd4;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
/* === 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: #00ff88; color: #00ff88; background: rgba(0, 255, 136, 0.08); }
|
||||||
|
.tt-tile.answered.wrong { border-color: #ff6b6b; color: #ff6b6b; background: rgba(255, 107, 107, 0.08); }
|
||||||
|
|
||||||
|
/* --- 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: #00ff88; background: rgba(0, 255, 136, 0.12); color: #b8ffdc; }
|
||||||
|
.tt-answer.wrong { border-color: #ff6b6b; background: rgba(255, 107, 107, 0.12); color: #ffd0d0; }
|
||||||
|
|
||||||
|
.tt-q-feedback { margin-top: 0.9rem; font-weight: 700; min-height: 1.2rem; }
|
||||||
|
.tt-q-feedback.correct { color: #00ff88; }
|
||||||
|
.tt-q-feedback.wrong { color: #ff6b6b; }
|
||||||
|
|
||||||
|
.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: #00ff88; 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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/* ============================================================
|
||||||
|
MINDFLUX THEME – zentrale Design-Tokens + Komponenten
|
||||||
|
Dark = Grün-Neon | Light = heller Grund + Magenta-Glow
|
||||||
|
Umschaltung über <html data-theme="dark|light"> (siehe theme.js)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
:root, [data-theme="dark"] {
|
||||||
|
--bg:#06090a; --bg-glow: radial-gradient(900px 420px at 60% -10%, rgba(43,217,106,.10), transparent 60%);
|
||||||
|
--panel:#0c1110; --panel-2:#0e1513; --surface:#0b100e; --surface-2:#101713;
|
||||||
|
--border:#1a2520; --line:#16201b;
|
||||||
|
--border-accent:rgba(43,217,106,.45); --accent:#2bd96a; --accent-2:#6ef58f; --accent-ink:#04140a;
|
||||||
|
--text:#d7e0d9; --muted:#717c74; --heading:#ffffff; --danger:#ff5f6b;
|
||||||
|
--glow-s:rgba(43,217,106,.55); --glow-w:rgba(43,217,106,.16);
|
||||||
|
--g-soft:0 0 18px var(--glow-w);
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg:#f4eef3; --bg-glow: radial-gradient(900px 420px at 60% -10%, rgba(209,29,146,.10), transparent 60%);
|
||||||
|
--panel:#ffffff; --panel-2:#fbf6fa; --surface:#ffffff; --surface-2:#faf4f9;
|
||||||
|
--border:#e9dee8; --line:#efe6ee;
|
||||||
|
--border-accent:rgba(209,29,146,.40); --accent:#d11d92; --accent-2:#ff5bc0; --accent-ink:#ffffff;
|
||||||
|
--text:#3c2c39; --muted:#8a7d88; --heading:#1b1019; --danger:#d12942;
|
||||||
|
--glow-s:rgba(209,29,146,.40); --glow-w:rgba(209,29,146,.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(--accent); }
|
||||||
|
::-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:var(--accent-dim,rgba(43,217,106,.12)); 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); }
|
||||||
|
|
||||||
|
@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){ .mf-shell{grid-template-columns:1fr;} .mf-sidebar{display:none;} .mf-topbar,.mf-main{grid-column:1;} .mf-grid{grid-template-columns:repeat(2,1fr);} }
|
||||||
|
@media (max-width:560px){ .mf-grid{grid-template-columns:1fr;} }
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!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 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,68 @@
|
|||||||
|
<!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>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,66 @@
|
|||||||
|
<!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 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,68 @@
|
|||||||
|
<!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>Lernfokus</h2>
|
||||||
|
<p>Trainiere Binär-, Dezimal- und Hexadezimalumrechnung sowie Speichereinheiten für typische AP1-Rechenaufgaben.</p>
|
||||||
|
<ul class="key-list">
|
||||||
|
<li>Binär, Dezimal und Hexadezimal sicher unterscheiden.</li>
|
||||||
|
<li>Bit, Byte, KiB, MiB und GiB sauber umrechnen.</li>
|
||||||
|
<li>Rechenwege nachvollziehbar dokumentieren.</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 Masken verstehen.",
|
||||||
|
"text": "Was beschreibt IPv4-Adresse am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Sie besteht aus 4 Oktetten mit Werten von 0 bis 255.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sie besteht aus 4 Oktetten mit Werten von 0 bis 255."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Welche Aussage zu Oktett ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Ein Oktett umfasst 8 Bit.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Ein Oktett umfasst 8 Bit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Woran erkennst du Subnetzmaske in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Sie trennt Netz- und Hostanteil."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Sie trennt Netz- und Hostanteil."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Was ist bei CIDR besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"CIDR gibt die Anzahl der Netzbits an.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "CIDR gibt die Anzahl der Netzbits an."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Was beschreibt /24 am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"/24 entspricht häufig 255.255.255.0.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "/24 entspricht häufig 255.255.255.0."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Welche Aussage zu Netzanteil ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Der Netzanteil identifiziert das Subnetz."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Der Netzanteil identifiziert das Subnetz."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Woran erkennst du Hostanteil in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Der Hostanteil identifiziert Geräte im Subnetz.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Der Hostanteil identifiziert Geräte im Subnetz."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Was ist bei private IPv4-Bereiche besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Sie werden intern genutzt, etwa 192.168.0.0/16.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Sie werden intern genutzt, etwa 192.168.0.0/16."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Was beschreibt Standardgateway am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Es verbindet das lokale Netz mit anderen Netzen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Es verbindet das lokale Netz mit anderen Netzen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "IPv4-Grundlagen",
|
||||||
|
"hint": "Adressaufbau, Oktette und Masken verstehen.",
|
||||||
|
"text": "Welche Aussage zu APIPA ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"169.254.x.x deutet oft auf DHCP-Probleme hin.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "169.254.x.x deutet oft auf DHCP-Probleme hin."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Was beschreibt Netzadresse am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Sie ist die erste Adresse eines Subnetzes.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sie ist die erste Adresse eines Subnetzes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Welche Aussage zu Broadcast-Adresse ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Sie ist die letzte Adresse eines klassischen IPv4-Subnetzes.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Sie ist die letzte Adresse eines klassischen IPv4-Subnetzes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Woran erkennst du Hostanzahl in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Bei normalen Subnetzen gilt 2 hoch Hostbits minus 2."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Bei normalen Subnetzen gilt 2 hoch Hostbits minus 2."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Was ist bei /26 besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"/26 hat 64 Adressen pro Subnetz.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "/26 hat 64 Adressen pro Subnetz."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Was beschreibt /30 am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"/30 wird oft für Punkt-zu-Punkt-Netze genutzt.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "/30 wird oft für Punkt-zu-Punkt-Netze genutzt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Welche Aussage zu Blockgröße ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Sie ergibt sich aus der Schrittweite im relevanten Oktett."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Sie ergibt sich aus der Schrittweite im relevanten Oktett."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Woran erkennst du Hostbits in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Hostbits bestimmen die Anzahl möglicher Adressen.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Hostbits bestimmen die Anzahl möglicher Adressen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Was ist bei Netzbits besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Netzbits bestimmen die Subnetzgröße.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Netzbits bestimmen die Subnetzgröße."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Was beschreibt Subnetzgrenze am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Adressen müssen zur richtigen Blockgrenze gehören."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Adressen müssen zur richtigen Blockgrenze gehören."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Subnetting",
|
||||||
|
"hint": "Netz, Broadcast und Hostanzahl berechnen.",
|
||||||
|
"text": "Welche Aussage zu Broadcast nicht vergeben ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Broadcast ist keine normale Hostadresse.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Broadcast ist keine normale Hostadresse."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Was beschreibt falsche Maske am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Sie kann Hosts scheinbar in falsche Netze legen.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sie kann Hosts scheinbar in falsche Netze legen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Welche Aussage zu falsches Gateway ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Dann sind andere Netze oft nicht erreichbar.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Dann sind andere Netze oft nicht erreichbar."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Woran erkennst du DHCP-Problem in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Fehlende automatische Adresse kann auf DHCP hindeuten."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Fehlende automatische Adresse kann auf DHCP hindeuten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Was ist bei Doppelvergabe besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Gleiche IP auf zwei Hosts verursacht Konflikte.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Gleiche IP auf zwei Hosts verursacht Konflikte."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Was beschreibt DNS vs IP am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Ping per IP, aber nicht per Name deutet auf DNS.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Ping per IP, aber nicht per Name deutet auf DNS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Welche Aussage zu lokale Erreichbarkeit ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Erst IP, Maske und Link prüfen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Erst IP, Maske und Link prüfen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Woran erkennst du Routingproblem in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Andere Netze brauchen passende Route oder Gateway.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Andere Netze brauchen passende Route oder Gateway."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Was ist bei Dokumentation besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Adresspläne verhindern Überschneidungen.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Adresspläne verhindern Überschneidungen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Was beschreibt Reservierungen am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Server und Drucker bekommen oft feste Adressen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Server und Drucker bekommen oft feste Adressen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse IP",
|
||||||
|
"hint": "IP-Probleme in Handlungssituationen erkennen.",
|
||||||
|
"text": "Welche Aussage zu Prüfungsweg IP ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Rechenschritte sauber notieren und Einheiten prüfen.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Rechenschritte sauber notieren und Einheiten prüfen."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
{
|
||||||
|
"title": "Zahlensysteme & Speicher",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Was beschreibt Binärsystem am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es nutzt nur 0 und 1 und basiert auf Potenzen von 2.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Es nutzt nur 0 und 1 und basiert auf Potenzen von 2."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Welche Aussage zu Stellenwert ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Jede Position hat einen Wert wie 1, 2, 4, 8 oder 16.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Jede Position hat einen Wert wie 1, 2, 4, 8 oder 16."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Woran erkennst du Dezimalumrechnung in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Binärstellen werden mit ihren Zweierpotenzen addiert."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Binärstellen werden mit ihren Zweierpotenzen addiert."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Was ist bei Bit besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Ein Bit ist die kleinste Informationseinheit mit 0 oder 1.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ein Bit ist die kleinste Informationseinheit mit 0 oder 1."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Was beschreibt Nibble am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Ein Nibble besteht aus 4 Bit.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Ein Nibble besteht aus 4 Bit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Welche Aussage zu Byte ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Ein Byte besteht aus 8 Bit."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Ein Byte besteht aus 8 Bit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Woran erkennst du Potenzen von 2 in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Sie sind Grundlage vieler IT-Rechenaufgaben.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sie sind Grundlage vieler IT-Rechenaufgaben."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Was ist bei führende Nullen besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Sie ändern den Zahlenwert nicht, können aber die Darstellung auffüllen.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Sie ändern den Zahlenwert nicht, können aber die Darstellung auffüllen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Was beschreibt Dualzahl am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Eine Dualzahl ist eine Zahl im Binärsystem."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Eine Dualzahl ist eine Zahl im Binärsystem."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Binär & Dezimal",
|
||||||
|
"hint": "Stellenwerte, Potenzen und Umrechnung verstehen.",
|
||||||
|
"text": "Welche Aussage zu Prüfungsweg ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Der Rechenweg ist oft wichtiger als nur das Endergebnis.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Der Rechenweg ist oft wichtiger als nur das Endergebnis."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Was beschreibt Hexadezimalsystem am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es nutzt Basis 16 mit Ziffern 0-9 und A-F.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Es nutzt Basis 16 mit Ziffern 0-9 und A-F."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Welche Aussage zu A bis F ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"A bis F stehen für die Werte 10 bis 15.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "A bis F stehen für die Werte 10 bis 15."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Woran erkennst du Hex zu Binär in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Eine Hexstelle entspricht genau 4 Bit."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Eine Hexstelle entspricht genau 4 Bit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Was ist bei Binär zu Hex besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Binärzahlen werden in Vierergruppen geteilt.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Binärzahlen werden in Vierergruppen geteilt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Was beschreibt Hex FF am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"FF entspricht dezimal 255.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "FF entspricht dezimal 255."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Welche Aussage zu Hex in Adressen ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Hex wird häufig bei Speicher- und Netzwerkdarstellungen genutzt."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Hex wird häufig bei Speicher- und Netzwerkdarstellungen genutzt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Woran erkennst du Basis 16 in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Jede Stelle ist eine Potenz von 16.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Jede Stelle ist eine Potenz von 16."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Was ist bei 2F besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"2F bedeutet 2 mal 16 plus 15.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "2F bedeutet 2 mal 16 plus 15."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Was beschreibt BC am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"BC kann aus der Binärgruppe 1011 1100 entstehen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "BC kann aus der Binärgruppe 1011 1100 entstehen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Hexadezimal",
|
||||||
|
"hint": "Hexwerte kompakt lesen und in Binär/Dezimal übertragen.",
|
||||||
|
"text": "Welche Aussage zu Prüfungstipp Hex ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Vierergruppen verhindern viele Umrechnungsfehler.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Vierergruppen verhindern viele Umrechnungsfehler."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Was beschreibt kB und KiB am besten?",
|
||||||
|
"answers": [
|
||||||
|
"kB nutzt 1000, KiB nutzt 1024 Byte.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "kB nutzt 1000, KiB nutzt 1024 Byte."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Welche Aussage zu MB und MiB ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"MB ist dezimal, MiB ist binär gerechnet.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "MB ist dezimal, MiB ist binär gerechnet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Woran erkennst du GiB in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Ein GiB entspricht 1024 hoch 3 Byte."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Ein GiB entspricht 1024 hoch 3 Byte."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Was ist bei Bit zu Byte besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"8 Bit ergeben 1 Byte.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "8 Bit ergeben 1 Byte."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Was beschreibt Übertragungsrate am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Bit pro Sekunde ist nicht dasselbe wie Byte pro Sekunde.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Bit pro Sekunde ist nicht dasselbe wie Byte pro Sekunde."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Welche Aussage zu Marketingangaben ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Speicherhersteller nutzen oft dezimale Einheiten."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Speicherhersteller nutzen oft dezimale Einheiten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Woran erkennst du Betriebssystemanzeige in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Systeme zeigen Kapazitäten häufig binärnah an.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Systeme zeigen Kapazitäten häufig binärnah an."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Was ist bei Rundung besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Rundungen müssen in Prüfungen nachvollziehbar sein.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Rundungen müssen in Prüfungen nachvollziehbar sein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Was beschreibt Einheitenfehler am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Verwechslung von Bit und Byte ist ein klassischer Fehler."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Verwechslung von Bit und Byte ist ein klassischer Fehler."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Speichereinheiten",
|
||||||
|
"hint": "Bit, Byte und Binärpräfixe in Aufgaben sicher anwenden.",
|
||||||
|
"text": "Welche Aussage zu Rechenweg Speicher ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Schreibe Einheitenschritte sauber auf.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schreibe Einheitenschritte sauber auf."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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", "Daten absichtlich verlangsamen", "Passwörter öffentlich speichern"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Backups dienen der Wiederherstellung nach Verlust, Fehlern oder Ausfällen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||||
|
"text": "Was bedeutet Restore?",
|
||||||
|
"answers": ["Wiederherstellung von Daten aus einer Sicherung", "Löschen aller Sicherungen", "Komprimieren eines Monitors"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Restore ist das Zurückspielen gesicherter Daten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 Backups größer werden", "Damit keine Dokumentation nötig ist"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ein Backup ist nur wertvoll, wenn die Wiederherstellung klappt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||||
|
"text": "Was ist RPO?",
|
||||||
|
"answers": ["Maximal tolerierbarer Datenverlust in Zeit", "Maximale Bildschirmauflösung", "Ein Druckerprotokoll"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "RPO beschreibt, wie alt die wiederhergestellten Daten höchstens sein dürfen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||||
|
"text": "Was ist RTO?",
|
||||||
|
"answers": ["Maximal tolerierbare Wiederherstellungszeit", "Router-Temperatur-Option", "Eine Dateiendung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "RTO beschreibt, wie schnell ein System nach Ausfall wieder laufen muss."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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", "Damit niemand Backups findet", "Damit Tests überflüssig sind"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Dokumentation macht Sicherung und Restore planbar."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||||
|
"text": "Welche Daten sollten gesichert werden?",
|
||||||
|
"answers": ["Geschäftskritische Daten und notwendige Konfigurationen", "Nur Hintergrundbilder", "Ausschließlich leere Ordner"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sicherungsumfang richtet sich nach Wiederherstellungsbedarf und Kritikalität."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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", "Ein Fenster im Serverraum", "Eine Monitorfunktion"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Backups werden oft in Zeiten geringer Nutzung geplant."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||||
|
"text": "Was ist bei personenbezogenen Daten in Backups wichtig?",
|
||||||
|
"answers": ["Schutz, Zugriff und Löschkonzept beachten", "Backups immer öffentlich machen", "Datenschutz gilt dort nie"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Auch Backups können personenbezogene Daten enthalten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Begriffe und Anforderungen an Datensicherung.",
|
||||||
|
"text": "Welche Aussage ist korrekt?",
|
||||||
|
"answers": ["Backups müssen zum Risiko und Wiederherstellungsbedarf passen", "Ein Backup ersetzt jede IT-Sicherheit", "Ein einziges ungetestetes Backup reicht immer"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Backup-Konzepte müssen Anforderungen, Risiken und Tests berücksichtigen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Was sichert ein Vollbackup?",
|
||||||
|
"answers": ["Alle ausgewählten Daten", "Nur Änderungen seit gestern", "Nur gelöschte Dateien"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ein Vollbackup enthält den kompletten ausgewählten Datenbestand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Was sichert ein differentielles Backup?",
|
||||||
|
"answers": ["Alle Änderungen seit dem letzten Vollbackup", "Nur Änderungen seit dem letzten beliebigen Backup", "Immer alle Daten"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Differentiell bezieht sich auf das letzte Vollbackup."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Was sichert ein inkrementelles Backup?",
|
||||||
|
"answers": ["Änderungen seit dem letzten Backup", "Immer alle Daten", "Nur Betriebssystemdateien"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Inkrementell bezieht sich auf das letzte Backup, egal ob voll oder inkrementell."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Welche Sicherungsart braucht meist am meisten Speicherplatz?",
|
||||||
|
"answers": ["Vollbackup", "Inkrementelles Backup", "Keine Sicherung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Vollbackups enthalten den gesamten ausgewählten Datenbestand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Welche Sicherungsart spart im Alltag meist am meisten Speicherplatz?",
|
||||||
|
"answers": ["Inkrementelles Backup", "Tägliches Vollbackup", "Doppelte Vollsicherung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Inkrementelle Backups sichern nur die Änderungen seit der letzten Sicherung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Was brauchst du beim differentiellen Restore?",
|
||||||
|
"answers": ["Letztes Vollbackup und letztes differentielles Backup", "Alle differentiellen Backups", "Nur das erste inkrementelle Backup"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Differentiell enthält alle Änderungen seit dem letzten Vollbackup."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Was brauchst du beim inkrementellen Restore typischerweise?",
|
||||||
|
"answers": ["Vollbackup und alle benötigten inkrementellen Backups der Kette", "Nur das letzte inkrementelle Backup", "Kein Vollbackup"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Die inkrementelle Kette muss vollständig sein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Was ist ein Nachteil langer inkrementeller Ketten?",
|
||||||
|
"answers": ["Restore kann länger dauern und anfälliger sein", "Sie brauchen immer mehr Kabel", "Sie verhindern Datenschutz"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Wenn ein Teil der Kette fehlt oder defekt ist, kann der Restore scheitern."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Welche Strategie ist häufig praktikabel?",
|
||||||
|
"answers": ["Regelmäßige Vollbackups plus tägliche inkrementelle oder differentielle Sicherungen", "Nie sichern", "Nur einmal im Jahr prüfen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Kombinationen balancieren Speicherbedarf, Sicherungsdauer und Restore-Aufwand."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Voll, differentiell, inkrementell",
|
||||||
|
"hint": "Unterscheide Sicherungsarten nach Speicherbedarf, Dauer und Restore-Aufwand.",
|
||||||
|
"text": "Welche Aussage zum Vollbackup stimmt?",
|
||||||
|
"answers": ["Restore ist oft einfacher als bei langen Ketten", "Es enthält nie vollständige Daten", "Es ist immer verboten"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ein vollständiger Sicherungsstand vereinfacht die Wiederherstellung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Was bedeutet die 3-2-1-Regel grob?",
|
||||||
|
"answers": ["3 Kopien, 2 Medientypen, 1 Kopie extern", "3 Passwörter, 2 Nutzer, 1 Drucker", "3 Monitore, 2 Kabel, 1 Maus"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Die Regel reduziert Risiko durch mehrere Kopien und externe Lagerung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Warum ist eine externe oder offline Kopie sinnvoll?",
|
||||||
|
"answers": ["Sie schützt besser gegen Brand, Diebstahl oder Ransomware", "Sie macht Daten automatisch öffentlich", "Sie ersetzt alle Benutzerrechte"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Getrennte Kopien erhöhen die Chance auf Wiederherstellung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Was ist bei Cloud-Backups wichtig?",
|
||||||
|
"answers": ["Datenschutz, Verschlüsselung, Vertrag und Wiederherstellbarkeit", "Nur die Farbe des Cloud-Logos", "Keine Zugriffskontrolle"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Cloud-Backups brauchen technische, rechtliche und organisatorische Bewertung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Warum sollten Backups verschlüsselt werden?",
|
||||||
|
"answers": ["Zum Schutz vor unbefugtem Zugriff", "Damit sie langsamer gelöscht werden", "Damit sie nie wiederherstellbar sind"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Verschlüsselung unterstützt die Vertraulichkeit der gesicherten Daten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Was sollte nach einer Backup-Einrichtung dokumentiert werden?",
|
||||||
|
"answers": ["Zeitplan, Umfang, Speicherort, Verantwortliche und Restore-Verfahren", "Nur der Name des Praktikanten", "Nichts"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Dokumentation macht Betrieb und Notfallwiederherstellung möglich."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Ein Backup läuft täglich, schlägt aber seit Wochen fehl. Was ist das Hauptproblem?",
|
||||||
|
"answers": ["Fehlende Überwachung oder Reaktion auf Fehler", "Zu viele erfolgreiche Tests", "Zu viel Dokumentation"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Backups müssen überwacht und Fehler müssen bearbeitet werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Was ist bei Ransomware besonders wichtig?",
|
||||||
|
"answers": ["Nicht dauerhaft beschreibbare oder getrennte Backup-Kopien", "Backups immer im selben Ordner offen lassen", "Backups nie prüfen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ransomware kann erreichbare Backups verschlüsseln oder löschen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Was ist ein typischer Fehler in Backup-Konzepten?",
|
||||||
|
"answers": ["Sicherung nie wiederherstellen testen", "RPO und RTO definieren", "Zugriffe einschränken"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ungetestete Sicherungen können im Ernstfall wertlos sein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Welche Frage ist bei Backup-Planung zentral?",
|
||||||
|
"answers": ["Wie viel Datenverlust und Ausfallzeit sind akzeptabel?", "Welche Tapete hat der Serverraum?", "Welcher Monitor ist am hellsten?"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "RPO und RTO bestimmen die passende Backup-Strategie."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Backup-Strategie & Praxis",
|
||||||
|
"hint": "Bewerte Sicherungsorte, Sicherheit, Tests und typische AP1-Szenarien.",
|
||||||
|
"text": "Welche Aussage ist richtig?",
|
||||||
|
"answers": ["Backup, Restore-Test und Dokumentation gehören zusammen", "Backup ohne Restore ist immer ausreichend", "Dokumentation verhindert Wiederherstellung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ein gutes Konzept umfasst Sicherung, Prüfung und nachvollziehbare Abläufe."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
{
|
||||||
|
"title": "Datenschutz & Datensicherheit",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Welche Angabe ist eindeutig personenbezogen?",
|
||||||
|
"answers": ["Seriennummer eines Switches", "Name und E-Mail-Adresse eines Kunden", "Temperatur im Serverraum"],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Name und E-Mail-Adresse können direkt einer Person zugeordnet werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Welche Daten können indirekt personenbezogen sein?",
|
||||||
|
"answers": ["Eine IP-Adresse in Logdateien", "Eine zufällige Gehäusefarbe", "Eine leere Tabelle ohne Bezug"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "IP-Adressen können unter Umständen einer Person oder einem Anschluss zugeordnet werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Welche Aussage zu Personalnummern ist richtig?",
|
||||||
|
"answers": ["Sie sind nie personenbezogen", "Sie können Beschäftigte eindeutig identifizieren", "Sie sind nur bei Auszubildenden geschützt"],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Personalnummern beziehen sich auf konkrete Beschäftigte und sind personenbezogen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Was bedeutet Anonymisierung?",
|
||||||
|
"answers": ["Personenbezug wird so entfernt, dass keine Zuordnung mehr möglich ist", "Daten werden nur umbenannt", "Daten werden unverschlüsselt gespeichert"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Anonyme Daten lassen sich nicht mehr auf eine Person zurückführen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Was bedeutet Pseudonymisierung?",
|
||||||
|
"answers": ["Identifizierende Merkmale werden ersetzt, eine Zuordnung bleibt mit Zusatzwissen möglich", "Alle Daten werden gelöscht", "Daten werden öffentlich gemacht"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Pseudonyme reduzieren Risiken, beseitigen den Personenbezug aber nicht vollständig."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Welche Information ist besonders sensibel?",
|
||||||
|
"answers": ["Gesundheitsdaten eines Mitarbeiters", "Rack-Nummer eines Servers", "Hersteller eines Monitors"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Gesundheitsdaten gehören zu besonders schützenswerten personenbezogenen Daten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Was ist bei Logdateien mit Benutzerkennungen wichtig?",
|
||||||
|
"answers": ["Sie können personenbezogene Daten enthalten", "Sie sind automatisch anonym", "Sie dürfen nie ausgewertet werden"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Benutzerkennungen, Zeitpunkte und Aktionen können Nutzerverhalten nachvollziehbar machen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Welche Kombination ist personenbezogen?",
|
||||||
|
"answers": ["Abteilung, Name und Telefonnummer", "Nur die Anzahl der Switchports", "Ein leerer USB-Stick"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Die Kombination ermöglicht eine klare Zuordnung zu einer Person."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Warum ist der Personenbezug in Prüfungsaufgaben wichtig?",
|
||||||
|
"answers": ["Er entscheidet, ob Datenschutzregeln zu beachten sind", "Er entscheidet nur über die Bildschirmauflösung", "Er ist nie relevant"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sobald personenbezogene Daten verarbeitet werden, greifen Datenschutzanforderungen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Personenbezogene Daten",
|
||||||
|
"hint": "Erkenne, wann Daten sich auf eine identifizierte oder identifizierbare Person beziehen.",
|
||||||
|
"text": "Welche Aussage ist korrekt?",
|
||||||
|
"answers": ["Auch Kundendaten und Mitarbeiterdaten können personenbezogen sein", "Nur Passwörter sind personenbezogen", "Personenbezogene Daten gibt es nur in Papierakten"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Personenbezogene Daten können digital oder analog und in vielen Kontexten vorkommen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was bedeutet Datenminimierung?",
|
||||||
|
"answers": ["Nur Daten verarbeiten, die für den Zweck notwendig sind", "Alle verfügbaren Daten speichern", "Daten nie löschen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Es sollen nicht mehr personenbezogene Daten verarbeitet werden als nötig."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was bedeutet Zweckbindung?",
|
||||||
|
"answers": ["Daten dürfen nur für festgelegte Zwecke verarbeitet werden", "Daten dürfen für alles genutzt werden", "Der Zweck muss geheim bleiben"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Der Zweck muss vorab feststehen und die Verarbeitung daran gebunden sein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was beschreibt der Grundsatz der Richtigkeit?",
|
||||||
|
"answers": ["Personenbezogene Daten sollen sachlich richtig und aktuell sein", "Daten sollen möglichst bunt sein", "Daten sollen nie geprüft werden"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Unrichtige Daten können zu falschen Entscheidungen führen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was bedeutet Speicherbegrenzung?",
|
||||||
|
"answers": ["Daten nur so lange speichern, wie es für den Zweck nötig ist", "Daten unbegrenzt behalten", "Nur kleine Dateien speichern"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Personenbezogene Daten brauchen Löschfristen oder Prüffristen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was bedeutet Integrität und Vertraulichkeit im Datenschutzkontext?",
|
||||||
|
"answers": ["Daten vor unbefugtem Zugriff, Verlust und Manipulation schützen", "Daten öffentlich verteilen", "Daten ohne Backup speichern"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Dieser Grundsatz verlangt angemessene Sicherheit der Verarbeitung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was ist Rechenschaftspflicht?",
|
||||||
|
"answers": ["Nachweisen können, dass Datenschutzregeln eingehalten werden", "Nur mündlich behaupten, alles sei sicher", "Alle Nachweise löschen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Organisationen müssen Datenschutzmaßnahmen dokumentieren und belegen können."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Welche Verarbeitung ist problematisch?",
|
||||||
|
"answers": ["Kundendaten ohne Zweck unbegrenzt speichern", "Zugriffe rollenbasiert einschränken", "Daten nach Frist löschen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ohne Zweck und Frist werden Zweckbindung und Speicherbegrenzung verletzt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Warum braucht eine Verarbeitung eine Rechtsgrundlage?",
|
||||||
|
"answers": ["Damit personenbezogene Daten rechtmäßig verarbeitet werden", "Damit die Daten größer werden", "Damit Backups schneller laufen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Rechtmäßigkeit ist ein zentraler Grundsatz."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Welche Aussage passt zur Transparenz?",
|
||||||
|
"answers": ["Betroffene sollen verständlich informiert werden", "Verarbeitung soll geheim bleiben", "Informationen dürfen absichtlich verwirrend sein"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Transparenz verlangt verständliche Informationen über die Verarbeitung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "DSGVO-Grundsätze",
|
||||||
|
"hint": "Grundsätze helfen, Verarbeitung rechtmäßig und nachvollziehbar zu bewerten.",
|
||||||
|
"text": "Was ist ein typischer Prüfungsfehler?",
|
||||||
|
"answers": ["Mehr Daten erfassen als für den Auftrag nötig", "Zugriffsrechte begrenzen", "Löschfristen definieren"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Übermäßige Datenerhebung widerspricht Datenminimierung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was bedeutet Auskunftsrecht?",
|
||||||
|
"answers": ["Betroffene können Informationen über gespeicherte Daten verlangen", "Betroffene müssen alle Daten selbst sichern", "Betroffene verlieren alle Rechte"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Betroffene dürfen erfahren, welche Daten zu welchem Zweck verarbeitet werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was bedeutet Recht auf Berichtigung?",
|
||||||
|
"answers": ["Falsche personenbezogene Daten müssen korrigiert werden", "Daten müssen immer veröffentlicht werden", "Nur Passwörter dürfen geändert werden"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Unrichtige Daten können berichtigt werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was bedeutet Recht auf Löschung?",
|
||||||
|
"answers": ["Daten können unter bestimmten Voraussetzungen gelöscht werden", "Alle Daten müssen immer sofort gelöscht werden", "Daten dürfen nie gelöscht werden"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Löschung hängt von Zweck, Rechtsgrundlage und Aufbewahrungspflichten ab."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was bedeutet Widerspruchsrecht?",
|
||||||
|
"answers": ["Betroffene können bestimmten Verarbeitungen widersprechen", "Betroffene müssen jede Verarbeitung akzeptieren", "Nur Administratoren dürfen widersprechen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Unter bestimmten Bedingungen kann Verarbeitung abgelehnt werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was bedeutet Datenübertragbarkeit?",
|
||||||
|
"answers": ["Daten in einem strukturierten, gängigen Format erhalten können", "Daten nur auf Papier bekommen", "Daten nie exportieren dürfen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Das Recht erleichtert Wechsel zwischen Diensten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Wer ist eine betroffene Person?",
|
||||||
|
"answers": ["Die Person, auf die sich personenbezogene Daten beziehen", "Nur der Datenschutzbeauftragte", "Nur der Serveradministrator"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Betroffen ist die identifizierte oder identifizierbare Person."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was sollte bei einer Auskunftsanfrage zuerst geprüft werden?",
|
||||||
|
"answers": ["Identität der anfragenden Person", "Monitorauflösung", "Farbe der Datenbank"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Daten dürfen nicht an unberechtigte Personen herausgegeben werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Warum dürfen Betroffenenrechte nicht ignoriert werden?",
|
||||||
|
"answers": ["Sie sind rechtlich relevante Ansprüche", "Sie sind freiwillige Dekoration", "Sie gelten nur im Urlaub"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Organisationen müssen Anfragen angemessen bearbeiten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Was ist bei Löschanfragen zu beachten?",
|
||||||
|
"answers": ["Ob gesetzliche Aufbewahrungspflichten entgegenstehen", "Ob der Drucker eingeschaltet ist", "Ob die Datei hübsch benannt ist"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Nicht jede Löschung ist sofort möglich, wenn Pflichten zur Aufbewahrung bestehen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Betroffenenrechte",
|
||||||
|
"hint": "Betroffene Personen haben Rechte gegenüber Verantwortlichen.",
|
||||||
|
"text": "Welche Antwort auf eine Betroffenenanfrage ist professionell?",
|
||||||
|
"answers": ["Anfrage dokumentieren, prüfen und fristgerecht beantworten", "Anfrage löschen", "Anfrage öffentlich posten"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Dokumentation und fristgerechte Bearbeitung sind wichtig."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Wofür steht TOM?",
|
||||||
|
"answers": ["Technische und organisatorische Maßnahmen", "Tägliche Online-Meldung", "Temporäre Ordner-Migration"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "TOMs sind Schutzmaßnahmen für die Verarbeitung personenbezogener Daten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Welches Schutzziel wird durch Verschlüsselung besonders unterstützt?",
|
||||||
|
"answers": ["Vertraulichkeit", "Lautstärke", "Bildschirmgröße"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Verschlüsselung schützt Inhalte vor unberechtigtem Lesen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Welches Schutzziel wird durch Backups besonders unterstützt?",
|
||||||
|
"answers": ["Verfügbarkeit", "Farbtreue", "Kabellänge"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Backups helfen, Daten nach Verlust wiederherzustellen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Was bedeutet Integrität?",
|
||||||
|
"answers": ["Daten sind vollständig und unverändert", "Daten sind immer öffentlich", "Daten sind besonders groß"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Integrität schützt vor unbemerkter Manipulation oder Fehlern."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Welche Maßnahme unterstützt Zugriffskontrolle?",
|
||||||
|
"answers": ["Rollen- und Rechtekonzept", "Offene Admin-Passwörter", "Gemeinsamer Account für alle"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Rollen und Rechte begrenzen Zugriff nach Aufgabe."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Warum ist Protokollierung wichtig?",
|
||||||
|
"answers": ["Zugriffe und Änderungen können nachvollzogen werden", "Alle Daten werden automatisch anonym", "Sie ersetzt jedes Backup"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Logs unterstützen Kontrolle, Nachvollziehbarkeit und Fehleranalyse."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Was ist ein gutes Passwortkonzept?",
|
||||||
|
"answers": ["Starke Passwörter, individuelle Accounts und wenn möglich Mehrfaktor-Authentifizierung", "Ein Passwort für alle", "Passwörter auf Monitor kleben"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Individuelle starke Authentifizierung reduziert Missbrauch."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Welche organisatorische Maßnahme ist sinnvoll?",
|
||||||
|
"answers": ["Mitarbeiter zu Datenschutz und Sicherheit schulen", "Sicherheitsregeln geheim halten", "Keine Zuständigkeiten definieren"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schulung hilft, Fehlverhalten und Unsicherheit zu reduzieren."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Was bedeutet Need-to-know?",
|
||||||
|
"answers": ["Zugriff nur, wenn er für die Aufgabe nötig ist", "Alle dürfen alles sehen", "Nur externe Personen bekommen Zugriff"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Need-to-know begrenzt Zugriff auf notwendige Informationen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "TOMs & Schutzziele",
|
||||||
|
"hint": "Technische und organisatorische Maßnahmen schützen Daten im Betrieb.",
|
||||||
|
"text": "Welche Maßnahme passt zur Verfügbarkeit?",
|
||||||
|
"answers": ["USV, Backup und Redundanz", "Unverschlüsselte Passwortliste", "Keine Wartung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Verfügbarkeit wird durch Ausfallschutz und Wiederherstellbarkeit unterstützt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Ein USB-Stick mit Kundendaten wurde verloren. Was ist sinnvoll?",
|
||||||
|
"answers": ["Vorfall melden, Risiko prüfen und zuständige Stellen informieren", "Den Vorfall verschweigen", "Neue Aufkleber drucken"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Datenschutzvorfälle müssen bewertet und nach Prozess behandelt werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Wer ist Verantwortlicher im Datenschutz?",
|
||||||
|
"answers": ["Die Stelle, die über Zweck und Mittel der Verarbeitung entscheidet", "Immer der Auszubildende", "Immer der Druckerhersteller"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Verantwortliche bestimmen Zweck und Mittel der Verarbeitung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Was ist Auftragsverarbeitung?",
|
||||||
|
"answers": ["Ein Dienstleister verarbeitet personenbezogene Daten im Auftrag", "Ein Kunde kauft einen Monitor", "Ein Projekt wird ohne Auftrag begonnen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Bei Auftragsverarbeitung braucht es klare vertragliche Regelungen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Ein externer IT-Dienstleister wartet ein System mit Kundendaten. Was ist zu prüfen?",
|
||||||
|
"answers": ["Zugriff, Vertrag, Vertraulichkeit und Zweckbindung", "Nur die Farbe seines Firmenwagens", "Ob er private Musik hört"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Externe Zugriffe müssen rechtlich und technisch geregelt sein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Was ist bei Bildschirmarbeitsplätzen mit Kundendaten sinnvoll?",
|
||||||
|
"answers": ["Bildschirm sperren und Sichtschutz bei Bedarf nutzen", "Daten dauerhaft offen anzeigen", "Passwort unter die Tastatur legen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Arbeitsplätze müssen gegen unbefugte Einsicht geschützt werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Warum sollten alte Datenträger sicher gelöscht oder vernichtet werden?",
|
||||||
|
"answers": ["Damit personenbezogene Daten nicht wiederhergestellt werden können", "Damit sie schöner aussehen", "Damit Backups schneller werden"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Normales Löschen reicht oft nicht gegen Wiederherstellung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Was ist bei Testdaten in Entwicklungssystemen sinnvoll?",
|
||||||
|
"answers": ["Keine echten personenbezogenen Daten nutzen oder sie anonymisieren", "Produktivdaten frei kopieren", "Kundendaten öffentlich bereitstellen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Testsysteme sollten keine unnötigen echten personenbezogenen Daten enthalten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Was sollte bei neuen IT-Systemen früh bedacht werden?",
|
||||||
|
"answers": ["Datenschutz durch Technikgestaltung und datenschutzfreundliche Voreinstellungen", "Nur die Gehäusefarbe", "Möglichst viele unnötige Daten sammeln"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Privacy by Design und Privacy by Default sind wichtige Prinzipien."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Ein Kollege bittet um fremde Personaldaten ohne dienstlichen Grund. Was ist richtig?",
|
||||||
|
"answers": ["Zugriff verweigern und auf Berechtigung verweisen", "Daten sofort senden", "Daten öffentlich im Chat posten"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ohne berechtigten Zweck darf kein Zugriff erfolgen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Praxisfälle & Verantwortung",
|
||||||
|
"hint": "Wende Datenschutz in typischen AP1-Handlungssituationen an.",
|
||||||
|
"text": "Warum ist Datenschutz in AP1-Aufgaben oft mit IT-Sicherheit verbunden?",
|
||||||
|
"answers": ["Weil technische Schutzmaßnahmen personenbezogene Daten sichern", "Weil Datenschutz nur aus Firewalls besteht", "Weil Sicherheit Datenschutz ersetzt"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Datenschutz und Datensicherheit überschneiden sich, sind aber nicht identisch."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
{
|
||||||
|
"title": "OSI-Modell",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Wozu dient das OSI-Modell hauptsächlich?",
|
||||||
|
"answers": ["Zur strukturierten Beschreibung von Netzwerkkommunikation", "Zum Formatieren von Festplatten", "Zum Berechnen von Rabatten"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Das OSI-Modell teilt Netzwerkkommunikation in sieben verständliche Schichten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Wie viele Schichten hat das OSI-Modell?",
|
||||||
|
"answers": ["4", "7", "12"],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Das OSI-Modell besteht aus sieben Schichten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Welche Schicht ist die unterste Schicht?",
|
||||||
|
"answers": ["Anwendung", "Bitübertragung", "Transport"],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Schicht 1 ist die Bitübertragungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Welche Schicht ist die oberste Schicht?",
|
||||||
|
"answers": ["Anwendung", "Vermittlung", "Sicherung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schicht 7 ist die Anwendungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Warum ist das OSI-Modell in der Fehleranalyse nützlich?",
|
||||||
|
"answers": ["Fehler können schichtweise eingegrenzt werden", "Es löscht automatisch Störungen", "Es ersetzt jede Dokumentation"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Man prüft vom physischen Anschluss bis zur Anwendung systematisch."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Welche Reihenfolge ist korrekt?",
|
||||||
|
"answers": ["Bitübertragung, Sicherung, Vermittlung, Transport", "Anwendung, Sicherung, Bitübertragung, Transport", "Transport, Darstellung, Bitübertragung, Sicherung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Die unteren vier Schichten lauten 1 Bitübertragung, 2 Sicherung, 3 Vermittlung, 4 Transport."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Was bedeutet Kapselung in Netzwerkmodellen?",
|
||||||
|
"answers": ["Jede Schicht fügt eigene Informationen hinzu", "Daten werden immer gelöscht", "Nur Kabel werden beschriftet"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Beim Senden werden Daten von Schicht zu Schicht mit Headern versehen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Welche Aussage ist korrekt?",
|
||||||
|
"answers": ["Protokolle lassen sich bestimmten Schichten zuordnen", "Alle Protokolle gehören immer zu Schicht 1", "Das OSI-Modell gilt nur für Drucker"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Beispiele sind Ethernet auf Schicht 2, IP auf Schicht 3 und HTTP auf Schicht 7."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Welcher Begriff passt zum OSI-Modell?",
|
||||||
|
"answers": ["Schichtenmodell", "Bilanzkonto", "Zeiterfassung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "OSI ist ein Schichtenmodell für Kommunikation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "OSI-Grundlagen",
|
||||||
|
"hint": "Verstehe Zweck, Reihenfolge und Nutzen des Schichtenmodells.",
|
||||||
|
"text": "Was ist in AP1 besonders wichtig?",
|
||||||
|
"answers": ["Typische Geräte, Protokolle und Fehler den Schichten zuordnen", "Alle RFCs auswendig lernen", "Nur IPv6-Adressen malen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Prüfungsaufgaben arbeiten oft mit Zuordnung und Fehlerdiagnose."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Was gehört typischerweise zu Schicht 1?",
|
||||||
|
"answers": ["Kabel und elektrische Signale", "HTTP-Statuscodes", "Benutzerrechte"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schicht 1 beschreibt die physische Übertragung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Welches Gerät arbeitet klassisch auf Schicht 2?",
|
||||||
|
"answers": ["Switch", "Webserver", "Druckertreiber"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Switches treffen Weiterleitungsentscheidungen anhand von MAC-Adressen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Welche Adresse gehört zu Schicht 2?",
|
||||||
|
"answers": ["MAC-Adresse", "IP-Adresse", "URL"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "MAC-Adressen werden auf der Sicherungsschicht verwendet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Welche Einheit passt zu Schicht 2?",
|
||||||
|
"answers": ["Frame", "Segment", "Datei"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Auf Schicht 2 werden Daten typischerweise als Frames betrachtet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Ein Netzwerkkabel ist defekt. Welche Schicht ist zuerst betroffen?",
|
||||||
|
"answers": ["Schicht 1", "Schicht 4", "Schicht 7"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Kabel gehören zur Bitübertragungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Was macht ein Switch mit seiner MAC-Tabelle?",
|
||||||
|
"answers": ["Er ordnet MAC-Adressen Ports zu", "Er vergibt HTTP-Cookies", "Er berechnet Kreditzinsen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Die MAC-Tabelle hilft beim gezielten Weiterleiten von Frames."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Was ist ein typisches Schicht-1-Problem?",
|
||||||
|
"answers": ["Kein Link-Signal am Port", "Falsches Passwort im Webformular", "Falscher Mehrwertsteuersatz"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Fehlendes Link-Signal deutet auf physische Verbindung oder Portproblem hin."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Was ist ein typisches Schicht-2-Thema?",
|
||||||
|
"answers": ["VLAN", "DNS-Name", "HTML-Formular"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "VLANs werden häufig der Sicherungsschicht zugeordnet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Was prüfst du bei Verdacht auf Schicht-1-Fehler?",
|
||||||
|
"answers": ["Kabel, Link-LED, Port und physische Verbindung", "Nur den Browsercache", "Nur Benutzerrechte"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schicht 1 beginnt bei der physischen Verbindung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 1-2: Bitübertragung & Sicherung",
|
||||||
|
"hint": "Kabel, Signale, MAC-Adressen, Frames und Switches sicher zuordnen.",
|
||||||
|
"text": "Welche Aussage zu Switches ist korrekt?",
|
||||||
|
"answers": ["Sie arbeiten überwiegend mit MAC-Adressen", "Sie ersetzen jede IP-Adresse", "Sie sind immer Webbrowser"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Switches leiten Frames anhand von MAC-Adressen weiter."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Auf welcher Schicht arbeitet IP?",
|
||||||
|
"answers": ["Schicht 3", "Schicht 1", "Schicht 7"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "IP gehört zur Vermittlungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Auf welcher Schicht arbeitet TCP?",
|
||||||
|
"answers": ["Schicht 4", "Schicht 2", "Schicht 6"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "TCP ist ein Transportprotokoll."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Welches Gerät arbeitet klassisch auf Schicht 3?",
|
||||||
|
"answers": ["Router", "Hub", "Monitor"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Router verbinden Netze anhand von IP-Informationen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Welche Adresse gehört zu Schicht 3?",
|
||||||
|
"answers": ["IP-Adresse", "MAC-Adresse", "Seriennummer"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "IP-Adressen dienen zur logischen Adressierung im Netzwerk."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Welche Protokolle gehören typischerweise zu Schicht 4?",
|
||||||
|
"answers": ["TCP und UDP", "HTTP und HTML", "VGA und HDMI"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "TCP und UDP sind Transportprotokolle."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Wozu dienen Ports?",
|
||||||
|
"answers": ["Zur Zuordnung von Daten zu Anwendungen oder Diensten", "Zur Stromversorgung", "Zur Berechnung von Subventionen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ports helfen, Daten an den richtigen Dienst weiterzugeben."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Was ist ein typischer TCP-Vorteil?",
|
||||||
|
"answers": ["Zuverlässige, verbindungsorientierte Übertragung", "Keine Fehlerkontrolle", "Nur Broadcast"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "TCP bestätigt Daten und sorgt für Reihenfolge und Wiederholung bei Verlust."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Was ist ein typischer UDP-Vorteil?",
|
||||||
|
"answers": ["Geringer Overhead und schnelle Übertragung", "Garantierte Zustellung jedes Pakets", "Automatische Verschlüsselung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "UDP ist schlanker, aber bietet weniger Zuverlässigkeitsmechanismen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Ein Host hat eine falsche Standardgateway-Adresse. Welche Schicht ist besonders betroffen?",
|
||||||
|
"answers": ["Schicht 3", "Schicht 1", "Schicht 6"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Gateway und Routing gehören zur Vermittlungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 3-4: Vermittlung & Transport",
|
||||||
|
"hint": "IP, Routing, TCP, UDP und Ports sauber unterscheiden.",
|
||||||
|
"text": "Ein Dienst hört nicht auf Port 443. Welche Schicht ist bei der Portprüfung relevant?",
|
||||||
|
"answers": ["Schicht 4", "Schicht 1", "Schicht 2"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ports sind ein Thema der Transportschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welche Schicht ist die Anwendungsschicht?",
|
||||||
|
"answers": ["Schicht 7", "Schicht 3", "Schicht 1"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schicht 7 stellt Dienste für Anwendungen bereit."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welches Protokoll gehört typischerweise zur Anwendungsschicht?",
|
||||||
|
"answers": ["HTTP", "IP", "Ethernet"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "HTTP ist ein Anwendungsprotokoll."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welche Aufgabe passt zur Darstellungsschicht?",
|
||||||
|
"answers": ["Datenformat, Kodierung oder Verschlüsselung", "Kabel einstecken", "MAC-Tabelle lernen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schicht 6 befasst sich unter anderem mit Darstellung und Kodierung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welche Aufgabe passt zur Sitzungsschicht?",
|
||||||
|
"answers": ["Sitzungen und Dialoge zwischen Systemen steuern", "IP-Routing durchführen", "Kabelschirmung prüfen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schicht 5 koordiniert Sitzungen und Verbindungsdialoge."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "DNS wird in Prüfungen meist welcher Schicht zugeordnet?",
|
||||||
|
"answers": ["Schicht 7", "Schicht 2", "Schicht 1"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "DNS ist ein Anwendungsdienst zur Namensauflösung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "SMTP wird typischerweise wofür verwendet?",
|
||||||
|
"answers": ["E-Mail-Versand", "MAC-Adressierung", "Stromversorgung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "SMTP ist ein Protokoll für den Versand von E-Mails."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welche Aussage zu HTTPS ist passend?",
|
||||||
|
"answers": ["Es schützt HTTP-Kommunikation durch TLS", "Es ersetzt jedes Routing", "Es gehört nur zu Schicht 1"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "HTTPS nutzt TLS für verschlüsselte Webkommunikation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Ein Browser zeigt 'DNS_PROBE_FINISHED'. Welcher Bereich ist naheliegend?",
|
||||||
|
"answers": ["Namensauflösung auf Anwendungsebene", "Defektes Netzwerkkabel zwingend", "Falsche Mehrwertsteuer"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "DNS-Probleme liegen bei der Namensauflösung, typischerweise Anwendungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welches Beispiel passt zur Darstellungsschicht?",
|
||||||
|
"answers": ["Zeichencodierung wie UTF-8", "Switchport lernen", "Subnetzmaske berechnen"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Zeichencodierung betrifft die Darstellung von Daten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Schicht 5-7: Sitzung, Darstellung, Anwendung",
|
||||||
|
"hint": "Dienste, Formate, Verschlüsselung und Anwendungsprotokolle zuordnen.",
|
||||||
|
"text": "Welche Protokolle sind typische Anwendungsprotokolle?",
|
||||||
|
"answers": ["HTTP, DNS, SMTP", "IP, ICMP, ARP", "Kupfer, Glasfaser, Funk"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "HTTP, DNS und SMTP werden meist der Anwendungsschicht zugeordnet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ein PC hat keine Link-LED am Netzwerkport. Was prüfst du zuerst?",
|
||||||
|
"answers": ["Kabel, Port und physische Verbindung", "DNS-Eintrag", "HTTP-Cookie"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ohne Link ist zuerst Schicht 1 zu prüfen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ein PC hat Link, aber keine gültige IP-Adresse. Welche Ebene ist naheliegend?",
|
||||||
|
"answers": ["Schicht 3 beziehungsweise IP-Konfiguration", "Nur Schicht 7", "Nur das Netzteil"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "IP-Adresse, Gateway und Netzmaske gehören zur Vermittlungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ping zur IP funktioniert, aber der Name nicht. Was ist wahrscheinlich?",
|
||||||
|
"answers": ["DNS-Problem", "Kabel zwingend defekt", "Monitor kaputt"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Wenn IP erreichbar ist, aber Name nicht, liegt der Fokus auf Namensauflösung."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ein Webserver ist per Ping erreichbar, aber HTTPS geht nicht. Was prüfst du?",
|
||||||
|
"answers": ["Port 443, Dienst und Firewall", "Nur die Kabelfarbe", "Nur den Druckertreiber"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Hier sind Transport- und Anwendungsebene relevant."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Warum ist schichtweises Vorgehen sinnvoll?",
|
||||||
|
"answers": ["Es verhindert planloses Raten", "Es macht Dokumentation unnötig", "Es löscht automatisch Fehler"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Schichtweises Prüfen grenzt Ursachen systematisch ein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Welche Reihenfolge ist bei Fehlersuche oft sinnvoll?",
|
||||||
|
"answers": ["Physisch, lokale Konfiguration, Routing, Ports, Anwendung", "Anwendung, Rechnung, Gehäusefarbe", "Nur Neustart ohne Prüfung"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Man beginnt häufig unten und arbeitet sich nach oben."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ein falsches VLAN am Switchport betrifft vor allem welche Schicht?",
|
||||||
|
"answers": ["Schicht 2", "Schicht 6", "Schicht 7"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "VLAN-Zuordnung ist ein Thema der Sicherungsschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ein falsches Standardgateway verhindert Zugriff auf andere Netze. Welche Schicht ist betroffen?",
|
||||||
|
"answers": ["Schicht 3", "Schicht 1", "Schicht 5"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Gateway und Routing gehören zu Schicht 3."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Ein Dienst läuft, aber die Firewall blockiert den Port. Welche Prüfung ist relevant?",
|
||||||
|
"answers": ["Transportebene und Portfreigabe", "Nur Bildschirmhelligkeit", "Nur Speicherplatz"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Ports und Verbindungen gehören zur Transportschicht."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Fehleranalyse nach OSI",
|
||||||
|
"hint": "Gehe systematisch von physischer Verbindung bis Anwendung vor.",
|
||||||
|
"text": "Eine Webseite liefert HTTP 500. Wo liegt der Fokus eher?",
|
||||||
|
"answers": ["Anwendung oder Serverdienst", "Defektes Patchkabel zwingend", "MAC-Adresse des Monitors"],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "HTTP-Fehlercodes gehören zur Anwendungsebene beziehungsweise zum Webdienst."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"title": "Datenbanksysteme",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Was beschreibt Tabelle am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Sie strukturiert Daten in Zeilen und Spalten.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sie strukturiert Daten in Zeilen und Spalten."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Welche Aussage zu Datensatz ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Eine Zeile beschreibt ein Objekt oder Ereignis.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Eine Zeile beschreibt ein Objekt oder Ereignis."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Woran erkennst du Attribut in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Eine Spalte beschreibt eine Eigenschaft."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Eine Spalte beschreibt eine Eigenschaft."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Was ist bei Primärschlüssel besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Er identifiziert Datensätze eindeutig.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Er identifiziert Datensätze eindeutig."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Was beschreibt Fremdschlüssel am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Er verweist auf Datensätze anderer Tabellen.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Er verweist auf Datensätze anderer Tabellen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Welche Aussage zu Relation ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Beziehung zwischen Tabellen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Beziehung zwischen Tabellen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Woran erkennst du Redundanz in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Doppelte Daten können Inkonsistenzen erzeugen.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Doppelte Daten können Inkonsistenzen erzeugen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Was ist bei Normalisierung besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Sie reduziert unnötige Wiederholungen.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Sie reduziert unnötige Wiederholungen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Was beschreibt DBMS am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Es verwaltet Daten, Zugriffe und Transaktionen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Es verwaltet Daten, Zugriffe und Transaktionen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Datenbank-Grundlagen",
|
||||||
|
"hint": "Tabellen, Datensätze und Beziehungen verstehen.",
|
||||||
|
"text": "Welche Aussage zu Transaktion ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Sie fasst Operationen zuverlässig zusammen.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Sie fasst Operationen zuverlässig zusammen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Was beschreibt SELECT am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Liest Daten aus Tabellen.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Liest Daten aus Tabellen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Welche Aussage zu INSERT ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Fügt neue Datensätze ein.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Fügt neue Datensätze ein."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Woran erkennst du UPDATE in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Ändert bestehende Datensätze."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Ändert bestehende Datensätze."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Was ist bei DELETE besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Löscht Datensätze.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Löscht Datensätze."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Was beschreibt WHERE am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Filtert Datensätze nach Bedingungen.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Filtert Datensätze nach Bedingungen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Welche Aussage zu JOIN ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Verknüpft Tabellen über Beziehungen."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Verknüpft Tabellen über Beziehungen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Woran erkennst du Backup in einer AP1-Aufgabe?",
|
||||||
|
"answers": [
|
||||||
|
"Datenbanken brauchen Sicherung und Restore-Test.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Datenbanken brauchen Sicherung und Restore-Test."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Was ist bei Rechtekonzept besonders wichtig?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Nutzer sollten nur notwendige Rechte haben.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 1,
|
||||||
|
"explanation": "Nutzer sollten nur notwendige Rechte haben."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Was beschreibt Datenschutz am besten?",
|
||||||
|
"answers": [
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung.",
|
||||||
|
"Personenbezogene Daten müssen geschützt werden."
|
||||||
|
],
|
||||||
|
"correct": 2,
|
||||||
|
"explanation": "Personenbezogene Daten müssen geschützt werden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "SQL & Betrieb",
|
||||||
|
"hint": "Abfragen, Rechte und Betriebsaspekte einordnen.",
|
||||||
|
"text": "Welche Aussage zu Index ist richtig?",
|
||||||
|
"answers": [
|
||||||
|
"Kann Abfragen beschleunigen, braucht aber Speicher.",
|
||||||
|
"Es ist nur Dekoration und hat keinen Prüfungsbezug.",
|
||||||
|
"Es ist ein zufälliger Begriff ohne technische Bedeutung."
|
||||||
|
],
|
||||||
|
"correct": 0,
|
||||||
|
"explanation": "Kann Abfragen beschleunigen, braucht aber Speicher."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user