From 6732c75475254b114e365f6c57694c0e632e9f5a Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Sat, 28 Feb 2026 05:24:11 +0100 Subject: [PATCH] UI: lock host session-status during in-flight request (#101) --- lobby/templates/lobby/host_screen.html | 13 ++++++++----- lobby/tests.py | 8 ++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lobby/templates/lobby/host_screen.html b/lobby/templates/lobby/host_screen.html index 98bc86b..954d5c2 100644 --- a/lobby/templates/lobby/host_screen.html +++ b/lobby/templates/lobby/host_screen.html @@ -22,7 +22,8 @@

Angiv sessionkode for at aktivere host-actions.

Ingen fejl.

- + +

Session-opdatering klar.

Auto-refresh er slået fra.

Session-data ikke opdateret endnu.

@@ -34,6 +35,7 @@ var autoRefreshTimer=null; var hostActionInFlight=false; var lastRefreshAtLabel=""; var lastRefreshFailed=false; +var sessionDetailInFlight=false; function csrf(){var m=document.cookie.match(/csrftoken=([^;]+)/);return m?m[1]:"";} function code(){return document.getElementById("code").value.trim().toUpperCase();} function rq(){return document.getElementById("roundQuestionId").value.trim();} @@ -43,7 +45,7 @@ function restoreHostContext(){try{var raw=localStorage.getItem("wppHostContext") 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 updateAutoRefreshUi(){var btn=document.getElementById("autoRefreshToggleBtn");var hint=document.getElementById("autoRefreshHint");if(btn){btn.textContent="Auto-refresh: "+(autoRefreshEnabled?"ON":"OFF");}if(!hint){return;}if(!autoRefreshEnabled){hint.textContent="Auto-refresh er slået fra.";return;}if(!code()){hint.textContent="Auto-refresh venter: angiv sessionkode.";return;}if(currentSessionStatus==="finished"){hint.textContent="Auto-refresh stoppet: spillet er afsluttet.";return;}hint.textContent="Auto-refresh aktiv (10s) for lobby-status.";} function stopAutoRefresh(reason){autoRefreshEnabled=false;if(autoRefreshTimer){clearInterval(autoRefreshTimer);autoRefreshTimer=null;}if(reason){var hint=document.getElementById("autoRefreshHint");if(hint){hint.textContent=reason;}}updateAutoRefreshUi();saveHostContext();} -function startAutoRefresh(){if(!code()){updateAutoRefreshUi();return;}autoRefreshEnabled=true;if(autoRefreshTimer){clearInterval(autoRefreshTimer);}autoRefreshTimer=setInterval(function(){if(!code()){return;}if(currentSessionStatus==="finished"){stopAutoRefresh("Auto-refresh stoppet: spillet er afsluttet.");return;}sessionDetail();},10000);updateAutoRefreshUi();saveHostContext();} +function startAutoRefresh(){if(!code()){updateAutoRefreshUi();return;}autoRefreshEnabled=true;if(autoRefreshTimer){clearInterval(autoRefreshTimer);}autoRefreshTimer=setInterval(function(){if(!code()||sessionDetailInFlight){return;}if(currentSessionStatus==="finished"){stopAutoRefresh("Auto-refresh stoppet: spillet er afsluttet.");return;}sessionDetail();},10000);updateAutoRefreshUi();saveHostContext();} function toggleAutoRefresh(){if(autoRefreshEnabled){stopAutoRefresh();return;}startAutoRefresh();} function formatTimeLabel(dateObj){return dateObj.toLocaleTimeString("da-DK",{hour12:false});} function markSessionRefresh(status){if(status>=200&&status<300){lastRefreshAtLabel=formatTimeLabel(new Date());lastRefreshFailed=false;}else{lastRefreshFailed=true;}updateLastRefreshStatus();} @@ -51,6 +53,7 @@ function updateLastRefreshStatus(){var el=document.getElementById("lastRefreshSt 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 updateErrorHint(status,data){var el=document.getElementById("hostErrorHint");if(!el){return;}if(status>=200&&status<300){el.textContent="Ingen fejl.";return;}var errKey=normalizeApiError(data);el.textContent="Fejl: "+mapUiErrorMessage(errKey)+" ("+(errKey||("http_"+status))+")";} +function updateSessionDetailState(){var btn=document.getElementById("sessionDetailBtn");var hint=document.getElementById("sessionDetailHint");if(btn){btn.disabled=sessionDetailInFlight||hostActionInFlight||!code();}if(!hint){return;}if(sessionDetailInFlight){hint.textContent="Opdaterer session-status…";return;}if(hostActionInFlight){hint.textContent="Session-opdatering er låst mens en host-handling kører.";return;}if(!code()){hint.textContent="Angiv sessionkode for at opdatere session-status.";return;}hint.textContent="Session-opdatering klar.";} 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 syncStartRoundGuard(data){var btn=document.getElementById("startRoundBtn");var hint=document.getElementById("startRoundHint");var status=document.getElementById("playerCountStatus");if(!btn||!hint||!status){return;}var count=(data&&data.session&&typeof data.session.players_count==="number")?data.session.players_count:null;var phase=currentSessionStatus||"";if(phase&&phase!=="lobby"){btn.disabled=true;status.textContent=count===null?"Spillere i session: ukendt":"Spillere i session: "+count;hint.textContent="Start runde er kun tilladt i lobby-fasen.";return;}if(count===null){btn.disabled=true;status.textContent="Spillere i session: ukendt";hint.textContent="Opdatér session-status for at validere 3-5 spillere.";return;}status.textContent="Spillere i session: "+count;if(count<3){btn.disabled=true;hint.textContent="Mangler spillere: kræver mindst 3 for at starte runde.";return;}if(count>5){btn.disabled=true;hint.textContent="For mange spillere: maks 5 i MVP før runde-start.";return;}btn.disabled=false;hint.textContent="Klar: spillerantal er indenfor 3-5 til runde-start.";} @@ -58,7 +61,7 @@ function updateHostActionState(){var hasCode=!!code();var hasRound=!!rq();var ph 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.headers["X-CSRFToken"]=csrf();o.body=JSON.stringify(payload);}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){markSessionRefresh(r.status);}document.getElementById("out").textContent=JSON.stringify({status:r.status,data:d},null,2);if(d.session&&d.session.code){document.getElementById("code").value=d.session.code;}if(d.session&&d.session.status){currentSessionStatus=d.session.status;}if(d.round_question&&d.round_question.id){document.getElementById("roundQuestionId").value=d.round_question.id;}updateErrorHint(r.status,d);updatePhaseStatus();syncStartRoundGuard(d);updateHostActionState();if(currentSessionStatus==="finished"&&autoRefreshEnabled){stopAutoRefresh("Auto-refresh stoppet: spillet er afsluttet.");}else{updateAutoRefreshUi();}saveHostContext();return d;} function withHostActionLock(fn){if(hostActionInFlight){return Promise.resolve({error:"host_action_in_flight"});}hostActionInFlight=true;updateHostActionState();return Promise.resolve().then(fn).finally(function(){hostActionInFlight=false;updateHostActionState();});} function createSession(){return withHostActionLock(function(){return api("/lobby/sessions/create","POST",{});});} -function sessionDetail(){return api("/lobby/sessions/"+code(),"GET",null);} +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 startRound(){if(document.getElementById("startRoundBtn").disabled){return Promise.resolve({error:"not_enough_players_client_guard"});}return withHostActionLock(function(){return api("/lobby/sessions/"+code()+"/rounds/start","POST",{category_slug:document.getElementById("category").value});});} function showQuestion(){return withHostActionLock(function(){return api("/lobby/sessions/"+code()+"/questions/show","POST",{});});} function mixAnswers(){return withHostActionLock(function(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/answers/mix","POST",{});});} @@ -66,9 +69,9 @@ function calcScores(){return withHostActionLock(function(){return api("/lobby/se function showScoreboard(){return withHostActionLock(function(){return api("/lobby/sessions/"+code()+"/scoreboard","GET",null);});} function nextRound(){return withHostActionLock(function(){return api("/lobby/sessions/"+code()+"/rounds/next","POST",{});});} function finishGame(){return withHostActionLock(function(){return api("/lobby/sessions/"+code()+"/finish","POST",{});});} -["code","roundQuestionId"].forEach(function(fieldId){var field=document.getElementById(fieldId);if(!field){return;}field.addEventListener("input",function(){syncStartRoundGuard(null);updateHostActionState();saveHostContext();});field.addEventListener("change",function(){syncStartRoundGuard(null);updateHostActionState();saveHostContext();});}); +["code","roundQuestionId"].forEach(function(fieldId){var field=document.getElementById(fieldId);if(!field){return;}field.addEventListener("input",function(){syncStartRoundGuard(null);updateHostActionState();updateSessionDetailState();saveHostContext();});field.addEventListener("change",function(){syncStartRoundGuard(null);updateHostActionState();updateSessionDetailState();saveHostContext();});}); -updatePhaseStatus();syncStartRoundGuard(null);updateHostActionState();updateAutoRefreshUi();updateLastRefreshStatus(); +updatePhaseStatus();syncStartRoundGuard(null);updateHostActionState();updateSessionDetailState();updateAutoRefreshUi();updateLastRefreshStatus(); if(restoreHostContext()){updatePhaseStatus();if(autoRefreshEnabled){startAutoRefresh();}sessionDetail();}else{saveHostContext();} diff --git a/lobby/tests.py b/lobby/tests.py index 8eb8314..dda44ab 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -797,6 +797,14 @@ class UiScreenTests(TestCase): self.assertContains(response, "Session-data ikke opdateret endnu.") self.assertContains(response, "Sidst opdateret:") self.assertContains(response, "Session-data kan være forældet") + self.assertContains(response, "id=\"sessionDetailBtn\"") + self.assertContains(response, "id=\"sessionDetailHint\"") + self.assertContains(response, "updateSessionDetailState") + self.assertContains(response, "sessionDetailInFlight") + self.assertContains(response, "session_detail_in_flight") + self.assertContains(response, "Opdaterer session-status…") + self.assertContains(response, "Session-opdatering er låst mens en host-handling kører.") + self.assertContains(response, "Angiv sessionkode for at opdatere session-status.") self.assertContains(response, "markSessionRefresh") self.assertContains(response, "updateLastRefreshStatus") self.assertContains(response, "isSessionDetailRead") -- 2.39.5