diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a6769ef --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +dist +.kilo +*.lock +e2e/ +load/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..5014f01 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + root: true, + env: { + es2021: true, + }, + extends: ['eslint:recommended'], + parserOptions: { + ecmaVersion: 'latest', + }, + overrides: [ + { + files: ['api/**/*.js', '*.config.js', '*.config.mjs', '.eslintrc.js'], + env: { node: true, es2021: true }, + parserOptions: { sourceType: 'script' }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'no-console': 'off', + }, + }, + { + files: ['pro/**/*.js', 'pro/**/*.jsx'], + env: { browser: true, es2021: true }, + parserOptions: { + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + plugins: ['react', 'react-hooks'], + extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended'], + settings: { react: { version: 'detect' } }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'no-console': 'off', + }, + }, + { + files: ['*.test.js', '*.test.jsx'], + env: { mocha: true, jest: true }, + rules: { + 'no-unused-expressions': 'off', + }, + }, + ], +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6aeb5ff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,157 @@ +name: CI + +on: + push: + branches: [main, fix/*, feat/*] + pull_request: + branches: [main] + +jobs: + api: + name: API (Server) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + package-lock.json + api/package-lock.json + + - name: Install root dependencies + run: npm ci + working-directory: . + - name: Install API dependencies + run: npm ci + working-directory: api + - name: Lint + run: npx eslint --ext .js . --max-warnings 50 + working-directory: . + - name: Test + run: npm test + working-directory: api + + pro: + name: Frontend + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + package-lock.json + pro/package-lock.json + + - name: Install root dependencies + run: npm ci + working-directory: . + - name: Install Frontend dependencies + run: npm ci + working-directory: pro + - name: Lint + run: npx eslint --ext .js,.jsx . --max-warnings 50 + working-directory: . + - name: Test + run: npx vitest run + working-directory: pro + + e2e: + name: E2E (Playwright) + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + api/package-lock.json + pro/package-lock.json + + - name: Install API dependencies + run: npm ci + working-directory: api + + - name: Install Frontend dependencies + run: npm ci + working-directory: pro + + - name: Install E2E dependencies + run: npm install + working-directory: e2e + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: e2e + + - name: Run E2E tests + run: npx playwright test + working-directory: e2e + env: + CI: true + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 7 + + load: + name: Load Test + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + api/package-lock.json + + - name: Install API dependencies + run: npm ci + working-directory: api + + - name: Install Load Test dependencies + run: npm install + working-directory: load + + - name: Start API server + run: | + node index.js & + echo "Waiting for server..." + for i in $(seq 1 10); do + curl -s http://localhost:4000/health > /dev/null && echo "Server ready" && break + sleep 1 + done + working-directory: api + + - name: Run load test + run: node runner.js --connections 10 --duration 15 + working-directory: load + env: + LOAD_SERVER_URL: http://localhost:4000 + + - name: Stop API server + run: kill $(lsof -t -i:4000) 2>/dev/null || true + + - name: Upload load report + uses: actions/upload-artifact@v4 + if: always() + with: + name: load-report + path: load/*.log + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.kilo/package-lock.json b/.kilo/package-lock.json new file mode 100644 index 0000000..29429a8 --- /dev/null +++ b/.kilo/package-lock.json @@ -0,0 +1,378 @@ +{ + "name": ".kilo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@kilocode/plugin": "7.2.31" + } + }, + "node_modules/@kilocode/plugin": { + "version": "7.2.31", + "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.31.tgz", + "integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==", + "license": "MIT", + "dependencies": { + "@kilocode/sdk": "7.2.31", + "effect": "4.0.0-beta.48", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@kilocode/sdk": { + "version": "7.2.31", + "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.31.tgz", + "integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.48", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", + "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz", + "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz", + "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/.kilo/plans/1779339173170-crisp-wizard.md b/.kilo/plans/1779339173170-crisp-wizard.md new file mode 100644 index 0000000..df9d893 --- /dev/null +++ b/.kilo/plans/1779339173170-crisp-wizard.md @@ -0,0 +1,225 @@ +# Comprehensive Fix for DAB (Dots & Boxes) Game + +## Bugs Identified + +### 1. Loading State - No Room Creation/Join UI + +**File:** `pro/src/dab/DabGame.jsx` lines 361-370 + +- Shows "Connecting..." with no buttons to create/join a room +- Missing `handleCreateRoom` and `handleJoinRoom` functions + +### 2. Event Name Mismatches (Critical) + +| Component | Emits | Should Be | +| --------- | ------------------ | --------------------- | +| Backend | `dab_redoRequest` | → `dab_redoRequested` | +| Frontend | `dab_approveRedo` | → `dab_respondRedo` | +| Frontend | `dab_denyRedo` | → `dab_respondRedo` | +| Backend | `dab_redoAccepted` | → `dab_moveUndone` | + +**Verification:** + +- Backend line 174: `dab_redoRequest` +- Frontend line 253: listens for `dab_redoRequested` +- Backend line 14: listens for `dab_respondRedo` +- Frontend lines 707, 717: emit `dab_approveRedo`/`dab_denyRedo` +- Frontend line 264: listens for `dab_moveUndone` +- Backend line 214: emits `dab_redoAccepted` + +### 3. Crash Bug in Redo System + +**File:** `api/game-logic/dab.js` line 198 + +- `respondRedo` tries to destructure `room.redoRequest.lastMove` +- But `requestRedo` (lines 167-172) never stores `lastMove` or `claimedBox` in the request object +- **Result:** `TypeError: Cannot destructure property 'lineType' of 'undefined'` + +### 4. Memory Leak in Frontend + +**File:** `pro/src/dab/DabGame.jsx` line 296 + +- `useEffect` dependency array includes `[socket, players, rows, cols, playLine, playBox]` +- When `players`, `rows`, `cols` change, new socket listeners are added without proper cleanup +- **Result:** Multiple listeners fire for single events + +--- + +## Fix Implementation + +### Phase 1: Backend Fixes (`api/game-logic/dab.js`) + +#### 1.1 Fix Event Name: `dab_redoRequest` → `dab_redoRequested` + +**Line 174:** + +```javascript +// Change from: +this.io.to(room.id).emit('dab_redoRequest', {...}); +// To: +this.io.to(room.id).emit('dab_redoRequested', { requester: room.players[playerIndex].name }); +``` + +#### 1.2 Fix RedoRequest Object - Store Move Data + +**Lines 167-172 - Replace with:** + +```javascript +room.redoRequest = { + requesterId: socket.id, + requesterIndex: playerIndex, + requesterName: room.players[playerIndex].name, + targetId: prevPlayer.id, + targetIndex: prevPlayerIndex, + lastMove: room.lastMove ? { ...room.lastMove } : null, // CRITICAL FIX + claimedBox: room.lastClaimedBox || null, // Track last claimed box +}; +``` + +#### 1.3 Track lastClaimedBox in makeMove + +**After line 299 (after checkBoxes call):** + +```javascript +// Track last claimed box for potential redo: +if (claimedBoxes.length > 0) { + room.lastClaimedBox = claimedBoxes[claimedBoxes.length - 1]; +} +``` + +#### 1.4 Fix Event Name: `dab_redoAccepted` → `dab_moveUndone` + +**Lines 214-221 - Replace with:** + +```javascript +if (accept) { + const { lineType, r, c } = room.redoRequest.lastMove; + if (lineType === 'h') { + room.horizontalLines[r][c] = null; + } else { + room.verticalLines[r][c] = null; + } + const claimedBox = room.redoRequest.claimedBox; + if (claimedBox) { + room.boxes[claimedBox.r][claimedBox.c] = null; + room.scores[room.redoRequest.requesterIndex]--; + } + room.lastMove = null; + room.lastClaimedBox = null; + room.currentTurn = room.redoRequest.targetIndex; + + // Emit full state update that frontend expects: + this.io.to(room.id).emit('dab_moveUndone', { + horizontalLines: room.horizontalLines, + verticalLines: room.verticalLines, + boxes: room.boxes, + scores: [...room.scores], + currentTurn: room.currentTurn, + }); +} +``` + +#### 1.5 Add `dab_startGame` Listener + +**Add in handleConnection (line 15):** + +```javascript +socket.on('dab_startGame', (data) => this.startGame(socket, data)); +``` + +**Add new method:** + +```javascript +startGame(socket, { roomId }) { + const room = this.rooms.get(roomId); + if (!room || room.creator !== socket.id) return; + room.gameState = 'playing'; + this.io.to(roomId).emit('dab_gameStarted', { firstTurn: room.players[0].id }); + this.sendRoomInfo(roomId); +} +``` + +--- + +### Phase 2: Frontend Fixes (`pro/src/dab/DabGame.jsx`) + +#### 2.1 Add Create/Join Room Functions + +**After line 75 (after reconnectAttempted ref):** + +```javascript +const handleCreateRoom = () => { + logger.socket('➡️', 'dab_createRoom', { mode, customRows, customCols, customPlayers, playerName }); + socket.emit('dab_createRoom', { mode, customRows, customCols, customPlayers, playerName }); +}; + +const handleJoinRoom = async () => { + const { value: joinRoomId } = await Swal.fire({ + title: 'Join DAB Room', + input: 'text', + inputPlaceholder: 'Enter Room ID', + showCancelButton: true, + customClass: { popup: 'sketch-popup' }, + }); + if (joinRoomId) { + logger.socket('➡️', 'dab_joinRoom', { roomId: joinRoomId.toUpperCase(), playerName }); + socket.emit('dab_joinRoom', { roomId: joinRoomId.toUpperCase(), playerName }); + } +}; +``` + +#### 2.2 Replace Loading Screen with Lobby UI + +**Lines 361-370 - Replace:** + +```javascript +{!roomId ? ( +