/* Live klasdashboard: rendert actieve en recente leersessies en houdt de weergave via Server-Sent Events actueel. Een 30-secondencontrole is alleen een vangnet voor tijdelijk weggevallen mobiele/proxyverbindingen. */ "use strict"; function liveSessionTime(value){ const seconds=Math.max(0,Math.round((Date.now()-new Date(value).getTime())/1000)); if(seconds<60)return T("amLiveJustNow"); const minutes=Math.floor(seconds/60); if(minutes<60)return T("amLiveMinutes").replace("{count}",minutes); const hours=Math.floor(minutes/60); return T("amLiveHours").replace("{count}",hours); } function updateLiveConnectionState(host,state){ const badge=host.querySelector(".live-connection"); if(!badge)return; badge.className="live-connection "+state; badge.textContent=T(state==="connected"?"amLiveConnected":"amLiveReconnecting"); } function renderLiveSessionOverview(host,data,onPupil){ const section=document.createElement("section"); section.className="live-session-overview"; const head=document.createElement("div"); head.className="live-session-head"; const title=document.createElement("strong"); title.textContent=T("amLiveSessions"); const connection=document.createElement("span"); connection.className="live-connection connecting"; connection.textContent=T("amLiveReconnecting"); head.append(title,connection); section.appendChild(head); const sessions=data.sessions||[]; const active=sessions.filter(session=>session.status==="live"); const recent=sessions.filter(session=>session.status!=="live").slice(0,20); const renderGroup=(label,items,emptyKey)=>{ const group=document.createElement("div"); group.className="live-session-group"; const groupHead=document.createElement("div"); groupHead.className="live-session-group-head"; const groupTitle=document.createElement("strong"); groupTitle.textContent=label; const count=document.createElement("span"); count.textContent=String(items.length); groupHead.append(groupTitle,count); group.appendChild(groupHead); if(!items.length){ const empty=document.createElement("div"); empty.className="guestnote"; empty.textContent=T(emptyKey); group.appendChild(empty); section.appendChild(group); return; } const grid=document.createElement("div"); grid.className="live-session-grid"; items.forEach(session=>{ const card=document.createElement("button"); card.type="button"; const justStarted=session.status==="live" && new Date(data.serverTime||Date.now()).getTime()-new Date(session.startedAt).getTime()<60000; card.className="live-session-card "+(session.status==="live"?"live":"recent"); const status=document.createElement("span"); status.className="live-session-status"; status.textContent=T(justStarted?"amLiveNew":session.status==="live"?"amLiveWorking":"amLiveEnded"); const name=document.createElement("strong"); name.className="live-session-name"; name.textContent=session.pupilName; const widget=document.createElement("span"); widget.className="live-session-widget"; widget.textContent=(T("wg_"+session.widgetType)||session.widgetType)+" · "+T(session.mode==="kijken"?"amModeWatch":"amModeWork"); const time=document.createElement("span"); time.className="live-session-time"; time.textContent=T(session.status==="live"?"amLiveStarted":"amLiveLastSeen")+" "+ liveSessionTime(session.status==="live"?session.startedAt:session.lastSeenAt); card.append(status,name,widget,time); if(session.progressCount){ const score=document.createElement("span"); score.className="live-session-score"; score.textContent=T("amLiveUpdates").replace("{count}",session.progressCount); if(session.attempts)score.textContent+=" · "+T("amLiveScore") .replace("{correct}",session.correct).replace("{attempts}",session.attempts); card.appendChild(score); } card.addEventListener("click",()=>onPupil(session.pupilId)); grid.appendChild(card); }); group.appendChild(grid); section.appendChild(group); }; renderGroup(T("amLiveNow"),active,"amLiveNone"); renderGroup(T("amLiveRecent"),recent,"amLiveRecentNone"); host.appendChild(section); } function connectLiveClassDashboard(classId,onChange,isActive,onState){ let source=null; let stopped=false; let reloadTimer=null; const queueReload=()=>{ clearTimeout(reloadTimer); reloadTimer=setTimeout(()=>{if(!stopped&&isActive())onChange();},180); }; if(typeof EventSource!=="undefined"){ source=new EventSource("/api/progress/live/class/"+encodeURIComponent(classId)+"/stream"); source.onopen=()=>onState("connected"); source.onerror=()=>onState("reconnecting"); source.addEventListener("sessions",queueReload); } const fallback=setInterval(()=>{ if(!isActive()){stop();return;} onChange(); },30000); function stop(){ if(stopped)return; stopped=true; clearInterval(fallback); clearTimeout(reloadTimer); if(source)source.close(); } return stop; }