From 9477788709127ffd5611caed0fa7ee191326ce00 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sat, 10 Aug 2024 13:31:52 +0900
Subject: feat(frontend): partially implement watch page

---
 frontend/app/components/GolfWatchApp.client.tsx | 66 ++++++++++++++++++-------
 1 file changed, 47 insertions(+), 19 deletions(-)

(limited to 'frontend/app/components/GolfWatchApp.client.tsx')

diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 829f709..355f7e3 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -3,7 +3,9 @@ import useWebSocket, { ReadyState } from "react-use-websocket";
 import type { components } from "../.server/api/schema";
 import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting";
 import GolfWatchAppFinished from "./GolfWatchApps/GolfWatchAppFinished";
-import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming";
+import GolfWatchAppGaming, {
+	PlayerInfo,
+} from "./GolfWatchApps/GolfWatchAppGaming";
 import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting";
 import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting";
 
@@ -34,12 +36,12 @@ export default function GolfWatchApp({
 
 	const [startedAt, setStartedAt] = useState<number | null>(null);
 
-	const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null);
+	const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null);
 
 	useEffect(() => {
 		if (gameState === "starting" && startedAt !== null) {
 			const timer1 = setInterval(() => {
-				setTimeLeftSeconds((prev) => {
+				setLeftTimeSeconds((prev) => {
 					if (prev === null) {
 						return null;
 					}
@@ -68,10 +70,23 @@ export default function GolfWatchApp({
 		}
 	}, [gameState, startedAt, game.duration_seconds]);
 
-	const [scoreA, setScoreA] = useState<number | null>(null);
-	const [scoreB, setScoreB] = useState<number | null>(null);
-	const [codeA, setCodeA] = useState<string>("");
-	const [codeB, setCodeB] = useState<string>("");
+	const playerA = game.players[0];
+	const playerB = game.players[1];
+
+	const [playerInfoA, setPlayerInfoA] = useState<PlayerInfo>({
+		displayName: playerA?.display_name ?? null,
+		iconPath: playerA?.icon_path ?? null,
+		score: null,
+		code: "",
+		submissionResult: undefined,
+	});
+	const [playerInfoB, setPlayerInfoB] = useState<PlayerInfo>({
+		displayName: playerB?.display_name ?? null,
+		iconPath: playerB?.icon_path ?? null,
+		score: null,
+		code: "",
+		submissionResult: undefined,
+	});
 
 	if (readyState === ReadyState.UNINSTANTIATED) {
 		throw new Error("WebSocket is not connected");
@@ -96,38 +111,51 @@ export default function GolfWatchApp({
 						const { start_at } = lastJsonMessage.data;
 						setStartedAt(start_at);
 						const nowSec = Math.floor(Date.now() / 1000);
-						setTimeLeftSeconds(start_at - nowSec);
+						setLeftTimeSeconds(start_at - nowSec);
 						setGameState("starting");
 					}
 				} else if (lastJsonMessage.type === "watcher:s2c:code") {
 					const { player_id, code } = lastJsonMessage.data;
-					setCodeA(code);
-				} else if (lastJsonMessage.type === "watcher:s2c:execresult") {
-					const { score } = lastJsonMessage.data;
-					if (score !== null && (scoreA === null || score < scoreA)) {
-						setScoreA(score);
+					if (player_id === playerA?.user_id) {
+						setPlayerInfoA((prev) => ({ ...prev, code }));
+					} else if (player_id === playerB?.user_id) {
+						setPlayerInfoB((prev) => ({ ...prev, code }));
+					} else {
+						throw new Error("Unknown player_id");
 					}
+				} else if (lastJsonMessage.type === "watcher:s2c:execresult") {
+					// const { score } = lastJsonMessage.data;
+					// if (score !== null && (scoreA === null || score < scoreA)) {
+					// 	setScoreA(score);
+					// }
 				}
 			} else {
 				setGameState("waiting");
 			}
 		}
-	}, [lastJsonMessage, readyState, gameState, scoreA]);
+	}, [
+		lastJsonMessage,
+		readyState,
+		gameState,
+		playerInfoA,
+		playerInfoB,
+		playerA?.user_id,
+		playerB?.user_id,
+	]);
 
 	if (gameState === "connecting") {
 		return <GolfWatchAppConnecting />;
 	} else if (gameState === "waiting") {
 		return <GolfWatchAppWaiting />;
 	} else if (gameState === "starting") {
-		return <GolfWatchAppStarting timeLeft={timeLeftSeconds!} />;
+		return <GolfWatchAppStarting leftTimeSeconds={leftTimeSeconds!} />;
 	} else if (gameState === "gaming") {
 		return (
 			<GolfWatchAppGaming
 				problem={game.problem!.description}
-				codeA={codeA}
-				scoreA={scoreA}
-				codeB={codeB}
-				scoreB={scoreB}
+				playerInfoA={playerInfoA}
+				playerInfoB={playerInfoB}
+				leftTimeSeconds={leftTimeSeconds!}
 			/>
 		);
 	} else if (gameState === "finished") {
-- 
cgit v1.2.3-70-g09d2


From b4ab693aa438f3f1a335369568aabe7849fc1370 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sat, 10 Aug 2024 20:25:59 +0900
Subject: feat: implement watch page

---
 backend/game/hub.go                                | 153 +++++++++++++++++----
 backend/game/message.go                            |  14 +-
 frontend/app/components/GolfWatchApp.client.tsx    |  88 ++++++++++--
 .../GolfWatchApps/GolfWatchAppGaming.tsx           |  82 ++++++++---
 frontend/app/root.tsx                              |   2 +-
 5 files changed, 283 insertions(+), 56 deletions(-)

(limited to 'frontend/app/components/GolfWatchApp.client.tsx')

diff --git a/backend/game/hub.go b/backend/game/hub.go
index aa1b9f2..54c559c 100644
--- a/backend/game/hub.go
+++ b/backend/game/hub.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/oapi-codegen/nullable"
 
 	"github.com/nsfisis/iosdc-japan-2024-albatross/backend/api"
 	"github.com/nsfisis/iosdc-japan-2024-albatross/backend/db"
@@ -118,14 +119,12 @@ func (hub *gameHub) run() {
 							},
 						}
 					}
-					for watcher := range hub.watchers {
-						watcher.s2cMessages <- &watcherMessageS2CStart{
-							Type: watcherMessageTypeS2CStart,
-							Data: watcherMessageS2CStartPayload{
-								StartAt: int(startAt.Unix()),
-							},
-						}
-					}
+					hub.broadcastToWatchers(&watcherMessageS2CStart{
+						Type: watcherMessageTypeS2CStart,
+						Data: watcherMessageS2CStartPayload{
+							StartAt: int(startAt.Unix()),
+						},
+					})
 					err := hub.q.UpdateGameStartedAt(hub.ctx, db.UpdateGameStartedAtParams{
 						GameID: int32(hub.game.gameID),
 						StartedAt: pgtype.Timestamp{
@@ -151,15 +150,13 @@ func (hub *gameHub) run() {
 				// TODO: assert game state is gaming
 				log.Printf("code: %v", message.message)
 				code := msg.Data.Code
-				for watcher := range hub.watchers {
-					watcher.s2cMessages <- &watcherMessageS2CCode{
-						Type: watcherMessageTypeS2CCode,
-						Data: watcherMessageS2CCodePayload{
-							PlayerID: message.client.playerID,
-							Code:     code,
-						},
-					}
-				}
+				hub.broadcastToWatchers(&watcherMessageS2CCode{
+					Type: watcherMessageTypeS2CCode,
+					Data: watcherMessageS2CCodePayload{
+						PlayerID: message.client.playerID,
+						Code:     code,
+					},
+				})
 			case *playerMessageC2SSubmit:
 				// TODO: assert game state is gaming
 				log.Printf("submit: %v", message.message)
@@ -176,6 +173,13 @@ func (hub *gameHub) run() {
 					// TODO: notify failure to player
 					log.Fatalf("failed to enqueue task: %v", err)
 				}
+				hub.broadcastToWatchers(&watcherMessageS2CSubmit{
+					Type: watcherMessageTypeS2CSubmit,
+					Data: watcherMessageS2CSubmitPayload{
+						PlayerID:         message.client.playerID,
+						PreliminaryScore: codeSize,
+					},
+				})
 			default:
 				log.Printf("unexpected message type: %T", message.message)
 			}
@@ -209,6 +213,12 @@ func (hub *gameHub) run() {
 	}
 }
 
+func (hub *gameHub) broadcastToWatchers(msg watcherMessageS2C) {
+	for watcher := range hub.watchers {
+		watcher.s2cMessages <- msg
+	}
+}
+
 type codeSubmissionError struct {
 	Status string
 	Stdout string
@@ -237,7 +247,13 @@ func (hub *gameHub) processTaskResults() {
 						},
 					}
 				}
-				// TODO: broadcast to watchers
+				hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{
+					Type: watcherMessageTypeS2CSubmitResult,
+					Data: watcherMessageS2CSubmitResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status),
+					},
+				})
 			}
 		case *taskqueue.TaskResultCompileSwiftToWasm:
 			err := hub.processTaskResultCompileSwiftToWasm(taskResult)
