v0.11.0 harden persistence and prepare game replays

This commit is contained in:
2026-09-07 14:39:00 -04:00
parent 21dea47694
commit 5d2be4abb2
42 changed files with 3591 additions and 693 deletions
@@ -357,7 +357,20 @@ function authFetch(url, options = {}) {
}
async function getConfig() {
return { apiUrl: '/chess' };
try {
const response = await fetch('./config', { cache: 'no-store' });
if (!response.ok) throw new Error(`config returned ${response.status}`);
const config = await response.json();
if (typeof config.apiUrl !== 'string' || !config.apiUrl.trim()) {
throw new Error('config is missing apiUrl');
}
return { apiUrl: config.apiUrl.replace(/\/+$/, '') };
} catch (error) {
// Static production hosting currently reverse-proxies the API here.
// The embedded server supplies /config and does not use this fallback.
console.debug('Using default API route:', error.message);
return { apiUrl: '/chess' };
}
}
function startHealthCheck() {
@@ -835,7 +848,7 @@ async function pollOnce() {
gameState.pollController = new AbortController();
try {
const response = await fetch(
const response = await authFetch(
`${gameState.apiUrl}/api/v1/games/${gameState.gameId}?wait=true&moveCount=${moveCount}`,
{ signal: gameState.pollController.signal }
);
@@ -895,7 +908,7 @@ async function undoMoves() {
}
try {
const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/undo`, {
const response = await authFetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/undo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ count: 2 })
+12 -8
View File
@@ -16,7 +16,7 @@ import (
var webFS embed.FS
// Start initializes and starts the web UI server
func Start(host string, port int, apiURL string) error {
func Start(host string, port int, apiURL string, logRequests bool) error {
app := fiber.New(fiber.Config{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
@@ -24,13 +24,17 @@ func Start(host string, port int, apiURL string) error {
})
// Middleware
app.Use(logger.New(logger.Config{
Format: "${time} WEB ${status} ${method} ${path} ${latency}\n",
}))
if logRequests {
app.Use(logger.New(logger.Config{
Format: "${time} WEB ${status} ${method} ${path} ${latency}\n",
TimeFormat: time.RFC3339,
TimeZone: "UTC",
}))
}
app.Use(cors.New())
// Create a sub-filesystem that points to the 'web' directory
webContent, err := fs.Sub(webFS, "web")
// Create a sub-filesystem rooted at the embedded web client.
webContent, err := fs.Sub(webFS, "chess-client-web")
if err != nil {
return fmt.Errorf("failed to create web sub-filesystem: %w", err)
}
@@ -42,7 +46,7 @@ func Start(host string, port int, apiURL string) error {
})
})
// Serve static files from the embedded 'web' directory
// Serve static files from the embedded client directory.
app.Get("*", func(c *fiber.Ctx) error {
path := c.Path()
@@ -84,4 +88,4 @@ func Start(host string, port int, apiURL string) error {
addr := fmt.Sprintf("%s:%d", host, port)
return app.Listen(addr)
}
}
+22
View File
@@ -0,0 +1,22 @@
package webserver
import (
"io/fs"
"testing"
)
func TestEmbeddedWebClientRoot(t *testing.T) {
content, err := fs.Sub(webFS, "chess-client-web")
if err != nil {
t.Fatal(err)
}
for _, name := range []string{"index.html", "app.js", "style.css"} {
data, err := fs.ReadFile(content, name)
if err != nil {
t.Errorf("read embedded %s: %v", name, err)
}
if len(data) == 0 {
t.Errorf("embedded %s is empty", name)
}
}
}