v0.9.1 web ui fixes and improvements

This commit is contained in:
2026-02-25 10:27:52 -05:00
parent 820ad7eb27
commit fc8a6ba6a6
6 changed files with 418 additions and 99 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ db
build.sh build.sh
.chess_history .chess_history
*.wasm *.wasm
catalog.txt

21
go.mod
View File

@ -1,34 +1,33 @@
module chess module chess
go 1.25.4 go 1.26.0
require ( require (
github.com/go-playground/validator/v10 v10.30.1 github.com/go-playground/validator/v10 v10.30.1
github.com/gofiber/fiber/v2 v2.52.11 github.com/gofiber/fiber/v2 v2.52.12
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226
github.com/mattn/go-sqlite3 v1.14.33 github.com/mattn/go-sqlite3 v1.14.34
golang.org/x/term v0.39.0 golang.org/x/term v0.40.0
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/philhofer/fwd v1.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/tinylib/msgp v1.6.3 // indirect github.com/tinylib/msgp v1.6.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
) )

64
go.sum
View File

@ -1,15 +1,9 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -18,24 +12,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs=
github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 h1:c7wfyZGdy6RkM/b6mIazoYrAS+3qDL7d9M1CFm2e1VA= github.com/lixenwraith/auth v0.0.0-20251104131016-e5a810f4e226 h1:c7wfyZGdy6RkM/b6mIazoYrAS+3qDL7d9M1CFm2e1VA=
@ -44,46 +30,32 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.5.0 h1:GWnqAE54wmnlFazjq2+vgr736Akg58iiHImh+kPY2pc=
github.com/tinylib/msgp v1.5.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,12 +5,16 @@ let gameState = {
turn: 'w', turn: 'w',
isPlayerWhite: true, isPlayerWhite: true,
isLocked: false, isLocked: false,
pollInterval: null, polling: false,
pollController: null,
apiUrl: '', apiUrl: '',
selectedSquare: null, selectedSquare: null,
healthCheckInterval: null, healthCheckInterval: null,
networkError: false, networkError: false,
moveList: [], moveList: [],
authToken: null,
userId: null,
username: null,
}; };
// Chess piece Unicode: all black pieces for better fill, white pawn due to inability to override emoji variant display // Chess piece Unicode: all black pieces for better fill, white pawn due to inability to override emoji variant display
@ -42,9 +46,218 @@ document.addEventListener('DOMContentLoaded', async () => {
// Don't auto-show modal on load // Don't auto-show modal on load
}); });
document.getElementById('auth-indicator').addEventListener('click', handleAuthClick);
document.querySelectorAll('.auth-tab').forEach(tab => {
tab.addEventListener('click', (e) => switchAuthTab(e.target.dataset.tab));
});
document.getElementById('login-submit-btn').addEventListener('click', handleLogin);
document.getElementById('register-submit-btn').addEventListener('click', handleRegister);
document.getElementById('auth-cancel-btn').addEventListener('click', hideAuthModal);
document.getElementById('auth-cancel-btn-2').addEventListener('click', hideAuthModal);
// Check for existing session on load
restoreAuthSession();
// Auth functions
function restoreAuthSession() {
const token = localStorage.getItem('authToken');
if (token) {
gameState.authToken = token;
validateSession();
}
}
async function validateSession() {
try {
const response = await authFetch(`${gameState.apiUrl}/api/v1/auth/me`);
if (response.ok) {
const user = await response.json();
gameState.userId = user.userId;
gameState.username = user.username;
updateAuthIndicator(true);
} else {
clearAuthState();
}
} catch {
clearAuthState();
}
}
function clearAuthState() {
gameState.authToken = null;
gameState.userId = null;
gameState.username = null;
localStorage.removeItem('authToken');
updateAuthIndicator(false);
}
function updateAuthIndicator(authenticated) {
const indicator = document.getElementById('auth-indicator');
const light = indicator.querySelector('.light');
if (authenticated) {
light.setAttribute('data-status', 'authenticated');
indicator.setAttribute('data-status', gameState.username);
} else {
light.setAttribute('data-status', 'anonymous');
indicator.setAttribute('data-status', 'anonymous');
}
}
function handleAuthClick() {
if (gameState.authToken) {
// Logged in - confirm logout
if (confirm(`Logout ${gameState.username}?`)) {
handleLogout();
}
} else {
showAuthModal();
}
}
function showAuthModal() {
document.getElementById('auth-modal-overlay').classList.add('show');
document.getElementById('login-identifier').focus();
document.addEventListener('keydown', handleAuthModalKeydown);
}
function hideAuthModal() {
document.getElementById('auth-modal-overlay').classList.remove('show');
document.querySelectorAll('.auth-form input').forEach(input => input.value = '');
document.removeEventListener('keydown', handleAuthModalKeydown);
}
function handleAuthModalKeydown(e) {
const modal = document.getElementById('auth-modal-overlay');
if (!modal.classList.contains('show')) return;
if (e.key === 'Escape') {
e.preventDefault();
hideAuthModal();
} else if (e.key === 'Enter') {
e.preventDefault();
const loginForm = document.getElementById('login-form');
if (loginForm.style.display !== 'none') {
handleLogin();
} else {
handleRegister();
}
}
}
function switchAuthTab(tab) {
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
document.getElementById('login-form').style.display = tab === 'login' ? 'block' : 'none';
document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none';
}
async function handleLogin() {
const identifier = document.getElementById('login-identifier').value.trim();
const password = document.getElementById('login-password').value;
if (!identifier || !password) {
flashErrorMessage('Fill all fields');
return;
}
try {
const response = await fetch(`${gameState.apiUrl}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier, password })
});
if (!response.ok) {
const err = await response.json();
flashErrorMessage(err.error || 'Login failed');
return;
}
const auth = await response.json();
gameState.authToken = auth.token;
gameState.userId = auth.userId;
gameState.username = auth.username;
localStorage.setItem('authToken', auth.token);
updateAuthIndicator(true);
hideAuthModal();
} catch (error) {
flashErrorMessage('Connection failed');
}
}
async function handleRegister() {
const username = document.getElementById('register-username').value.trim();
const email = document.getElementById('register-email').value.trim();
const password = document.getElementById('register-password').value;
if (!username || !password) {
flashErrorMessage('Username and password required');
return;
}
if (password.length < 8) {
flashErrorMessage('Password min 8 chars');
return;
}
try {
const body = { username, password };
if (email) body.email = email;
const response = await fetch(`${gameState.apiUrl}/api/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
const err = await response.json();
flashErrorMessage(err.details || err.error || 'Registration failed');
return;
}
const auth = await response.json();
gameState.authToken = auth.token;
gameState.userId = auth.userId;
gameState.username = auth.username;
localStorage.setItem('authToken', auth.token);
updateAuthIndicator(true);
hideAuthModal();
} catch (error) {
flashErrorMessage('Connection failed');
}
}
async function handleLogout() {
if (gameState.authToken) {
try {
await authFetch(`${gameState.apiUrl}/api/v1/auth/logout`, { method: 'POST' });
} catch {
// Ignore - clear local state regardless
}
}
clearAuthState();
}
// Wrapper for authenticated requests
function authFetch(url, options = {}) {
if (gameState.authToken) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${gameState.authToken}`
};
}
return fetch(url, options);
}
async function getConfig() { async function getConfig() {
try { try {
const response = await fetch('/config'); const response = await fetch('/config');
const contentType = response.headers.get('content-type') || '';
if (!response.ok || !contentType.includes('application/json')) {
throw new Error(`unexpected response: ${response.status} ${contentType}`);
}
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error('Failed to get config:', error); console.error('Failed to get config:', error);
@ -266,13 +479,11 @@ async function startNewGame() {
} }
try { try {
const response = await fetch(`${gameState.apiUrl}/api/v1/games`, { const response = await authFetch(`${gameState.apiUrl}/api/v1/games`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody) body: JSON.stringify(requestBody)
}); });
if (!response.ok) throw new Error('Failed to create game');
if (!response.ok) { if (!response.ok) {
const errorInfo = handleApiError('create game', null, response); const errorInfo = handleApiError('create game', null, response);
throw new Error(errorInfo.statusMessage); throw new Error(errorInfo.statusMessage);
@ -404,7 +615,7 @@ async function handleHumanMove(from, to) {
const toEl = document.querySelector(`[data-square="${to}"]`); const toEl = document.querySelector(`[data-square="${to}"]`);
try { try {
const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, { const response = await authFetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ move }) body: JSON.stringify({ move })
@ -441,7 +652,7 @@ async function handleHumanMove(from, to) {
async function triggerComputerMove() { async function triggerComputerMove() {
lockBoard(); lockBoard();
try { try {
const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, { const response = await authFetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}/moves`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ move: 'cccc' }) body: JSON.stringify({ move: 'cccc' })
@ -462,43 +673,64 @@ async function triggerComputerMove() {
} }
function startPolling() { function startPolling() {
gameState.pollInterval = setInterval(async () => { gameState.polling = true;
try { pollOnce();
const response = await fetch(`${gameState.apiUrl}/api/v1/games/${gameState.gameId}`);
if (!response.ok) {
// Use error handler but continue polling for 404 (game might be deleted)
const errorInfo = handleApiError('poll game state', null, response);
if (response.status === 404) {
stopPolling();
unlockBoard();
flashErrorMessage('Game no longer exists');
gameState.gameId = null;
return;
}
// For other errors, display but keep polling
handleApiError('poll game state', null, response);
return;
}
const game = await response.json();
if (game.state !== 'pending') {
stopPolling();
updateGameDisplay(game);
unlockBoard();
}
gameState.networkError = false;
updateServerIndicator('healthy');
} catch (error) {
handleApiError('poll game state', error);
stopPolling();
unlockBoard();
}
}, 1500);
} }
function stopPolling() { function stopPolling() {
clearInterval(gameState.pollInterval); gameState.polling = false;
gameState.pollInterval = null; if (gameState.pollController) {
gameState.pollController.abort();
gameState.pollController = null;
}
}
async function pollOnce() {
if (!gameState.polling || !gameState.gameId) return;
const moveCount = (gameState.moveList || []).length;
gameState.pollController = new AbortController();
try {
const response = await fetch(
`${gameState.apiUrl}/api/v1/games/${gameState.gameId}?wait=true&moveCount=${moveCount}`,
{ signal: gameState.pollController.signal }
);
if (!gameState.polling) return;
if (!response.ok) {
const errorInfo = handleApiError('poll game state', null, response);
if (response.status === 404) {
stopPolling();
unlockBoard();
flashErrorMessage('Game no longer exists');
gameState.gameId = null;
return;
}
// Retry after delay for transient errors
setTimeout(pollOnce, 2000);
return;
}
const game = await response.json();
gameState.networkError = false;
updateServerIndicator('healthy');
if (game.state !== 'pending') {
stopPolling();
updateGameDisplay(game);
unlockBoard();
} else {
// Still pending, long-poll again
pollOnce();
}
} catch (error) {
if (error.name === 'AbortError') return;
handleApiError('poll game state', error);
stopPolling();
unlockBoard();
}
} }
function lockBoard() { function lockBoard() {
@ -753,5 +985,5 @@ function flashErrorMessage(message) {
// Auto-hide after animation completes // Auto-hide after animation completes
setTimeout(() => { setTimeout(() => {
overlay.classList.remove('show'); overlay.classList.remove('show');
}, 500); }, 1500);
} }

View File

@ -37,6 +37,9 @@
<div class="indicator" id="turn-indicator" data-tooltip="Turn"> <div class="indicator" id="turn-indicator" data-tooltip="Turn">
<span class="light turn-light" data-status="white"></span> <span class="light turn-light" data-status="white"></span>
</div> </div>
<div class="indicator auth-indicator" id="auth-indicator" data-tooltip="Auth">
<span class="light" data-status="anonymous"></span>
</div>
<div class="error-flash-overlay" id="error-flash-overlay"> <div class="error-flash-overlay" id="error-flash-overlay">
<div class="error-flash-message" id="error-flash-message"></div> <div class="error-flash-message" id="error-flash-message"></div>
</div> </div>
@ -64,6 +67,53 @@
</div> </div>
</div> </div>
<!-- Auth Modal -->
<div id="auth-modal-overlay" class="modal-overlay">
<div class="modal auth-modal">
<div class="auth-tabs">
<button class="auth-tab active" data-tab="login">Login</button>
<button class="auth-tab" data-tab="register">Register</button>
</div>
<!-- Login Form -->
<div id="login-form" class="auth-form">
<div class="form-group">
<label for="login-identifier">Username or Email</label>
<input type="text" id="login-identifier" autocomplete="username">
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" autocomplete="current-password">
</div>
<div class="modal-buttons">
<button id="login-submit-btn" class="btn btn-primary">Login</button>
<button id="auth-cancel-btn" class="btn btn-secondary">Cancel</button>
</div>
</div>
<!-- Register Form -->
<div id="register-form" class="auth-form" style="display: none;">
<div class="form-group">
<label for="register-username">Username</label>
<input type="text" id="register-username" autocomplete="username">
</div>
<div class="form-group">
<label for="register-email">Email (optional)</label>
<input type="email" id="register-email" autocomplete="email">
</div>
<div class="form-group">
<label for="register-password">Password (min 8 chars)</label>
<input type="password" id="register-password" autocomplete="new-password">
</div>
<div class="modal-buttons">
<button id="register-submit-btn" class="btn btn-primary">Register</button>
<button id="auth-cancel-btn-2" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Overlay Modal -->
<div id="modal-overlay" class="modal-overlay"> <div id="modal-overlay" class="modal-overlay">
<div class="modal"> <div class="modal">
<h2>New Game</h2> <h2>New Game</h2>

View File

@ -315,6 +315,7 @@ main {
/* Status colors */ /* Status colors */
.indicator .light[data-status="healthy"] { color: var(--tokyo-green); } .indicator .light[data-status="healthy"] { color: var(--tokyo-green); }
.indicator .light[data-status="ok"] { color: var(--tokyo-green); }
.indicator .light[data-status="disabled"] { color: var(--tokyo-yellow); } .indicator .light[data-status="disabled"] { color: var(--tokyo-yellow); }
.indicator .light[data-status="degraded"] { color: var(--tokyo-red); } .indicator .light[data-status="degraded"] { color: var(--tokyo-red); }
.indicator .light[data-status="unknown"] { color: var(--tokyo-border); } .indicator .light[data-status="unknown"] { color: var(--tokyo-border); }
@ -594,6 +595,70 @@ input[type="range"]::-webkit-slider-thumb {
margin-top: 2rem; margin-top: 2rem;
} }
/* Auth Tabs */
.auth-modal {
max-width: 360px;
}
.auth-tabs {
display: flex;
gap: 0;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--tokyo-border);
}
.auth-tab {
flex: 1;
padding: 0.75rem;
background: transparent;
border: none;
color: var(--tokyo-fg);
cursor: pointer;
font-size: 0.95rem;
transition: color 0.2s, border-color 0.2s;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.auth-tab:hover {
color: var(--host-royal);
}
.auth-tab.active {
color: var(--host-royal);
border-bottom-color: var(--host-royal);
}
.auth-form input[type="text"],
.auth-form input[type="email"],
.auth-form input[type="password"] {
width: 100%;
padding: 0.5rem;
background: var(--host-bg);
border: 1px solid var(--tokyo-border);
border-radius: 4px;
color: var(--tokyo-fg);
font-size: 0.9rem;
}
.auth-form input:focus {
outline: none;
border-color: var(--host-royal);
}
/* Auth Indicator */
.auth-indicator .light[data-status="anonymous"] {
color: var(--tokyo-border);
}
.auth-indicator .light[data-status="authenticated"] {
color: var(--tokyo-green);
}
.auth-indicator {
cursor: pointer;
}
/* Mobile/Responsiveness */ /* Mobile/Responsiveness */
@media (max-width: 978px) { @media (max-width: 978px) {