diff --git a/App/.htaccess b/App/.htaccess index 91ba8de..109ba40 100644 --- a/App/.htaccess +++ b/App/.htaccess @@ -1,3 +1,12 @@ +# Sensible Dateien niemals ausliefern (Defense-in-Depth), auch wenn sie im +# Docroot liegen und die -f-Regel unten sie sonst direkt serviert. + + Require all denied + + + Require all denied + + RewriteEngine On # Kein RewriteBase — Apache nutzt den Request-Pfad automatisch. # Funktioniert fuer alle Deploy-Ziele: diff --git a/App/assets/js/sim-metrics.js b/App/assets/js/sim-metrics.js index 0592822..08655ed 100644 --- a/App/assets/js/sim-metrics.js +++ b/App/assets/js/sim-metrics.js @@ -20,7 +20,7 @@ {k:'final_temperature',type:'temp',l:'Temperatur'}, {k:'final_flooded_pct',type:'pctInv',l:'Überflutung'}, {k:'final_co2',type:'num',l:'CO₂',u:'ppm',dir:'lo'}, - {k:'final_budget',type:'money',l:'Budget'}, + {k:'final_budget',type:'num',l:'Budget',u:'Mio €'}, {k:'final_population',type:'num',l:'Bevölkerung'}, {k:'measures_bought',type:'num',l:'Maßnahmen'} ], diff --git a/App/php/api/assessment.php b/App/php/api/assessment.php index 53f9c52..b7aa896 100644 --- a/App/php/api/assessment.php +++ b/App/php/api/assessment.php @@ -25,6 +25,16 @@ if (!defined('GGS_MAX_SESSION_MS')) define('GGS_MAX_SESSION_MS', 7200000); $rawDuration = (int)($body['duration'] ?? 0); $durationMs = max(0, min($rawDuration, GGS_MAX_SESSION_MS)); +// Zwischen-Telemetrie eines laufenden Durchgangs NICHT in assessments persistieren. +// Der Live-Zustand für das Lehrer-Cockpit kommt über den Heartbeat (live_sessions); +// diese 30-s-Pings (phase 'running'/'started') haben früher die Tabelle zugemüllt +// (~96 % der Zeilen). Nur echte Abschlüsse/Ergebnisse werden gespeichert. +$pl = $body['processLog'] ?? []; +$phase = (is_array($pl) && isset($pl['phase'])) ? $pl['phase'] : null; +if ($phase === 'running' || $phase === 'started') { + Response::ok(['skipped' => true]); +} + $db->execute( 'INSERT INTO assessments (session_id, sim_id, class_id, process_log, predictions, results, reflections, duration_ms, completed_phases) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', diff --git a/App/php/api/licenses.php b/App/php/api/licenses.php index bceea02..aafc539 100644 --- a/App/php/api/licenses.php +++ b/App/php/api/licenses.php @@ -15,8 +15,7 @@ if ($method === 'GET') { if (isset($_GET['admin']) && $_GET['admin'] === '1') { Session::start(); $adminId = $_SESSION['admin_id'] ?? null; - $teacherId = Session::teacherId(); - if (!$adminId && !$teacherId) { + if (!$adminId) { Response::error('Nicht autorisiert', 401); } @@ -169,16 +168,18 @@ if ($method === 'POST') { Response::ok(['licenseId' => (int)$license['id']]); } - // Super-Admin: neue Lizenzen generieren + // Super-Admin: neue Lizenzen generieren (NUR echter Admin) if ($action === 'generate') { - $teacherId = Session::requireTeacher(); + Session::start(); + if (empty($_SESSION['admin_id'])) Response::error('Nur Admin', 403); $count = min(1000, max(1, (int)($body['count'] ?? 100))); $year = (int)($body['year'] ?? date('Y')); $schoolYear = ($year % 100) . '/' . (($year % 100) + 1); $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; - $generated = 0; - for ($i = 0; $i < $count; $i++) { + $generated = 0; $tries = 0; $maxTries = $count * 10 + 100; + while ($generated < $count && $tries < $maxTries) { + $tries++; $code = ''; for ($j = 0; $j < 3; $j++) { if ($j > 0) $code .= '-'; @@ -189,7 +190,7 @@ if ($method === 'POST') { $db->execute('INSERT INTO licenses (code, school_year) VALUES (?, ?)', [$code, $schoolYear]); $generated++; } catch (\Exception $e) { - $i--; // Collision, retry + // Kollision (unwahrscheinlich) → nächster Versuch; $maxTries schützt vor Endlosschleife } } Response::ok(['generated' => $generated]); diff --git a/App/php/api/progress.php b/App/php/api/progress.php index 614d734..d44b81e 100644 --- a/App/php/api/progress.php +++ b/App/php/api/progress.php @@ -19,10 +19,21 @@ $db = Database::get(); if ($method === 'GET') { $simId = $_GET['sim_id'] ?? ''; - $studentId = (int)($_GET['student_id'] ?? 0); + $reqStudentId = (int)($_GET['student_id'] ?? 0); + $studentId = 0; - // Schüler*in: eigener Fortschritt - if ($simId && !$studentId) { + if ($reqStudentId) { + // Lehrer-Ansicht: nur eingeloggte Lehrperson, und die:der Schüler:in + // MUSS zu einer Klasse dieser Lehrperson gehören (sonst IDOR). + $teacherId = Session::requireTeacher(); + $owned = $db->fetchOne( + 'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?', + [$reqStudentId, $teacherId] + ); + if (!$owned) Response::error('Kein Zugriff auf diese:n Schüler:in', 403); + $studentId = $reqStudentId; + } elseif ($simId) { + // Schüler*in: eigener Fortschritt (aus der Session abgeleitet) $sessionId = Session::requireStudent(); $session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]); if ($session) { diff --git a/App/schueler.html b/App/schueler.html index 06dea6a..1e89b89 100644 --- a/App/schueler.html +++ b/App/schueler.html @@ -95,8 +95,18 @@ .hidden{display:none} @media(max-width:550px){.mod-grid{grid-template-columns:1fr}.badge-grid{grid-template-columns:repeat(2,1fr)}} + + /* Gemeinsames Sim-Kennzahlen-Schema (renderSimMetrics aus sim-metrics.js) */ + .ivm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:7px;margin-top:.5rem} + .ivm{background:#f4f1e8;border-radius:8px;padding:6px 9px;min-width:0} + .ivm-l{display:block;font-size:.58rem;color:#8a8a8a;text-transform:uppercase;letter-spacing:.03em;margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .ivm-v{font-size:.92rem;font-weight:750}.ivm-u{font-size:.66rem;font-weight:600;color:#8a8a8a}.ivm-txt{font-size:.8rem;font-weight:700} + .ivm-stars{color:#e0a838;font-size:.95rem;letter-spacing:1px}.ivm-stars.sm{font-size:.8rem}.ivm-star-off{color:#ddd} + .ivm-bar{display:block;height:7px;border-radius:4px;background:#ece8dc;overflow:hidden;margin-top:3px}.ivm-bar>i{display:block;height:100%;border-radius:4px;background:currentColor} + .ivm-g{color:#4a7c4e}.ivm-li{color:#7fa856}.ivm-y{color:#e0a838}.ivm-o{color:#e08a3a}.ivm-r{color:#c0574a} +
@@ -496,7 +506,17 @@ function renderOwnRunCard(simId, run) { if (simId === 'busfahrt') return renderBusfahrtOwnRun(dt, dur, r, d); if (simId === 'fluggesellschaft') return renderFluggesellschaftOwnRun(dt, dur, r, d); if (simId === 'heli') return renderHeliOwnRun(dt, dur, r, d); - return '
'+dt+''+dur+'
'; + // Generisch: gemeinsames Kennzahlen-Schema (Sterne + grafische Kacheln) — + // deckt klima, klima-3d, farmer, fluss, logistik, weltkueche, kofferdetektiv, + // tourismustal, tourismusregion ab (vorher nur Datum + Dauer). + var st = (typeof d.stars === 'number') ? d.stars : (typeof d.sterneGesamt === 'number' ? d.sterneGesamt : null); + var starsHtml = ''; + if (st != null) { var n = Math.max(0, Math.min(5, Math.round(st))); + starsHtml = ''+'★'.repeat(n)+''+'★'.repeat(5-n)+''; } + var metrics = (typeof renderSimMetrics === 'function') ? renderSimMetrics(simId, d) : ''; + return '
'+dt+'' + + ''+dur+(starsHtml?' · '+starsHtml:'')+'
' + + metrics + '
'; } function renderBusfahrtOwnRun(dt, dur, r, d) { var stars = Math.max(0, Math.min(5, r.stars || 0)); diff --git a/App/sims/heli/game.html b/App/sims/heli/game.html index d3f2bb1..e0be4d7 100644 --- a/App/sims/heli/game.html +++ b/App/sims/heli/game.html @@ -683,9 +683,9 @@ function assessmentStart() { duration: 0, completedPhases: [], }); - // 30-s-Ping waehrend des Einsatzes + // Kein 30-s-Zwischen-Ping mehr an /assessment — der Live-Zustand läuft über + // den Heartbeat (live_sessions). Früher blähte das die assessments-Tabelle auf. if (heliAssessment.runTimer) clearInterval(heliAssessment.runTimer); - heliAssessment.runTimer = setInterval(assessmentPing, 30000); } function assessmentPing() { if (!heliAssessment.startMs || !game.mission) return;