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>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user