33fb423967
- 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>
119 lines
5.1 KiB
JavaScript
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();
|
|
}
|
|
})();
|