Stand 2026-04-13: PHP/MySQL Infrastruktur, Flussmanagement, Stadt-Prototyp
- PHP/MySQL Backend (XAMPP + Produktionsserver) - Front-Controller, API-Endpunkte, Session-Management - Flussmanagement-Simulation (Echtzeit, Punkt-basierter Fluss) - Stadt & Raumplanung (Prototyp, Top-Down Kachelsystem) - Klimawaechter 3D: Deiche kleiner, Baeume kippen, Budget angepasst - persistence.ts: Dualer Speicher (localStorage + Server-API) - 6 Unit-Test-Dateien fuer bestehende Simulationen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Assessment-Daten speichern
|
||||
* POST /api/assessment {simId, processLog, predictions, results, reflections, duration, completedPhases}
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Response::error('Nur POST erlaubt', 405);
|
||||
}
|
||||
|
||||
$sessionId = Session::requireStudent();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body || !isset($body['simId'])) {
|
||||
Response::error('simId erforderlich');
|
||||
}
|
||||
|
||||
$db = Database::get();
|
||||
|
||||
// class_id aus student_session holen
|
||||
$session = $db->fetchOne('SELECT class_id FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
$classId = $session ? $session['class_id'] : null;
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO assessments (session_id, sim_id, class_id, process_log, predictions, results, reflections, duration_ms, completed_phases)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$sessionId,
|
||||
$body['simId'],
|
||||
$classId,
|
||||
json_encode($body['processLog'] ?? []),
|
||||
json_encode($body['predictions'] ?? []),
|
||||
json_encode($body['results'] ?? []),
|
||||
json_encode($body['reflections'] ?? []),
|
||||
$body['duration'] ?? 0,
|
||||
json_encode($body['completedPhases'] ?? []),
|
||||
]
|
||||
);
|
||||
|
||||
Response::ok(['id' => $db->lastInsertId()]);
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Lehrkraefte-Dashboard Daten
|
||||
* GET /api/dashboard?class_id=X
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
Response::error('Nur GET erlaubt', 405);
|
||||
}
|
||||
|
||||
$teacherId = Session::requireTeacher();
|
||||
$db = Database::get();
|
||||
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
|
||||
// Verifizieren dass die Klasse dem Lehrer gehoert
|
||||
if ($classId) {
|
||||
$class = $db->fetchOne(
|
||||
'SELECT * FROM classes WHERE id = ? AND teacher_id = ?',
|
||||
[$classId, $teacherId]
|
||||
);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
}
|
||||
|
||||
// Klassen des Lehrers
|
||||
$classes = $db->fetchAll('SELECT * FROM classes WHERE teacher_id = ? ORDER BY created_at DESC', [$teacherId]);
|
||||
|
||||
if (!$classId && !empty($classes)) {
|
||||
$classId = $classes[0]['id'];
|
||||
}
|
||||
|
||||
// Aktive Schueler (letzte 60 Minuten)
|
||||
$activeStudents = $db->fetchAll(
|
||||
'SELECT id, display_name, last_seen FROM student_sessions
|
||||
WHERE class_id = ? AND last_seen > DATE_SUB(NOW(), INTERVAL 60 MINUTE)
|
||||
ORDER BY last_seen DESC',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Alle Schueler der Klasse
|
||||
$allStudents = $db->fetchAll(
|
||||
'SELECT id, display_name, created_at, last_seen FROM student_sessions WHERE class_id = ? ORDER BY display_name',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Assessments fuer diese Klasse
|
||||
$assessments = $db->fetchAll(
|
||||
'SELECT a.*, s.display_name FROM assessments a
|
||||
JOIN student_sessions s ON s.id = a.session_id
|
||||
WHERE a.class_id = ?
|
||||
ORDER BY a.submitted_at DESC
|
||||
LIMIT 100',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Modulfreigaben
|
||||
$modules = $db->fetchAll('SELECT * FROM class_modules WHERE class_id = ?', [$classId]);
|
||||
|
||||
Response::ok([
|
||||
'classes' => $classes,
|
||||
'currentClassId' => $classId,
|
||||
'activeStudents' => $activeStudents,
|
||||
'allStudents' => $allStudents,
|
||||
'assessments' => $assessments,
|
||||
'modules' => $modules,
|
||||
]);
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Spielstaende speichern/laden
|
||||
* GET /api/saves?key=klimawaechter-save → Spielstand laden
|
||||
* POST /api/saves {key, data, version} → Spielstand speichern
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$sessionId = Session::studentId();
|
||||
if (!$sessionId) {
|
||||
Response::json(['data' => null]);
|
||||
}
|
||||
$key = $_GET['key'] ?? '';
|
||||
if (!$key) Response::error('key fehlt');
|
||||
|
||||
$row = $db->fetchOne(
|
||||
'SELECT save_data, save_version FROM game_saves WHERE session_id = ? AND save_key = ?',
|
||||
[$sessionId, $key]
|
||||
);
|
||||
Response::json(['data' => $row ? $row['save_data'] : null, 'version' => $row ? (int)$row['save_version'] : null]);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body || !isset($body['key']) || !isset($body['data'])) {
|
||||
Response::error('key und data erforderlich');
|
||||
}
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO game_saves (session_id, save_key, save_data, save_version)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE save_data = VALUES(save_data), save_version = VALUES(save_version)',
|
||||
[$sessionId, $body['key'], $body['data'], $body['version'] ?? 2]
|
||||
);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Session-Management
|
||||
* POST /api/sessions {action: "join", joinCode: "ABC123", displayName: "Anna"}
|
||||
* POST /api/sessions {action: "status"}
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
Response::error('Nur POST erlaubt', 405);
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? '';
|
||||
$db = Database::get();
|
||||
|
||||
if ($action === 'join') {
|
||||
$joinCode = strtoupper(trim($body['joinCode'] ?? ''));
|
||||
if (strlen($joinCode) !== 6) {
|
||||
Response::error('Ungültiger Code');
|
||||
}
|
||||
|
||||
$class = $db->fetchOne('SELECT id, name FROM classes WHERE join_code = ?', [$joinCode]);
|
||||
if (!$class) {
|
||||
Response::error('Klasse nicht gefunden');
|
||||
}
|
||||
|
||||
$displayName = trim($body['displayName'] ?? '');
|
||||
$uuid = Session::createStudent((int)$class['id'], $displayName);
|
||||
|
||||
Response::ok([
|
||||
'sessionId' => $uuid,
|
||||
'className' => $class['name'],
|
||||
'classId' => (int)$class['id'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'status') {
|
||||
$sessionId = Session::studentId();
|
||||
if (!$sessionId) {
|
||||
Response::json(['loggedIn' => false]);
|
||||
}
|
||||
|
||||
$session = $db->fetchOne(
|
||||
'SELECT s.display_name, s.class_id, c.name as class_name
|
||||
FROM student_sessions s
|
||||
LEFT JOIN classes c ON c.id = s.class_id
|
||||
WHERE s.id = ?',
|
||||
[$sessionId]
|
||||
);
|
||||
|
||||
if (!$session) {
|
||||
Response::json(['loggedIn' => false]);
|
||||
}
|
||||
|
||||
// last_seen aktualisieren
|
||||
$db->execute('UPDATE student_sessions SET last_seen = NOW() WHERE id = ?', [$sessionId]);
|
||||
|
||||
Response::json([
|
||||
'loggedIn' => true,
|
||||
'sessionId' => $sessionId,
|
||||
'displayName' => $session['display_name'],
|
||||
'classId' => $session['class_id'],
|
||||
'className' => $session['class_name'],
|
||||
]);
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* GeoGraSim — App Configuration
|
||||
* Umgebungserkennung und Grundkonstanten
|
||||
*/
|
||||
|
||||
// Umgebungserkennung via Hostname
|
||||
define('IS_PRODUCTION', isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] !== 'localhost');
|
||||
define('BASE_PATH', '/geograsim/App');
|
||||
define('BASE_URL', (IS_PRODUCTION ? 'https://www.staatsgeheimnis.at' : 'http://localhost') . BASE_PATH);
|
||||
define('APP_ROOT', dirname(__DIR__, 2));
|
||||
|
||||
// .env laden
|
||||
$envFile = IS_PRODUCTION
|
||||
? APP_ROOT . '/.env.production'
|
||||
: APP_ROOT . '/.env.local';
|
||||
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') continue;
|
||||
if (strpos($line, '=') === false) continue;
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
if (!defined($key)) {
|
||||
define($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults fuer lokale Entwicklung (XAMPP)
|
||||
if (!defined('DB_HOST')) define('DB_HOST', 'localhost');
|
||||
if (!defined('DB_NAME')) define('DB_NAME', 'geograsim');
|
||||
if (!defined('DB_USER')) define('DB_USER', 'root');
|
||||
if (!defined('DB_PASS')) define('DB_PASS', '');
|
||||
if (!defined('DB_PORT')) define('DB_PORT', '3306');
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* GeoGraSim — Database Connection (PDO)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
|
||||
function getDB(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_PORT, DB_NAME);
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* GeoGraSim — Thin Database Wrapper
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
|
||||
class Database {
|
||||
private PDO $pdo;
|
||||
private static ?Database $instance = null;
|
||||
|
||||
private function __construct() {
|
||||
$this->pdo = getDB();
|
||||
}
|
||||
|
||||
public static function get(): Database {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new Database();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function query(string $sql, array $params = []): PDOStatement {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
public function fetchOne(string $sql, array $params = []): ?array {
|
||||
$stmt = $this->query($sql, $params);
|
||||
$row = $stmt->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function fetchAll(string $sql, array $params = []): array {
|
||||
return $this->query($sql, $params)->fetchAll();
|
||||
}
|
||||
|
||||
public function execute(string $sql, array $params = []): int {
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
public function lastInsertId(): string {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* GeoGraSim — JSON Response Helpers
|
||||
*/
|
||||
|
||||
class Response {
|
||||
public static function json($data, int $status = 200): void {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function error(string $message, int $status = 400): void {
|
||||
self::json(['error' => $message], $status);
|
||||
}
|
||||
|
||||
public static function ok($data = null): void {
|
||||
self::json($data ?? ['ok' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* GeoGraSim — Session Management
|
||||
* Schueler: UUID-Cookie (anonym, kein Login)
|
||||
* Lehrkraefte: PHP-Session mit teacher_id
|
||||
*/
|
||||
|
||||
class Session {
|
||||
const COOKIE_NAME = 'ggs_session';
|
||||
const COOKIE_TTL = 86400 * 30; // 30 Tage
|
||||
|
||||
public static function start(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/** Schueler-Session-ID aus Cookie */
|
||||
public static function studentId(): ?string {
|
||||
return $_COOKIE[self::COOKIE_NAME] ?? null;
|
||||
}
|
||||
|
||||
/** Neue Schueler-Session erstellen */
|
||||
public static function createStudent(int $classId, string $displayName = ''): string {
|
||||
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000,
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
|
||||
$db = Database::get();
|
||||
$db->execute(
|
||||
'INSERT INTO student_sessions (id, class_id, display_name) VALUES (?, ?, ?)',
|
||||
[$uuid, $classId, $displayName]
|
||||
);
|
||||
|
||||
setcookie(self::COOKIE_NAME, $uuid, [
|
||||
'expires' => time() + self::COOKIE_TTL,
|
||||
'path' => BASE_PATH . '/',
|
||||
'samesite' => 'Lax',
|
||||
'secure' => IS_PRODUCTION,
|
||||
'httponly' => true,
|
||||
]);
|
||||
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
/** API-Guard: 401 wenn keine Session */
|
||||
public static function requireStudent(): string {
|
||||
$id = self::studentId();
|
||||
if (!$id) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Keine Session']);
|
||||
exit;
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** Lehrer eingeloggt? */
|
||||
public static function teacherId(): ?int {
|
||||
return $_SESSION['teacher_id'] ?? null;
|
||||
}
|
||||
|
||||
/** Lehrer-Login */
|
||||
public static function loginTeacher(int $teacherId): void {
|
||||
self::start();
|
||||
$_SESSION['teacher_id'] = $teacherId;
|
||||
}
|
||||
|
||||
/** Lehrer-Guard: 401 wenn nicht eingeloggt */
|
||||
public static function requireTeacher(): int {
|
||||
$id = self::teacherId();
|
||||
if (!$id) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Nicht eingeloggt']);
|
||||
exit;
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
self::start();
|
||||
session_destroy();
|
||||
setcookie(self::COOKIE_NAME, '', ['expires' => 1, 'path' => BASE_PATH . '/']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Vite Manifest Reader
|
||||
* Liest dist/.vite/manifest.json und gibt die korrekten Script-Tags aus.
|
||||
* Im Dev-Modus (kein manifest vorhanden) wird direkt auf die TS-Quellen verwiesen.
|
||||
*/
|
||||
|
||||
function viteAssets(string $entry): void {
|
||||
$manifestPath = APP_ROOT . '/dist/.vite/manifest.json';
|
||||
|
||||
// Production: Lese Manifest
|
||||
if (file_exists($manifestPath)) {
|
||||
$manifest = json_decode(file_get_contents($manifestPath), true);
|
||||
$chunk = $manifest[$entry] ?? null;
|
||||
if (!$chunk) return;
|
||||
|
||||
// CSS
|
||||
foreach ($chunk['css'] ?? [] as $css) {
|
||||
echo '<link rel="stylesheet" href="' . BASE_PATH . '/dist/' . $css . '">' . "\n";
|
||||
}
|
||||
// Preload imports
|
||||
foreach ($chunk['imports'] ?? [] as $importKey) {
|
||||
$imp = $manifest[$importKey] ?? null;
|
||||
if ($imp) {
|
||||
echo '<link rel="modulepreload" crossorigin href="' . BASE_PATH . '/dist/' . $imp['file'] . '">' . "\n";
|
||||
}
|
||||
}
|
||||
// Main entry
|
||||
echo '<script type="module" crossorigin src="' . BASE_PATH . '/dist/' . $chunk['file'] . '"></script>' . "\n";
|
||||
}
|
||||
// Dev fallback: keine Manifest-Datei → direkte TS-Referenz (Vite Dev Server)
|
||||
}
|
||||
|
||||
/** Session-Kontext als JS-Variable injizieren */
|
||||
function injectSessionContext(): void {
|
||||
$ctx = [
|
||||
'sessionId' => Session::studentId(),
|
||||
'teacherId' => Session::teacherId(),
|
||||
'baseUrl' => BASE_URL,
|
||||
'basePath' => BASE_PATH,
|
||||
];
|
||||
echo '<script>window.__GGS__ = ' . json_encode($ctx, JSON_UNESCAPED_UNICODE) . ';</script>' . "\n";
|
||||
}
|
||||
Reference in New Issue
Block a user