@@ -254,7 +270,22 @@ func (hub *gameHub) processTaskResults() {
 						},
 					}
 				}
-				// TODO: broadcast to watchers
+				hub.broadcastToWatchers(&watcherMessageS2CExecResult{
+					Type: watcherMessageTypeS2CExecResult,
+					Data: watcherMessageS2CExecResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CExecResultPayloadStatus(err.Status),
+						Stdout:   err.Stdout,
+						Stderr:   err.Stderr,
+					},
+				})
+				hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{
+					Type: watcherMessageTypeS2CSubmitResult,
+					Data: watcherMessageS2CSubmitResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status),
+					},
+				})
 			}
 		case *taskqueue.TaskResultCompileWasmToNativeExecutable:
 			err := hub.processTaskResultCompileWasmToNativeExecutable(taskResult)
@@ -271,11 +302,38 @@ func (hub *gameHub) processTaskResults() {
 						},
 					}
 				}
-				// TODO: broadcast to watchers
+				hub.broadcastToWatchers(&watcherMessageS2CExecResult{
+					Type: watcherMessageTypeS2CExecResult,
+					Data: watcherMessageS2CExecResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CExecResultPayloadStatus(err.Status),
+						Stdout:   err.Stdout,
+						Stderr:   err.Stderr,
+					},
+				})
+				hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{
+					Type: watcherMessageTypeS2CSubmitResult,
+					Data: watcherMessageS2CSubmitResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status),
+					},
+				})
+			} else {
+				hub.broadcastToWatchers(&watcherMessageS2CExecResult{
+					Type: watcherMessageTypeS2CExecResult,
+					Data: watcherMessageS2CExecResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CExecResultPayloadStatus("success"),
+						// TODO: inherit the command stdout/stderr.
+						Stdout: "Successfully compiled",
+						Stderr: "",
+					},
+				})
 			}
 		case *taskqueue.TaskResultRunTestcase:
