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 '
';
+ // 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;