From a40fad7194caaecd7c8f46d603817060e7d5e165 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 10 Feb 2026 00:42:48 +0100 Subject: [PATCH 1/8] Separa app in client-server con WebSocket - Aggiunto server Express + WebSocket (server.js) - Creata pagina Display (solo visualizzazione punteggio) - Creata pagina Controller (pannello comandi da mobile) - Aggiunto Vue Router con rotte / e /controller - Estratta logica di gioco condivisa in gameState.js --- package-lock.json | 1021 ++++++++++++++++++++++++----- package.json | 11 +- server.js | 235 +++++++ src/App.vue | 6 +- src/components/ControllerPage.vue | 704 ++++++++++++++++++++ src/components/DisplayPage.vue | 269 ++++++++ src/gameState.js | 204 ++++++ src/main.js | 13 +- vite.config.js | 2 +- 9 files changed, 2280 insertions(+), 185 deletions(-) create mode 100644 server.js create mode 100644 src/components/ControllerPage.vue create mode 100644 src/components/DisplayPage.vue create mode 100644 src/gameState.js diff --git a/package-lock.json b/package-lock.json index 1a2ec93..628b517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,12 @@ "name": "segnapuntianto", "version": "0.0.0", "dependencies": { + "express": "^5.2.1", "nosleep.js": "^0.12.0", "vue": "^3.2.47", - "wave-ui": "^3.3.0" + "vue-router": "^4.6.4", + "wave-ui": "^3.3.0", + "ws": "^8.19.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.1.0", @@ -384,19 +387,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", - "dev": true, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "engines": { "node": ">=6.9.0" } @@ -454,9 +455,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", - "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dependencies": { + "@babel/types": "^7.29.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1701,14 +1705,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.4.tgz", - "integrity": "sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==", - "dev": true, + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dependencies": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2109,9 +2111,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", @@ -2314,106 +2316,112 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", - "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", "dependencies": { - "@babel/parser": "^7.21.3", - "@vue/shared": "3.3.4", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", "estree-walker": "^2.0.2", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", - "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", "dependencies": { - "@vue/compiler-core": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", - "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", "dependencies": { - "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.4", - "@vue/compiler-dom": "3.3.4", - "@vue/compiler-ssr": "3.3.4", - "@vue/reactivity-transform": "3.3.4", - "@vue/shared": "3.3.4", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", - "magic-string": "^0.30.0", - "postcss": "^8.1.10", - "source-map-js": "^1.0.2" + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", - "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", "dependencies": { - "@vue/compiler-dom": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, "node_modules/@vue/reactivity": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", - "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", "dependencies": { - "@vue/shared": "3.3.4" - } - }, - "node_modules/@vue/reactivity-transform": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", - "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", - "dependencies": { - "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.4", - "@vue/shared": "3.3.4", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.0" + "@vue/shared": "3.5.28" } }, "node_modules/@vue/runtime-core": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", - "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", "dependencies": { - "@vue/reactivity": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" } }, "node_modules/@vue/runtime-dom": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", - "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", "dependencies": { - "@vue/runtime-core": "3.3.4", - "@vue/shared": "3.3.4", - "csstype": "^3.1.1" + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", - "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", "dependencies": { - "@vue/compiler-ssr": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" }, "peerDependencies": { - "vue": "3.3.4" + "vue": "3.5.28" } }, "node_modules/@vue/shared": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", - "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/acorn": { "version": "8.8.2", @@ -2540,6 +2548,29 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2611,6 +2642,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2624,6 +2663,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001495", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001495.tgz", @@ -2694,12 +2760,48 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-js-compat": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", @@ -2723,17 +2825,16 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2769,6 +2870,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "node_modules/ejs": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", @@ -2790,6 +2917,25 @@ "integrity": "sha512-y4A7YfQcDGPAeSWM1IuoWzXpg9RY1nwHzHSwRtCSQFp9FgAVDgdWlFf0RbdWfLWQ2WUI+bddUgk5RgTjqRE6FQ==", "dev": true }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -2838,6 +2984,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -2915,6 +3088,11 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -2938,6 +3116,56 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3008,6 +3236,26 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3017,6 +3265,22 @@ "is-callable": "^1.1.3" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -3053,10 +3317,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -3095,15 +3361,23 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3115,6 +3389,18 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -3188,12 +3474,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3260,10 +3545,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -3286,6 +3570,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -3305,8 +3634,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.5", @@ -3322,6 +3650,14 @@ "node": ">= 0.4" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3475,6 +3811,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3808,14 +4149,38 @@ } }, "node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "engines": { - "node": ">=12" + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/merge-stream": { @@ -3846,6 +4211,29 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3869,15 +4257,14 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -3891,6 +4278,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", @@ -3902,10 +4297,12 @@ "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==" }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3937,15 +4334,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3961,10 +4376,19 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3979,9 +4403,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -3997,9 +4421,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4017,6 +4441,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -4026,6 +4462,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4055,6 +4505,28 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -4210,6 +4682,21 @@ "rollup": "^2.0.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4267,6 +4754,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -4276,6 +4768,31 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -4285,15 +4802,92 @@ "randombytes": "^2.1.0" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4312,9 +4906,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -4345,6 +4939,14 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "dev": true }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -4501,15 +5103,6 @@ "node": ">=10" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4522,6 +5115,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -4543,6 +5144,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -4633,6 +5247,14 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -4682,6 +5304,14 @@ "punycode": "^2.1.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "4.3.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", @@ -4770,15 +5400,37 @@ } }, "node_modules/vue": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", - "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "dependencies": { - "@vue/compiler-dom": "3.3.4", - "@vue/compiler-sfc": "3.3.4", - "@vue/runtime-dom": "3.3.4", - "@vue/server-renderer": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" } }, "node_modules/wave-ui": { @@ -5051,8 +5703,27 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, "node_modules/yallist": { "version": "3.1.1", diff --git a/package.json b/package.json index 94f6c30..000d982 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,21 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "start": "node server.js", + "serve": "vite build && node server.js" }, "dependencies": { + "express": "^5.2.1", "nosleep.js": "^0.12.0", "vue": "^3.2.47", - "wave-ui": "^3.3.0" + "vue-router": "^4.6.4", + "wave-ui": "^3.3.0", + "ws": "^8.19.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.1.0", "vite": "^4.3.9", "vite-plugin-pwa": "^0.16.0" } -} +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..75cb5c7 --- /dev/null +++ b/server.js @@ -0,0 +1,235 @@ +import { createServer } from 'http' +import express from 'express' +import { WebSocketServer } from 'ws' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' + +// Import shared game logic +// We need to read it as a dynamic import since it uses ES modules +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +// Inline the game state logic for the server (avoid complex ESM import from src/) +function createInitialState() { + return { + order: true, + visuForm: false, + visuStriscia: true, + modalitaPartita: "3/5", + sp: { + striscia: { home: [0], guest: [0] }, + servHome: true, + punt: { home: 0, guest: 0 }, + set: { home: 0, guest: 0 }, + nomi: { home: "Antoniana", guest: "Guest" }, + form: { + home: ["1", "2", "3", "4", "5", "6"], + guest: ["1", "2", "3", "4", "5", "6"], + }, + storicoServizio: [], + }, + } +} + +function checkVittoria(state) { + const puntHome = state.sp.punt.home + const puntGuest = state.sp.punt.guest + const setHome = state.sp.set.home + const setGuest = state.sp.set.guest + const totSet = setHome + setGuest + let isSetDecisivo = false + if (state.modalitaPartita === "2/3") { + isSetDecisivo = totSet >= 2 + } else { + isSetDecisivo = totSet >= 4 + } + const punteggioVittoria = isSetDecisivo ? 15 : 25 + if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) return true + if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) return true + return false +} + +function applyAction(state, action) { + const s = JSON.parse(JSON.stringify(state)) + switch (action.type) { + case "incPunt": { + const team = action.team + if (checkVittoria(s)) break + s.sp.storicoServizio.push({ + servHome: s.sp.servHome, + cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome), + }) + s.sp.punt[team]++ + if (team === "home") { + s.sp.striscia.home.push(s.sp.punt.home) + s.sp.striscia.guest.push(" ") + } else { + s.sp.striscia.guest.push(s.sp.punt.guest) + s.sp.striscia.home.push(" ") + } + const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome) + if (cambioPalla) { + s.sp.form[team].push(s.sp.form[team].shift()) + } + s.sp.servHome = team === "home" + break + } + case "decPunt": { + if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) { + const tmpHome = s.sp.striscia.home.pop() + s.sp.striscia.guest.pop() + const statoServizio = s.sp.storicoServizio.pop() + if (tmpHome === " ") { + s.sp.punt.guest-- + if (statoServizio.cambioPalla) { + s.sp.form.guest.unshift(s.sp.form.guest.pop()) + } + } else { + s.sp.punt.home-- + if (statoServizio.cambioPalla) { + s.sp.form.home.unshift(s.sp.form.home.pop()) + } + } + s.sp.servHome = statoServizio.servHome + } + break + } + case "incSet": { + const team = action.team + if (s.sp.set[team] === 2) { s.sp.set[team] = 0 } else { s.sp.set[team]++ } + break + } + case "cambiaPalla": { + if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) { + s.sp.servHome = !s.sp.servHome + } + break + } + case "resetta": { + s.visuForm = false + s.sp.punt.home = 0 + s.sp.punt.guest = 0 + s.sp.form = { + home: ["1", "2", "3", "4", "5", "6"], + guest: ["1", "2", "3", "4", "5", "6"], + } + s.sp.striscia = { home: [0], guest: [0] } + s.sp.storicoServizio = [] + break + } + case "toggleFormazione": { s.visuForm = !s.visuForm; break } + case "toggleStriscia": { s.visuStriscia = !s.visuStriscia; break } + case "toggleOrder": { s.order = !s.order; break } + case "setNomi": { + if (action.home !== undefined) s.sp.nomi.home = action.home + if (action.guest !== undefined) s.sp.nomi.guest = action.guest + break + } + case "setModalita": { s.modalitaPartita = action.modalita; break } + case "setFormazione": { + if (action.team && action.form) { + s.sp.form[action.team] = [...action.form] + } + break + } + case "confermaCambi": { + const team = action.team + const cambi = action.cambi || [] + const form = s.sp.form[team].map((val) => String(val).trim()) + const formAggiornata = [...form] + let valid = true + for (const cambio of cambi) { + const cin = (cambio.in || "").trim() + const cout = (cambio.out || "").trim() + if (!cin || !cout) continue + if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break } + if (cin === cout) { valid = false; break } + if (formAggiornata.includes(cin)) { valid = false; break } + if (!formAggiornata.includes(cout)) { valid = false; break } + const idx = formAggiornata.findIndex((val) => String(val).trim() === cout) + if (idx !== -1) { formAggiornata.splice(idx, 1, cin) } + } + if (valid) { s.sp.form[team] = formAggiornata } + break + } + default: break + } + return s +} + +// ——— Server Setup ——— + +const app = express() +const PORT = process.env.PORT || 3000 + +// Serve the Vite build output +app.use(express.static(join(__dirname, 'dist'))) + +// SPA fallback: serve index.html for all non-file routes +app.get('/{*splat}', (req, res) => { + res.sendFile(join(__dirname, 'dist', 'index.html')) +}) + +const server = createServer(app) + +// WebSocket server +const wss = new WebSocketServer({ server }) + +// Global game state +let gameState = createInitialState() + +// Track client roles +const clients = new Map() // ws -> { role: 'display' | 'controller' } + +wss.on('connection', (ws) => { + console.log('New WebSocket connection') + + ws.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()) + + if (msg.type === 'register') { + clients.set(ws, { role: msg.role || 'display' }) + console.log(`Client registered as: ${msg.role || 'display'}`) + // Send current state immediately + ws.send(JSON.stringify({ type: 'state', state: gameState })) + return + } + + if (msg.type === 'action') { + // Only controllers can send actions + const client = clients.get(ws) + if (!client || client.role !== 'controller') { + console.log('Action rejected: not a controller') + return + } + + // Apply the action to game state + gameState = applyAction(gameState, msg.action) + + // Broadcast new state to ALL connected clients + const stateMsg = JSON.stringify({ type: 'state', state: gameState }) + wss.clients.forEach((c) => { + if (c.readyState === 1) { // WebSocket.OPEN + c.send(stateMsg) + } + }) + } + } catch (err) { + console.error('Error processing message:', err) + } + }) + + ws.on('close', () => { + clients.delete(ws) + console.log('Client disconnected') + }) +}) + +server.listen(PORT, '0.0.0.0', () => { + console.log(`\n🏐 Segnapunti Server running on:`) + console.log(` Display: http://localhost:${PORT}/`) + console.log(` Controller: http://localhost:${PORT}/controller`) + console.log(`\n Per accedere da altri dispositivi sulla rete locale,`) + console.log(` usa l'IP di questo computer, es: http://192.168.1.x:${PORT}/controller\n`) +}) diff --git a/src/App.vue b/src/App.vue index 8355651..98240ae 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,3 @@ - - diff --git a/src/components/ControllerPage.vue b/src/components/ControllerPage.vue new file mode 100644 index 0000000..b9ca30d --- /dev/null +++ b/src/components/ControllerPage.vue @@ -0,0 +1,704 @@ + + + + + diff --git a/src/components/DisplayPage.vue b/src/components/DisplayPage.vue new file mode 100644 index 0000000..f3c1318 --- /dev/null +++ b/src/components/DisplayPage.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/gameState.js b/src/gameState.js new file mode 100644 index 0000000..6486b11 --- /dev/null +++ b/src/gameState.js @@ -0,0 +1,204 @@ +/** + * Shared game logic for segnapunti. + * Used by both the WebSocket server and the client-side for local preview. + */ + +export function createInitialState() { + return { + order: true, + visuForm: false, + visuStriscia: true, + modalitaPartita: "3/5", + sp: { + striscia: { home: [0], guest: [0] }, + servHome: true, + punt: { home: 0, guest: 0 }, + set: { home: 0, guest: 0 }, + nomi: { home: "Antoniana", guest: "Guest" }, + form: { + home: ["1", "2", "3", "4", "5", "6"], + guest: ["1", "2", "3", "4", "5", "6"], + }, + storicoServizio: [], + }, + } +} + +export function checkVittoria(state) { + const puntHome = state.sp.punt.home + const puntGuest = state.sp.punt.guest + const setHome = state.sp.set.home + const setGuest = state.sp.set.guest + const totSet = setHome + setGuest + + let isSetDecisivo = false + if (state.modalitaPartita === "2/3") { + isSetDecisivo = totSet >= 2 + } else { + isSetDecisivo = totSet >= 4 + } + + const punteggioVittoria = isSetDecisivo ? 15 : 25 + + if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) { + return true + } + if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) { + return true + } + + return false +} + +export function applyAction(state, action) { + // Deep-clone to avoid mutation issues (server-side) + // Returns new state + const s = JSON.parse(JSON.stringify(state)) + + switch (action.type) { + case "incPunt": { + const team = action.team + if (checkVittoria(s)) break + + s.sp.storicoServizio.push({ + servHome: s.sp.servHome, + cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome), + }) + + s.sp.punt[team]++ + if (team === "home") { + s.sp.striscia.home.push(s.sp.punt.home) + s.sp.striscia.guest.push(" ") + } else { + s.sp.striscia.guest.push(s.sp.punt.guest) + s.sp.striscia.home.push(" ") + } + + const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome) + if (cambioPalla) { + s.sp.form[team].push(s.sp.form[team].shift()) + } + + s.sp.servHome = team === "home" + break + } + + case "decPunt": { + if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) { + const tmpHome = s.sp.striscia.home.pop() + s.sp.striscia.guest.pop() + const statoServizio = s.sp.storicoServizio.pop() + + if (tmpHome === " ") { + s.sp.punt.guest-- + if (statoServizio.cambioPalla) { + s.sp.form.guest.unshift(s.sp.form.guest.pop()) + } + } else { + s.sp.punt.home-- + if (statoServizio.cambioPalla) { + s.sp.form.home.unshift(s.sp.form.home.pop()) + } + } + s.sp.servHome = statoServizio.servHome + } + break + } + + case "incSet": { + const team = action.team + if (s.sp.set[team] === 2) { + s.sp.set[team] = 0 + } else { + s.sp.set[team]++ + } + break + } + + case "cambiaPalla": { + if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) { + s.sp.servHome = !s.sp.servHome + } + break + } + + case "resetta": { + s.visuForm = false + s.sp.punt.home = 0 + s.sp.punt.guest = 0 + s.sp.form = { + home: ["1", "2", "3", "4", "5", "6"], + guest: ["1", "2", "3", "4", "5", "6"], + } + s.sp.striscia = { home: [0], guest: [0] } + s.sp.storicoServizio = [] + break + } + + case "toggleFormazione": { + s.visuForm = !s.visuForm + break + } + + case "toggleStriscia": { + s.visuStriscia = !s.visuStriscia + break + } + + case "toggleOrder": { + s.order = !s.order + break + } + + case "setNomi": { + if (action.home !== undefined) s.sp.nomi.home = action.home + if (action.guest !== undefined) s.sp.nomi.guest = action.guest + break + } + + case "setModalita": { + s.modalitaPartita = action.modalita + break + } + + case "setFormazione": { + if (action.team && action.form) { + s.sp.form[action.team] = [...action.form] + } + break + } + + case "confermaCambi": { + const team = action.team + const cambi = action.cambi || [] + const form = s.sp.form[team].map((val) => String(val).trim()) + const formAggiornata = [...form] + + let valid = true + for (const cambio of cambi) { + const cin = (cambio.in || "").trim() + const cout = (cambio.out || "").trim() + if (!cin || !cout) continue + if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break } + if (cin === cout) { valid = false; break } + if (formAggiornata.includes(cin)) { valid = false; break } + if (!formAggiornata.includes(cout)) { valid = false; break } + + const idx = formAggiornata.findIndex((val) => String(val).trim() === cout) + if (idx !== -1) { + formAggiornata.splice(idx, 1, cin) + } + } + + if (valid) { + s.sp.form[team] = formAggiornata + } + break + } + + default: + break + } + + return s +} diff --git a/src/main.js b/src/main.js index 5018ffd..4889403 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,21 @@ import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' import './style.css' import App from './App.vue' import WaveUI from 'wave-ui' import 'wave-ui/dist/wave-ui.css' +import DisplayPage from './components/DisplayPage.vue' +import ControllerPage from './components/ControllerPage.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: '/', component: DisplayPage }, + { path: '/controller', component: ControllerPage }, + ], +}) const app = createApp(App) - +app.use(router) app.use(WaveUI) app.mount('#app') diff --git a/vite.config.js b/vite.config.js index 0dc0206..dc75b25 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,7 +4,7 @@ import { VitePWA } from 'vite-plugin-pwa' // https://vitejs.dev/config/ export default defineConfig({ - base: process.env.NODE_ENV === 'production' ? '/segnap' : '/', + base: '/', plugins: [ vue(), VitePWA({ From f7c4fdc2ef72c3905a661be6fb4e2acca3fcc72a Mon Sep 17 00:00:00 2001 From: davide3011 Date: Tue, 10 Feb 2026 09:53:46 +0100 Subject: [PATCH 2/8] refactor(server): separa la logica WebSocket e centralizza le utility di avvio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Estrae la gestione dei messaggi WebSocket in un modulo dedicato. Rende server.js più snello e focalizzato su bootstrap HTTP/WS. Introduce utility per stampa URL di accesso e discovery IP di rete. Mantiene la logica di stato partita condivisa in gameState.js. --- server.js | 221 ++------------------------------------- src/gameState.js | 8 +- src/server-utils.js | 45 ++++++++ src/websocket-handler.js | 171 ++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 215 deletions(-) create mode 100644 src/server-utils.js create mode 100644 src/websocket-handler.js diff --git a/server.js b/server.js index 75cb5c7..0f8363e 100644 --- a/server.js +++ b/server.js @@ -3,233 +3,32 @@ import express from 'express' import { WebSocketServer } from 'ws' import { fileURLToPath } from 'url' import { dirname, join } from 'path' +import { setupWebSocketHandler } from './src/websocket-handler.js' +import { printServerInfo } from './src/server-utils.js' -// Import shared game logic -// We need to read it as a dynamic import since it uses ES modules const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) -// Inline the game state logic for the server (avoid complex ESM import from src/) -function createInitialState() { - return { - order: true, - visuForm: false, - visuStriscia: true, - modalitaPartita: "3/5", - sp: { - striscia: { home: [0], guest: [0] }, - servHome: true, - punt: { home: 0, guest: 0 }, - set: { home: 0, guest: 0 }, - nomi: { home: "Antoniana", guest: "Guest" }, - form: { - home: ["1", "2", "3", "4", "5", "6"], - guest: ["1", "2", "3", "4", "5", "6"], - }, - storicoServizio: [], - }, - } -} - -function checkVittoria(state) { - const puntHome = state.sp.punt.home - const puntGuest = state.sp.punt.guest - const setHome = state.sp.set.home - const setGuest = state.sp.set.guest - const totSet = setHome + setGuest - let isSetDecisivo = false - if (state.modalitaPartita === "2/3") { - isSetDecisivo = totSet >= 2 - } else { - isSetDecisivo = totSet >= 4 - } - const punteggioVittoria = isSetDecisivo ? 15 : 25 - if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) return true - if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) return true - return false -} - -function applyAction(state, action) { - const s = JSON.parse(JSON.stringify(state)) - switch (action.type) { - case "incPunt": { - const team = action.team - if (checkVittoria(s)) break - s.sp.storicoServizio.push({ - servHome: s.sp.servHome, - cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome), - }) - s.sp.punt[team]++ - if (team === "home") { - s.sp.striscia.home.push(s.sp.punt.home) - s.sp.striscia.guest.push(" ") - } else { - s.sp.striscia.guest.push(s.sp.punt.guest) - s.sp.striscia.home.push(" ") - } - const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome) - if (cambioPalla) { - s.sp.form[team].push(s.sp.form[team].shift()) - } - s.sp.servHome = team === "home" - break - } - case "decPunt": { - if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) { - const tmpHome = s.sp.striscia.home.pop() - s.sp.striscia.guest.pop() - const statoServizio = s.sp.storicoServizio.pop() - if (tmpHome === " ") { - s.sp.punt.guest-- - if (statoServizio.cambioPalla) { - s.sp.form.guest.unshift(s.sp.form.guest.pop()) - } - } else { - s.sp.punt.home-- - if (statoServizio.cambioPalla) { - s.sp.form.home.unshift(s.sp.form.home.pop()) - } - } - s.sp.servHome = statoServizio.servHome - } - break - } - case "incSet": { - const team = action.team - if (s.sp.set[team] === 2) { s.sp.set[team] = 0 } else { s.sp.set[team]++ } - break - } - case "cambiaPalla": { - if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) { - s.sp.servHome = !s.sp.servHome - } - break - } - case "resetta": { - s.visuForm = false - s.sp.punt.home = 0 - s.sp.punt.guest = 0 - s.sp.form = { - home: ["1", "2", "3", "4", "5", "6"], - guest: ["1", "2", "3", "4", "5", "6"], - } - s.sp.striscia = { home: [0], guest: [0] } - s.sp.storicoServizio = [] - break - } - case "toggleFormazione": { s.visuForm = !s.visuForm; break } - case "toggleStriscia": { s.visuStriscia = !s.visuStriscia; break } - case "toggleOrder": { s.order = !s.order; break } - case "setNomi": { - if (action.home !== undefined) s.sp.nomi.home = action.home - if (action.guest !== undefined) s.sp.nomi.guest = action.guest - break - } - case "setModalita": { s.modalitaPartita = action.modalita; break } - case "setFormazione": { - if (action.team && action.form) { - s.sp.form[action.team] = [...action.form] - } - break - } - case "confermaCambi": { - const team = action.team - const cambi = action.cambi || [] - const form = s.sp.form[team].map((val) => String(val).trim()) - const formAggiornata = [...form] - let valid = true - for (const cambio of cambi) { - const cin = (cambio.in || "").trim() - const cout = (cambio.out || "").trim() - if (!cin || !cout) continue - if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break } - if (cin === cout) { valid = false; break } - if (formAggiornata.includes(cin)) { valid = false; break } - if (!formAggiornata.includes(cout)) { valid = false; break } - const idx = formAggiornata.findIndex((val) => String(val).trim() === cout) - if (idx !== -1) { formAggiornata.splice(idx, 1, cin) } - } - if (valid) { s.sp.form[team] = formAggiornata } - break - } - default: break - } - return s -} - -// ——— Server Setup ——— +// --- Configurazione del server --- const app = express() const PORT = process.env.PORT || 3000 -// Serve the Vite build output +// Espone i file generati dalla build di Vite. app.use(express.static(join(__dirname, 'dist'))) -// SPA fallback: serve index.html for all non-file routes -app.get('/{*splat}', (req, res) => { +// Fallback per SPA: restituisce `index.html` per tutte le route che non puntano a file statici. +app.get('/{*splat}', (_req, res) => { res.sendFile(join(__dirname, 'dist', 'index.html')) }) const server = createServer(app) -// WebSocket server +// Inizializza il server WebSocket con la logica di gioco. const wss = new WebSocketServer({ server }) +setupWebSocketHandler(wss) -// Global game state -let gameState = createInitialState() - -// Track client roles -const clients = new Map() // ws -> { role: 'display' | 'controller' } - -wss.on('connection', (ws) => { - console.log('New WebSocket connection') - - ws.on('message', (data) => { - try { - const msg = JSON.parse(data.toString()) - - if (msg.type === 'register') { - clients.set(ws, { role: msg.role || 'display' }) - console.log(`Client registered as: ${msg.role || 'display'}`) - // Send current state immediately - ws.send(JSON.stringify({ type: 'state', state: gameState })) - return - } - - if (msg.type === 'action') { - // Only controllers can send actions - const client = clients.get(ws) - if (!client || client.role !== 'controller') { - console.log('Action rejected: not a controller') - return - } - - // Apply the action to game state - gameState = applyAction(gameState, msg.action) - - // Broadcast new state to ALL connected clients - const stateMsg = JSON.stringify({ type: 'state', state: gameState }) - wss.clients.forEach((c) => { - if (c.readyState === 1) { // WebSocket.OPEN - c.send(stateMsg) - } - }) - } - } catch (err) { - console.error('Error processing message:', err) - } - }) - - ws.on('close', () => { - clients.delete(ws) - console.log('Client disconnected') - }) -}) - +// Avvia il server HTTP. server.listen(PORT, '0.0.0.0', () => { - console.log(`\n🏐 Segnapunti Server running on:`) - console.log(` Display: http://localhost:${PORT}/`) - console.log(` Controller: http://localhost:${PORT}/controller`) - console.log(`\n Per accedere da altri dispositivi sulla rete locale,`) - console.log(` usa l'IP di questo computer, es: http://192.168.1.x:${PORT}/controller\n`) + printServerInfo(PORT) }) diff --git a/src/gameState.js b/src/gameState.js index 6486b11..66b980f 100644 --- a/src/gameState.js +++ b/src/gameState.js @@ -1,6 +1,6 @@ /** - * Shared game logic for segnapunti. - * Used by both the WebSocket server and the client-side for local preview. + * Logica di gioco condivisa per il segnapunti. + * Utilizzata sia dal server WebSocket sia dal client per l'anteprima locale. */ export function createInitialState() { @@ -51,8 +51,8 @@ export function checkVittoria(state) { } export function applyAction(state, action) { - // Deep-clone to avoid mutation issues (server-side) - // Returns new state + // Esegue un deep clone per evitare mutazioni indesiderate dello stato lato server. + // Restituisce sempre un nuovo oggetto di stato. const s = JSON.parse(JSON.stringify(state)) switch (action.type) { diff --git a/src/server-utils.js b/src/server-utils.js new file mode 100644 index 0000000..356a06f --- /dev/null +++ b/src/server-utils.js @@ -0,0 +1,45 @@ +import { networkInterfaces } from 'os' + +/** + * Restituisce gli indirizzi IP di rete del sistema, escludendo loopback e bridge Docker. + * @returns {string[]} Elenco degli indirizzi IP disponibili. + */ +export function getNetworkIPs() { + const nets = networkInterfaces() + const networkIPs = [] + + for (const name of Object.keys(nets)) { + for (const net of nets[name]) { + // Esclude loopback (127.0.0.1), indirizzi non IPv4 e bridge Docker (172.17.x.x, 172.18.x.x). + if (net.family === 'IPv4' && + !net.internal && + !net.address.startsWith('172.17.') && + !net.address.startsWith('172.18.')) { + networkIPs.push(net.address) + } + } + } + + return networkIPs +} + +/** + * Stampa il riepilogo di avvio del server con gli URL di accesso. + * @param {number} port - Porta sulla quale il server e in ascolto. + */ +export function printServerInfo(port = 5173) { + const networkIPs = getNetworkIPs() + + console.log(`\nSegnapunti Server`) + console.log(` Display: http://localhost:${port}/`) + console.log(` Controller: http://localhost:${port}/controller`) + + if (networkIPs.length > 0) { + console.log(`\n Da dispositivi remoti:`) + networkIPs.forEach(ip => { + console.log(` http://${ip}:${port}/controller`) + }) + } + + console.log() +} diff --git a/src/websocket-handler.js b/src/websocket-handler.js new file mode 100644 index 0000000..5941bb8 --- /dev/null +++ b/src/websocket-handler.js @@ -0,0 +1,171 @@ +import { createInitialState, applyAction } from './gameState.js' + +/** + * Crea e configura il server WebSocket per la gestione dello stato di gioco. + * @param {WebSocketServer} wss - Istanza del server WebSocket. + * @returns {Object} Oggetto con metodi di gestione dello stato. + */ +export function setupWebSocketHandler(wss) { + // Stato globale della partita. + let gameState = createInitialState() + + // Mappa dei ruoli associati ai client connessi. + const clients = new Map() // ws -> { role: 'display' | 'controller' } + + /** + * Gestisce i messaggi in arrivo dal client + */ + function handleMessage(ws, data) { + try { + // Converte il payload in stringa in modo sicuro, anche se arriva come Buffer. + const dataStr = typeof data === 'string' ? data : data.toString('utf8') + const msg = JSON.parse(dataStr) + + if (msg.type === 'register') { + return handleRegister(ws, msg) + } + + if (msg.type === 'action') { + return handleAction(ws, msg) + } + } catch (err) { + console.error('Error processing message:', err, 'data:', data) + // Invia l'errore solo se la connessione e ancora aperta. + if (ws.readyState === 1) { // Stato WebSocket.OPEN + try { + sendError(ws, 'Invalid message format') + } catch (sendErr) { + console.error('Error sending error message:', sendErr) + } + } + } + } + + /** + * Gestisce la registrazione di un client (display o controller) + */ + function handleRegister(ws, msg) { + const role = msg.role || 'display' + + // Valida il ruolo dichiarato dal client. + if (!['display', 'controller'].includes(role)) { + sendError(ws, 'Invalid role') + return + } + + clients.set(ws, { role }) + console.log(`[WebSocket] Client registered as: ${role} (total clients: ${clients.size})`) + + // Invia subito lo stato corrente, se la connessione e aperta. + if (ws.readyState === 1) { // Stato WebSocket.OPEN + try { + ws.send(JSON.stringify({ type: 'state', state: gameState })) + } catch (err) { + console.error('Error sending initial state:', err) + } + } + } + + /** + * Gestisce un'azione di gioco dal controller + */ + function handleAction(ws, msg) { + // Solo i client controller possono inviare azioni. + const client = clients.get(ws) + if (!client || client.role !== 'controller') { + sendError(ws, 'Only controllers can send actions') + return + } + + // Verifica il formato dell'azione ricevuta. + if (!msg.action || !msg.action.type) { + sendError(ws, 'Invalid action format') + return + } + + // Applica l'azione allo stato della partita. + const previousState = gameState + try { + gameState = applyAction(gameState, msg.action) + } catch (err) { + console.error('Error applying action:', err) + sendError(ws, 'Failed to apply action') + gameState = previousState + return + } + + // Propaga il nuovo stato a tutti i client connessi. + broadcastState() + } + + /** + * Invia un messaggio di errore al client + */ + function sendError(ws, message) { + if (ws.readyState === 1) { // Stato WebSocket.OPEN + try { + ws.send(JSON.stringify({ type: 'error', message })) + } catch (err) { + console.error('Failed to send error message:', err) + } + } + } + + /** + * Invia lo stato corrente a tutti i client connessi. + */ + function broadcastState() { + const stateMsg = JSON.stringify({ type: 'state', state: gameState }) + wss.clients.forEach((client) => { + if (client.readyState === 1) { // Stato WebSocket.OPEN + client.send(stateMsg) + } + }) + } + + /** + * Gestisce la chiusura della connessione + */ + function handleClose(ws) { + const client = clients.get(ws) + const role = client?.role || 'unknown' + console.log(`[WebSocket] Client disconnected (role: ${role})`) + clients.delete(ws) + } + + /** + * Gestisce gli errori WebSocket + */ + function handleError(err, ws) { + console.error('WebSocket error:', err) + + // In caso di frame non validi, chiude forzatamente la connessione. + if (err.code === 'WS_ERR_INVALID_CLOSE_CODE' || err.code === 'WS_ERR_INVALID_UTF8') { + try { + if (ws && ws.readyState === 1) { // Stato WebSocket.OPEN + ws.terminate() // Chiusura forzata senza handshake di chiusura. + } + } catch (closeErr) { + console.error('Error closing connection:', closeErr) + } + } + } + + // Registra gli handler per ogni nuova connessione. + wss.on('connection', (ws) => { + // Imposta il tipo binario per ridurre i problemi di codifica. + ws.binaryType = 'arraybuffer' + + ws.on('message', (data) => handleMessage(ws, data)) + ws.on('close', () => handleClose(ws)) + ws.on('error', (err) => handleError(err, ws)) + }) + + // Espone un'API pubblica per controllo esterno, se necessario. + return { + getState: () => gameState, + setState: (newState) => { gameState = newState }, + broadcastState, + getClients: () => clients, + } +} From 082a52dc3e927ab28d3440f3d611300852847013 Mon Sep 17 00:00:00 2001 From: davide3011 Date: Tue, 10 Feb 2026 09:54:10 +0100 Subject: [PATCH 3/8] feat(client): migliora robustezza connessioni WebSocket su display e controller Aggiunge gestione riconnessione con backoff esponenziale e protezione da reconnect multipli. Migliora cleanup su unmount/HMR per evitare listener e timeout pendenti. Uniforma gestione errori e stato connessione lato client. Semplifica etichette pulsanti controller rimuovendo emoji e aggiorna commenti. --- src/components/ControllerPage.vue | 219 +++++++++++++++++++++++++----- src/components/DisplayPage.vue | 141 +++++++++++++++++-- src/style.css | 2 +- 3 files changed, 311 insertions(+), 51 deletions(-) diff --git a/src/components/ControllerPage.vue b/src/components/ControllerPage.vue index b9ca30d..8188d55 100644 --- a/src/components/ControllerPage.vue +++ b/src/components/ControllerPage.vue @@ -1,12 +1,12 @@