+			// FIXME: error handling
 			var err error
-			err = hub.processTaskResultRunTestcase(taskResult)
+			err1 := hub.processTaskResultRunTestcase(taskResult)
 			_ = err // TODO: handle err?
 			aggregatedStatus, err := hub.q.AggregateTestcaseResults(hub.ctx, int32(taskResult.TaskPayload.SubmissionID))
 			_ = err // TODO: handle err?
@@ -298,7 +356,24 @@ func (hub *gameHub) processTaskResults() {
 						},
 					}
 				}
-				// TODO: broadcast to watchers
+				hub.broadcastToWatchers(&watcherMessageS2CExecResult{
+					Type: watcherMessageTypeS2CExecResult,
+					Data: watcherMessageS2CExecResultPayload{
+						PlayerID:   taskResult.TaskPayload.UserID(),
+						TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)),
+						Status:     api.GameWatcherMessageS2CExecResultPayloadStatus("internal_error"),
+						// TODO: inherit the command stdout/stderr?
+						Stdout: "",
+						Stderr: "internal error",
+					},
+				})
+				hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{
+					Type: watcherMessageTypeS2CSubmitResult,
+					Data: watcherMessageS2CSubmitResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CSubmitResultPayloadStatus("internal_error"),
+					},
+				})
 				continue
 			}
 			for player := range hub.players {
@@ -312,7 +387,39 @@ func (hub *gameHub) processTaskResults() {
 						Status: api.GamePlayerMessageS2CExecResultPayloadStatus(aggregatedStatus),
 					},
 				}
