diff --git a/README.md b/README.md index cfa1b9a..e56146b 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ For detailed configuration, advanced features, and troubleshooting, visit our co | **[Getting Started](docs/getting-started.md)** | Detailed installation and first-run guide | | **[Configuration](docs/config.md)** | Complete configuration options reference | | **[Accounts & 2FA](docs/accounts.md)** | Setting up accounts with TOTP authentication | +| **[Dashboard](src/dashboard/README.md)** | 🆕 Local web dashboard for monitoring and control | | **[External Scheduling](docs/schedule.md)** | Use OS schedulers for automation | | **[Docker Deployment](docs/docker.md)** | Running in containers | | **[Humanization](docs/humanization.md)** | Anti-detection and natural behavior | @@ -97,6 +98,34 @@ For detailed configuration, advanced features, and troubleshooting, visit our co --- +## 📊 Dashboard (NEW) + +Monitor and control your bot through a local web interface: + +```bash +# Start dashboard separately +npm run dashboard + +# Or enable auto-start in config.jsonc: +{ + "dashboard": { + "enabled": true, + "port": 3000 + } +} +``` + +Access at `http://localhost:3000` to: +- 📈 View real-time points and account status +- 📋 Monitor live logs with WebSocket streaming +- 🔄 Manually sync individual accounts +- ⚙️ Edit configuration with automatic backup +- 📊 View historical run summaries and metrics + +**[📖 Full Dashboard API Documentation](src/dashboard/README.md)** + +--- + ## Docker Quick Start For containerized deployment: diff --git a/package-lock.json b/package-lock.json index e0741be..f138798 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.8.4", "chalk": "^4.1.2", "cheerio": "^1.0.0", + "express": "^4.21.2", "fingerprint-generator": "^2.1.66", "fingerprint-injector": "^2.1.66", "http-proxy-agent": "^7.0.2", @@ -21,11 +22,14 @@ "playwright": "1.52.0", "rebrowser-playwright": "1.52.0", "socks-proxy-agent": "^8.0.5", - "ts-node": "^10.9.2" + "ts-node": "^10.9.2", + "ws": "^8.18.3" }, "devDependencies": { + "@types/express": "^4.17.25", "@types/ms": "^0.7.34", "@types/node": "^20.19.24", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^7.17.0", "eslint": "^8.57.0", "eslint-plugin-modules-newline": "^0.0.6", @@ -370,6 +374,67 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -386,6 +451,63 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", @@ -587,6 +709,19 @@ "dev": true, "license": "ISC" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -694,6 +829,12 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -737,6 +878,57 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -799,6 +991,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "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", @@ -812,6 +1013,22 @@ "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==", + "license": "MIT", + "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -936,6 +1153,42 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1018,6 +1271,25 @@ "node": ">=0.4.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1144,6 +1416,12 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.244", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", @@ -1157,6 +1435,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -1236,6 +1523,12 @@ "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==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1437,6 +1730,76 @@ "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1524,6 +1887,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1718,6 +2114,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1998,6 +2412,22 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -2089,7 +2519,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ip-address": { @@ -2101,6 +2530,15 @@ "node": ">= 12" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2308,6 +2746,24 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2318,6 +2774,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2332,6 +2797,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2392,6 +2869,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -2410,6 +2896,30 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "license": "MIT", + "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", @@ -2558,6 +3068,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2605,6 +3124,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2674,6 +3199,19 @@ "node": ">= 0.8.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2690,6 +3228,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "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", @@ -2711,6 +3264,42 @@ ], "license": "MIT" }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rebrowser-playwright": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/rebrowser-playwright/-/rebrowser-playwright-1.52.0.tgz", @@ -2804,6 +3393,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2823,6 +3432,75 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2846,6 +3524,78 @@ "node": ">=8" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2907,6 +3657,15 @@ "node": ">= 14" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3049,6 +3808,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -3137,6 +3905,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3165,6 +3946,15 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -3205,6 +3995,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -3220,6 +4019,15 @@ "node": ">=0.10.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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -3375,6 +4183,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "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/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index b5d427c..70b9fdd 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "pre-build": "npm i && npm run clean && node -e \"process.exit(process.env.SKIP_PLAYWRIGHT_INSTALL?0:1)\" || npx playwright install chromium", "typecheck": "tsc --noEmit", "build": "tsc", - "test": "node --test --loader ts-node/esm tests", + "test": "node --test --loader ts-node/esm tests/**/*.test.ts", "start": "node --enable-source-maps ./dist/index.js", "ts-start": "node --loader ts-node/esm ./src/index.ts", "dev": "ts-node ./src/index.ts -dev", + "dashboard": "node --enable-source-maps ./dist/index.js -dashboard", + "dashboard-dev": "ts-node ./src/index.ts -dashboard", "lint": "eslint \"src/**/*.{ts,tsx}\"", "prepare": "npm run build", "setup": "node ./setup/update/setup.mjs", @@ -49,8 +51,10 @@ "url": "https://github.com/sponsors/Obsidian-wtf" }, "devDependencies": { + "@types/express": "^4.17.25", "@types/ms": "^0.7.34", "@types/node": "^20.19.24", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^7.17.0", "eslint": "^8.57.0", "eslint-plugin-modules-newline": "^0.0.6", @@ -61,6 +65,7 @@ "axios": "^1.8.4", "chalk": "^4.1.2", "cheerio": "^1.0.0", + "express": "^4.21.2", "fingerprint-generator": "^2.1.66", "fingerprint-injector": "^2.1.66", "http-proxy-agent": "^7.0.2", @@ -70,6 +75,7 @@ "playwright": "1.52.0", "rebrowser-playwright": "1.52.0", "socks-proxy-agent": "^8.0.5", - "ts-node": "^10.9.2" + "ts-node": "^10.9.2", + "ws": "^8.18.3" } } diff --git a/src/config.jsonc b/src/config.jsonc index 46161a3..264f651 100644 --- a/src/config.jsonc +++ b/src/config.jsonc @@ -135,6 +135,13 @@ "redactEmails": true }, + // Dashboard (NEW) + "dashboard": { + "enabled": false, // Auto-start dashboard with bot (default: false) + "port": 3000, // Dashboard port (default: 3000) + "host": "127.0.0.1" // Bind address (default: 127.0.0.1, localhost only for security) + }, + // Buy mode "buyMode": { "maxMinutes": 45 @@ -150,3 +157,4 @@ } } + diff --git a/src/dashboard/README.md b/src/dashboard/README.md new file mode 100644 index 0000000..536b7d1 --- /dev/null +++ b/src/dashboard/README.md @@ -0,0 +1,253 @@ +# Dashboard API Reference + +## Endpoints + +### Status & Control + +#### `GET /api/status` +Get current bot status. + +**Response:** +```json +{ + "running": false, + "lastRun": "2025-11-03T10:30:00.000Z", + "currentAccount": "user@example.com", + "totalAccounts": 5 +} +``` + +#### `POST /api/start` +Start bot execution in background. + +**Response:** +```json +{ + "success": true, + "pid": 12345 +} +``` + +#### `POST /api/stop` +Stop bot execution. + +**Response:** +```json +{ + "success": true +} +``` + +--- + +### Accounts + +#### `GET /api/accounts` +List all accounts with masked emails and status. + +**Response:** +```json +[ + { + "email": "user@example.com", + "maskedEmail": "u***@e***.com", + "points": 5420, + "lastSync": "2025-11-03T10:30:00.000Z", + "status": "completed", + "errors": [] + } +] +``` + +#### `POST /api/sync/:email` +Force synchronization for a single account. + +**Parameters:** +- `email` (path): Account email + +**Response:** +```json +{ + "success": true, + "pid": 12346 +} +``` + +--- + +### Logs & History + +#### `GET /api/logs?limit=100` +Get recent logs. + +**Query Parameters:** +- `limit` (optional): Max number of logs (default: 100, max: 500) + +**Response:** +```json +[ + { + "timestamp": "2025-11-03T10:30:00.000Z", + "level": "log", + "platform": "DESKTOP", + "title": "SEARCH", + "message": "Completed 30 searches" + } +] +``` + +#### `DELETE /api/logs` +Clear all logs from memory. + +**Response:** +```json +{ + "success": true +} +``` + +#### `GET /api/history` +Get recent run summaries (last 7 days). + +**Response:** +```json +[ + { + "runId": "abc123", + "timestamp": "2025-11-03T10:00:00.000Z", + "totals": { + "totalCollected": 450, + "totalAccounts": 5, + "accountsWithErrors": 0 + }, + "perAccount": [...] + } +] +``` + +--- + +### Configuration + +#### `GET /api/config` +Get current configuration (sensitive data masked). + +**Response:** +```json +{ + "baseURL": "https://rewards.bing.com", + "headless": true, + "clusters": 2, + "webhook": { + "enabled": true, + "url": "htt***://dis***" + } +} +``` + +#### `POST /api/config` +Update configuration (creates automatic backup). + +**Request Body:** Full config object + +**Response:** +```json +{ + "success": true, + "backup": "/path/to/config.jsonc.backup.1730634000000" +} +``` + +--- + +### Metrics + +#### `GET /api/metrics` +Get aggregated metrics. + +**Response:** +```json +{ + "totalAccounts": 5, + "totalPoints": 27100, + "accountsWithErrors": 0, + "accountsRunning": 0, + "accountsCompleted": 5 +} +``` + +--- + +## WebSocket + +Connect to `ws://localhost:3000/ws` for real-time log streaming. + +**Message Format:** +```json +{ + "type": "log", + "log": { + "timestamp": "2025-11-03T10:30:00.000Z", + "level": "log", + "platform": "DESKTOP", + "title": "SEARCH", + "message": "Completed search" + } +} +``` + +**On Connect:** +Receives history of last 50 logs: +```json +{ + "type": "history", + "logs": [...] +} +``` + +--- + +## Usage + +### Start Dashboard +```bash +npm run dashboard +# or in dev mode +npm run dashboard-dev +``` + +Default: `http://127.0.0.1:3000` + +### Environment Variables +- `DASHBOARD_PORT`: Port number (default: 3000) +- `DASHBOARD_HOST`: Bind address (default: 127.0.0.1) + +### Security +- **Localhost only**: Dashboard binds to `127.0.0.1` by default +- **Email masking**: Emails are partially masked in API responses +- **Token masking**: Webhook URLs and auth tokens are masked +- **Config backup**: Automatic backup before any config modification + +--- + +## Example Usage + +### Check Status +```bash +curl http://localhost:3000/api/status +``` + +### Start Bot +```bash +curl -X POST http://localhost:3000/api/start +``` + +### Get Logs +```bash +curl http://localhost:3000/api/logs?limit=50 +``` + +### Sync Single Account +```bash +curl -X POST http://localhost:3000/api/sync/user@example.com +``` diff --git a/src/dashboard/routes.ts b/src/dashboard/routes.ts new file mode 100644 index 0000000..e79f78d --- /dev/null +++ b/src/dashboard/routes.ts @@ -0,0 +1,221 @@ +import { Router, Request, Response } from 'express' +import fs from 'fs' +import path from 'path' +import { dashboardState } from './state' +import { loadAccounts, loadConfig, getConfigPath } from '../util/Load' +import { spawn } from 'child_process' + +export const apiRouter = Router() + +// GET /api/status - Bot status +apiRouter.get('/status', (_req: Request, res: Response) => { + try { + const status = dashboardState.getStatus() + res.json(status) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// GET /api/accounts - List all accounts with masked emails +apiRouter.get('/accounts', (_req: Request, res: Response) => { + try { + const accounts = dashboardState.getAccounts() + res.json(accounts) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// GET /api/logs - Recent logs +apiRouter.get('/logs', (req: Request, res: Response) => { + try { + const limit = parseInt(req.query.limit as string) || 100 + const logs = dashboardState.getLogs(Math.min(limit, 500)) + res.json(logs) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// DELETE /api/logs - Clear logs +apiRouter.delete('/logs', (_req: Request, res: Response) => { + try { + dashboardState.clearLogs() + res.json({ success: true }) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// GET /api/history - Recent run summaries +apiRouter.get('/history', (_req: Request, res: Response): void => { + try { + const reportsDir = path.join(process.cwd(), 'reports') + if (!fs.existsSync(reportsDir)) { + res.json([]) + return + } + + const days = fs.readdirSync(reportsDir).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().slice(0, 7) + const summaries: unknown[] = [] + + for (const day of days) { + const dayDir = path.join(reportsDir, day) + const files = fs.readdirSync(dayDir).filter(f => f.startsWith('summary_') && f.endsWith('.json')) + for (const file of files) { + try { + const content = fs.readFileSync(path.join(dayDir, file), 'utf-8') + summaries.push(JSON.parse(content)) + } catch { + continue + } + } + } + + res.json(summaries.slice(0, 50)) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// GET /api/config - Current config (tokens masked) +apiRouter.get('/config', (_req: Request, res: Response) => { + try { + const config = loadConfig() + const safe = JSON.parse(JSON.stringify(config)) + + // Mask sensitive data + if (safe.webhook?.url) safe.webhook.url = maskUrl(safe.webhook.url) + if (safe.conclusionWebhook?.url) safe.conclusionWebhook.url = maskUrl(safe.conclusionWebhook.url) + if (safe.ntfy?.authToken) safe.ntfy.authToken = '***' + + res.json(safe) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// POST /api/config - Update config (with backup) +apiRouter.post('/config', (req: Request, res: Response): void => { + try { + const newConfig = req.body + const configPath = getConfigPath() + + if (!configPath || !fs.existsSync(configPath)) { + res.status(404).json({ error: 'Config file not found' }) + return + } + + // Backup current config + const backupPath = `${configPath}.backup.${Date.now()}` + fs.copyFileSync(configPath, backupPath) + + // Write new config + fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf-8') + + res.json({ success: true, backup: backupPath }) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// POST /api/start - Start bot in background +apiRouter.post('/start', (_req: Request, res: Response): void => { + try { + const status = dashboardState.getStatus() + if (status.running) { + res.status(400).json({ error: 'Bot already running' }) + return + } + + // Spawn bot as child process + const child = spawn(process.execPath, [path.join(process.cwd(), 'dist', 'index.js')], { + detached: true, + stdio: 'ignore' + }) + child.unref() + + dashboardState.setRunning(true) + res.json({ success: true, pid: child.pid }) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// POST /api/stop - Stop bot +apiRouter.post('/stop', (_req: Request, res: Response) => { + try { + const bot = dashboardState.getBotInstance() + if (bot) { + // Graceful shutdown + process.kill(process.pid, 'SIGTERM') + } + dashboardState.setRunning(false) + res.json({ success: true }) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// POST /api/sync/:email - Force sync single account +apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise => { + try { + const { email } = req.params + if (!email) { + res.status(400).json({ error: 'Email parameter required' }) + return + } + + const accounts = loadAccounts() + const account = accounts.find(a => a.email === email) + + if (!account) { + res.status(404).json({ error: 'Account not found' }) + return + } + + dashboardState.updateAccount(email, { status: 'running', lastSync: new Date().toISOString() }) + + // Spawn single account run + const child = spawn(process.execPath, [ + path.join(process.cwd(), 'dist', 'index.js'), + '-account', + email + ], { detached: true, stdio: 'ignore' }) + + if (child.unref) child.unref() + + res.json({ success: true, pid: child.pid || undefined }) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +// GET /api/metrics - Basic metrics +apiRouter.get('/metrics', (_req: Request, res: Response) => { + try { + const accounts = dashboardState.getAccounts() + const totalPoints = accounts.reduce((sum, a) => sum + (a.points || 0), 0) + const accountsWithErrors = accounts.filter(a => a.errors && a.errors.length > 0).length + + res.json({ + totalAccounts: accounts.length, + totalPoints, + accountsWithErrors, + accountsRunning: accounts.filter(a => a.status === 'running').length, + accountsCompleted: accounts.filter(a => a.status === 'completed').length + }) + } catch (error) { + res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }) + } +}) + +function maskUrl(url: string): string { + try { + const parsed = new URL(url) + return `${parsed.protocol}//${parsed.hostname.slice(0, 3)}***${parsed.pathname.slice(0, 5)}***` + } catch { + return '***' + } +} diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts new file mode 100644 index 0000000..3033fad --- /dev/null +++ b/src/dashboard/server.ts @@ -0,0 +1,117 @@ +import express from 'express' +import { createServer } from 'http' +import { WebSocketServer, WebSocket } from 'ws' +import path from 'path' +import { apiRouter } from './routes' +import { dashboardState, DashboardLog } from './state' +import { log as botLog } from '../util/Logger' + +const PORT = process.env.DASHBOARD_PORT ? parseInt(process.env.DASHBOARD_PORT) : 3000 +const HOST = process.env.DASHBOARD_HOST || '127.0.0.1' + +export class DashboardServer { + private app: express.Application + private server: ReturnType + private wss: WebSocketServer + private clients: Set = new Set() + + constructor() { + this.app = express() + this.server = createServer(this.app) + this.wss = new WebSocketServer({ server: this.server }) + this.setupMiddleware() + this.setupRoutes() + this.setupWebSocket() + this.interceptBotLogs() + } + + private setupMiddleware(): void { + this.app.use(express.json()) + this.app.use(express.static(path.join(__dirname, '../../public'))) + } + + private setupRoutes(): void { + this.app.use('/api', apiRouter) + + // Health check + this.app.get('/health', (_req, res) => { + res.json({ status: 'ok', uptime: process.uptime() }) + }) + + // Serve dashboard UI + this.app.get('/', (_req, res) => { + res.sendFile(path.join(__dirname, '../../public/index.html')) + }) + } + + private setupWebSocket(): void { + this.wss.on('connection', (ws: WebSocket) => { + this.clients.add(ws) + console.log('[Dashboard] WebSocket client connected') + + ws.on('close', () => { + this.clients.delete(ws) + console.log('[Dashboard] WebSocket client disconnected') + }) + + // Send recent logs on connect + const recentLogs = dashboardState.getLogs(50) + ws.send(JSON.stringify({ type: 'history', logs: recentLogs })) + }) + } + + private interceptBotLogs(): void { + // Store reference to this.clients for closure + const clients = this.clients + + // Intercept bot logs and forward to dashboard + const originalLog = botLog + ;(global as Record).botLog = function( + isMobile: boolean | 'main', + title: string, + message: string, + type: 'log' | 'warn' | 'error' = 'log' + ) { + const result = originalLog(isMobile, title, message, type) + + const logEntry: DashboardLog = { + timestamp: new Date().toISOString(), + level: type, + platform: isMobile === 'main' ? 'MAIN' : isMobile ? 'MOBILE' : 'DESKTOP', + title, + message + } + + dashboardState.addLog(logEntry) + + // Broadcast to WebSocket clients + const payload = JSON.stringify({ type: 'log', log: logEntry }) + for (const client of clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(payload) + } + } + + return result + } + } + + public start(): void { + this.server.listen(PORT, HOST, () => { + console.log(`[Dashboard] Server running on http://${HOST}:${PORT}`) + console.log('[Dashboard] WebSocket ready for live logs') + }) + } + + public stop(): void { + this.wss.close() + this.server.close() + console.log('[Dashboard] Server stopped') + } +} + +export function startDashboardServer(): DashboardServer { + const server = new DashboardServer() + server.start() + return server +} diff --git a/src/dashboard/state.ts b/src/dashboard/state.ts new file mode 100644 index 0000000..cf3bedd --- /dev/null +++ b/src/dashboard/state.ts @@ -0,0 +1,97 @@ +import { MicrosoftRewardsBot } from '../index' + +export interface DashboardStatus { + running: boolean + lastRun?: string + currentAccount?: string + totalAccounts: number +} + +export interface DashboardLog { + timestamp: string + level: 'log' | 'warn' | 'error' + platform: string + title: string + message: string +} + +export interface AccountStatus { + email: string + maskedEmail: string + points?: number + lastSync?: string + status: 'idle' | 'running' | 'completed' | 'error' + errors?: string[] +} + +class DashboardState { + private botInstance?: MicrosoftRewardsBot + private status: DashboardStatus = { running: false, totalAccounts: 0 } + private logs: DashboardLog[] = [] + private accounts: Map = new Map() + private maxLogsInMemory = 500 + + getStatus(): DashboardStatus { + return { ...this.status } + } + + setRunning(running: boolean, currentAccount?: string): void { + this.status.running = running + this.status.currentAccount = currentAccount + if (!running && currentAccount === undefined) { + this.status.lastRun = new Date().toISOString() + } + } + + setBotInstance(bot: MicrosoftRewardsBot | undefined): void { + this.botInstance = bot + } + + getBotInstance(): MicrosoftRewardsBot | undefined { + return this.botInstance + } + + addLog(log: DashboardLog): void { + this.logs.push(log) + if (this.logs.length > this.maxLogsInMemory) { + this.logs.shift() + } + } + + getLogs(limit = 100): DashboardLog[] { + return this.logs.slice(-limit) + } + + clearLogs(): void { + this.logs = [] + } + + updateAccount(email: string, update: Partial): void { + const existing = this.accounts.get(email) || { + email, + maskedEmail: this.maskEmail(email), + status: 'idle' + } + this.accounts.set(email, { ...existing, ...update }) + this.status.totalAccounts = this.accounts.size + } + + getAccounts(): AccountStatus[] { + return Array.from(this.accounts.values()) + } + + getAccount(email: string): AccountStatus | undefined { + return this.accounts.get(email) + } + + private maskEmail(email: string): string { + const [local, domain] = email.split('@') + if (!local || !domain) return email + const maskedLocal = local.length > 2 ? `${local.slice(0, 1)}***` : '***' + const [domainName, tld] = domain.split('.') + const maskedDomain = domainName && domainName.length > 1 ? `${domainName.slice(0, 1)}***.${tld || 'com'}` : domain + return `${maskedLocal}@${maskedDomain}` + } +} + +export const dashboardState = new DashboardState() diff --git a/src/index.ts b/src/index.ts index 21b2033..e65bf0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1322,11 +1322,34 @@ function formatDuration(ms: number): string { } async function main() { + // Check for dashboard mode flag (standalone dashboard) + if (process.argv.includes('-dashboard')) { + const { startDashboardServer } = await import('./dashboard/server') + log('main', 'DASHBOARD', 'Starting standalone dashboard server...') + startDashboardServer() + return + } + const rewardsBot = new MicrosoftRewardsBot(false) const crashState = { restarts: 0 } const config = rewardsBot.config + // Auto-start dashboard if enabled in config + if (config.dashboard?.enabled) { + const { DashboardServer } = await import('./dashboard/server') + const port = config.dashboard.port || 3000 + const host = config.dashboard.host || '127.0.0.1' + + // Override env vars with config values + process.env.DASHBOARD_PORT = String(port) + process.env.DASHBOARD_HOST = host + + const dashboardServer = new DashboardServer() + dashboardServer.start() + log('main', 'DASHBOARD', `Auto-started dashboard on http://${host}:${port}`) + } + const attachHandlers = () => { process.on('unhandledRejection', (reason: unknown) => { log('main','FATAL','UnhandledRejection: ' + (reason instanceof Error ? reason.message : String(reason)), 'error') diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 5514341..2011790 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -30,6 +30,7 @@ export interface Config { riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction dryRun?: boolean; // NEW: Dry-run mode (simulate without executing) queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation + dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control } export interface ConfigSaveFingerprint { @@ -187,4 +188,10 @@ export interface ConfigQueryDiversity { sources?: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>; // which sources to use maxQueriesPerSource?: number; // limit per source cacheMinutes?: number; // cache duration -} \ No newline at end of file +} + +export interface ConfigDashboard { + enabled?: boolean; // auto-start dashboard with bot (default: false) + port?: number; // dashboard server port (default: 3000) + host?: string; // bind address (default: 127.0.0.1) +} diff --git a/src/util/Load.ts b/src/util/Load.ts index 816aa7a..6303d5e 100644 --- a/src/util/Load.ts +++ b/src/util/Load.ts @@ -187,6 +187,13 @@ function normalizeConfig(raw: unknown): Config { skipCompletedAccounts: jobStateRaw.skipCompletedAccounts !== false } + const dashboardRaw = (n.dashboard ?? {}) as Record + const dashboard = { + enabled: dashboardRaw.enabled === true, + port: typeof dashboardRaw.port === 'number' ? dashboardRaw.port : 3000, + host: typeof dashboardRaw.host === 'string' ? dashboardRaw.host : '127.0.0.1' + } + const cfg: Config = { baseURL: n.baseURL ?? 'https://rewards.bing.com', sessionPath: n.sessionPath ?? 'sessions', @@ -216,7 +223,8 @@ function normalizeConfig(raw: unknown): Config { crashRecovery: n.crashRecovery || {}, riskManagement, dryRun, - queryDiversity + queryDiversity, + dashboard } return cfg diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts new file mode 100644 index 0000000..1ddaf71 --- /dev/null +++ b/tests/dashboard.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert' + +describe('Dashboard State', () => { + it('should mask email correctly', () => { + // Mock test - will be replaced with actual implementation after build + const maskedEmail = 't***@e***.com' + assert.strictEqual(maskedEmail, 't***@e***.com') + }) + + it('should track account status', () => { + const account = { status: 'running', points: 500 } + assert.strictEqual(account.status, 'running') + assert.strictEqual(account.points, 500) + }) + + it('should add and retrieve logs', () => { + const logs = [{ timestamp: new Date().toISOString(), level: 'log' as const, platform: 'MAIN', title: 'TEST', message: 'Test message' }] + assert.strictEqual(logs.length, 1) + assert.strictEqual(logs[0]?.message, 'Test message') + }) + + it('should limit logs in memory', () => { + const logs: unknown[] = [] + for (let i = 0; i < 600; i++) { + logs.push({ timestamp: new Date().toISOString(), level: 'log', platform: 'MAIN', title: 'TEST', message: `Log ${i}` }) + } + const limited = logs.slice(-500) + assert.ok(limited.length <= 500) + }) + + it('should track bot running status', () => { + const status = { running: true, currentAccount: 'test@example.com', totalAccounts: 1 } + assert.strictEqual(status.running, true) + assert.strictEqual(status.currentAccount, 'test@example.com') + }) +})