Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (handler *V1Handler) SetupRoutes(rootPath string, register func(string, str
// We support both path parameter and cookie.
register("GET", path.Join(v1, "lobby", "ws"), handler.websocketUpgrade)

register("POST", path.Join(v1, "lobby", "resurrect"), handler.resurrectLobby)
register("POST", path.Join(v1, "lobby", "{lobby_id}", "player"), handler.postPlayer)
}

Expand Down
26 changes: 24 additions & 2 deletions internal/api/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package api

import (
"encoding/base64"
json "encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -63,7 +64,6 @@ type LobbyEntry struct {
func (handler *V1Handler) getLobbies(writer http.ResponseWriter, _ *http.Request) {
// REMARK: If paging is ever implemented, we might want to maintain order
// when deleting lobbies from state in the state package.

lobbies := state.GetPublicLobbies()
lobbyEntries := make(LobbyEntries, 0, len(lobbies))
for _, lobby := range lobbies {
Expand All @@ -80,7 +80,7 @@ func (handler *V1Handler) getLobbies(writer http.ResponseWriter, _ *http.Request
MaxClientsPerIP: lobby.ClientsPerIPLimit,
Wordpack: lobby.Wordpack,
State: lobby.State,
Scoring: lobby.ScoreCalculation.Identifier(),
Scoring: lobby.ScoreCalculationIdentifier,
})
}

Expand All @@ -92,6 +92,28 @@ func (handler *V1Handler) getLobbies(writer http.ResponseWriter, _ *http.Request
}
}

func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) {
var data game.LobbyRestoreData
base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body)
if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil {
log.Println("Error unmarshalling lobby resurrection data:", err)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
Comment on lines +95 to +100
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Limit resurrection payload size to prevent memory abuse.

The handler currently reads an unbounded request body through base64+JSON decoding. Cap the size to avoid memory exhaustion and return a 4xx on invalid payloads.

🛡️ Proposed fix
 func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) {
 	var data game.LobbyRestoreData
+	const maxRestorePayloadBytes = 1 << 20 // 1 MiB (tune as needed)
+	request.Body = http.MaxBytesReader(writer, request.Body, maxRestorePayloadBytes)
 	base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body)
 	if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil {
 		log.Println("Error unmarshalling lobby resurrection data:", err)
-		http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+		http.Error(writer, "invalid resurrection payload", http.StatusBadRequest)
 		return
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) {
var data game.LobbyRestoreData
base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body)
if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil {
log.Println("Error unmarshalling lobby resurrection data:", err)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) {
var data game.LobbyRestoreData
const maxRestorePayloadBytes = 1 << 20 // 1 MiB (tune as needed)
request.Body = http.MaxBytesReader(writer, request.Body, maxRestorePayloadBytes)
base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body)
if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil {
log.Println("Error unmarshalling lobby resurrection data:", err)
http.Error(writer, "invalid resurrection payload", http.StatusBadRequest)
return
}
}
🤖 Prompt for AI Agents
In `@internal/api/v1.go` around lines 95 - 100, The resurrectLobby handler reads
an unbounded request body via base64.NewDecoder and json.NewDecoder which can
lead to memory exhaustion; wrap request.Body with an io.LimitReader (e.g., limit
to a reasonable max like a few MB) before creating base64.NewDecoder, validate
decoding errors and return a 4xx (Bad Request) for payload/decoding issues
instead of 500, and ensure you close or consume the remaining body as
appropriate; update references in the function (resurrectLobby, base64Decoder,
json.NewDecoder, and the error handling branch) to enforce the size cap and
return the proper 4xx response on invalid payloads.

return
}