-				// TODO: broadcast to watchers
+			}
+			if err1 != nil {
+				hub.broadcastToWatchers(&watcherMessageS2CExecResult{
+					Type: watcherMessageTypeS2CExecResult,
+					Data: watcherMessageS2CExecResultPayload{
+						PlayerID:   taskResult.TaskPayload.UserID(),
+						TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)),
+						Status:     api.GameWatcherMessageS2CExecResultPayloadStatus(err1.Status),
+						Stdout:     err1.Stdout,
+						Stderr:     err1.Stderr,
+					},
+				})
+			} else {
+				hub.broadcastToWatchers(&watcherMessageS2CExecResult{
+					Type: watcherMessageTypeS2CExecResult,
+					Data: watcherMessageS2CExecResultPayload{
+						PlayerID:   taskResult.TaskPayload.UserID(),
+						TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)),
+						Status:     api.GameWatcherMessageS2CExecResultPayloadStatus("success"),
+						// TODO: inherit the command stdout/stderr?
+						Stdout: "Testcase passed",
+						Stderr: "",
+					},
+				})
+			}
+			if aggregatedStatus != "running" {
+				hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{
+					Type: watcherMessageTypeS2CSubmitResult,
+					Data: watcherMessageS2CSubmitResultPayload{
+						PlayerID: taskResult.TaskPayload.UserID(),
+						Status:   api.GameWatcherMessageS2CSubmitResultPayloadStatus(aggregatedStatus),
+					},
+				})
 			}
 		default:
 			panic("unexpected task result type")
diff --git a/backend/game/message.go b/backend/game/message.go
index 031222d..1fb30cb 100644
--- a/backend/game/message.go
+++ b/backend/game/message.go
@@ -77,9 +77,11 @@ func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error
 }
 
 const (
-	watcherMessageTypeS2CStart      = "watcher:s2c:start"
-	watcherMessageTypeS2CExecResult = "watcher:s2c:execresult"
-	watcherMessageTypeS2CCode       = "watcher:s2c:code"
+	watcherMessageTypeS2CStart        = "watcher:s2c:start"
+	watcherMessageTypeS2CCode         = "watcher:s2c:code"
+	watcherMessageTypeS2CSubmit       = "watcher:s2c:submit"
+	watcherMessageTypeS2CExecResult   = "watcher:s2c:execresult"
+	watcherMessageTypeS2CSubmitResult = "watcher:s2c:submitresult"
 )
 
 type watcherMessageS2C = interface{}
@@ -87,3 +89,9 @@ type watcherMessageS2CStart = api.GameWatcherMessageS2CStart
 type watcherMessageS2CStartPayload = api.GameWatcherMessageS2CStartPayload
 type watcherMessageS2CCode = api.GameWatcherMessageS2CCode
 type watcherMessageS2CCodePayload = api.GameWatcherMessageS2CCodePayload
