Files
weirsoe-party-protocol/lobby/templates/lobby/player_screen.html
DEV-bot 2882a7737b
All checks were successful
CI / test-and-quality (push) Successful in 1m56s
CI / test-and-quality (pull_request) Successful in 1m56s
feat(spa): add top-level app-shell error boundary recover actions (#151)
2026-03-01 10:46:48 +00:00

139 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WPP Player</title>
<style>
#answerOptions { margin: 8px 0; display: flex; flex-wrap: wrap; gap: 6px; }
#answerOptions button { border: 1px solid #999; padding: 6px 10px; border-radius: 8px; background: #f4f4f4; cursor: pointer; }
#answerOptions button.active { border-color: #1652f0; background: #dfe9ff; }
#guessStatus { margin: 6px 0 10px; font-size: 0.95rem; color: #333; }
#lieStatus { margin: 6px 0 10px; font-size: 0.95rem; color: #333; }
#joinStatus { margin: 6px 0 10px; font-size: 0.95rem; color: #333; }
#contextLockHint { margin: 6px 0 10px; font-size: 0.95rem; color: #333; }
#phaseStatus { margin: 6px 0 10px; font-size: 0.95rem; color: #333; }
#connectionBanner { margin: 8px 0 10px; padding: 8px 10px; border-radius: 8px; border: 1px solid #b91c1c; background: #fee2e2; color: #7f1d1d; display: none; }
#connectionBanner button { margin-left: 8px; border: 1px solid #991b1b; background: #fff; color: #7f1d1d; border-radius: 6px; padding: 4px 8px; cursor: pointer; }
#connectionBanner button[disabled] { opacity: 0.55; cursor: not-allowed; }
#playerShellErrorBoundary { margin: 8px 0 10px; padding: 8px 10px; border-radius: 8px; border: 1px solid #b91c1c; background: #fee2e2; color: #7f1d1d; display: none; }
#playerShellErrorBoundary button { margin-left: 8px; border: 1px solid #991b1b; background: #fff; color: #7f1d1d; border-radius: 6px; padding: 4px 8px; cursor: pointer; }
#playerShellErrorBoundary button[disabled] { opacity: 0.55; cursor: not-allowed; }
#lieStatus.locked { color: #0a5f2d; font-weight: 600; }
#guessStatus.locked { color: #0a5f2d; font-weight: 600; }
</style>
</head>
<body>
<h1>Player panel (MVP)</h1>
<input id="code" placeholder="Sessionkode">
<input id="nickname" placeholder="Nickname">
<button id="joinBtn" onclick="joinSession()">1) Join</button>
<p id="joinStatus">Klar til join.</p>
<p id="contextLockHint">Kontekst er ikke låst endnu.</p>
<input id="playerId" placeholder="Player id">
<input id="sessionToken" placeholder="Session token" type="password" readonly>
<input id="roundQuestionId" placeholder="Round question id" readonly>
<input id="lieText" placeholder="Din løgn">
<button id="lieSubmitBtn" onclick="submitLie()">2) Submit løgn</button>
<p id="lieStatus">Skriv din løgn.</p>
<input id="guessText" placeholder="Dit gæt" readonly>
<div id="answerOptions"></div>
<p id="guessStatus">Vælg et svar.</p>
<button id="guessSubmitBtn" onclick="submitGuess()" disabled>3) Submit gæt</button>
<button id="sessionDetailBtn" onclick="sessionDetail()">Opdater session-status</button>
<p id="sessionRefreshHint">Session-opdatering klar.</p>
<p id="roundContextHint">Runde-kontekst afventer session-opdatering.</p>
<button id="playerAutoRefreshToggleBtn" onclick="togglePlayerAutoRefresh()">Auto-refresh: OFF</button>
<p id="playerAutoRefreshHint">Auto-refresh er slået fra.</p>
<p id="playerLastRefreshStatus">Session-data ikke opdateret endnu.</p>
<p id="phaseStatus">Fase: ukendt (opdatér session-status).</p>
<p id="playerErrorHint">Ingen fejl.</p>
<div id="connectionBanner">Forbindelsen til serveren blev afbrudt.<button id="connectionRetryBtn" type="button" onclick="retryConnection()">Prøv igen</button></div>
<div id="playerShellErrorBoundary">Der opstod en kritisk fejl i app-skallen.
<button id="playerRecoverRetryBtn" type="button" onclick="recoverPlayerShell('retry')">Prøv gendan</button>
<button id="playerRecoverReloadBtn" type="button" onclick="recoverPlayerShell('reload')">Genindlæs siden</button>
</div>
<pre id="out">Klar.</pre>
<script>
var availableAnswers=[];
var guessSubmitted=false;
var lieSubmitted=false;
var lieSubmitInFlight=false;
var guessSubmitInFlight=false;
var PLAYER_CONTEXT_KEY="wppPlayerContext";
var joinInFlight=false;
var currentSessionStatus="";
var playerAutoRefreshEnabled=false;
var playerAutoRefreshTimer=null;
var sessionDetailInFlight=false;
var playerLastRefreshAtLabel="";
var playerLastRefreshFailed=false;
var connectionLost=false;
var connectionRetryInFlight=false;
var playerShellFatalError=false;
var playerShellRecoverInFlight=false;
function code(){return document.getElementById("code").value.trim().toUpperCase();}
function updatePlayerShellErrorBoundary(){var panel=document.getElementById("playerShellErrorBoundary");var retryBtn=document.getElementById("playerRecoverRetryBtn");var reloadBtn=document.getElementById("playerRecoverReloadBtn");if(!panel||!retryBtn||!reloadBtn){return;}panel.style.display=playerShellFatalError?"block":"none";retryBtn.disabled=playerShellRecoverInFlight||sessionDetailInFlight||joinInFlight||!code();reloadBtn.disabled=playerShellRecoverInFlight;}
function setPlayerShellFatalError(detail){playerShellFatalError=true;var out=document.getElementById("out");if(out){out.textContent=JSON.stringify({status:0,data:{error:"player_shell_runtime_error",detail:detail||"Ukendt runtime-fejl"}},null,2);}var hint=document.getElementById("playerErrorHint");if(hint){hint.textContent="Fejl: Kritisk app-fejl. Brug recover-handlingerne for at fortsætte.";}updatePlayerShellErrorBoundary();}
function clearPlayerShellFatalError(){playerShellFatalError=false;playerShellRecoverInFlight=false;updatePlayerShellErrorBoundary();}
function recoverPlayerShell(mode){if(playerShellRecoverInFlight){return Promise.resolve({error:"recover_in_flight"});}playerShellRecoverInFlight=true;updatePlayerShellErrorBoundary();if(mode==="reload"){window.location.reload();return Promise.resolve({ok:true});}if(!code()){playerShellRecoverInFlight=false;updatePlayerShellErrorBoundary();return Promise.resolve({error:"missing_session_code"});}return sessionDetail().then(function(result){clearPlayerShellFatalError();return result;}).catch(function(err){playerShellRecoverInFlight=false;updatePlayerShellErrorBoundary();throw err;});}
function pid(){return document.getElementById("playerId").value.trim();}
function rq(){return document.getElementById("roundQuestionId").value.trim();}
function phaseLabel(status){if(status==="lobby"){return"Lobby";}if(status==="lie"){return"Løgn";}if(status==="guess"){return"Gæt";}if(status==="reveal"){return"Reveal";}if(status==="finished"){return"Afsluttet";}return"Ukendt";}
function updatePhaseStatus(){var el=document.getElementById("phaseStatus");if(!el){return;}if(!currentSessionStatus){el.textContent="Fase: ukendt (opdatér session-status).";return;}el.textContent="Fase: "+phaseLabel(currentSessionStatus)+" ("+currentSessionStatus+")";}
function savePlayerContext(){try{localStorage.setItem(PLAYER_CONTEXT_KEY,JSON.stringify({code:code(),nickname:document.getElementById("nickname").value.trim(),player_id:pid(),session_token:document.getElementById("sessionToken").value.trim(),round_question_id:rq(),auto_refresh:playerAutoRefreshEnabled}));}catch(_e){}}
function loadPlayerContext(){try{var raw=localStorage.getItem(PLAYER_CONTEXT_KEY);if(!raw){return null;}return JSON.parse(raw);}catch(_e){return null;}}
function restorePlayerContext(){var ctx=loadPlayerContext();if(!ctx){return false;}if(ctx.code){document.getElementById("code").value=(ctx.code||"").toUpperCase();}if(ctx.nickname){document.getElementById("nickname").value=ctx.nickname;}if(ctx.player_id){document.getElementById("playerId").value=ctx.player_id;}if(ctx.session_token){document.getElementById("sessionToken").value=ctx.session_token;}if(ctx.round_question_id){document.getElementById("roundQuestionId").value=ctx.round_question_id;}playerAutoRefreshEnabled=!!ctx.auto_refresh;updatePlayerAutoRefreshUi();return !!(ctx.code&&ctx.player_id&&ctx.session_token);}
function hasSubmitContext(){return !!(code()&&pid()&&rq()&&document.getElementById("sessionToken").value.trim());}
function hasRoundQuestionContext(){return !!rq();}
function isPlayerContextLocked(){return !!(pid()&&document.getElementById("sessionToken").value.trim());}
function updateRoundContextHint(){var el=document.getElementById("roundContextHint");if(!el){return;}if(rq()){el.textContent="Runde-kontekst aktiv for spørgsmål #"+rq()+".";return;}el.textContent="Runde-kontekst afventer session-opdatering.";}
function resetRoundContextForManualChange(){document.getElementById("roundQuestionId").value="";renderAnswerOptions(null);updateRoundContextHint();}
function updateContextLockState(){var locked=isPlayerContextLocked()||joinInFlight;var codeField=document.getElementById("code");var nicknameField=document.getElementById("nickname");var playerIdField=document.getElementById("playerId");var tokenField=document.getElementById("sessionToken");if(codeField){codeField.readOnly=locked;}if(nicknameField){nicknameField.readOnly=locked;}if(playerIdField){playerIdField.readOnly=locked;}if(tokenField){tokenField.readOnly=true;}var hint=document.getElementById("contextLockHint");if(!hint){return;}if(joinInFlight){hint.textContent="Låser kontekst…";return;}if(locked){hint.textContent="Spillerkontekst er låst efter join.";return;}hint.textContent="Kontekst er ikke låst endnu.";}
function canAttemptJoin(){return !!(code()&&document.getElementById("nickname").value.trim());}
function normalizeApiError(data){if(!data||typeof data!=="object"){return"";}return (data.error_code||data.error||"").toString();}
function mapUiErrorMessage(errorKey){if(!errorKey){return"";}var key=errorKey.toLowerCase();if(key.indexOf("phase")!==-1){return"Ugyldig fase for handlingen. Opdatér session-status og prøv igen.";}if(key.indexOf("token")!==-1||key.indexOf("auth")!==-1){return"Session-token er ugyldig eller udløbet. Rejoin sessionen og prøv igen.";}if(key.indexOf("round")!==-1||key.indexOf("question")!==-1||key.indexOf("state")!==-1){return"Runde-kontekst matcher ikke længere. Opdatér session-status før næste handling.";}if(key.indexOf("session")!==-1){return"Sessionkoden er ugyldig eller sessionen findes ikke længere.";}return"Handling fejlede. Opdatér session-status og prøv igen.";}
function formatTimeLabel(dateObj){return dateObj.toLocaleTimeString("da-DK",{hour12:false});}
function markPlayerSessionRefresh(status){if(status>=200&&status<300){playerLastRefreshAtLabel=formatTimeLabel(new Date());playerLastRefreshFailed=false;}else{playerLastRefreshFailed=true;}updatePlayerLastRefreshStatus();}
function updatePlayerLastRefreshStatus(){var el=document.getElementById("playerLastRefreshStatus");if(!el){return;}if(!playerLastRefreshAtLabel){el.textContent=playerLastRefreshFailed?"Session-data kan være forældet (ingen succesfuld opdatering endnu).":"Session-data ikke opdateret endnu.";return;}if(playerLastRefreshFailed){el.textContent="Session-data kan være forældet (seneste succes: "+playerLastRefreshAtLabel+").";return;}el.textContent="Sidst opdateret: "+playerLastRefreshAtLabel+".";}
function updatePlayerAutoRefreshUi(){var btn=document.getElementById("playerAutoRefreshToggleBtn");var hint=document.getElementById("playerAutoRefreshHint");if(btn){btn.textContent="Auto-refresh: "+(playerAutoRefreshEnabled?"ON":"OFF");btn.disabled=sessionDetailInFlight||joinInFlight||!code();}if(!hint){return;}if(sessionDetailInFlight){hint.textContent="Auto-refresh-lås: afvent aktiv session-opdatering.";return;}if(joinInFlight){hint.textContent="Auto-refresh-lås: afvent aktiv join.";return;}if(!code()){hint.textContent="Auto-refresh kræver sessionkode.";return;}if(!playerAutoRefreshEnabled){hint.textContent="Auto-refresh er slået fra.";return;}if(currentSessionStatus==="finished"){hint.textContent="Auto-refresh stoppet: spillet er afsluttet.";return;}hint.textContent="Auto-refresh aktiv (10s) for spillerstatus.";}
function stopPlayerAutoRefresh(reason){playerAutoRefreshEnabled=false;if(playerAutoRefreshTimer){clearInterval(playerAutoRefreshTimer);playerAutoRefreshTimer=null;}if(reason){var hint=document.getElementById("playerAutoRefreshHint");if(hint){hint.textContent=reason;}}updatePlayerAutoRefreshUi();savePlayerContext();}
function startPlayerAutoRefresh(){if(!code()){updatePlayerAutoRefreshUi();return;}playerAutoRefreshEnabled=true;if(playerAutoRefreshTimer){clearInterval(playerAutoRefreshTimer);}playerAutoRefreshTimer=setInterval(function(){if(!code()||sessionDetailInFlight){return;}if(currentSessionStatus==="finished"){stopPlayerAutoRefresh("Auto-refresh stoppet: spillet er afsluttet.");return;}sessionDetail().catch(function(){});},10000);updatePlayerAutoRefreshUi();savePlayerContext();}
function togglePlayerAutoRefresh(){if(joinInFlight){updatePlayerAutoRefreshUi();return;}if(playerAutoRefreshEnabled){stopPlayerAutoRefresh();return;}startPlayerAutoRefresh();}
function updateConnectionBanner(){var banner=document.getElementById("connectionBanner");var retryBtn=document.getElementById("connectionRetryBtn");if(!banner||!retryBtn){return;}banner.style.display=connectionLost?"block":"none";retryBtn.disabled=connectionRetryInFlight||sessionDetailInFlight||joinInFlight||!code();}
function setConnectionLost(isLost){connectionLost=!!isLost;updateConnectionBanner();}
function updatePlayerErrorHint(status,data){var el=document.getElementById("playerErrorHint");if(!el){return;}if(status>=200&&status<300){el.textContent="Ingen fejl.";setConnectionLost(false);return;}var errKey=normalizeApiError(data);el.textContent="Fejl: "+mapUiErrorMessage(errKey)+" ("+(errKey||("http_"+status))+")";}
function updateSessionDetailState(){var btn=document.getElementById("sessionDetailBtn");var hint=document.getElementById("sessionRefreshHint");var submitInFlight=lieSubmitInFlight||guessSubmitInFlight;if(btn){btn.disabled=sessionDetailInFlight||joinInFlight||submitInFlight||!code();}if(!hint){updatePlayerAutoRefreshUi();updateConnectionBanner();return;}if(sessionDetailInFlight){hint.textContent="Opdaterer session-status…";updatePlayerAutoRefreshUi();updateConnectionBanner();return;}if(joinInFlight){hint.textContent="Session-opdatering er låst mens join kører.";updatePlayerAutoRefreshUi();updateConnectionBanner();return;}if(submitInFlight){hint.textContent="Session-opdatering er låst mens submit kører.";updatePlayerAutoRefreshUi();updateConnectionBanner();return;}if(!code()){hint.textContent="Angiv sessionkode for at opdatere session-status.";updatePlayerAutoRefreshUi();updateConnectionBanner();updatePlayerShellErrorBoundary();return;}hint.textContent="Session-opdatering klar.";updatePlayerAutoRefreshUi();updateConnectionBanner();updatePlayerShellErrorBoundary();}
function updateJoinState(){var btn=document.getElementById("joinBtn");var status=document.getElementById("joinStatus");var joined=!!(pid()&&document.getElementById("sessionToken").value.trim());var canJoin=canAttemptJoin();if(btn){btn.disabled=joinInFlight||sessionDetailInFlight||joined||!canJoin;}if(!status){updateContextLockState();updateConnectionBanner();return;}if(joinInFlight){status.textContent="Joiner…";updateContextLockState();updateConnectionBanner();return;}if(sessionDetailInFlight&&!joined){status.textContent="Afvent aktiv session-opdatering før join.";updateContextLockState();updateConnectionBanner();return;}if(joined){status.textContent="Join gennemført.";updateContextLockState();updateConnectionBanner();return;}if(!canJoin){status.textContent="Udfyld kode og nickname for at join.";updateContextLockState();updateConnectionBanner();return;}status.textContent="Klar til join.";updateContextLockState();updateConnectionBanner();}
function guessStorageKey(){var c=code();var p=pid();var q=rq();if(!c||!p||!q){return "";}return ["wppGuess",c,p,q].join(":");}
function lieStorageKey(){var c=code();var p=pid();var q=rq();if(!c||!p||!q){return "";}return ["wppLie",c,p,q].join(":");}
function persistLieState(text,submitted){var key=lieStorageKey();if(!key){return;}try{localStorage.setItem(key,JSON.stringify({text:text||"",submitted:!!submitted}));}catch(_e){}}
function loadLieState(){var key=lieStorageKey();if(!key){return null;}try{var raw=localStorage.getItem(key);if(!raw){return null;}return JSON.parse(raw);}catch(_e){return null;}}
function updateLieSubmitState(){var text=(document.getElementById("lieText").value||"").trim();var btn=document.getElementById("lieSubmitBtn");var input=document.getElementById("lieText");var status=document.getElementById("lieStatus");var hasContext=hasSubmitContext();var inLiePhase=currentSessionStatus==="lie";if(input){input.readOnly=lieSubmitted||lieSubmitInFlight||sessionDetailInFlight||!inLiePhase;}if(btn){btn.disabled=lieSubmitted||lieSubmitInFlight||sessionDetailInFlight||!text||!hasContext||!inLiePhase;}if(status){if(lieSubmitted){status.textContent="Løgn sendt input er låst.";status.classList.add("locked");}else{status.classList.remove("locked");if(lieSubmitInFlight){status.textContent="Sender løgn…";}else if(sessionDetailInFlight){status.textContent="Afvent aktiv session-opdatering før løgn-submit.";}else if(!hasContext){status.textContent="Join først for at aktivere submit.";}else if(!currentSessionStatus){status.textContent="Opdatér session-status for at validere løgn-fase.";}else if(!inLiePhase){status.textContent="Løgn-input er låst i fase: "+phaseLabel(currentSessionStatus)+".";}else{status.textContent=text?"Løgn klar til afsendelse.":"Skriv din løgn.";}}}}
function setLieState(text,submitted){document.getElementById("lieText").value=text||"";if(typeof submitted==="boolean"){lieSubmitted=submitted;}updateLieSubmitState();}
function persistGuessState(text,submitted){var key=guessStorageKey();if(!key){return;}try{localStorage.setItem(key,JSON.stringify({selected_text:text||"",submitted:!!submitted}));}catch(_e){}}
function loadGuessState(){var key=guessStorageKey();if(!key){return null;}try{var raw=localStorage.getItem(key);if(!raw){return null;}return JSON.parse(raw);}catch(_e){return null;}}
function updateGuessStatus(){var el=document.getElementById("guessStatus");if(!el){return;}var selected=document.getElementById("guessText").value;var hasContext=hasSubmitContext();var hasRoundContext=hasRoundQuestionContext();var inGuessPhase=currentSessionStatus==="guess";if(guessSubmitted){el.textContent="Gæt sendt valg er låst.";el.classList.add("locked");return;}el.classList.remove("locked");if(guessSubmitInFlight){el.textContent="Sender gæt…";return;}if(sessionDetailInFlight){el.textContent="Afvent aktiv session-opdatering før gæt-submit.";return;}if(!code()||!pid()||!document.getElementById("sessionToken").value.trim()){el.textContent="Join først for at aktivere gæt.";return;}if(!hasRoundContext){el.textContent="Afvent aktivt spørgsmål fra host før du kan gætte.";return;}if(!hasContext){el.textContent="Join først for at aktivere gæt.";return;}if(!currentSessionStatus){el.textContent="Opdatér session-status for at validere gæt-fase.";return;}if(!inGuessPhase){el.textContent="Gæt er låst i fase: "+phaseLabel(currentSessionStatus)+".";return;}el.textContent=selected?"Valgt svar klar til afsendelse.":"Vælg et svar.";}
function updateGuessSubmitState(){var selected=document.getElementById("guessText").value;var hasValid=availableAnswers.indexOf(selected)!==-1;var hasContext=hasSubmitContext();var hasRoundContext=hasRoundQuestionContext();var inGuessPhase=currentSessionStatus==="guess";document.getElementById("guessSubmitBtn").disabled=guessSubmitted||guessSubmitInFlight||sessionDetailInFlight||!hasValid||!hasContext||!hasRoundContext||!inGuessPhase;var buttons=document.querySelectorAll("#answerOptions button");buttons.forEach(function(btn){btn.disabled=guessSubmitted||guessSubmitInFlight||sessionDetailInFlight||!hasContext||!hasRoundContext||!inGuessPhase;});updateGuessStatus();}
function setGuess(text,submitted){document.getElementById("guessText").value=text||"";if(typeof submitted==="boolean"){guessSubmitted=submitted;}var buttons=document.querySelectorAll("#answerOptions button");buttons.forEach(function(btn){btn.classList.toggle("active",btn.dataset.answer===text);});updateGuessSubmitState();
updateJoinState();}
function renderAnswerOptions(roundQuestion){var wrap=document.getElementById("answerOptions");wrap.innerHTML="";availableAnswers=[];guessSubmitted=false;setGuess("",false);lieSubmitted=false;setLieState("",false);if(!roundQuestion||!Array.isArray(roundQuestion.answers)){updateGuessSubmitState();updateLieSubmitState();return;}roundQuestion.answers.forEach(function(item){if(!item||!item.text){return;}availableAnswers.push(item.text);var btn=document.createElement("button");btn.type="button";btn.dataset.answer=item.text;btn.textContent=item.text;btn.onclick=function(){if(guessSubmitted){return;}setGuess(item.text,false);persistGuessState(item.text,false);};wrap.appendChild(btn);});var saved=loadGuessState();if(saved&&availableAnswers.indexOf(saved.selected_text)!==-1){setGuess(saved.selected_text,!!saved.submitted);}updateGuessSubmitState();}
async function api(path,method,payload){var o={method:method||"GET",headers:{"Accept":"application/json"}};if(payload!==null){o.headers["Content-Type"]="application/json";o.body=JSON.stringify(payload);}try{var r=await fetch(path,o);var d=await r.json().catch(function(){return {};});var isSessionDetailRead=(method||"GET")==="GET"&&/^\/lobby\/sessions\/[A-Z0-9]+$/.test(path);if(isSessionDetailRead){markPlayerSessionRefresh(r.status);}document.getElementById("out").textContent=JSON.stringify({status:r.status,data:d},null,2);if(d.player&&d.player.id){document.getElementById("playerId").value=d.player.id;}if(d.player&&d.player.session_token){document.getElementById("sessionToken").value=d.player.session_token;}if(d.round_question&&d.round_question.id){document.getElementById("roundQuestionId").value=d.round_question.id;}if(d.session&&d.session.status){currentSessionStatus=d.session.status;}if(d.round_question){renderAnswerOptions(d.round_question);var savedLie=loadLieState();if(savedLie){setLieState(savedLie.text||"",!!savedLie.submitted);}}else{document.getElementById("roundQuestionId").value="";renderAnswerOptions(null);}if(d.guess&&d.guess.round_question_id){document.getElementById("roundQuestionId").value=d.guess.round_question_id;setGuess(d.guess.selected_text||"",true);persistGuessState(d.guess.selected_text||"",true);}updateRoundContextHint();updatePlayerErrorHint(r.status,d);updatePhaseStatus();updateLieSubmitState();updateGuessSubmitState();if(currentSessionStatus==="finished"&&playerAutoRefreshEnabled){stopPlayerAutoRefresh("Auto-refresh stoppet: spillet er afsluttet.");}else{updatePlayerAutoRefreshUi();}if(playerShellFatalError){clearPlayerShellFatalError();}savePlayerContext();return d;}catch(err){setConnectionLost(true);if((method||"GET")==="GET"&&/^\/lobby\/sessions\/[A-Z0-9]+$/.test(path)){markPlayerSessionRefresh(0);}document.getElementById("out").textContent=JSON.stringify({status:0,data:{error:"connection_lost",detail:"Kunne ikke kontakte serveren."}},null,2);document.getElementById("playerErrorHint").textContent="Fejl: Mistede forbindelsen til serveren. Prøv igen.";updateSessionDetailState();throw err;}}
function joinSession(){if(joinInFlight){return Promise.resolve({error:"join_in_flight"});}if(!canAttemptJoin()){updateJoinState();return Promise.resolve({error:"missing_join_input"});}if(pid()&&document.getElementById("sessionToken").value.trim()){updateJoinState();return Promise.resolve({error:"already_joined_client"});}joinInFlight=true;updateJoinState();updatePlayerAutoRefreshUi();return api("/lobby/sessions/join","POST",{code:code(),nickname:document.getElementById("nickname").value.trim()}).then(function(d){joinInFlight=false;if(d&&d.player&&d.player.id){updateJoinState();updatePlayerAutoRefreshUi();return d;}updateJoinState();updatePlayerAutoRefreshUi();document.getElementById("joinStatus").textContent="Join fejlede prøv igen.";return d;}).catch(function(err){joinInFlight=false;updateJoinState();updatePlayerAutoRefreshUi();document.getElementById("joinStatus").textContent="Join fejlede prøv igen.";throw err;});}
function sessionDetail(){if(!code()){updateSessionDetailState();return Promise.resolve({error:"missing_session_code"});}if(sessionDetailInFlight){return Promise.resolve({error:"session_detail_in_flight"});}sessionDetailInFlight=true;updateSessionDetailState();return api("/lobby/sessions/"+code(),"GET",null).finally(function(){sessionDetailInFlight=false;updateSessionDetailState();});}
function retryConnection(){if(connectionRetryInFlight||!code()){updateConnectionBanner();return Promise.resolve({error:"retry_unavailable"});}connectionRetryInFlight=true;updateConnectionBanner();return sessionDetail().then(function(result){setConnectionLost(false);return result;}).finally(function(){connectionRetryInFlight=false;updateConnectionBanner();});}
function submitLie(){if(lieSubmitted){return Promise.resolve({error:"lie_already_submitted_client"});}if(lieSubmitInFlight){return Promise.resolve({error:"lie_submit_in_flight"});}if(!hasSubmitContext()){updateLieSubmitState();return Promise.resolve({error:"missing_submit_context"});}var text=(document.getElementById("lieText").value||"").trim();if(!text){updateLieSubmitState();return Promise.resolve({error:"empty_lie_text"});}lieSubmitInFlight=true;updateLieSubmitState();return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/lies/submit","POST",{player_id:parseInt(pid(),10),session_token:document.getElementById("sessionToken").value,text:text}).then(function(d){if(d&&d.lie&&d.lie.id){lieSubmitted=true;persistLieState(text,true);}return d;}).finally(function(){lieSubmitInFlight=false;updateLieSubmitState();});}
document.getElementById("lieText").addEventListener("input",function(){if(!lieSubmitted){updateLieSubmitState();persistLieState(document.getElementById("lieText").value,false);}});updateLieSubmitState();
function submitGuess(){if(guessSubmitted){return Promise.resolve({error:"guess_already_submitted_client"});}if(guessSubmitInFlight){return Promise.resolve({error:"guess_submit_in_flight"});}if(!hasSubmitContext()){updateGuessSubmitState();document.getElementById("out").textContent=JSON.stringify({status:400,data:{error:"Join først for at aktivere gæt"}},null,2);return Promise.resolve({error:"missing_submit_context"});}var selected=document.getElementById("guessText").value;if(availableAnswers.indexOf(selected)===-1){document.getElementById("out").textContent=JSON.stringify({status:400,data:{error:"Vælg et af de viste svarmuligheder"}},null,2);return Promise.resolve({error:"invalid_client_guess"});}guessSubmitInFlight=true;updateGuessSubmitState();return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/guesses/submit","POST",{player_id:parseInt(pid(),10),session_token:document.getElementById("sessionToken").value,selected_text:selected}).finally(function(){guessSubmitInFlight=false;updateGuessSubmitState();});}
["code","nickname","playerId","sessionToken","roundQuestionId"].forEach(function(fieldId){var field=document.getElementById(fieldId);if(!field){return;}field.addEventListener("input",function(){if(fieldId!=="roundQuestionId"){resetRoundContextForManualChange();}updateLieSubmitState();updateGuessSubmitState();updateJoinState();updateSessionDetailState();savePlayerContext();});field.addEventListener("change",function(){if(fieldId!=="roundQuestionId"){resetRoundContextForManualChange();}updateLieSubmitState();updateGuessSubmitState();updateJoinState();updateSessionDetailState();savePlayerContext();});});
window.addEventListener("error",function(event){setPlayerShellFatalError((event&&event.message)||"Ukendt runtime-fejl");});
window.addEventListener("unhandledrejection",function(event){var reason=event&&event.reason;var detail=(reason&&reason.message)||String(reason||"Unhandled promise rejection");setPlayerShellFatalError(detail);});
updatePhaseStatus();
updateGuessSubmitState();
updateJoinState();
updatePlayerAutoRefreshUi();
updatePlayerLastRefreshStatus();
updateRoundContextHint();
updateConnectionBanner();
updatePlayerShellErrorBoundary();
if(restorePlayerContext()){if(playerAutoRefreshEnabled){startPlayerAutoRefresh();}sessionDetail().catch(function(){});}else{savePlayerContext();}
</script>
</body></html>