lobby := data.Lobby
// We add the lobby, while the lobby mutex is aqcuired. This prevents us
// from attempting to connect to the lobby, before the internal state has
// been restored correctly.
lobby.Synchronized(func() {
if state.ResurrectLobby(lobby) {
lobby.WriteObject = WriteObject
lobby.WritePreparedMessage = WritePreparedMessage
lobby.ResurrectUnsynchronized(&data)
Comment on lines +95 to +112
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against nil lobby payloads before calling methods.

If data.Lobby is nil, lobby.Synchronized will panic and allow a trivial POST to crash the server. Validate and return 400 before use.

🐛 Proposed fix
 	lobby := data.Lobby
+	if lobby == nil {
+		http.Error(writer, "missing lobby payload", http.StatusBadRequest)
+		return
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) {
var data game.LobbyRestoreData
base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body)
if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil {
log.Println("Error unmarshalling lobby resurrection data:", err)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
lobby := data.Lobby
// We add the lobby, while the lobby mutex is aqcuired. This prevents us
// from attempting to connect to the lobby, before the internal state has
// been restored correctly.
lobby.Synchronized(func() {
if state.ResurrectLobby(lobby) {
lobby.WriteObject = WriteObject
lobby.WritePreparedMessage = WritePreparedMessage
lobby.ResurrectUnsynchronized(&data)
func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) {
var data game.LobbyRestoreData
base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body)
if err := json.NewDecoder(base64Decoder).Decode(&data); err != nil {
log.Println("Error unmarshalling lobby resurrection data:", err)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
lobby := data.Lobby
if lobby == nil {
http.Error(writer, "missing lobby payload", http.StatusBadRequest)
return
}
// We add the lobby, while the lobby mutex is aqcuired. This prevents us
// from attempting to connect to the lobby, before the internal state has
// been restored correctly.
lobby.Synchronized(func() {
if state.ResurrectLobby(lobby) {
lobby.WriteObject = WriteObject
lobby.WritePreparedMessage = WritePreparedMessage
lobby.ResurrectUnsynchronized(&data)
🤖 Prompt for AI Agents
In `@internal/api/v1.go` around lines 95 - 112, In resurrectLobby, validate that
data.Lobby is non-nil before calling lobby methods: after decoding into
game.LobbyRestoreData, check if data.Lobby == nil and if so log the problem and
respond with http.Error(writer, http.StatusText(http.StatusBadRequest),
http.StatusBadRequest) to return 400; only then assign lobby := data.Lobby and
call lobby.Synchronized, state.ResurrectLobby,
lobby.WriteObject/WritePreparedMessage and lobby.ResurrectUnsynchronized as
currently implemented.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n -C2 "Resurrect|LobbyRestoreData|ShutdownTime|signature|HMAC|jwt|token|verify" internal

Repository: scribble-rs/scribble.rs

Length of output: 3174


🏁 Script executed:

rg -n "resurrectLobby|/v1/lobby/resurrect" internal/api

Repository: scribble-rs/scribble.rs

Length of output: 285


🏁 Script executed:

cat internal/api/http.go | head -100

Repository: scribble-rs/scribble.rs

Length of output: 3716


🏁 Script executed:

rg -n "SetupRoutes" --type go internal

Repository: scribble-rs/scribble.rs

Length of output: 690


🏁 Script executed:

fd -e "*.go" -path "*/cmd/*" -o -path "*/main*" | head -10

Repository: scribble-rs/scribble.rs

Length of output: 235


🏁 Script executed:

rg -n "\.SetupRoutes\|http\.HandleFunc\|http\.Handle" --type go | head -20

Repository: scribble-rs/scribble.rs

Length of output: 49


🏁 Script executed:

find . -name "main.go" -o -name "*main*" | grep -v node_modules | head -5

Repository: scribble-rs/scribble.rs

Length of output: 212


🏁 Script executed:

rg -n "SetupRoutes" --type-list | head -5

Repository: scribble-rs/scribble.rs

Length of output: 456


🏁 Script executed:

git ls-files | grep -E "(main|cmd)" | head -10

Repository: scribble-rs/scribble.rs

Length of output: 202


🏁 Script executed:

cat cmd/scribblers/main.go | head -200

Repository: scribble-rs/scribble.rs

Length of output: 2859


Validate cryptographic signature and TTL before accepting resurrection payloads.

The endpoint accepts base64-decoded JSON payloads without any cryptographic verification. The LobbyRestoreData struct contains no signature field, and ResurrectUnsynchronized only adjusts timing without validating authenticity. This allows any client to forge arbitrary lobby state, including player data and game progress, without restriction.

Add HMAC or signature validation to the payload and verify it before calling state.ResurrectLobby().

🤖 Prompt for AI Agents
In `@internal/api/v1.go` around lines 95 - 112, The resurrectLobby handler
currently decodes base64 JSON into LobbyRestoreData and calls
state.ResurrectLobby without authenticity or TTL checks; update the flow to
validate a cryptographic signature and TTL on the payload before any state
change: extend LobbyRestoreData to include a signature and timestamp, decode the
payload (as done via base64Decoder), verify the timestamp is within allowed TTL,
compute/verify an HMAC or signature using the server-side secret/public key and
the canonical payload (exclude the signature field) and reject with 401/400 if
verification fails, and only then call state.ResurrectLobby and
ResurrectUnsynchronized; keep checks centralized in resurrectLobby so
WriteObject, WritePreparedMessage and the lobby mutex are only applied after
successful validation.

}
})
}

func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Request) {
if err := request.ParseForm(); err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
Expand Down
7 changes: 6 additions & 1 deletion internal/api/ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ func (handler *V1Handler) websocketUpgrade(writer http.ResponseWriter, request *

lobby := state.GetLobby(lobbyId)
if lobby == nil {
http.Error(writer, ErrLobbyNotExistent.Error(), http.StatusNotFound)
socket, err := upgrader.Upgrade(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
socket.WriteClose(1000, []byte("lobby_gone"))
return
}
Comment on lines 53 to 61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle WriteClose errors and ensure socket cleanup.

When the lobby is not found, the code upgrades the connection and writes a close frame, but:

  1. The error from WriteClose is ignored
  2. The socket may not be fully closed if WriteClose fails

Consider logging errors and ensuring the socket is closed.

🛡️ Proposed fix
 	if lobby == nil {
 		socket, err := upgrader.Upgrade(writer, request)
 		if err != nil {
 			http.Error(writer, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		socket.WriteClose(1000, []byte("lobby_gone"))
+		if err := socket.WriteClose(1000, []byte("lobby_gone")); err != nil {
+			log.Printf("error writing close frame: %v", err)
+		}
 		return
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if lobby == nil {
http.Error(writer, ErrLobbyNotExistent.Error(), http.StatusNotFound)
socket, err := upgrader.Upgrade(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
socket.WriteClose(1000, []byte("lobby_gone"))
return
}
if lobby == nil {
socket, err := upgrader.Upgrade(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
if err := socket.WriteClose(1000, []byte("lobby_gone")); err != nil {
log.Printf("error writing close frame: %v", err)
}
return
}
🤖 Prompt for AI Agents
In `@internal/api/ws.go` around lines 53 - 61, When lobby == nil, handle
WriteClose errors and always close the websocket: after calling
upgrader.Upgrade(writer, request) defer or ensure socket.Close() is called to
guarantee cleanup, then capture the error returned by socket.WriteClose(1000,
[]byte("lobby_gone")), log that error (e.g. with the package logger or
log.Printf) and, if WriteClose fails, still call socket.Close() (or ensure the
deferred Close runs) before returning; update the block around upgrader.Upgrade
and socket.WriteClose to perform error checking and explicit socket cleanup.


Expand Down
40 changes: 40 additions & 0 deletions internal/frontend/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
const discordInstanceId = getCookie("discord-instance-id")
const rootPath = `${discordInstanceId ? ".proxy/" : ""}{{.RootPath}}`

function createLabel(cssClass, forElement, text) {
const label = document.createElement("label");
label.setAttribute("for", forElement);
label.classList.add(cssClass);
label.innerText = text;
return label;
}

function createNumberInput(id, min, max, value) {
const input = document.createElement("input");
input.setAttribute("type", "number");
// Figure out why I did this exactly.
input.setAttribute("size", "4");
Comment on lines +15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider removing or addressing the TODO comment.

The comment "Figure out why I did this exactly" suggests uncertainty about the size="4" attribute. Consider documenting the reason or removing the attribute if it's unnecessary.

🤖 Prompt for AI Agents
In `@internal/frontend/index.js` around lines 15 - 16, The TODO-like comment
indicates uncertainty about the input size attribute; either remove the
attribute or replace the vague comment with a clear justification: if the input
element (variable input) must display 4 characters by default for UI/UX reasons,
update the comment to state that (e.g., "set size=4 to show 4 chars for X
reason"), otherwise remove the input.setAttribute("size","4") call; locate the
occurrence of input.setAttribute("size", "4") and apply the change and update
the inline comment accordingly.

input.setAttribute("id", id);
input.setAttribute("name", id);
input.setAttribute("value", Number.toString(value));
input.setAttribute("min", Number.toString(min));
input.setAttribute("max", Number.toString(max));
Comment on lines +19 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Number.toString() is used incorrectly; attributes will receive wrong values.

Number.toString is a prototype method, not a static method. Calling Number.toString(value) ignores the argument and returns "function toString() { [native code] }" or similar, not the stringified number.

🐛 Proposed fix
-    input.setAttribute("value", Number.toString(value));
-    input.setAttribute("min", Number.toString(min));
-    input.setAttribute("max", Number.toString(max));
+    input.setAttribute("value", String(value));
+    input.setAttribute("min", String(min));
+    input.setAttribute("max", String(max));

Alternatively, use value.toString(), min.toString(), max.toString().

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
input.setAttribute("value", Number.toString(value));
input.setAttribute("min", Number.toString(min));
input.setAttribute("max", Number.toString(max));
input.setAttribute("value", String(value));
input.setAttribute("min", String(min));
input.setAttribute("max", String(max));
🤖 Prompt for AI Agents
In `@internal/frontend/index.js` around lines 19 - 21, The three calls using
Number.toString(...) are wrong because Number.toString is a prototype function;
update the input.setAttribute calls that use Number.toString for the variables
value, min, and max to convert those numeric variables to strings properly
(e.g., use String(value) or value.toString()) so input.setAttribute("value",
...), input.setAttribute("min", ...), and input.setAttribute("max", ...) receive
correct string representations; locate the statements with input.setAttribute in
internal/frontend/index.js and replace the Number.toString usages with a proper
string conversion for value, min, and max.


const decButton = document.createElement("button");
decButton.setAttribute("type", "button");
decButton.classList.add("number-decrement");
decButton.addEventListener("click", function() {
input.stepDown();
})
decButton.innerText = "-";

const incButton = document.createElement("button");
incButton.setAttribute("type", "button");
incButton.classList.add("number-increment");
incButton.addEventListener("click", function() {
input.stepUp();
})
incButton.innerText = "+";

const div = document.createElement("div")
div.append(decButton, input, incButton);
return div;
}

Array
.from(document.getElementsByClassName("number-input"))
.forEach(number_input => {
Expand Down
69 changes: 55 additions & 14 deletions internal/frontend/lobby.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,73 @@ let hasSocketEverConnected = false;
let socket;
function connectToWebsocket() {
if (socketIsConnecting === true) {
console.log("aborting connection attempt.");
return;
}

socketIsConnecting = true;

socket = new WebSocket(`${rootPath}/v1/lobby/ws`);
try {
socket = new WebSocket(`${rootPath}/v1/lobby/ws`);
} catch (exception) {
console.log("Connection error:" + exception)
socketIsConnecting = false;
connectToWebsocket();
return;
}

socket.onerror = error => {
//Is not connected and we haven't yet said that we are done trying to
//connect, this means that we could never even establish a connection.
if (socket.readyState != 1 && !hasSocketEverConnected) {
if (socket.readyState != 1) {
socketIsConnecting = false;
showTextDialog("connection-error-dialog",
'{{.Translation.Get "error-connecting"}}',
`{{.Translation.Get "error-connecting-text"}}`);
console.log("Error establishing connection: ", error);
if (!hasSocketEverConnected) {
showTextDialog("connection-error-dialog",
'{{.Translation.Get "error-connecting"}}',
`{{.Translation.Get "error-connecting-text"}}`);
console.log("Error establishing connection: ", error);
} else {
connectToWebsocket();
}
Comment on lines +19 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid immediate recursive reconnects; schedule retries instead.

Calling connectToWebsocket() directly in the catch/onerror path can recurse and loop tightly if the constructor fails synchronously, risking stack overflow and CPU thrash. Schedule a retry (and ideally add backoff).

🛠️ Proposed fix
 } catch (exception) {
     console.log("Connection error:" + exception)
     socketIsConnecting = false;
-    connectToWebsocket();
+    setTimeout(connectToWebsocket, 1000);
     return;
 }
 ...
         if (!hasSocketEverConnected) {
             showTextDialog("connection-error-dialog",
                 '{{.Translation.Get "error-connecting"}}',
                 `{{.Translation.Get "error-connecting-text"}}`);
             console.log("Error establishing connection: ", error);
         } else {
-            connectToWebsocket();
+            setTimeout(connectToWebsocket, 1000);
         }
🤖 Prompt for AI Agents
In `@internal/frontend/lobby.js` around lines 19 - 40, The catch block and
socket.onerror currently call connectToWebsocket() directly, which can cause
immediate recursive calls when WebSocket construction fails; modify both the
try/catch exception handler and the socket.onerror path (inside the readyState
!= 1 branch where connectToWebsocket() is called) to schedule reconnect attempts
via setTimeout and implement simple backoff (e.g., track a retryDelay variable
or attempts counter on the module scope and increase it up to a max), ensure
socketIsConnecting is set appropriately before scheduling, and reset backoff on
a successful connection in the connectToWebsocket or onopen handler so retries
are not tight or recursive.

} else {
console.log("Socket error: ", error)
}
};

socket.onopen = () => {
closeDialog(shutdownDialogId);
closeDialog(reconnectDialogId);

hasSocketEverConnected = true;
socketIsConnecting = false;

socket.onclose = event => {
//We want to avoid handling the error multiple times and showing the incorrect dialogs.
//We w to avoid handling the error multiple times and showing the incorrect dialogs.
socket.onerror = null;

console.log("Socket Closed Connection: ", event);
console.log("Attempting to reestablish socket connection.");
showReconnectDialogIfNotShown();
connectToWebsocket();

if (restoreData && event.reason === "lobby_gone") {
console.log("Resurrecting lobby ...",);
fetch('/v1/lobby/resurrect', {
method: 'POST',
body: restoreData,
}).then(() => {
console.log("Attempting to reestablish socket connection after resurrection ...");
socketIsConnecting = false;
Comment on lines +59 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use rootPath for resurrect endpoint to support proxy/root-path deployments.

The fetch currently uses an absolute /v1/... path, which will break for .proxy/ and custom root paths. Use rootPath like other API calls.

🛠️ Proposed fix
-                fetch('/v1/lobby/resurrect', {
+                fetch(`${rootPath}/v1/lobby/resurrect`, {
                     method: 'POST',
                     body: restoreData,
                 }).then(() => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (restoreData && event.reason === "lobby_gone") {
console.log("Resurrecting lobby ...",);
fetch('/v1/lobby/resurrect', {
method: 'POST',
body: restoreData,
}).then(() => {
console.log("Attempting to reestablish socket connection after resurrection ...");
socketIsConnecting = false;
if (restoreData && event.reason === "lobby_gone") {
console.log("Resurrecting lobby ...",);
fetch(`${rootPath}/v1/lobby/resurrect`, {
method: 'POST',
body: restoreData,
}).then(() => {
console.log("Attempting to reestablish socket connection after resurrection ...");
socketIsConnecting = false;
🤖 Prompt for AI Agents
In `@internal/frontend/lobby.js` around lines 59 - 66, The fetch to resurrect the
lobby uses a hardcoded '/v1/lobby/resurrect' path which breaks proxy/root-path
deployments; update the call in the block where restoreData is truthy and
event.reason === "lobby_gone" (around the fetch and socketIsConnecting handling)
to prepend the application rootPath (e.g. use rootPath + '/v1/lobby/resurrect'
or a template string `${rootPath}/v1/lobby/resurrect`) so the endpoint respects
custom root paths like other API calls.

connectToWebsocket();
});

return
}

if (event.reason !== "lobby_gone" && event.reason !== "server_restart") {
console.log("Attempting to reestablish socket connection.");
showReconnectDialogIfNotShown();
}
if (event.reason === "server_restart") {
connectToWebsocket();
}
};

registerMessageHandler(socket);
Expand All @@ -53,6 +85,7 @@ function connectToWebsocket() {
};
}

const shutdownDialogId = "shutdown-dialog";
const reconnectDialogId = "reconnect-dialog";
function showReconnectDialogIfNotShown() {
const previousReconnectDialog = document.getElementById(reconnectDialogId);
Expand Down Expand Up @@ -833,6 +866,7 @@ let rounds = 0;
let roundEndTime = 0;
let gameState = "unstarted";
let drawingTimeSetting = "∞";
let restoreData;

function registerMessageHandler(targetSocket) {
targetSocket.onmessage = event => {
Expand Down Expand Up @@ -985,10 +1019,16 @@ function registerMessageHandler(targetSocket) {
+ '{{.Translation.Get "custom-words-per-turn-setting"}}: ' + parsed.data.customWordsPerTurn + "%\n"
+ '{{.Translation.Get "players-per-ip-limit-setting"}}: ' + parsed.data.clientsPerIpLimit);
} else if (parsed.type === "shutdown") {
socket.onclose = null;
socket.close();
showDialog("shutdown-info", "Server shutting down",
document.createTextNode("Sorry, but the server is about to shut down. Please come back at a later time."));
console.log("Shutdown event received");
if (parsed.data) {
restoreData = parsed.data;
// FIXMe Text anpassen!
showDialog("shutdown-dialog", "Server shutting down",
document.createTextNode("Sorry, but the server is about to shut down. Attempting to restore lobby on restart ..."));
} else {
showDialog("shutdown-dialog", "Server shutting down",
document.createTextNode("Sorry, but the server is about to shut down. Please come back at a later time."));
}
Comment on lines +1022 to +1031
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resolve the FIXME and localize shutdown messaging.

There’s a FIXMe and hardcoded English strings; please replace with translation keys or finalized copy.

If you want, I can draft localized strings and wire them into the translation map.

🤖 Prompt for AI Agents
In `@internal/frontend/lobby.js` around lines 1022 - 1031, Replace the FIXME and
hard-coded English messages in the shutdown handling block by using localized
translation keys instead of literal strings: when parsed.data exists set
restoreData as before, and call showDialog("shutdown-dialog",
t("shutdown.title"), document.createTextNode(t("shutdown.restoreMessage"))) else
call showDialog("shutdown-dialog", t("shutdown.title"),
document.createTextNode(t("shutdown.genericMessage"))); remove the FIXME
comment; add the keys shutdown.title, shutdown.restoreMessage and
shutdown.genericMessage to the app's translations map (and provide translated
text for each locale) so showDialog and restoreData logic remain unchanged.

}
}
};
Expand Down Expand Up @@ -1033,6 +1073,7 @@ function setRoundTimeLeft(timeLeftMs) {
}

function handleReadyEvent(ready) {
restoreData = null;
ownerID = ready.ownerId;
ownID = ready.playerId;

Expand Down
Loading