+type watcherMessageS2CSubmit = api.GameWatcherMessageS2CSubmit
+type watcherMessageS2CSubmitPayload = api.GameWatcherMessageS2CSubmitPayload
+type watcherMessageS2CExecResult = api.GameWatcherMessageS2CExecResult
+type watcherMessageS2CExecResultPayload = api.GameWatcherMessageS2CExecResultPayload
+type watcherMessageS2CSubmitResult = api.GameWatcherMessageS2CSubmitResult
+type watcherMessageS2CSubmitResultPayload = api.GameWatcherMessageS2CSubmitResultPayload
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 355f7e3..a9c9989 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -116,29 +116,91 @@ export default function GolfWatchApp({
 					}
 				} else if (lastJsonMessage.type === "watcher:s2c:code") {
 					const { player_id, code } = lastJsonMessage.data;
-					if (player_id === playerA?.user_id) {
-						setPlayerInfoA((prev) => ({ ...prev, code }));
-					} else if (player_id === playerB?.user_id) {
-						setPlayerInfoB((prev) => ({ ...prev, code }));
-					} else {
-						throw new Error("Unknown player_id");
-					}
+					const setter =
+						player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
+					setter((prev) => ({ ...prev, code }));
+				} else if (lastJsonMessage.type === "watcher:s2c:submit") {
+					const { player_id, preliminary_score } = lastJsonMessage.data;
+					const setter =
+						player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
+					setter((prev) => ({
+						...prev,
+						submissionResult: {
+							status: "running",
+							preliminaryScore: preliminary_score,
+							verificationResults: game.verification_steps.map((v) => ({
+								testcase_id: v.testcase_id,
+								status: "running",
+								label: v.label,
+								stdout: "",
+								stderr: "",
+							})),
+						},
+					}));
 				} else if (lastJsonMessage.type === "watcher:s2c:execresult") {
-					// const { score } = lastJsonMessage.data;
-					// if (score !== null && (scoreA === null || score < scoreA)) {
-					// 	setScoreA(score);
-					// }
+					const { player_id, testcase_id, status, stdout, stderr } =
+						lastJsonMessage.data;
+					const setter =
+						player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
+					setter((prev) => {
+						const ret = { ...prev };
+						if (ret.submissionResult === undefined) {
+							return ret;
+						}
+						ret.submissionResult = {
+							...ret.submissionResult,
+							verificationResults: ret.submissionResult.verificationResults.map(
+								(v) =>
+									v.testcase_id === testcase_id && v.status === "running"
+										? {
+												...v,
+												status,
+												stdout,
+												stderr,
+											}
+										: v,
+							),
+						};
+						return ret;
+					});
+				} else if (lastJsonMessage.type === "watcher:s2c:submitresult") {
+					const { player_id, status } = lastJsonMessage.data;
+					const setter =
+						player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB;
+					setter((prev) => {
+						const ret = { ...prev };
+						if (ret.submissionResult === undefined) {
+							return ret;
+						}
+						ret.submissionResult = {
+							...ret.submissionResult,
+							status,
+						};
+						if (status === "success") {
+							if (
+								ret.score === null ||
+								ret.submissionResult.preliminaryScore < ret.score
+							) {
+								ret.score = ret.submissionResult.preliminaryScore;
+							}
+						} else {
+							ret.submissionResult.verificationResults =
+								ret.submissionResult.verificationResults.map((v) =>
+									v.status === "running" ? { ...v, status: "canceled" } : v,
+								);
+						}
+						return ret;
+					});
 				}
 			} else {
 				setGameState("waiting");
 			}
 		}
 	}, [
+		game.verification_steps,
 		lastJsonMessage,
 		readyState,
 		gameState,
-		playerInfoA,
-		playerInfoB,
 		playerA?.user_id,
 		playerB?.user_id,
 	]);
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
index 470a00c..992ce7a 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
@@ -14,17 +14,57 @@ export type PlayerInfo = {
 };
 
 type SubmissionResult = {
-	status: string;
-	nextScore: number;
-	executionResults: ExecutionResult[];
+	status:
+		| "running"
+		| "success"
+		| "wrong_answer"
+		| "timeout"
+		| "compile_error"
+		| "runtime_error"
+		| "internal_error";
+	preliminaryScore: number;
+	verificationResults: VerificationResult[];
 };
 
