/** * 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 = '
' + '
Auftrag pausiert
' + '
Deine Lehrperson hat das Tempo angehalten. Sobald sie freigibt, kannst du weitermachen.
'; 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. '; 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(); } })();