Files
geograsim/App/assets/js/live-client.js
T
Adminator 33fb423967 Atlas: Live-View-Feature + Submit-Bug-Fixes + Admin-Styleguide
- Neuer API-Endpoint /api/live (heartbeat / spectator) + DB-Tabelle
  live_sessions + class_modules.paused-Spalte
- Plattform-Live-Client (assets/js/live-client.js): Heartbeat,
  Pause-Overlay, Spectator-Mode bei ?view=teacher
- ggs_inject_live() in _scripts.php als Helper, in alle 11 Sim-Wrapper
  integriert
- Lehrer-Cockpit: tabellarische Klassen-Live-Ansicht pro Sim,
  Pause-Toggle, blinkender Tab-Indikator wenn aktive Sessions
- Submit-Bug erschlagen: progress.php hat jetzt submit_assessment-
  und reflection-Action; saves.php akzeptiert beide Key-Konventionen;
  alle Direkt-Pfad-API-Files mit require_once-Bootstrap
- Avatar-Default: zufaelliger DALL-E-Avatar fuer neue Schueler:innen
- Admin-Styleguide-Seite mit Bildstil, Farb-Tokens, Layout, iPad-Pattern
- Klima 2D + 3D: GGS_LIVE_STATE-Hook, Klima 2D zusaetzlich skipResume
  fuer Klassenaufgaben-Reset
- Test-Lehrer Jakob + 3 Schueler:innen lokal + auf Live angelegt
- Rundbriefe an alle Sim-Instanzen + Submit-Briefe an sonnensystem,
  busfahrt, energiemanager + Glossar-Anfrage zu Grundriess
- Status-Uebergabe in _inbox/zentrale/_status.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:20:53 +02:00

119 lines
5.1 KiB
JavaScript

/**
* GeoGraSim Plattform: Live-Client
*
* Sendet Heartbeats an /api/live (Live-View für Lehrkräfte) und pollt das
* Pause-Flag aus /api/modules?student=1. Wird vom Sim-Wrapper als Plattform-
* Asset geladen — Sims selbst müssen nichts implementieren.
*
* Erwartet: window.__GGS__ = { sessionId, simId, baseUrl, basePath, apiUrl }.
*
* Sims können optional window.GGS_LIVE_STATE() zurückgeben, um einen kleinen
* State-Snapshot mitzusenden (max 50 KB). Ohne diese Funktion wird nur ein
* leerer State gemeldet — Lehrer sieht trotzdem WER online ist und WAS spielt.
*/
(function () {
'use strict';
if (window.__GGS_LIVE_INITIALIZED__) return;
window.__GGS_LIVE_INITIALIZED__ = true;
var ctx = window.__GGS__ || {};
// Ohne simId können wir nicht melden, welches Modul gespielt wird.
if (!ctx.simId) return;
// sessionId nicht vorab prüfen — der Browser sendet das ggs_session-Cookie
// automatisch mit den fetch-Calls (credentials:'same-origin'), und der
// Server entscheidet, ob es eine gültige Schüler-Session ist.
var apiBase = ctx.apiUrl || ((ctx.baseUrl || '') + '/php/api');
var simId = ctx.simId;
// --- Heartbeat ---
var heartbeatTimer = null;
function snapshotState() {
try {
if (typeof window.GGS_LIVE_STATE === 'function') {
var s = window.GGS_LIVE_STATE();
if (s && typeof s === 'object') return s;
}
} catch (_) {}
return {};
}
function sendHeartbeat() {
fetch(apiBase + '/live.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ action: 'heartbeat', module_id: simId, state: snapshotState() })
}).catch(function () {});
}
// --- Pause-Polling ---
var pauseTimer = null;
var pauseOverlay = null;
function ensureOverlay() {
if (pauseOverlay) return pauseOverlay;
var el = document.createElement('div');
el.id = 'ggs-pause-overlay';
el.style.cssText = 'position:fixed;inset:0;background:rgba(31,75,55,.78);color:#fff;z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:Inter,sans-serif;font-weight:700;text-align:center;padding:2rem;backdrop-filter:blur(3px)';
el.innerHTML = '<div style="font-size:4rem;margin-bottom:.6rem">⏸</div>'
+ '<div style="font-size:1.6rem;margin-bottom:.4rem">Auftrag pausiert</div>'
+ '<div style="font-size:.95rem;font-weight:500;opacity:.85;max-width:420px;line-height:1.4">Deine Lehrperson hat das Tempo angehalten. Sobald sie freigibt, kannst du weitermachen.</div>';
document.body.appendChild(el);
pauseOverlay = el;
return el;
}
function showPause() { ensureOverlay().style.display = 'flex'; }
function hidePause() { if (pauseOverlay) pauseOverlay.style.display = 'none'; }
function checkPause() {
fetch(apiBase + '/modules.php?student=1&module_id=' + encodeURIComponent(simId), {
credentials: 'same-origin'
}).then(function (r) { return r.json(); })
.then(function (m) {
if (m && m.paused && m.mode === 'teacher_started') showPause();
else hidePause();
})
.catch(function () {});
}
// --- Spectator-Mode für Lehrkräfte ---
// ?view=teacher schaltet Input ab und blendet ein dezentes Banner ein.
function isSpectator() {
try { return new URLSearchParams(location.search).get('view') === 'teacher'; }
catch (_) { return false; }
}
function applySpectator() {
if (!isSpectator()) return;
var s = document.createElement('style');
s.textContent = 'html,body{pointer-events:none!important}'
+ '#ggs-spectator-banner{pointer-events:auto!important;position:fixed;top:0;left:0;right:0;background:#4a7c8a;color:#fff;padding:.5rem;text-align:center;font-family:Inter,sans-serif;font-size:.78rem;font-weight:700;z-index:99998}'
+ '#ggs-spectator-banner button{pointer-events:auto;margin-left:.6rem;padding:.2rem .6rem;border:none;border-radius:5px;background:#fff;color:#4a7c8a;font-weight:700;cursor:pointer}';
document.head.appendChild(s);
var bar = document.createElement('div');
bar.id = 'ggs-spectator-banner';
bar.innerHTML = '👁 Live-Ansicht (Lehrkraft) — Eingaben sind deaktiviert. <button onclick="window.close()">Fenster schließen</button>';
document.body.appendChild(bar);
}
// --- Lifecycle ---
function start() {
if (isSpectator()) { applySpectator(); return; } // Spectator nicht heartbeaten
sendHeartbeat();
heartbeatTimer = setInterval(sendHeartbeat, 4000);
checkPause();
pauseTimer = setInterval(checkPause, 5000);
// Bei Tab-Schließung sauberer „end"-Heartbeat (keepalive für unload)
window.addEventListener('beforeunload', function () {
try {
navigator.sendBeacon && navigator.sendBeacon(
apiBase + '/live.php',
new Blob([JSON.stringify({ action: 'end' })], { type: 'application/json' })
);
} catch (_) {}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();