-type ExecutionResult = {
-	status: string;
+type VerificationResult = {
+	testcase_id: number | null;
+	status:
+		| "running"
+		| "success"
+		| "wrong_answer"
+		| "timeout"
+		| "compile_error"
+		| "runtime_error"
+		| "internal_error"
+		| "canceled";
 	label: string;
-	output: string;
+	stdout: string;
+	stderr: string;
 };
 
+function submissionResultStatusToLabel(
+	status: SubmissionResult["status"] | null,
+) {
+	switch (status) {
+		case null:
+			return "-";
+		case "running":
+			return "Running...";
+		case "success":
+			return "Accepted";
+		case "wrong_answer":
+			return "Wrong Answer";
+		case "timeout":
+			return "Time Limit Exceeded";
+		case "compile_error":
+			return "Compile Error";
+		case "runtime_error":
+			return "Runtime Error";
+		case "internal_error":
+			return "Internal Error";
+	}
+}
+
 export default function GolfWatchAppGaming({
 	problem,
 	playerInfoA,
@@ -40,7 +80,7 @@ export default function GolfWatchAppGaming({
 		const scoreA = playerInfoA.score ?? 0;
 		const scoreB = playerInfoB.score ?? 0;
 		const totalScore = scoreA + scoreB;
-		return totalScore === 0 ? 50 : (scoreA / totalScore) * 100;
+		return totalScore === 0 ? 50 : (scoreB / totalScore) * 100;
 	})();
 
 	return (
@@ -68,7 +108,7 @@ export default function GolfWatchAppGaming({
 					{playerInfoB.score ?? "-"}
 				</div>
 			</div>
-			<div className="grid grid-cols-[2fr_1fr_2fr_1fr] p-2">
+			<div className="grid grid-cols-[3fr_2fr_3fr_2fr] p-2">
 				<div>
 					<pre>
 						<code>{playerInfoA.code}</code>
@@ -76,19 +116,24 @@ export default function GolfWatchAppGaming({
 				</div>
 				<div>
 					<div>
-						{playerInfoA.submissionResult?.status}(
-						{playerInfoA.submissionResult?.nextScore})
+						{submissionResultStatusToLabel(
+							playerInfoA.submissionResult?.status ?? null,
+						)}{" "}
+						({playerInfoA.submissionResult?.preliminaryScore})
 					</div>
 					<div>
 						<ol>
-							{playerInfoA.submissionResult?.executionResults.map(
+							{playerInfoA.submissionResult?.verificationResults.map(
 								(result, idx) => (
 									<li key={idx}>
 										<div>
 											<div>
 												{result.status} {result.label}
 											</div>
-											<div>{result.output}</div>
+											<div>
+												{result.stdout}
+												{result.stderr}
+											</div>
 										</div>
 									</li>
 								),
@@ -103,19 +148,24 @@ export default function GolfWatchAppGaming({
 				</div>
 				<div>
 					<div>
-						{playerInfoB.submissionResult?.status}(
-						{playerInfoB.submissionResult?.nextScore})
+						{submissionResultStatusToLabel(
+							playerInfoB.submissionResult?.status ?? null,
+						)}{" "}
+						({playerInfoB.submissionResult?.preliminaryScore ?? "-"})
 					</div>
 					<div>
 						<ol>
-							{playerInfoB.submissionResult?.executionResults.map(
+							{playerInfoB.submissionResult?.verificationResults.map(
 								(result, idx) => (
 									<li key={idx}>
 										<div>
 											<div>
 												{result.status} {result.label}
 											</div>
-											<div>{result.output}</div>
+											<div>
+												{result.stdout}
+												{result.stderr}
+											</div>
 										</div>
 									</li>
 								),
diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx
index 054474a..4d7a661 100644
--- a/frontend/app/root.tsx
+++ b/frontend/app/root.tsx
@@ -21,7 +21,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
 				<Meta />
 				<Links />
 			</head>
-			<body>
+			<body className="h-screen">
 				{children}
 				<ScrollRestoration />
 				<Scripts />
-- 
cgit v1.2.3-70-g09d2

