From 52ebde5f757487ed40a71e901b2c10edc3c90d97 Mon Sep 17 00:00:00 2001 From: pinjinx Date: Sat, 25 Jul 2026 18:51:21 +0530 Subject: [PATCH 1/6] [Architecture] Switched to h264 and added debug --- package-lock.json | 1240 ++++++++++++++++++------- package.json | 6 +- src/contexts/ConnectionProvider.tsx | 9 +- src/contexts/DebugContext.tsx | 139 +++ src/hooks/useWebRtcStream.ts | 436 +++------ src/routeTree.gen.ts | 24 +- src/routes/__root.tsx | 5 +- src/routes/debug.tsx | 670 +++++++++++++ src/server/api/InputPeerConnection.ts | 160 ---- src/server/api/apiHandlers.ts | 754 --------------- src/server/api/apiState.ts | 107 --- src/server/api/getLocalIp.ts | 44 - src/server/drivers/linux/index.ts | 10 +- src/server/drivers/linux/structs.ts | 2 +- src/server/drivers/mac/structs.ts | 4 +- src/server/drivers/windows/structs.ts | 2 +- src/server/gstreamer/gstManager.ts | 210 +---- src/server/gstreamer/hostRunner.ts | 95 -- src/server/server.ts | 434 +++++---- src/server/webRTC.ts | 299 ++++++ src/utils/logger.ts | 2 - vite.config.ts | 12 +- 22 files changed, 2525 insertions(+), 2139 deletions(-) create mode 100644 src/contexts/DebugContext.tsx create mode 100644 src/routes/debug.tsx delete mode 100644 src/server/api/InputPeerConnection.ts delete mode 100644 src/server/api/apiHandlers.ts delete mode 100644 src/server/api/apiState.ts delete mode 100644 src/server/api/getLocalIp.ts delete mode 100644 src/server/gstreamer/hostRunner.ts create mode 100644 src/server/webRTC.ts diff --git a/package-lock.json b/package-lock.json index 98bf3ab3..ddb84fc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,12 @@ "lucide-react": "^0.561.0", "nitro": "^3.0.260429-beta", "nitro-nightly": "^3.0.1-20260501-164602-aee73f19", - "node-datachannel": "^0.32.3", "qrcode": "^1.5.4", "react": "^19.2.5", "react-dom": "^19.2.5", "react-icons": "^5.5.0", + "recharts": "^3.10.0", + "werift": "^0.23.0", "winston": "^3.19.0", "ws": "^8.18.0" }, @@ -360,7 +361,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1594,6 +1594,34 @@ } } }, + "node_modules/@fidm/asn1": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", + "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@fidm/x509": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", + "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", + "license": "MIT", + "dependencies": { + "@fidm/asn1": "^1.0.4", + "tweetnacl": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@fidm/x509/node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -1706,6 +1734,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -1761,6 +1795,12 @@ "node": ">=10" } }, + "node_modules/@minhducsun2002/leb128": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@minhducsun2002/leb128/-/leb128-1.0.0.tgz", + "integrity": "sha512-eFrYUPDVHeuwWHluTG1kwNQUEUcFjVKYwPkU8z9DR1JH3AW7JtJsG9cRVGmwz809kKtGfwGJj58juCZxEvnI/g==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -1779,6 +1819,33 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", @@ -1858,11 +1925,101 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", + "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", + "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", + "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", + "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-rsa": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", + "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", + "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pfx": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", + "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, "node_modules/@peculiar/asn1-schema": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", - "dev": true, "license": "MIT", "dependencies": { "@peculiar/utils": "^2.0.2", @@ -1870,6 +2027,30 @@ "tslib": "^2.8.1" } }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", + "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", + "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, "node_modules/@peculiar/json-schema": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", @@ -1887,7 +2068,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -1910,6 +2090,54 @@ "node": ">=14.18.0" } }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.0.tgz", @@ -2572,6 +2800,24 @@ "win32" ] }, + "node_modules/@shinyoshiaki/binary-data": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@shinyoshiaki/binary-data/-/binary-data-0.6.1.tgz", + "integrity": "sha512-7HDb/fQAop2bCmvDIzU5+69i+UJaFgIVp99h1VzK1mpg1JwSODOkjbqD7ilTYnqlnadF8C4XjpwpepxDsGY6+w==", + "license": "MIT", + "dependencies": { + "generate-function": "^2.3.1", + "is-plain-object": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@shinyoshiaki/jspack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@shinyoshiaki/jspack/-/jspack-0.0.6.tgz", + "integrity": "sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg==" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2679,7 +2925,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@szmarczak/http-timer": { @@ -3907,6 +4158,69 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -3989,7 +4303,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4021,6 +4335,12 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -4160,6 +4480,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/aes-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", + "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==", + "license": "MIT" + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -4463,7 +4789,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.6", @@ -4715,17 +5040,6 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -4789,9 +5103,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -4809,7 +5123,16 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" } }, "node_modules/buffer-from": { @@ -5154,7 +5477,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5500,54 +5822,191 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, - "node_modules/daisyui": { - "version": "5.6.18", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.6.18.tgz", - "integrity": "sha512-6y9rboRQl8fosnusy/5pg11yp+H0LtK3YdtfS4Vf2QVnaYOKasNdYm6hDxeiT6bwZPdl4WRPzVIlIApdo2ssBg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/saadeghi/daisyui?sponsor=1" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "license": "MIT", - "optional": true, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { - "assert-plus": "^1.0.0" + "internmap": "1 - 2" }, "engines": { - "node": ">=0.10" + "node": ">=12" } }, - "node_modules/data-urls": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", - "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", - "dev": true, - "license": "MIT", + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^15.1.0" + "d3-color": "1 - 3" }, "engines": { - "node": ">=20" + "node": ">=12" } }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { - "node": ">=20" + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/daisyui": { + "version": "5.6.18", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.6.18.tgz", + "integrity": "sha512-6y9rboRQl8fosnusy/5pg11yp+H0LtK3YdtfS4Vf2QVnaYOKasNdYm6hDxeiT6bwZPdl4WRPzVIlIApdo2ssBg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" } }, "node_modules/dayjs": { @@ -5664,10 +6123,17 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -5689,15 +6155,6 @@ "node": ">=6" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -5860,6 +6317,18 @@ "js-yaml": "^4.1.0" } }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -6294,6 +6763,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -6437,6 +6907,17 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -6535,6 +7016,12 @@ "through": "~2.3.1" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/eventsource": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.1.0.tgz", @@ -6556,15 +7043,6 @@ "node": ">=18.0.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -6609,7 +7087,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -6799,12 +7276,6 @@ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "license": "MIT" }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6953,6 +7424,15 @@ "node": ">=0.10.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7036,12 +7516,6 @@ "assert-plus": "^1.0.0" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -7538,6 +8012,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7556,11 +8040,26 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "node_modules/int64-buffer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz", + "integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "license": "MIT" }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -7571,6 +8070,18 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -7578,6 +8089,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7636,6 +8153,15 @@ "node": ">=18" } }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8171,7 +8697,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, "license": "MIT" }, "node_modules/logform": { @@ -8327,6 +8852,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8355,6 +8881,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8397,11 +8924,11 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "node_modules/mp4box": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.5.4.tgz", + "integrity": "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA==", + "license": "BSD-3-Clause" }, "node_modules/ms": { "version": "2.1.3", @@ -8409,6 +8936,19 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, "node_modules/nan": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", @@ -8435,12 +8975,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/nf3": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/nf3/-/nf3-0.3.22.tgz", @@ -8596,16 +9130,6 @@ } } }, - "node_modules/nitro-nightly/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "extraneous": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/nitro-nightly/node_modules/ocache": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ocache/-/ocache-0.2.0.tgz", @@ -8723,16 +9247,6 @@ } } }, - "node_modules/nitro/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "extraneous": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/nitro/node_modules/unstorage": { "version": "2.0.0-alpha.7", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz", @@ -8884,19 +9398,6 @@ "node": ">=10" } }, - "node_modules/node-datachannel": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.32.3.tgz", - "integrity": "sha512-Aok1ZhLsll472lRefgWYuWJ0070jh0ecHravTdRyZEmoESumebMEQV8Y+poBwSW2ZbEwAokAOGsK5Cu8pDDT2g==", - "hasInstallScript": true, - "license": "MPL 2.0", - "dependencies": { - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": ">=18.20.0" - } - }, "node_modules/node-gyp": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", @@ -9106,6 +9607,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -9124,7 +9626,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9427,57 +9928,6 @@ "node": "^12.20.0 || >=14" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/node-abi": { - "version": "3.94.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", - "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prettier": { "version": "3.9.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", @@ -9588,6 +10038,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -9608,7 +10059,6 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -9618,7 +10068,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", - "dev": true, "license": "MIT", "engines": { "node": ">=16.0.0" @@ -9763,21 +10212,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -9812,9 +10246,31 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -9865,6 +10321,57 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recharts": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.0.tgz", + "integrity": "sha512-wulMvfncpIlmu2uFtRU/mE5/+NiVtASXkw2KdwJTdHs3WsASX0WxZlX+rpKgyn5BDbIhkPtCpUKkB9XNK5KE0w==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -9970,6 +10477,12 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -10124,6 +10637,11 @@ "integrity": "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==", "license": "MIT" }, + "node_modules/rx.mini": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/rx.mini/-/rx.mini-1.4.0.tgz", + "integrity": "sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA==" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -10319,51 +10837,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -10577,15 +11050,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -10680,40 +11144,6 @@ "node": ">=18" } }, - "node_modules/tar-fs": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", - "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -10762,6 +11192,12 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -10782,6 +11218,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -10963,7 +11405,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, "license": "0BSD" }, "node_modules/tsx": { @@ -10985,11 +11426,30 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -11463,6 +11923,28 @@ "license": "MIT", "optional": true }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", @@ -12243,6 +12725,143 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/werift": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/werift/-/werift-0.23.0.tgz", + "integrity": "sha512-/WcIN5DHFG9Ri4anGOmIkp8gxBGFMWSIB/m4sfZ5CWlLfD3iMhiaAUuTBuc+KV3SY9NDmvmLtiN2uaM7k3lVzw==", + "license": "MIT", + "dependencies": { + "@fidm/x509": "^1.2.1", + "@minhducsun2002/leb128": "^1.0.0", + "@noble/curves": "^1.8.1", + "@peculiar/x509": "^1.12.3", + "@shinyoshiaki/binary-data": "^0.6.1", + "@shinyoshiaki/jspack": "^0.0.6", + "aes-js": "^3.1.2", + "buffer": "^6.0.3", + "debug": "4.4.0", + "fast-deep-equal": "^3.1.3", + "int64-buffer": "1.1.0", + "ip": "^2.0.1", + "mp4box": "^0.5.3", + "multicast-dns": "^7.2.5", + "tweetnacl": "^1.0.3", + "werift-common": "*", + "werift-dtls": "*", + "werift-ice": "*", + "werift-rtp": "*", + "werift-sctp": "*" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/werift-common": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/werift-common/-/werift-common-0.0.3.tgz", + "integrity": "sha512-ma3E4BqKTyZVLhrdfTVs2T1tg9seeUtKMRn5e64LwgrogWa62+3LAUoLBUSl1yPWhgSkXId7GmcHuWDen9IJeQ==", + "license": "MIT", + "dependencies": { + "@shinyoshiaki/jspack": "^0.0.6", + "debug": "^4.4.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/werift-dtls": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/werift-dtls/-/werift-dtls-0.5.7.tgz", + "integrity": "sha512-z2fjbP7fFUFmu/Ky4bCKXzdgPTtmSY1DYi0TUf3GG2zJT4jMQ3TQmGY8y7BSSNGetvL4h3pRZ5un0EcSOWpPog==", + "license": "MIT", + "dependencies": { + "@fidm/x509": "^1.2.1", + "@noble/curves": "^1.3.0", + "@peculiar/x509": "^1.9.2", + "@shinyoshiaki/binary-data": "^0.6.1", + "date-fns": "^2.29.3", + "lodash": "^4.17.21", + "rx.mini": "^1.2.2", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/werift-dtls/node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/werift-ice": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/werift-ice/-/werift-ice-0.2.2.tgz", + "integrity": "sha512-td52pHp+JmFnUn5jfDr/SSNO0dMCbknhuPdN1tFp9cfRj5jaktN63qnAdUuZC20QCC3ETWdsOthcm+RalHpFCQ==", + "license": "MIT", + "dependencies": { + "@shinyoshiaki/jspack": "^0.0.6", + "buffer-crc32": "^1.0.0", + "debug": "^4.3.4", + "int64-buffer": "^1.0.1", + "ip": "^2.0.1", + "lodash": "^4.17.21", + "multicast-dns": "^7.2.5", + "p-cancelable": "^2.1.1", + "rx.mini": "^1.2.2" + } + }, + "node_modules/werift-rtp": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/werift-rtp/-/werift-rtp-0.8.8.tgz", + "integrity": "sha512-GiYMSdvCyScQaw5bnEsraSoHUVZpjfokJAiLV4R1FsiB06t6XiebPYPpkqB9nYNNKiA8Z/cYWsym7wISq1sYSQ==", + "license": "MIT", + "dependencies": { + "@minhducsun2002/leb128": "^1.0.0", + "@shinyoshiaki/jspack": "^0.0.6", + "aes-js": "^3.1.2", + "buffer": "^6.0.3", + "mp4box": "^0.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/werift-sctp": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/werift-sctp/-/werift-sctp-0.0.11.tgz", + "integrity": "sha512-7109yuI5U7NTEHjqjn0A8VeynytkgVaxM6lRr1Ziv0D8bPcaB8A7U/P88M7WaCpWDoELHoXiRUjQycMWStIgjQ==", + "license": "MIT", + "dependencies": { + "@shinyoshiaki/jspack": "^0.0.6" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/werift/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/werift/node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -12410,6 +13029,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, "license": "ISC" }, "node_modules/ws": { @@ -12443,30 +13063,6 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 65fe5fe0..0eb809f2 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,12 @@ "lucide-react": "^0.561.0", "nitro": "^3.0.260429-beta", "nitro-nightly": "^3.0.1-20260501-164602-aee73f19", - "node-datachannel": "^0.32.3", "qrcode": "^1.5.4", "react": "^19.2.5", "react-dom": "^19.2.5", "react-icons": "^5.5.0", + "recharts": "^3.10.0", + "werift": "^0.23.0", "winston": "^3.19.0", "ws": "^8.18.0" }, @@ -103,7 +104,6 @@ "electron@40.10.5": true, "electron-winstaller@5.4.0": true, "esbuild@0.28.1": true, - "koffi@2.16.2": true, - "node-datachannel@0.32.3": true + "koffi@2.16.2": true } } diff --git a/src/contexts/ConnectionProvider.tsx b/src/contexts/ConnectionProvider.tsx index b208990a..e17fcc1a 100644 --- a/src/contexts/ConnectionProvider.tsx +++ b/src/contexts/ConnectionProvider.tsx @@ -94,7 +94,14 @@ export function ConnectionProvider({ timestamp?: number } if (parsed.type === "pong" && parsed.timestamp) { - setLatency(Date.now() - parsed.timestamp) + const ms = Date.now() - parsed.timestamp + setLatency(ms) + // Report measured RTT to the host debug dashboard (best-effort) + fetch("/api/debug/report-latency", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ latencyMs: ms }), + }).catch(() => {}) } } catch {} } diff --git a/src/contexts/DebugContext.tsx b/src/contexts/DebugContext.tsx new file mode 100644 index 00000000..19140c5d --- /dev/null +++ b/src/contexts/DebugContext.tsx @@ -0,0 +1,139 @@ +import type React from "react" +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react" + +type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG" + +export interface ClientLogEntry { + id: string + timestamp: string + level: LogLevel + message: string + source: string + details?: string +} + +interface DebugContextType { + clientLogs: ClientLogEntry[] + clearClientLogs: () => void +} +const DebugContext = createContext(null) + +export function useClientLogs(): DebugContextType { + const ctx = useContext(DebugContext) + if (!ctx) throw new Error("useClientLogs must be used inside DebugProvider") + return ctx +} + +function nowLabel(): string { + const d = new Date() + return `${d.getHours().toString().padStart(2, "0")}:${d + .getMinutes() + .toString() + .padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}` +} + +function serialize(args: unknown[]): { message: string; details?: string } { + const parts = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))) + const [message, ...rest] = parts + return { + message: message ?? "", + details: rest.length > 0 ? rest.join(" ") : undefined, + } +} + +const MAX_LOGS = 500 + +export function DebugProvider({ children }: { children: React.ReactNode }) { + const [clientLogs, setClientLogs] = useState([]) + const logIdRef = useRef(0) + + const addLog = useCallback( + (level: LogLevel, args: unknown[], source = "console") => { + const { message, details } = serialize(args) + const entry: ClientLogEntry = { + id: `cl-${++logIdRef.current}`, + timestamp: nowLabel(), + level, + message, + source, + details, + } + setClientLogs((prev) => { + const next = [...prev, entry] + return next.length > MAX_LOGS ? next.slice(-MAX_LOGS) : next + }) + }, + [], + ) + + const clearClientLogs = useCallback(() => setClientLogs([]), []) + useEffect(() => { + const origLog = console.log.bind(console) + const origWarn = console.warn.bind(console) + const origError = console.error.bind(console) + const origDebug = console.debug.bind(console) + + console.log = (...args: unknown[]) => { + origLog(...args) + addLog("INFO", args) + } + console.warn = (...args: unknown[]) => { + origWarn(...args) + addLog("WARN", args) + } + console.error = (...args: unknown[]) => { + origError(...args) + addLog("ERROR", args) + } + console.debug = (...args: unknown[]) => { + origDebug(...args) + addLog("DEBUG", args) + } + + return () => { + console.log = origLog + console.warn = origWarn + console.error = origError + console.debug = origDebug + } + }, [addLog]) + useEffect(() => { + const onError = (e: ErrorEvent) => { + addLog( + "ERROR", + [ + `Unhandled error: ${e.message}`, + e.filename ? `@ ${e.filename}:${e.lineno}` : "", + ], + "window.onerror", + ) + } + const onUnhandledRejection = (e: PromiseRejectionEvent) => { + addLog( + "ERROR", + [`Unhandled rejection: ${String(e.reason)}`], + "unhandledrejection", + ) + } + + window.addEventListener("error", onError) + window.addEventListener("unhandledrejection", onUnhandledRejection) + return () => { + window.removeEventListener("error", onError) + window.removeEventListener("unhandledrejection", onUnhandledRejection) + } + }, [addLog]) + + return ( + + {children} + + ) +} diff --git a/src/hooks/useWebRtcStream.ts b/src/hooks/useWebRtcStream.ts index bef91582..9888ca5b 100644 --- a/src/hooks/useWebRtcStream.ts +++ b/src/hooks/useWebRtcStream.ts @@ -11,18 +11,17 @@ const MAX_RETRIES = 5 export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { const [trackActive, setTrackActive] = useState(false) const [videoStream, setVideoStream] = useState(null) - const [activeSessionId, setActiveSessionId] = useState(null) const [error, setError] = useState(null) const [errorHandle, setErrorHandle] = useState(null) const [reconnectAttempt, setReconnectAttempt] = useState(0) const { registerDataChannel, send: sendInputEvent } = useConnection() - const videoPcRef = useRef(null) - const inputPcRef = useRef(null) - const sseSourceRef = useRef(null) + const pcRef = useRef(null) + const wsRef = useRef(null) const retryTimerRef = useRef | null>(null) const trackActiveRef = useRef(false) const retryCountRef = useRef(0) + const isRetryingRef = useRef(false) useEffect(() => { trackActiveRef.current = trackActive @@ -36,27 +35,9 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { } }, []) - const checkServerActive = useCallback(async (): Promise => { - try { - const headers: Record = {} - if (token) { - headers.Authorization = `Bearer ${token}` - } - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 2000) - const response = await fetch("/api/host/status", { - headers, - signal: controller.signal, - }) - clearTimeout(timeoutId) - return response.ok - } catch { - return false - } - }, [token]) - const triggerRetry = useCallback(() => { - if (retryTimerRef.current) return + if (retryTimerRef.current || isRetryingRef.current) return + isRetryingRef.current = true if (retryCountRef.current >= MAX_RETRIES) { console.warn( @@ -64,124 +45,96 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { ) setErrorHandle("Connection Failed") setError("Failed to establish stream session after multiple attempts") + isRetryingRef.current = false return } - if (sseSourceRef.current) { - sseSourceRef.current.close() - sseSourceRef.current = null - } - if (videoPcRef.current) { - videoPcRef.current.close() - videoPcRef.current = null + if (wsRef.current) { + try { + wsRef.current.close() + } catch {} + wsRef.current = null } - if (inputPcRef.current) { - inputPcRef.current.close() - inputPcRef.current = null + if (pcRef.current) { + try { + pcRef.current.close() + } catch {} + pcRef.current = null } setTrackActive(false) setVideoStream(null) - setActiveSessionId(null) const backoffDelay = Math.min(2000 * 2 ** retryCountRef.current, 30000) console.log( - `[WebRTC] Startup/transient network failure, retrying automatically (attempt ${retryCountRef.current + 1}/${MAX_RETRIES}) in ${backoffDelay / 1000} seconds...`, + `[WebRTC] Transient network failure, retrying automatically (attempt ${retryCountRef.current + 1}/${MAX_RETRIES}) in ${backoffDelay / 1000} seconds...`, ) retryTimerRef.current = setTimeout(() => { retryTimerRef.current = null + isRetryingRef.current = false retryCountRef.current += 1 setReconnectAttempt((prev) => prev + 1) }, backoffDelay) }, []) - const handleNetworkFailure = useCallback(async () => { - const isServerOnline = await checkServerActive() - if (isServerOnline) { - triggerRetry() - } else { - setErrorHandle("Server Error") - setError("Server has quit or is unreachable") - } - }, [checkServerActive, triggerRetry]) + const handleNetworkFailure = useCallback(() => { + triggerRetry() + }, [triggerRetry]) const reconnect = () => { if (retryTimerRef.current) { clearTimeout(retryTimerRef.current) retryTimerRef.current = null } - if (sseSourceRef.current) { - sseSourceRef.current.close() - sseSourceRef.current = null - } - if (videoPcRef.current) { - videoPcRef.current.close() - videoPcRef.current = null + isRetryingRef.current = false + if (wsRef.current) { + try { + wsRef.current.close() + } catch {} + wsRef.current = null } - if (inputPcRef.current) { - inputPcRef.current.close() - inputPcRef.current = null + if (pcRef.current) { + try { + pcRef.current.close() + } catch {} + pcRef.current = null } setErrorHandle(null) setError(null) setTrackActive(false) setVideoStream(null) - setActiveSessionId(null) retryCountRef.current = 0 setReconnectAttempt((prev) => prev + 1) } - // Session provisioning useEffect(() => { - const urlParams = new URLSearchParams(window.location.search) - const querySessionId = urlParams.get("session") - - if (querySessionId && reconnectAttempt === 0) { - setActiveSessionId(querySessionId) - return - } if (!token) return - fetch("/api/session", { - method: "POST", - headers: { Authorization: `Bearer ${token}` }, - }) - .then((r) => { - if (!r.ok) { - throw new Error(`Session creation failed with status ${r.status}`) - } - return r.json() - }) - .then((data) => { - if (data.sessionId) { - setActiveSessionId(data.sessionId) - // Update URL query param with new sessionId - const newUrl = new URL(window.location.href) - newUrl.searchParams.set("session", data.sessionId) - window.history.replaceState({}, "", newUrl.toString()) - } else { - throw new Error("Session ID missing from response") - } - }) - .catch((err) => { - console.error("[WebRTC] Session init failed:", err) - handleNetworkFailure() - }) - }, [token, reconnectAttempt, handleNetworkFailure]) + let isDisposed = false - useEffect(() => { - if (!activeSessionId) return + if (reconnectAttempt > 0) { + console.log( + `[WebRTC] Re-establishing WebRTC session (attempt ${reconnectAttempt})...`, + ) + } + + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" + const wsUrl = `${protocol}//${window.location.host}/ws` + const ws = new WebSocket(wsUrl) + wsRef.current = ws - // ── Video PC: receives GStreamer stream, no DataChannel ────────────── - const videoPc = new RTCPeerConnection({ - iceServers: [], + const pc = new RTCPeerConnection({ + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], bundlePolicy: "max-bundle", }) - videoPcRef.current = videoPc - videoPc.addTransceiver("video", { direction: "recvonly" }) + pcRef.current = pc - videoPc.ontrack = (event) => { + let dcUnordered: RTCDataChannel | null = null + let dcOrdered: RTCDataChannel | null = null + + pc.ontrack = (event) => { + if (isDisposed || isRetryingRef.current) return if (event.track.kind === "video" && event.streams[0]) { setVideoStream(event.streams[0]) setTrackActive(true) @@ -189,33 +142,31 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { } } - videoPc.onicecandidate = async (event) => { - if (!event.candidate) return - try { - const response = await fetch("/api/webrtc/ice", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ - sessionId: activeSessionId, - from: "viewer", - candidate: event.candidate.candidate, - sdpMid: event.candidate.sdpMid, - sdpMLineIndex: event.candidate.sdpMLineIndex, - }), - }) - if (!response.ok) { - throw new Error(`ICE candidate post failed: ${response.status}`) - } - } catch (err) { - console.error(err) - handleNetworkFailure() + pc.ondatachannel = (event) => { + if (isDisposed || isRetryingRef.current) return + const channel = event.channel + if (channel.label === "input-unordered") { + dcUnordered = channel + } else if (channel.label === "input-ordered") { + dcOrdered = channel + } + + if (dcUnordered && dcOrdered) { + registerDataChannel(dcUnordered, dcOrdered) + } + } + + pc.onicecandidate = (event) => { + if (isDisposed || isRetryingRef.current) return + if (event.candidate && ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ type: "ice", candidate: event.candidate.toJSON() }), + ) } } - const handleConnectionStateChange = (pc: RTCPeerConnection) => () => { + pc.onconnectionstatechange = () => { + if (isDisposed || isRetryingRef.current) return if ( pc.connectionState === "failed" || pc.connectionState === "disconnected" @@ -224,207 +175,66 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { } } - videoPc.onconnectionstatechange = handleConnectionStateChange(videoPc) - - // ── Input PC: DataChannel only, no media ───────────────────────────── - const inputPc = new RTCPeerConnection({ iceServers: [] }) - inputPcRef.current = inputPc - - const dcUnordered = inputPc.createDataChannel("input-unordered", { - ordered: false, - maxRetransmits: 0, - }) - const dcOrdered = inputPc.createDataChannel("input-ordered", { - ordered: true, - }) - registerDataChannel(dcUnordered, dcOrdered) + const iceQueue: RTCIceCandidateInit[] = [] - inputPc.onicecandidate = async (event) => { - if (!event.candidate) return + ws.onmessage = async (event) => { + if (isDisposed || isRetryingRef.current) return try { - const response = await fetch("/api/webrtc/ice", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ - sessionId: activeSessionId, - from: "viewer-input", - candidate: event.candidate.candidate, - sdpMid: event.candidate.sdpMid, - sdpMLineIndex: event.candidate.sdpMLineIndex, - }), - }) - if (!response.ok) { - throw new Error(`ICE candidate post failed: ${response.status}`) - } - } catch (err) { - console.error(err) - handleNetworkFailure() - } - } - - inputPc.onconnectionstatechange = handleConnectionStateChange(inputPc) - - // ── SSE bridge: handles both video offer and input-answer ──────────── - const sseParams = new URLSearchParams({ sessionId: activeSessionId }) - if (token) sseParams.set("token", token) - const sseUrl = `/api/webrtc/events?${sseParams.toString()}` - const sse = new EventSource(sseUrl) - sseSourceRef.current = sse - - const videoIceQueue: RTCIceCandidateInit[] = [] - const inputIceQueue: RTCIceCandidateInit[] = [] - - sse.onerror = (event) => { - sse.close() - console.error("[WebRTC] SSE error:", event) - handleNetworkFailure() - } - - sse.addEventListener("stream-error", (event) => { - try { - const data = JSON.parse(event.data) - setErrorHandle("Network Error") - setError(data.message || `Stream error: ${data.type}`) - } catch { - setErrorHandle("Network Error") - setError("Stream error occurred") - } - }) - - // Video: GStreamer offers, browser answers - sse.addEventListener("offer", async (event) => { - const data = JSON.parse(event.data) - if (!data.sdp) return - try { - await videoPc.setRemoteDescription( - new RTCSessionDescription({ type: "offer", sdp: data.sdp }), - ) - const answer = await videoPc.createAnswer() - await videoPc.setLocalDescription(answer) - const response = await fetch("/api/webrtc/answer", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ sessionId: activeSessionId, sdp: answer.sdp }), - }) - if (!response.ok) { - throw new Error(`Answer post failed: ${response.status}`) - } - // Process queued video candidates - while (videoIceQueue.length > 0) { - const cand = videoIceQueue.shift() - if (cand) { - await videoPc - .addIceCandidate(new RTCIceCandidate(cand)) + const msg = JSON.parse(event.data) + if (msg.type === "offer") { + await pc.setRemoteDescription(msg.sdp) + const answer = await pc.createAnswer() + await pc.setLocalDescription(answer) + ws.send(JSON.stringify({ type: "answer", sdp: answer })) + + while (iceQueue.length > 0) { + const cand = iceQueue.shift() + if (cand) { + await pc + .addIceCandidate(new RTCIceCandidate(cand)) + .catch(() => {}) + } + } + } else if (msg.type === "ice" && msg.candidate) { + if (pc.remoteDescription) { + await pc + .addIceCandidate(new RTCIceCandidate(msg.candidate)) .catch(() => {}) + } else { + iceQueue.push(msg.candidate) } + } else if (msg.type === "error") { + console.error("[WebRTC] Host error received:", msg) + setErrorHandle(msg.errorType || "Host Error") + setError(msg.message || "Host reported an error") } } catch (err) { - console.error("[WebRTC] Video offer handling failed:", err) - handleNetworkFailure() - } - }) - - // Video: GStreamer ICE candidates - sse.addEventListener("host-ice", async (event) => { - const data = JSON.parse(event.data) - if (!data.candidate) return - const candidateInit = { - candidate: data.candidate, - sdpMid: data.sdpMid, - sdpMLineIndex: data.sdpMLineIndex, - } - if (videoPc.remoteDescription) { - try { - await videoPc.addIceCandidate(new RTCIceCandidate(candidateInit)) - } catch {} - } else { - videoIceQueue.push(candidateInit) - } - }) - - // Input: server sends back its answer to our input offer - sse.addEventListener("input-answer", async (event) => { - const data = JSON.parse(event.data) - if (!data.sdp) return - try { - await inputPc.setRemoteDescription( - new RTCSessionDescription({ type: "answer", sdp: data.sdp }), - ) - // Process queued input candidates - while (inputIceQueue.length > 0) { - const cand = inputIceQueue.shift() - if (cand) { - await inputPc - .addIceCandidate(new RTCIceCandidate(cand)) - .catch((err) => { - console.error( - "[WebRTC] Failed to add queued input candidate:", - err, - ) - }) - } + if (!isDisposed && !isRetryingRef.current) { + console.error("[WebRTC] WebSocket message handling failed:", err) + handleNetworkFailure() } - } catch (err) { - console.error("[WebRTC] Input answer failed:", err) - handleNetworkFailure() } - }) + } - // Input: server sends back its ICE candidates - sse.addEventListener("input-ice", async (event) => { - const data = JSON.parse(event.data) - if (!data.candidate) return - const candidateInit = { - candidate: data.candidate, - sdpMid: data.sdpMid, - sdpMLineIndex: data.sdpMLineIndex, - } - if (inputPc.remoteDescription) { - try { - await inputPc.addIceCandidate(new RTCIceCandidate(candidateInit)) - } catch (err) { - console.error("[WebRTC] Failed to add input candidate:", err) - } - } else { - inputIceQueue.push(candidateInit) - } - }) - const sendInputOffer = async () => { - const offer = await inputPc.createOffer() - await inputPc.setLocalDescription(offer) - const response = await fetch("/api/webrtc/input-offer", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ sessionId: activeSessionId, sdp: offer.sdp }), - }) - if (!response.ok) { - throw new Error(`[WebRTC] Input offer failed: ${response.status}`) - } + ws.onerror = (error) => { + if (isDisposed || isRetryingRef.current) return + console.error("[WebRTC] WebSocket error:", error) + handleNetworkFailure() } - sendInputOffer().catch((err) => { - console.error(err) + ws.onclose = () => { + if (isDisposed || isRetryingRef.current) return + console.warn("[WebRTC] WebSocket closed") handleNetworkFailure() - }) + } - // Video stream watchdog to automatically recover from silent stream freezes let lastBytesReceived = 0 let lastBytesTime = Date.now() - const cancelled = false const statsInterval = setInterval(async () => { - if (cancelled || !trackActiveRef.current) return + if (isDisposed || isRetryingRef.current || !trackActiveRef.current) return try { - const stats = await videoPc.getStats() + const stats = await pc.getStats() let videoInbound = null for (const report of stats.values()) { if (report.type === "inbound-rtp" && report.kind === "video") { @@ -438,9 +248,9 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { if (bytes > lastBytesReceived) { lastBytesReceived = bytes lastBytesTime = now - } else if (now - lastBytesTime > 4000) { + } else if (now - lastBytesTime > 15000) { console.warn( - "[WebRTC] Video stream freeze detected (no bytes received for 4s), reconnecting...", + "[WebRTC] Video stream freeze detected, reconnecting...", ) handleNetworkFailure() } @@ -451,14 +261,18 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { }, 2000) return () => { + isDisposed = true clearInterval(statsInterval) - sse.close() - videoPc.close() - inputPc.close() + try { + ws.close() + } catch {} + try { + pc.close() + } catch {} setTrackActive(false) setVideoStream(null) } - }, [activeSessionId, token, registerDataChannel, handleNetworkFailure]) + }, [token, registerDataChannel, handleNetworkFailure, reconnectAttempt]) return { trackActive, diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 9ff60f87..cac4c23a 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as TrackpadRouteImport } from './routes/trackpad' import { Route as SettingsRouteImport } from './routes/settings' +import { Route as DebugRouteImport } from './routes/debug' import { Route as IndexRouteImport } from './routes/index' const TrackpadRoute = TrackpadRouteImport.update({ @@ -23,6 +24,11 @@ const SettingsRoute = SettingsRouteImport.update({ path: '/settings', getParentRoute: () => rootRouteImport, } as any) +const DebugRoute = DebugRouteImport.update({ + id: '/debug', + path: '/debug', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -31,30 +37,34 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/debug': typeof DebugRoute '/settings': typeof SettingsRoute '/trackpad': typeof TrackpadRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/debug': typeof DebugRoute '/settings': typeof SettingsRoute '/trackpad': typeof TrackpadRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/debug': typeof DebugRoute '/settings': typeof SettingsRoute '/trackpad': typeof TrackpadRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/settings' | '/trackpad' + fullPaths: '/' | '/debug' | '/settings' | '/trackpad' fileRoutesByTo: FileRoutesByTo - to: '/' | '/settings' | '/trackpad' - id: '__root__' | '/' | '/settings' | '/trackpad' + to: '/' | '/debug' | '/settings' | '/trackpad' + id: '__root__' | '/' | '/debug' | '/settings' | '/trackpad' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + DebugRoute: typeof DebugRoute SettingsRoute: typeof SettingsRoute TrackpadRoute: typeof TrackpadRoute } @@ -75,6 +85,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsRouteImport parentRoute: typeof rootRouteImport } + '/debug': { + id: '/debug' + path: '/debug' + fullPath: '/debug' + preLoaderRoute: typeof DebugRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -87,6 +104,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + DebugRoute: DebugRoute, SettingsRoute: SettingsRoute, TrackpadRoute: TrackpadRoute, } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 8334bab1..50a05861 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -12,6 +12,7 @@ import { ConnectionProvider, useConnection, } from "../contexts/ConnectionProvider" +import { DebugProvider } from "../contexts/DebugContext" export const Route = createRootRoute({ component: AppWithConnection, @@ -28,7 +29,9 @@ export const Route = createRootRoute({ function AppWithConnection() { return ( - + + + ) } diff --git a/src/routes/debug.tsx b/src/routes/debug.tsx new file mode 100644 index 00000000..4aa1ae60 --- /dev/null +++ b/src/routes/debug.tsx @@ -0,0 +1,670 @@ +import { createFileRoute } from "@tanstack/react-router" +import { Terminal, Trash2, Wifi, Users } from "lucide-react" +import { useEffect, useRef, useState, useCallback } from "react" +import { + ComposedChart, + Area, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts" +import { useClientLogs } from "../contexts/DebugContext" + +export const Route = createFileRoute("/debug")({ + component: DebugScreen, +}) +type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG" + +interface ServerLogEntry { + id: string + timestamp: string + level: LogLevel + message: string +} + +interface TelemetryPoint { + t: string + recvKBps: number + sentKBps: number + latencyMs: number +} + +interface SessionInfo { + id: string + state: string + createdAt: number + sseViewerCount: number + hasInputConnection: boolean + bytesRecv: number + bytesSent: number +} + +interface ServerState { + hostStatus: "stopped" | "starting" | "running" | "error" + sessionCount: number + sessions: SessionInfo[] + inputConnectionCount: number + latencyMs: number | null +} +function nowLabel(): string { + const d = new Date() + return `${d.getHours().toString().padStart(2, "0")}:${d + .getMinutes() + .toString() + .padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}` +} + +function fmtAge(createdAt: number): string { + const s = Math.floor((Date.now() - createdAt) / 1000) + if (s < 60) return `${s}s ago` + if (s < 3600) return `${Math.floor(s / 60)}m ago` + return `${Math.floor(s / 3600)}h ago` +} + +function DebugScreen() { + const { clientLogs, clearClientLogs } = useClientLogs() + const [serverState, setServerState] = useState({ + hostStatus: "stopped", + sessionCount: 0, + sessions: [], + inputConnectionCount: 0, + latencyMs: null, + }) + const [selectedSessionId, setSelectedSessionId] = useState( + null, + ) + + const fetchServerState = useCallback(async () => { + try { + const res = await fetch("/api/debug/sessions") + if (!res.ok) return + const data = (await res.json()) as ServerState + setServerState(data) + } catch {} + }, []) + + useEffect(() => { + fetchServerState() + const id = setInterval(fetchServerState, 1000) + return () => clearInterval(id) + }, [fetchServerState]) + const [serverLogs, setServerLogs] = useState([]) + const [_sseConnected, setSseConnected] = useState(false) + const serverLogIdRef = useRef(0) + + useEffect(() => { + const sse = new EventSource("/api/debug/logs") + sse.onopen = () => setSseConnected(true) + sse.onmessage = (event) => { + try { + const raw = JSON.parse(event.data) as { + timestamp?: string + level?: string + message?: string + } + const entry: ServerLogEntry = { + id: `sl-${++serverLogIdRef.current}`, + timestamp: raw.timestamp ?? "", + level: ((raw.level ?? "INFO") as string).toUpperCase() as LogLevel, + message: raw.message ?? "", + } + setServerLogs((prev) => { + const next = [...prev, entry] + return next.length > 500 ? next.slice(-500) : next + }) + } catch {} + } + sse.onerror = () => setSseConnected(false) + return () => sse.close() + }, []) + const [telemetryHistory, setTelemetryHistory] = useState( + () => [{ t: nowLabel(), recvKBps: 0, sentKBps: 0, latencyMs: 0 }], + ) + const prevBytesRef = useRef({ recv: 0, sent: 0 }) + // biome-ignore lint/correctness/useExhaustiveDependencies: selectedSessionId is a change-trigger, not read inside — intentional reset-on-change pattern + useEffect(() => { + prevBytesRef.current = { recv: 0, sent: 0 } + setTelemetryHistory([ + { t: nowLabel(), recvKBps: 0, sentKBps: 0, latencyMs: 0 }, + ]) + }, [selectedSessionId]) + useEffect(() => { + const id = setInterval(() => { + const src = selectedSessionId + ? serverState.sessions.filter((s) => s.id === selectedSessionId) + : serverState.sessions + const totalRecv = src.reduce((sum, s) => sum + (s.bytesRecv ?? 0), 0) + const totalSent = src.reduce((sum, s) => sum + (s.bytesSent ?? 0), 0) + const deltaRecv = + Math.max(0, totalRecv - prevBytesRef.current.recv) / 1024 + const deltaSent = + Math.max(0, totalSent - prevBytesRef.current.sent) / 1024 + prevBytesRef.current = { recv: totalRecv, sent: totalSent } + + setTelemetryHistory((prev) => { + const next = [ + ...prev, + { + t: nowLabel(), + recvKBps: Math.round(deltaRecv * 10) / 10, + sentKBps: Math.round(deltaSent * 10) / 10, + latencyMs: serverState.latencyMs ?? 0, + }, + ] + return next.length > 60 ? next.slice(-60) : next + }) + }, 1000) + return () => clearInterval(id) + }, [selectedSessionId, serverState.sessions, serverState.latencyMs]) + + const [consoleTab, setConsoleTab] = useState<"server" | "client">("server") + const [levelFilter, setLevelFilter] = useState("ALL") + const [searchQuery, setSearchQuery] = useState("") + const [copiedId, setCopiedId] = useState(null) + const logEndRef = useRef(null) + const copyToClipboard = (id: string, text: string) => { + const done = () => { + setCopiedId(id) + setTimeout(() => setCopiedId(null), 1500) + } + if (navigator.clipboard) { + navigator.clipboard + .writeText(text) + .then(done) + .catch(() => {}) + } else { + const el = document.createElement("textarea") + el.value = text + el.style.position = "fixed" + el.style.opacity = "0" + document.body.appendChild(el) + el.focus() + el.select() + try { + document.execCommand("copy") + done() + } catch { + /* silent */ + } + document.body.removeChild(el) + } + } + + const activeLogs = consoleTab === "server" ? serverLogs : clientLogs + + const filteredLogs = activeLogs.filter((log) => { + const matchLevel = levelFilter === "ALL" || log.level === levelFilter + const matchSearch = + searchQuery === "" || + log.message.toLowerCase().includes(searchQuery.toLowerCase()) + return matchLevel && matchSearch + }) + + const levelClass = (level: string) => { + switch (level) { + case "WARN": + return "bg-amber-950 text-amber-400 border border-amber-800/60" + case "ERROR": + return "bg-red-950 text-red-400 border border-red-800/60" + default: + return "bg-blue-950 text-blue-400 border border-blue-800/60" + } + } + + const hostStatusCls = { + running: "text-success", + starting: "text-warning", + stopped: "text-base-content/50", + error: "text-error", + }[serverState.hostStatus] + const latest = telemetryHistory[telemetryHistory.length - 1] + + const maxRecv = Math.max(...telemetryHistory.map((p) => p.recvKBps), 0.01) + const maxLat = Math.max(...telemetryHistory.map((p) => p.latencyMs), 1) + const maxSent = Math.max(...telemetryHistory.map((p) => p.sentKBps), 0.01) + + const chartData = telemetryHistory.map((p) => ({ + t: p.t, + recvNorm: Math.round((p.recvKBps / maxRecv) * 100), + latNorm: Math.round((p.latencyMs / maxLat) * 100), + sentNorm: Math.round((p.sentKBps / maxSent) * 100), + recvKBps: p.recvKBps, + latencyMs: p.latencyMs, + sentKBps: p.sentKBps, + })) + + const latColor = + (latest?.latencyMs ?? 0) < 50 + ? "text-emerald-400" + : (latest?.latencyMs ?? 0) < 120 + ? "text-amber-400" + : "text-red-400" + + return ( +
+
+ {/* Header */} + {/* Server status summary row */} +
+ {[ + { + label: "GStreamer", + value: serverState.hostStatus, + cls: hostStatusCls, + }, + { + label: "Active Sessions", + value: String(serverState.sessionCount), + cls: serverState.sessionCount > 0 ? "text-success" : "", + }, + { + label: "Viewers (SSE)", + value: String( + serverState.sessions.reduce((a, s) => a + s.sseViewerCount, 0), + ), + cls: serverState.sessions.some((s) => s.sseViewerCount > 0) + ? "text-success" + : "", + }, + { + label: "Input Channels", + value: String(serverState.inputConnectionCount), + cls: serverState.inputConnectionCount > 0 ? "text-success" : "", + }, + ].map(({ label, value, cls }) => ( +
+
+ {label} +
+
{value}
+
+ ))} +
+ + {/* Telemetry + Connection panels */} +
+ {/* Telemetry chart */} +
+ {/* Header row */} +
+ +

Network

+ + {selectedSessionId + ? selectedSessionId + ? selectedSessionId + : "Invalid Id" + : "all sessions"} + +
+ {/* Live-value strip */} +
+ {/* Latency */} +
+ + + Latency + + + {latest?.latencyMs ?? 0} ms + + + peak {maxLat} ms + +
+ {/* Video recv */} +
+ + + Video Recv + + + {latest?.recvKBps ?? 0} KB/s + + + peak {maxRecv.toFixed(1)} KB/s + +
+ {/* Input sent */} +
+ + + Input Sent + + + {latest?.sentKBps ?? 0} KB/s + + + peak {maxSent.toFixed(1)} KB/s + +
+
+ + {/* Normalised chart */} +
+ + + + + + + + + + + + + + + + {/* Single normalised 0-100% Y-axis */} + + { + if (!active || !payload?.length) return null + const d = payload[0].payload as (typeof chartData)[0] + return ( +
+
{d.t}
+
+ Latency: {d.latencyMs} ms +
+
+ Video Recv: {d.recvKBps} KB/s +
+
+ Input Sent: {d.sentKBps} KB/s +
+
+ ) + }} + /> + {/* Latency — orange line, most important */} + + {/* Video recv — cyan area */} + + {/* Input sent — violet area */} + +
+
+
+
+ + {/* Live sessions sidebar */} +
+
+ +

Client Sessions

+
+ +
+ {serverState.sessions.length === 0 ? ( +
+ No active sessions +
+ ) : ( + serverState.sessions.map((session) => { + const isSelected = selectedSessionId === session.id + return ( + + ) + }) + )} +
+
+
+ + {/* Log Console */} +
+ {/* Toolbar */} +
+
+ + Log Console +
+ + +
+
+ +
+
+ setSearchQuery(e.target.value)} + className="input input-xs input-bordered pl-8 w-36 sm:w-44 font-mono text-[11px] rounded" + /> +
+ +
+ {["ALL", "INFO", "WARN", "ERROR"].map((lvl) => ( + + ))} +
+ +
+
+ + {/* Log viewport */} +
+ {filteredLogs.length === 0 ? ( +
+ No log records for the current filter. +
+ ) : ( + filteredLogs.map((log) => ( + + )) + )} +
+
+
+
+
+ ) +} diff --git a/src/server/api/InputPeerConnection.ts b/src/server/api/InputPeerConnection.ts deleted file mode 100644 index 0d50823a..00000000 --- a/src/server/api/InputPeerConnection.ts +++ /dev/null @@ -1,160 +0,0 @@ -import nodeDataChannel, { type PeerConnection } from "node-datachannel" -import { InputHandler } from "../InputHandler" -import logger from "../../utils/logger" -import type { InputMessage, InputConfig } from "../types" - -// Optimize SCTP settings globally for ultra-low latency. -// Disabling delayed SACK (setting it to 0ms) prevents acknowledgment delays. -// This keeps the browser client's transmission window fully open and avoids input lag bursts. -nodeDataChannel.setSctpSettings({ - delayedSackTime: 0, -}) - -const VALID_INPUT_TYPES = new Set([ - "move", - "click", - "scroll", - "key", - "text", - "zoom", - "combo", - "copy", - "paste", - "touch", -]) - -export class InputPeerConnection { - private pc: PeerConnection - private inputHandler: InputHandler - private sessionId: string - private answerResolve: ((sdp: string) => void) | null = null - - constructor( - sessionId: string, - onLocalCandidate?: (candidate: string, mid: string) => void, - initialConfig?: Partial, - onClosed?: () => void, - onError?: (errorType: string, message: string) => void, - ) { - this.sessionId = sessionId - this.inputHandler = new InputHandler( - initialConfig, - 8, - onError ? (errorType, message) => onError(errorType, message) : undefined, - ) - - this.pc = new nodeDataChannel.PeerConnection(`input-${sessionId}`, { - iceServers: [], - enableIceTcp: false, - }) - - if (onLocalCandidate) { - this.pc.onLocalCandidate((candidate, mid) => { - onLocalCandidate(candidate, mid) - }) - } - - this.pc.onDataChannel((dc) => { - logger.info( - `[Input] DataChannel "${dc.getLabel()}" open for session: ${sessionId}`, - ) - - dc.onMessage((msg) => { - try { - const raw = typeof msg === "string" ? msg : msg.toString() - const parsed = JSON.parse(raw) as { - type?: string - timestamp?: number - } - - if (parsed.type === "ping") { - dc.sendMessage( - JSON.stringify({ type: "pong", timestamp: parsed.timestamp }), - ) - return - } - - if (!parsed.type || !VALID_INPUT_TYPES.has(parsed.type)) { - logger.warn(`[Input] Unknown type: ${parsed.type}`) - return - } - - this.inputHandler - .handleMessage(parsed as InputMessage) - .catch((err) => - logger.error(`[Input] Handler error: ${String(err)}`), - ) - } catch (err) { - logger.error(`[Input] Parse error: ${String(err)}`) - } - }) - - dc.onClosed(() => { - logger.info(`[Input] DataChannel closed: ${sessionId}`) - }) - }) - - this.pc.onLocalDescription((sdp, type) => { - if (type === "answer" && this.answerResolve) { - this.answerResolve(sdp) - this.answerResolve = null - } - }) - - let closedCalled = false - this.pc.onStateChange((state) => { - logger.info(`[Input] PC state [${sessionId}]: ${state}`) - if ( - state === "closed" || - state === "disconnected" || - state === "failed" - ) { - if (onClosed && !closedCalled) { - closedCalled = true - onClosed() - } - } - }) - } - - processOffer(offerSdp: string): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error("Input PC answer generation timed out")) - }, 10_000) - - this.answerResolve = (sdp) => { - clearTimeout(timeout) - resolve(sdp) - } - - try { - logger.info(offerSdp.split("\n").slice(0, 10).join("\n")) - this.pc.setRemoteDescription(offerSdp, "offer") - } catch (err) { - clearTimeout(timeout) - reject(err) - } - }) - } - - addRemoteCandidate(candidate: string, mid: string): void { - try { - this.pc.addRemoteCandidate(candidate, mid) - } catch (err) { - logger.warn(`[Input] Failed to add candidate: ${String(err)}`) - } - } - - updateConfig(config: Partial): void { - this.inputHandler.updateConfig(config) - } - - close(): void { - try { - this.pc.close() - } catch {} - this.inputHandler.destroy() - logger.info(`[Input] Connection closed: ${this.sessionId}`) - } -} diff --git a/src/server/api/apiHandlers.ts b/src/server/api/apiHandlers.ts deleted file mode 100644 index f71015f4..00000000 --- a/src/server/api/apiHandlers.ts +++ /dev/null @@ -1,754 +0,0 @@ -/** - * Request handlers and authorization middleware for the HTTP signaling layer. - */ - -import type { IncomingMessage, ServerResponse } from "node:http" -import fs from "node:fs" -import crypto from "node:crypto" -import logger from "../../utils/logger" -import { - getActiveToken, - isKnownToken, - touchToken, - generateToken, - storeToken, -} from "../tokenStore" -import { InputPeerConnection } from "./InputPeerConnection" -import { getLocalIp } from "./getLocalIp" -import type { InputConfig } from "../types" -import { - sessions, - sseClients, - inputConnections, - hostStatus, - runnerInstance, - pendingConfigUpdates, - setHostStatus, - setRunnerInstance, - setPendingConfigUpdates, - pushEvent, - ensureHostRunnerActive, - reinStorage, -} from "./apiState" - -// --- Auth & Request Utilities --- - -export function isLocalRequest(req: IncomingMessage): boolean { - const addr = req.socket.remoteAddress - if (!addr) return false - return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" -} - -export function requireLocalhost( - req: IncomingMessage, - res: ServerResponse, -): boolean { - if (isLocalRequest(req)) return true - reinStorage.run(true, () => { - res.writeHead(403, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ error: "Localhost only" })) - }) - return false -} - -export function requireAuth( - req: IncomingMessage, - res: ServerResponse, -): boolean { - if (isLocalRequest(req)) return true - - const authHeader = req.headers.authorization ?? "" - let token = authHeader.startsWith("Bearer ") - ? authHeader.slice(7).trim() - : null - - if (!token) { - const url = new URL(req.url ?? "", `http://${req.headers.host}`) - token = url.searchParams.get("token") - } - - if (!token || !isKnownToken(token)) { - reinStorage.run(true, () => { - res.writeHead(401, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ error: "Unauthorized" })) - }) - return false - } - - touchToken(token) - return true -} - -export function json(res: ServerResponse, status: number, body: unknown): void { - const payload = JSON.stringify(body) - reinStorage.run(true, () => { - res.writeHead(status, { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(payload), - }) - res.end(payload) - }) -} - -export async function readBody(req: IncomingMessage): Promise { - return new Promise((resolve, reject) => { - let raw = "" - req.setEncoding("utf-8") - req.on("data", (chunk: string) => { - raw += chunk - if (raw.length > 64 * 1024) { - req.destroy() - reject(new Error("Request body too large")) - } - }) - req.on("end", () => resolve(raw)) - req.on("error", reject) - }) -} - -// --- Route Handlers --- - -export async function handleCreateSession( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - - try { - const session = { - id: crypto.randomUUID(), - createdAt: Date.now(), - state: "pending" as const, - offer: null, - answer: null, - viewerIce: [], - hostIce: [], - } - sessions.set(session.id, session) - logger.info(`Session created: ${session.id}`) - - const hostHeader = req.headers.host || "127.0.0.1:8000" - ensureHostRunnerActive(`http://${hostHeader}`) - - const protocol = req.headers["x-forwarded-proto"] ?? "http" - const host = req.headers.host ?? "localhost" - const viewerUrl = `${protocol}://${host}/trackpad?session=${session.id}` - json(res, 201, { sessionId: session.id, viewerUrl }) - } catch (_err) { - json(res, 500, { - error: "Failed to initialize and bind session host runtime cleanly", - }) - } -} - -export function handleGetSession( - req: IncomingMessage, - res: ServerResponse, - sessionId: string, -): void { - if (!requireAuth(req, res)) return - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Session not found" }) - return - } - - json(res, 200, { - id: session.id, - state: session.state, - createdAt: session.createdAt, - hasOffer: session.offer !== null, - hasAnswer: session.answer !== null, - viewerIceCandidates: session.viewerIce.length, - hostIceCandidates: session.hostIce.length, - }) -} - -export function handleDeleteSession( - req: IncomingMessage, - res: ServerResponse, - sessionId: string, -): void { - if (!requireAuth(req, res)) return - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Session not found" }) - return - } - - session.state = "closed" - sessions.delete(sessionId) - pushEvent(sessionId, "session-closed", { sessionId }) - sseClients.delete(sessionId) - inputConnections.get(sessionId)?.close() - inputConnections.delete(sessionId) - logger.info(`Session deleted: ${sessionId}`) - json(res, 200, { ok: true }) -} - -export async function handleOffer( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - - const bodyText = await readBody(req) - const { sessionId, sdp } = JSON.parse(bodyText || "{}") - - if (!sessionId || typeof sdp !== "string") { - json(res, 400, { error: "sessionId and sdp are required" }) - return - } - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Session not found" }) - return - } - if (session.state !== "pending") { - json(res, 409, { - error: `Session is in state '${session.state}', expected 'pending'`, - }) - return - } - - session.offer = sdp - session.state = "offering" - logger.info(`SDP offer received for session ${sessionId}`) - - pushEvent(sessionId, "offer", { sessionId, sdp }) - - const hostHeader = req.headers.host || "127.0.0.1:8000" - const runner = ensureHostRunnerActive(`http://${hostHeader}`) - runner.handleIncomingClientOffer(sessionId, sdp) - - json(res, 200, { ok: true }) -} - -export async function handleAnswer( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - - const bodyText = await readBody(req) - const { sessionId, sdp } = JSON.parse(bodyText || "{}") - - if (!sessionId || typeof sdp !== "string") { - json(res, 400, { error: "sessionId and sdp are required" }) - return - } - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Session not found" }) - return - } - - session.answer = sdp - session.state = "answered" - logger.info(`SDP answer stored for session ${sessionId}`) - - pushEvent(sessionId, "answer", { sessionId, sdp }) - json(res, 200, { ok: true }) -} - -export async function handleIce( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - - const bodyText = await readBody(req) - const body = JSON.parse(bodyText || "{}") - const { sessionId, candidate, sdpMid, sdpMLineIndex, from } = body - - if (!sessionId || typeof candidate !== "string" || !from) { - json(res, 400, { error: "sessionId, candidate, and from are required" }) - return - } - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Session not found" }) - return - } - - const ice = { - candidate, - sdpMid: sdpMid ?? null, - sdpMLineIndex: sdpMLineIndex ?? null, - } - if (from === "viewer-input") { - const inputPc = inputConnections.get(sessionId) - if (inputPc && ice.sdpMid !== null) { - inputPc.addRemoteCandidate(ice.candidate, ice.sdpMid) - } - json(res, 200, { ok: true }) - return - } else if (from === "viewer") { - session.viewerIce.push(ice) - pushEvent(sessionId, "viewer-ice", { sessionId, ...ice }) - } else { - session.hostIce.push(ice) - pushEvent(sessionId, "host-ice", { sessionId, ...ice }) - } - - json(res, 200, { ok: true }) -} - -export function handleEvents(req: IncomingMessage, res: ServerResponse): void { - if (!requireAuth(req, res)) return - - const url = new URL(req.url ?? "", `http://${req.headers.host}`) - const sessionId = url.searchParams.get("sessionId") - - if (!sessionId) { - json(res, 400, { error: "sessionId query param required" }) - return - } - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Session not found" }) - return - } - - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - "Access-Control-Allow-Origin": "*", - }) - res.flushHeaders() - - if (!sseClients.has(sessionId)) { - sseClients.set(sessionId, new Set()) - } - sseClients.get(sessionId)?.add(res) - - if (!session.offer && runnerInstance) { - logger.info( - `SSE viewer connected — starting GStreamer pipeline for session: ${sessionId}`, - ) - runnerInstance.handleIncomingClientOffer(sessionId, "") - } - - if (session.offer) { - res.write( - `event: offer\ndata: ${JSON.stringify({ sessionId: session.id, sdp: session.offer })}\n\n`, - ) - } - if (session.answer) { - res.write( - `event: answer\ndata: ${JSON.stringify({ sessionId: session.id, sdp: session.answer })}\n\n`, - ) - } - for (const ice of session.viewerIce) { - res.write( - `event: viewer-ice\ndata: ${JSON.stringify({ sessionId: session.id, ...ice })}\n\n`, - ) - } - for (const ice of session.hostIce) { - res.write( - `event: host-ice\ndata: ${JSON.stringify({ sessionId: session.id, ...ice })}\n\n`, - ) - } - - const keepAlive = setInterval(() => { - try { - reinStorage.run(true, () => { - res.write(": keepalive\n\n") - }) - } catch { - clearInterval(keepAlive) - } - }, 15_000) - - req.on("close", () => { - clearInterval(keepAlive) - sseClients.get(sessionId)?.delete(res) - logger.info(`SSE client disconnected from session ${sessionId}`) - }) - - logger.info(`SSE client connected to session ${sessionId}`) -} - -export async function handleGstSignalingGateway( - req: IncomingMessage, - res: ServerResponse, -): Promise { - const url = new URL(req.url ?? "", `http://${req.headers.host}`) - const sessionId = url.searchParams.get("sessionId") - - if (!sessionId) { - json(res, 400, { error: "sessionId target query missing" }) - return - } - - const bodyText = await readBody(req) - try { - const msg = JSON.parse(bodyText) - - if (msg.type === "answer") { - logger.info(`GStreamer SDP answer received for session: ${sessionId}`) - pushEvent(sessionId, "answer", { sessionId, sdp: msg.sdp }) - } else if (msg.candidate) { - pushEvent(sessionId, "host-ice", { - sessionId, - candidate: msg.candidate, - sdpMid: msg.sdpMid, - sdpMLineIndex: msg.sdpMLineIndex, - }) - } - json(res, 200, { status: "ok" }) - } catch (_err) { - json(res, 400, { error: "Inbound parsing crash" }) - } -} - -export async function handleHostStart( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireLocalhost(req, res)) return - if (hostStatus === "running" || hostStatus === "starting") { - json(res, 409, { error: `Host already ${hostStatus}` }) - return - } - try { - const hostHeader = req.headers.host || "127.0.0.1:8000" - ensureHostRunnerActive(`http://${hostHeader}`) - json(res, 200, { status: hostStatus }) - } catch (err) { - json(res, 500, { error: String(err) }) - } -} - -export async function handleHostStop( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireLocalhost(req, res)) return - if (hostStatus === "stopped" || !runnerInstance) { - json(res, 409, { error: "Host is not running" }) - return - } - - runnerInstance.shutdown() - setRunnerInstance(null) - setHostStatus("stopped") - json(res, 200, { status: hostStatus }) -} - -export function handleHostStatus( - req: IncomingMessage, - res: ServerResponse, -): void { - if (!requireAuth(req, res)) return - json(res, 200, { status: hostStatus }) -} - -export async function handleGenerateToken( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireLocalhost(req, res)) return - - let token = getActiveToken() - if (!token) { - token = generateToken() - storeToken(token) - } - json(res, 200, { token }) -} - -export function handleGetToken( - req: IncomingMessage, - res: ServerResponse, -): void { - if (!requireLocalhost(req, res)) return - const token = getActiveToken() - if (!token) { - json(res, 404, { error: "No active token" }) - return - } - json(res, 200, { token }) -} - -export async function handleInputOffer( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - - const bodyText = await readBody(req) - const { sessionId, sdp } = JSON.parse(bodyText || "{}") as { - sessionId?: string - sdp?: string - } - - if (!sessionId || typeof sdp !== "string") { - json(res, 400, { error: "sessionId and sdp are required" }) - return - } - if (!sessions.has(sessionId)) { - json(res, 404, { error: "Session not found" }) - return - } - if (inputConnections.has(sessionId)) { - json(res, 200, { ok: true }) - return - } - - let initialSensitivity = 1.0 - let initialInvertScroll = false - try { - const configPath = "./src/server-config.json" - if (fs.existsSync(configPath)) { - const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8")) - if (typeof cfg.sensitivity === "number") - initialSensitivity = cfg.sensitivity - if (typeof cfg.invertScroll === "boolean") - initialInvertScroll = cfg.invertScroll - } - } catch (e) { - logger.warn( - `Failed to read initial config from server-config.json: ${String(e)}`, - ) - } - - // Overlay any pending (unsaved) config - if (pendingConfigUpdates) { - if (typeof pendingConfigUpdates.sensitivity === "number") - initialSensitivity = pendingConfigUpdates.sensitivity - if (typeof pendingConfigUpdates.invertScroll === "boolean") - initialInvertScroll = pendingConfigUpdates.invertScroll - } - - const inputPc = new InputPeerConnection( - sessionId, - (candidate, mid) => { - pushEvent(sessionId, "input-ice", { - sessionId, - candidate, - sdpMid: mid, - sdpMLineIndex: null, - }) - }, - { sensitivity: initialSensitivity, invertScroll: initialInvertScroll }, - () => { - logger.info(`Input connection closed: ${sessionId}`) - inputConnections.delete(sessionId) - pushEvent(sessionId, "stream-error", { - type: "input-closed", - message: "Input connection closed", - }) - }, - (errorType, message) => { - pushEvent(sessionId, "stream-error", { type: errorType, message }) - }, - ) - inputConnections.set(sessionId, inputPc) - - try { - const answerSdp = await inputPc.processOffer(sdp) - pushEvent(sessionId, "input-answer", { sessionId, sdp: answerSdp }) - logger.info(`[Input] Answer dispatched via SSE for: ${sessionId}`) - json(res, 200, { ok: true }) - } catch (err) { - inputConnections.delete(sessionId) - inputPc.close() - json(res, 500, { error: String(err) }) - } -} - -export async function handleGetIp( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - try { - const ip = await getLocalIp() - json(res, 200, { ip }) - } catch (err) { - logger.error(`Failed to get local IP: ${String(err)}`) - json(res, 500, { error: "Failed to get local IP" }) - } -} - -export async function handleUpdateConfig( - req: IncomingMessage, - res: ServerResponse, -): Promise { - if (!requireAuth(req, res)) return - - try { - const bodyText = await readBody(req) - const body = JSON.parse(bodyText || "{}") as Record - - const configPath = "./src/server-config.json" - const SERVER_CONFIG_KEYS = [ - "host", - "frontendPort", - "address", - "inputThrottleMs", - "sensitivity", - "invertScroll", - ] as const - - const filtered: Record = {} - - for (const key of SERVER_CONFIG_KEYS) { - if (!(key in body)) continue - - if (key === "frontendPort") { - const port = Number(body[key]) - if ( - !Number.isFinite(port) || - port < 1 || - port > 65535 || - Math.floor(port) !== port - ) { - json(res, 400, { error: "Invalid port number (must be 1–65535)" }) - return - } - filtered[key] = port - } else if (key === "inputThrottleMs") { - const ms = Number(body[key]) - if (!Number.isFinite(ms) || ms < 1 || ms > 1000) { - json(res, 400, { error: "Invalid inputThrottleMs (must be 1–1000)" }) - return - } - filtered[key] = ms - } else if (key === "sensitivity") { - const sens = Number(body[key]) - if (!Number.isFinite(sens) || sens < 0.1 || sens > 10.0) { - json(res, 400, { error: "Invalid sensitivity (must be 0.1–10.0)" }) - return - } - filtered[key] = sens - } else if (key === "invertScroll") { - if (typeof body[key] !== "boolean") { - json(res, 400, { error: "invertScroll must be a boolean" }) - return - } - filtered[key] = body[key] - } else if ( - typeof body[key] === "string" && - (body[key] as string).length <= 255 - ) { - filtered[key] = body[key] - } - } - - if (Object.keys(filtered).length === 0) { - json(res, 400, { error: "No valid config keys provided" }) - return - } - - // Buffer config changes to prevent Vite server restart - setPendingConfigUpdates({ ...(pendingConfigUpdates || {}), ...filtered }) - logger.info( - "Configuration updates cached. Changes will be written to server-config.json when Vite/server exits.", - ) - - // Immediately propagate configuration changes to all active input connections - const inputConfigUpdate: Partial = {} - if (typeof filtered.sensitivity === "number") { - inputConfigUpdate.sensitivity = filtered.sensitivity - } - if (typeof filtered.invertScroll === "boolean") { - inputConfigUpdate.invertScroll = filtered.invertScroll - } - - if (Object.keys(inputConfigUpdate).length > 0) { - for (const inputPc of inputConnections.values()) { - inputPc.updateConfig(inputConfigUpdate) - } - } - - const current = fs.existsSync(configPath) - ? (JSON.parse(fs.readFileSync(configPath, "utf-8")) as Record< - string, - unknown - >) - : {} - const newConfig = { ...current, ...filtered } - json(res, 200, { ok: true, config: newConfig }) - } catch (err) { - logger.error(`Failed to update config: ${String(err)}`) - json(res, 500, { error: String(err) }) - } -} - -export async function handleWhipSignalingExchange( - req: IncomingMessage, - res: ServerResponse, -): Promise { - const url = new URL(req.url ?? "", `http://${req.headers.host}`) - const sessionId = url.searchParams.get("sessionId") - - if (!sessionId) { - json(res, 400, { error: "sessionId parameter context missing" }) - return - } - - let token = url.searchParams.get("token") - if (!token) { - const authHeader = req.headers.authorization ?? "" - if (authHeader.startsWith("Bearer ")) { - token = authHeader.slice(7).trim() - } else if (authHeader.startsWith("Bearer_")) { - token = authHeader.slice(7).trim() - } - } - - if (!token || !isKnownToken(token)) { - json(res, 401, { error: "Unauthorized" }) - return - } - - const hostOfferSdp = await readBody(req) - logger.info(`WHIP offer received for session: ${sessionId}`) - - const session = sessions.get(sessionId) - if (!session) { - json(res, 404, { error: "Target session context not found" }) - return - } - - session.offer = hostOfferSdp - session.state = "offering" - - pushEvent(sessionId, "offer", { sessionId, sdp: hostOfferSdp }) - - let checkCount = 0 - const answerCheckInterval = setInterval(() => { - reinStorage.run(true, () => { - const activeSession = sessions.get(sessionId) - checkCount++ - - if (activeSession?.answer) { - clearInterval(answerCheckInterval) - res.writeHead(201, { - "Content-Type": "application/sdp", - Location: `/api/webrtc/whip?sessionId=${sessionId}`, - }) - res.end(activeSession.answer) - logger.info(`WHIP handshake complete for session: ${sessionId}`) - } else if ( - checkCount >= 50 || - !activeSession || - activeSession.state === "closed" - ) { - clearInterval(answerCheckInterval) - res.writeHead(408, { "Content-Type": "application/json" }) - res.end(JSON.stringify({ error: "WHIP signaling handshake timeout" })) - } - }) - }, 100) - req.on("close", () => clearInterval(answerCheckInterval)) -} diff --git a/src/server/api/apiState.ts b/src/server/api/apiState.ts deleted file mode 100644 index 14c8a55b..00000000 --- a/src/server/api/apiState.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Shared memory state and state operations for the HTTP signaling layer. - */ - -import type { ServerResponse } from "node:http" -import { AsyncLocalStorage } from "node:async_hooks" -import { getActiveToken, generateToken, storeToken } from "../tokenStore" -import { HostRunner } from "../gstreamer/hostRunner" -import type { InputPeerConnection } from "./InputPeerConnection" -import logger from "../../utils/logger" - -export type SessionState = - | "pending" - | "offering" - | "answered" - | "connected" - | "closed" - -export interface IceCandidate { - candidate: string - sdpMid: string | null - sdpMLineIndex: number | null -} - -export interface Session { - id: string - createdAt: number - state: SessionState - offer: string | null - answer: string | null - viewerIce: IceCandidate[] - hostIce: IceCandidate[] -} - -export type HostStatus = "stopped" | "starting" | "running" | "error" - -export const reinStorage = new AsyncLocalStorage() - -export const sessions = new Map() -export const sseClients = new Map>() -export const inputConnections = new Map() - -export let hostStatus: HostStatus = "stopped" -export let runnerInstance: HostRunner | null = null -export let pendingConfigUpdates: Record | null = null - -export function setHostStatus(status: HostStatus): void { - hostStatus = status -} - -export function setRunnerInstance(runner: HostRunner | null): void { - runnerInstance = runner -} - -export function setPendingConfigUpdates( - updates: Record | null, -): void { - pendingConfigUpdates = updates -} - -export function pushEvent( - sessionId: string, - event: string, - data: unknown, -): void { - const clients = sseClients.get(sessionId) - if (!clients || clients.size === 0) return - - const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` - for (const res of clients) { - try { - reinStorage.run(true, () => { - res.write(payload) - }) - } catch { - clients.delete(res) - } - } -} - -export function ensureHostRunnerActive(localBaseUrl: string): HostRunner { - if (runnerInstance) return runnerInstance - - setHostStatus("starting") - logger.info("Initializing GStreamer HostRunner") - - try { - const localToken = getActiveToken() ?? generateToken() - storeToken(localToken) - - const runner = new HostRunner( - localBaseUrl, - localToken, - (sessionId, errorType, message) => { - pushEvent(sessionId, "stream-error", { type: errorType, message }) - }, - ) - setRunnerInstance(runner) - setHostStatus("running") - logger.info("GStreamer HostRunner is running") - return runner - } catch (err) { - setHostStatus("error") - logger.error(`Critical error initializing HostRunner: ${String(err)}`) - throw err - } -} diff --git a/src/server/api/getLocalIp.ts b/src/server/api/getLocalIp.ts deleted file mode 100644 index add5da6b..00000000 --- a/src/server/api/getLocalIp.ts +++ /dev/null @@ -1,44 +0,0 @@ -import dgram from "node:dgram" - -export async function getLocalIp(): Promise { - return new Promise((resolve) => { - const socket = dgram.createSocket("udp4") - let settled = false - - const finish = (ip: string) => { - if (settled) return - settled = true - - clearTimeout(timeout) - socket.removeAllListeners("connect") - socket.removeAllListeners("error") - - try { - socket.close() - } catch { - // socket may already be closed - } - - resolve(ip) - } - - const timeout = setTimeout(() => { - finish("127.0.0.1") - }, 1000) - - socket.connect(1, "1.1.1.1") - - socket.on("connect", () => { - const addr = socket.address() - if (typeof addr === "object") { - finish(addr.address) - } else { - finish("127.0.0.1") - } - }) - - socket.on("error", () => { - finish("127.0.0.1") - }) - }) -} diff --git a/src/server/drivers/linux/index.ts b/src/server/drivers/linux/index.ts index 8b5eddb5..4c32dfcb 100644 --- a/src/server/drivers/linux/index.ts +++ b/src/server/drivers/linux/index.ts @@ -260,12 +260,12 @@ export class LinuxInputInjector { const touchOk = this.setupTouchDevice() if (!mouseOk || !kbOk || !touchOk) { - console.error( - "[LinuxInputInjector] One or more devices failed to initialize", - ) + const msg = + "One or more virtual uinput devices failed to initialize (check /dev/uinput permissions)" + console.error(`[LinuxInputInjector] ${msg}`) this.destroy() - this.initialized = mouseOk - return + this.initialized = false + throw new Error(msg) } this.keyboard = new LinuxKeyboard(this.kbDev.fd) diff --git a/src/server/drivers/linux/structs.ts b/src/server/drivers/linux/structs.ts index 70beb39a..fc9e1c0c 100644 --- a/src/server/drivers/linux/structs.ts +++ b/src/server/drivers/linux/structs.ts @@ -41,7 +41,7 @@ let _open: KoffiFunc | null = null let _close: KoffiFunc | null = null let _write: KoffiFunc | null = null let _ioctl: KoffiFunc | null = null -let _dummyBuffer: Buffer | null = null +export let _dummyBuffer: Buffer | null = null function ensureLibc() { if (!_libc) { diff --git a/src/server/drivers/mac/structs.ts b/src/server/drivers/mac/structs.ts index bb8f2c14..39ef6527 100644 --- a/src/server/drivers/mac/structs.ts +++ b/src/server/drivers/mac/structs.ts @@ -24,8 +24,8 @@ let _CGEventCreateScrollWheelEvent: koffi.KoffiFunction | null = null let _CGEventPost: koffi.KoffiFunction | null = null let _CFRelease: koffi.KoffiFunction | null = null let _CGEventSetIntegerValueField: koffi.KoffiFunction | null = null -let _CGEventSetDoubleValueField: koffi.KoffiFunction | null = null -let _CGEventGetLocation: koffi.KoffiFunction | null = null +export let _CGEventSetDoubleValueField: koffi.KoffiFunction | null = null +export let _CGEventGetLocation: koffi.KoffiFunction | null = null function ensureFunctions() { const lib = cg() diff --git a/src/server/drivers/windows/structs.ts b/src/server/drivers/windows/structs.ts index 31beb924..7f61b49c 100644 --- a/src/server/drivers/windows/structs.ts +++ b/src/server/drivers/windows/structs.ts @@ -42,7 +42,7 @@ const POINTER_TOUCH_INFO = koffi.struct("POINTER_TOUCH_INFO", { pressure: "uint32", }) -const _POINTER_TYPE_INFO = koffi.struct("POINTER_TYPE_INFO", { +export const _POINTER_TYPE_INFO = koffi.struct("POINTER_TYPE_INFO", { type: "uint32", touchInfo: POINTER_TOUCH_INFO, }) diff --git a/src/server/gstreamer/gstManager.ts b/src/server/gstreamer/gstManager.ts index fdae0eb1..1f1318c1 100644 --- a/src/server/gstreamer/gstManager.ts +++ b/src/server/gstreamer/gstManager.ts @@ -1,12 +1,4 @@ -/** - * GStreamer pipeline process manager. - * - * Spawns and monitors the gst-launch-1.0 process for capturing and encoding - * the screen video stream, and streaming it via WHIP signaling client. - */ - import { spawn, type ChildProcess } from "node:child_process" -import EventEmitter from "node:events" import os from "node:os" import fs from "node:fs" import path from "node:path" @@ -14,115 +6,70 @@ import logger from "../../utils/logger" import { type CaptureProvider, createCaptureProvider } from "./captureProvider" import { resolveGstPaths } from "./gstPaths" -export class GstManager extends EventEmitter { +export class GstManager { private process: ChildProcess | null = null - private sessionId: string - private intentionalStop = false private provider: CaptureProvider | null = null - constructor(sessionId: string) { - super() - this.sessionId = sessionId - } - - private buildPipelineArgs( - sourceBlocks: string[], - token: string, - whipPort: number, - ): string[] { - const platform = os.platform() + private buildPipelineArgs(sourceBlocks: string[]): string[] { const args = [...sourceBlocks] - // Build the common suffix byte-for-byte identically to the old implementation - if (platform !== "win32") { - args.push( - "!", - "queue", - "max-size-buffers=5", - "leaky=downstream", - "!", - "videoconvert", - "!", - "videoscale", - "!", - "videorate", - ) - } else { - // Windows already appended d3d11convert/download in the source block - args.push("!", "videoconvert", "!", "videorate") - } - - if (platform === "darwin") { - args.push( - "!", - "video/x-raw,format=NV12,framerate=30/1", - "!", - "vtenc_h264", - "realtime=true", - "max-keyframe-interval=15", - "allow-frame-reordering=false", - "bitrate=2500", - "!", - "h264parse", - "config-interval=-1", - ) - } else { - args.push( - "!", - "video/x-raw,framerate=30/1", - "!", - "vp8enc", - "deadline=1", - "keyframe-max-dist=15", - "target-bitrate=2500000", - ) - } - - // Add WHIP sink args.push( "!", - "whipclientsink", - `signaller::whip-endpoint=http://localhost:${whipPort}/api/webrtc/whip?sessionId=${this.sessionId}&token=${token}`, - `signaller::auth-token=Bearer_${token}`, + "queue", + "max-size-buffers=1", + "leaky=downstream", + "!", + "videoconvert", + "!", + "videorate", + "!", + "video/x-raw,framerate=60/1", + "!", + "x264enc", + "tune=zerolatency", + "speed-preset=ultrafast", + "key-int-max=30", + "byte-stream=false", + "!", + "h264parse", + "!", + "video/x-h264,profile=baseline", + "!", + "rtph264pay", + "config-interval=-1", + "pt=96", + "!", + "udpsink", + "host=127.0.0.1", + "port=5004", + "sync=false", + "async=false", ) return args } - public async start(token: string, whipPort: number): Promise { + public async start(): Promise { if (this.process) return - this.intentionalStop = false - logger.info("Spawning GStreamer WHIP engine") + logger.info("Spawning GStreamer UDP engine") try { this.provider = createCaptureProvider() await this.provider.initialize(async (err) => { logger.error(`Capture provider failed after startup: ${err.message}`) - if (this.process) { - this.intentionalStop = true - this.stop() - await this.cleanup() - } else { - await this.cleanup() - } - this.emit("capture-failure", err) + await this.stop() }) const sourceBlocks = await this.provider.getGStreamerSource() - const pipelineArgs = this.buildPipelineArgs(sourceBlocks, token, whipPort) - this.executePipeline(pipelineArgs, whipPort, token) + const pipelineArgs = this.buildPipelineArgs(sourceBlocks) + this.executePipeline(pipelineArgs) } catch (error) { logger.error(`Capture initialization failed: ${String(error)}`) - this.emit("capture-failure", error) await this.cleanup() } } - private executePipeline( - pipelineArgs: string[], - whipPort: number, - token: string, - ): void { + private executePipeline(pipelineArgs: string[]): void { const gst = resolveGstPaths() const spawnedEnv = { ...process.env, ...gst.env } if (!spawnedEnv.DISPLAY) spawnedEnv.DISPLAY = ":0" @@ -144,14 +91,16 @@ export class GstManager extends EventEmitter { } } + logger.info(`GStreamer args: gst-launch-1.0 ${pipelineArgs.join(" ")}`) this.process = spawn(gst.gstLaunch, pipelineArgs, { env: spawnedEnv }) + this.process.on("error", async (err) => { logger.error(`GStreamer spawn failed: ${err.message}`) this.process = null await this.cleanup() - this.emit("capture-failure", err) return }) + this.process.stdout?.on("data", (data: Buffer) => { const output = data.toString() if (output.includes("State change") && output.includes("PLAYING")) { @@ -160,25 +109,13 @@ export class GstManager extends EventEmitter { }) this.process.stderr?.on("data", (data: Buffer) => { - let logStr = data.toString() - logStr = logStr.replace(/auth-token=\S+/g, "auth-token=REDACTED") + const logStr = data.toString() if ( - logStr.includes("ERROR") && - logStr.includes("pipeline doesn't want to preroll") - ) { - if (this.intentionalStop) return - logger.error( - "GStreamer pipeline failed to preroll, starting loopback fallback", - ) - this.intentionalStop = true - this.stop() - this.triggerTestFallbackPipeline(whipPort, token) - } else if ( logStr.includes("WARN") || logStr.includes("error") || logStr.includes("ERROR") ) { - logger.warn(`GStreamer [${this.sessionId}]: ${logStr.trim()}`) + logger.warn(`GStreamer: ${logStr.trim()}`) } }) @@ -186,67 +123,16 @@ export class GstManager extends EventEmitter { logger.info(`GStreamer process exited with status: ${code}`) this.process = null await this.cleanup() - if (!this.intentionalStop) { - this.emit("exit") - } }) } - private triggerTestFallbackPipeline(serverPort: number, token: string): void { - logger.info("Launching loopback video test pattern") - // (Existing fallback code remains identical) - const pipelineArgs = [ - "videotestsrc", - "is-live=true", - "pattern=ball", - "!", - "video/x-raw,framerate=30/1", - "!", - "videoconvert", - "!", - "vp8enc", - "deadline=1", - "keyframe-max-dist=15", - "target-bitrate=2500000", - "!", - "whipclientsink", - `signaller::whip-endpoint=http://localhost:${serverPort}/api/webrtc/whip?sessionId=${this.sessionId}&token=${token}`, - `signaller::auth-token=Bearer_${token}`, - ] - - const gst = resolveGstPaths() - const spawnedEnv = { ...process.env, ...gst.env } - delete spawnedEnv.DISPLAY - delete spawnedEnv.XAUTHORITY - - const proc = spawn(gst.gstLaunch, pipelineArgs, { env: spawnedEnv }) - this.process = proc - this.intentionalStop = false - - proc.on("error", (err) => { - logger.error(`GStreamer fallback spawn failed: ${err.message}`) - this.process = null - this.emit("capture-failure", err) - }) - - proc.stderr?.on("data", (data: Buffer) => { - logger.warn( - `GStreamer fallback [${this.sessionId}]: ${data.toString().trim()}`, - ) - }) - - proc.on("close", (code) => { - logger.info(`GStreamer fallback exited with status: ${code}`) + public async stop(): Promise { + if (this.process) { + logger.info("Terminating GStreamer video pipeline") + this.process.kill("SIGTERM") this.process = null - this.emit("exit") - }) - } - - public stop(): void { - if (!this.process) return - logger.info("Terminating GStreamer video pipeline") - this.process.kill("SIGTERM") - this.process = null + } + await this.cleanup() } private async cleanup(): Promise { diff --git a/src/server/gstreamer/hostRunner.ts b/src/server/gstreamer/hostRunner.ts deleted file mode 100644 index 248d10d7..00000000 --- a/src/server/gstreamer/hostRunner.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Host GStreamer runner orchestrator. - * - * Manages the collection of active session GstManager instances, starting, - * stopping, and resetting pipelines dynamically. - */ - -import { GstManager } from "./gstManager" -import logger from "../../utils/logger" - -export class HostRunner { - private activeSessions = new Map() - private stoppingSessions = new Set() - private token: string - private serverPort: number - private onStreamError?: ( - sessionId: string, - errorType: string, - message: string, - ) => void - - constructor( - baseUrl: string, - localAuthToken: string, - onStreamError?: ( - sessionId: string, - errorType: string, - message: string, - ) => void, - ) { - this.token = localAuthToken - this.onStreamError = onStreamError - - const portMatch = baseUrl.match(/:(\d+)/) - this.serverPort = portMatch ? Number.parseInt(portMatch[1], 10) : 8000 - - logger.info(`HostRunner initialized on port: ${this.serverPort}`) - } - - public handleIncomingClientOffer( - sessionId: string, - _clientOfferSdp: string, - ): void { - if (this.activeSessions.has(sessionId)) { - logger.info("GStreamer pipeline already running, skipping restart") - return - } - - logger.info(`HostRunner launching stream for session: ${sessionId}`) - - const gst = new GstManager(sessionId) - this.activeSessions.set(sessionId, gst) - - gst.on("exit", () => { - const wasIntentional = this.stoppingSessions.has(sessionId) - this.activeSessions.delete(sessionId) - this.stoppingSessions.delete(sessionId) - if (wasIntentional) { - logger.info(`GStreamer pipeline stopped for session: ${sessionId}`) - return - } - if (this.onStreamError) { - this.onStreamError( - sessionId, - "gstreamer-exit", - "GStreamer pipeline exited unexpectedly", - ) - } - }) - - gst.on("capture-failure", (err: unknown) => { - const errMsg = err instanceof Error ? err.message : String(err) - logger.error(`Capture failure for session: ${sessionId}: ${errMsg}`) - this.activeSessions.delete(sessionId) - if (this.onStreamError) { - this.onStreamError( - sessionId, - "capture-failure", - `Capture failure: ${errMsg}`, - ) - } - }) - - void gst.start(this.token, this.serverPort) - } - - public shutdown(): void { - for (const [sessionId, manager] of this.activeSessions.entries()) { - this.stoppingSessions.add(sessionId) - manager.stop() - } - this.activeSessions.clear() - logger.info("HostRunner shutdown") - } -} diff --git a/src/server/server.ts b/src/server/server.ts index 637aeafa..dc574776 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,172 +1,292 @@ -/** - * Routing and signaling engine setup. - */ - import type { IncomingMessage, ServerResponse } from "node:http" +import os from "node:os" +import { Transform } from "node:stream" import logger from "../utils/logger" -import { reinStorage } from "./api/apiState" +import winston from "winston" import { - handleCreateSession, - handleGetSession, - handleDeleteSession, - handleOffer, - handleAnswer, - handleIce, - handleEvents, - handleHostStart, - handleHostStop, - handleHostStatus, - handleInputOffer, - handleGenerateToken, - handleGetToken, - handleGstSignalingGateway, - handleWhipSignalingExchange, - handleGetIp, - handleUpdateConfig, - json, -} from "./api/apiHandlers" - -interface Route { - method: string - pattern: RegExp - handler: ( + getActiveToken, + generateToken, + storeToken, + isKnownToken, +} from "./tokenStore" +import { GstManager } from "./gstreamer/gstManager" +import { WebRTCManager } from "./webRTC" +import type { InputConfig } from "./types" + +let gstManager: GstManager | null = null +let webrtcManager: WebRTCManager | null = null +let hostStatus: "stopped" | "starting" | "running" | "error" = "stopped" +/** Latest RTT reported by the connected client (phone). Updated via POST /api/debug/report-latency. */ +let lastReportedLatencyMs: number | null = null + +// ── SSE log fan-out ─────────────────────────────────────────────────────────── +// A set of active SSE response streams. Each GET /api/debug/logs subscriber +// gets added here and removed on close/error. +const sseClients = new Set() + +/** Custom Winston transport that pushes every log line to SSE subscribers. */ +class SseTransport extends winston.transports.Stream { + constructor() { + const passthrough = new Transform({ + objectMode: true, + transform(chunk, _enc, cb) { + this.push(chunk) + cb() + }, + }) + super({ stream: passthrough }) + passthrough.on("data", (info: Record) => { + const payload = `data: ${JSON.stringify({ + timestamp: info.timestamp ?? new Date().toISOString(), + level: String(info.level ?? "info").toUpperCase(), + message: String(info.message ?? ""), + })}\n\n` + for (const res of sseClients) { + try { + res.write(payload) + } catch { + sseClients.delete(res) + } + } + }) + } +} + +// Attach SSE transport once at module load time so it captures all log output. +logger.add(new SseTransport()) + +function getPrimaryIp(): string { + const interfaces = os.networkInterfaces() + for (const name of Object.keys(interfaces)) { + const ifaceList = interfaces[name] + if (!ifaceList) continue + for (const iface of ifaceList) { + if (iface.family === "IPv4" && !iface.internal) { + return iface.address + } + } + } + return "127.0.0.1" +} + +function parseJsonBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = "" + req.on("data", (chunk) => { + body += chunk + }) + req.on("end", () => { + try { + resolve(body ? JSON.parse(body) : ({} as T)) + } catch (err) { + reject(err) + } + }) + req.on("error", reject) + }) +} + +function json(res: ServerResponse, status: number, body: unknown): void { + const payload = JSON.stringify(body) + res.writeHead(status, { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(payload), + }) + res.end(payload) +} + +function requireAuth(req: IncomingMessage, res: ServerResponse): boolean { + const addr = req.socket.remoteAddress + const isLocal = + addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" + if (isLocal) return true + + const authHeader = req.headers.authorization ?? "" + let token = authHeader.startsWith("Bearer ") + ? authHeader.slice(7).trim() + : null + + if (!token) { + const url = new URL(req.url ?? "", `http://${req.headers.host}`) + token = url.searchParams.get("token") + } + + if (!token || !isKnownToken(token)) { + json(res, 401, { error: "Unauthorized" }) + return false + } + return true +} + +// biome-ignore lint/suspicious/noExplicitAny: Vite server instance +export function attachSignalingRoutes(server: any): void { + const httpServer = server.httpServer || server + + if (!webrtcManager && httpServer) { + webrtcManager = new WebRTCManager(httpServer) + } + + if (!gstManager) { + gstManager = new GstManager() + hostStatus = "starting" + gstManager + .start() + .then(() => { + hostStatus = "running" + logger.info("GStreamer capture engine started") + }) + .catch((err) => { + logger.error(`Failed to start GStreamer capture engine: ${err}`) + hostStatus = "error" + }) + } + + const handleApiRequest = ( req: IncomingMessage, res: ServerResponse, - ...params: string[] - ) => void | Promise -} + next?: () => void, + ) => { + const pathname = new URL( + req.url ?? "", + `http://${req.headers.host ?? "localhost"}`, + ).pathname -const routes: Route[] = [ - { method: "GET", pattern: /^\/api\/host\/ip$/, handler: handleGetIp }, - { method: "POST", pattern: /^\/api\/config$/, handler: handleUpdateConfig }, - { method: "POST", pattern: /^\/api\/session$/, handler: handleCreateSession }, - { - method: "GET", - pattern: /^\/api\/session\/([^/]+)$/, - handler: (req, res, id) => handleGetSession(req, res, id), - }, - { - method: "DELETE", - pattern: /^\/api\/session\/([^/]+)$/, - handler: (req, res, id) => handleDeleteSession(req, res, id), - }, - { method: "POST", pattern: /^\/api\/webrtc\/offer$/, handler: handleOffer }, - { method: "POST", pattern: /^\/api\/webrtc\/answer$/, handler: handleAnswer }, - { method: "POST", pattern: /^\/api\/webrtc\/ice$/, handler: handleIce }, - { method: "GET", pattern: /^\/api\/webrtc\/events$/, handler: handleEvents }, - { method: "POST", pattern: /^\/api\/host\/start$/, handler: handleHostStart }, - { method: "POST", pattern: /^\/api\/host\/stop$/, handler: handleHostStop }, - { - method: "GET", - pattern: /^\/api\/host\/status$/, - handler: handleHostStatus, - }, - { - method: "POST", - pattern: /^\/api\/webrtc\/input-offer$/, - handler: handleInputOffer, - }, - { - method: "POST", - pattern: /^\/api\/auth\/token$/, - handler: handleGenerateToken, - }, - { method: "GET", pattern: /^\/api\/auth\/token$/, handler: handleGetToken }, - { - method: "POST", - pattern: /^\/api\/webrtc\/gateway$/, - handler: handleGstSignalingGateway, - }, - { - method: "POST", - pattern: /^\/api\/webrtc\/whip$/, - handler: handleWhipSignalingExchange, - }, -] - -export function attachSignalingRoutes( - server: NonNullable, -): void { - server.prependListener( - "request", - (req: IncomingMessage, res: ServerResponse) => { - const pathname = new URL( - req.url ?? "", - `http://${req.headers.host ?? "localhost"}`, - ).pathname - - if (!pathname.startsWith("/api/")) return - - const method = req.method?.toUpperCase() ?? "GET" - for (const route of routes) { - if (route.method !== method) continue - const match = pathname.match(route.pattern) - if (!match) continue - - const anyRes = res as ServerResponse & { - __handledByRein?: boolean - __reinOriginalWrite?: typeof originalWrite - } - anyRes.__handledByRein = true + if (!pathname.startsWith("/api/")) { + if (next) next() + return + } + + if (pathname === "/api/host/start" && req.method === "POST") { + if (!requireAuth(req, res)) return + if (hostStatus === "running") { + json(res, 200, { status: hostStatus }) + return + } + hostStatus = "starting" + if (!gstManager) gstManager = new GstManager() + gstManager + .start() + .then(() => { + hostStatus = "running" + }) + .catch((err) => { + logger.error(`Failed to start GStreamer: ${err}`) + hostStatus = "error" + }) - const originalSetHeader = res.setHeader.bind(res) - const originalWriteHead = res.writeHead.bind(res) - const originalWrite = res.write.bind(res) - const originalEnd = res.end.bind(res) + json(res, 200, { status: hostStatus }) + return + } - anyRes.__reinOriginalWrite = originalWrite + if (pathname === "/api/host/stop" && req.method === "POST") { + if (!requireAuth(req, res)) return + if (gstManager) { + gstManager.stop() + } + hostStatus = "stopped" + json(res, 200, { status: hostStatus }) + return + } - res.setHeader = ((...args: Parameters) => { - if (anyRes.__handledByRein && !reinStorage.getStore()) { - return res - } - if (res.headersSent) return res - return originalSetHeader(...args) - }) as typeof res.setHeader + if (pathname === "/api/host/status" && req.method === "GET") { + if (!requireAuth(req, res)) return + json(res, 200, { status: hostStatus }) + return + } - res.writeHead = ((...args: unknown[]) => { - if (anyRes.__handledByRein && !reinStorage.getStore()) { - return res - } - if (res.writableEnded) return res - return (originalWriteHead as (...a: unknown[]) => ServerResponse)( - ...args, - ) - }) as typeof res.writeHead - - res.write = ((...args: unknown[]) => { - if (anyRes.__handledByRein && !reinStorage.getStore()) { - return true - } - return (originalWrite as (...a: unknown[]) => boolean)(...args) - }) as typeof res.write + if (pathname === "/api/host/ip" && req.method === "GET") { + if (!requireAuth(req, res)) return + json(res, 200, { ip: getPrimaryIp() }) + return + } - res.end = ((...args: unknown[]) => { - if (anyRes.__handledByRein && !reinStorage.getStore()) { - return res - } - return (originalEnd as (...a: unknown[]) => ServerResponse)(...args) - }) as typeof res.end - - const params = match.slice(1) - Promise.resolve() - .then(() => - reinStorage.run(true, () => route.handler(req, res, ...params)), - ) - .catch((err) => { - logger.error(`Signaling route error: ${String(err)}`) - if (!res.headersSent) { - reinStorage.run(true, () => { - json(res, 500, { error: "Internal server error" }) - }) - } - }) + if (pathname === "/api/auth/token" && req.method === "POST") { + const addr = req.socket.remoteAddress + const isLocal = + addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" + if (!isLocal) { + json(res, 403, { error: "Localhost only" }) return } - }, - ) - logger.info("Signaling HTTP routes attached") + let token = getActiveToken() + if (!token) { + token = generateToken() + storeToken(token) + } + json(res, 200, { token }) + return + } + + if (pathname === "/api/config" && req.method === "POST") { + if (!requireAuth(req, res)) return + parseJsonBody>(req) + .then((config) => { + if (webrtcManager) { + webrtcManager.updateConfig(config) + } + json(res, 200, { ok: true }) + }) + .catch((err) => { + json(res, 400, { ok: false, error: String(err) }) + }) + return + } + + if (pathname === "/api/debug/sessions" && req.method === "GET") { + const sessions = webrtcManager?.getSessions() ?? [] + const inputConnectionCount = sessions.filter( + (s) => s.hasInputConnection, + ).length + json(res, 200, { + hostStatus, + sessionCount: sessions.length, + sessions, + inputConnectionCount, + latencyMs: lastReportedLatencyMs, + }) + return + } + + if (pathname === "/api/debug/report-latency" && req.method === "POST") { + parseJsonBody<{ latencyMs?: number }>(req) + .then((body) => { + if (typeof body.latencyMs === "number" && body.latencyMs >= 0) { + lastReportedLatencyMs = body.latencyMs + } + json(res, 200, { ok: true }) + }) + .catch(() => json(res, 400, { ok: false })) + return + } + + if (pathname === "/api/debug/logs" && req.method === "GET") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }) + // Send a comment to establish the connection + res.write(": connected\n\n") + sseClients.add(res) + req.on("close", () => sseClients.delete(res)) + req.on("error", () => sseClients.delete(res)) + return + } + + json(res, 404, { error: "API endpoint not found" }) + } + + if (server.middlewares) { + server.middlewares.use(handleApiRequest) + } else if (httpServer && typeof httpServer.on === "function") { + httpServer.on("request", handleApiRequest) + } + + logger.info("Signaling HTTP routes and WebSocket attached") } -// Re-export state and event systems for other signaling servers/files -export { sessions, hostStatus, pushEvent } from "./api/apiState" +export function stopServer() { + if (webrtcManager) webrtcManager.shutdown() + if (gstManager) gstManager.stop() +} diff --git a/src/server/webRTC.ts b/src/server/webRTC.ts new file mode 100644 index 00000000..f063509b --- /dev/null +++ b/src/server/webRTC.ts @@ -0,0 +1,299 @@ +import { + MediaStreamTrack, + RTCPeerConnection, + RTCRtpCodecParameters, +} from "werift" +import { WebSocketServer, WebSocket } from "ws" +import dgram from "node:dgram" +import { InputHandler } from "./InputHandler" +import logger from "../utils/logger" +import type { InputMessage, InputConfig } from "./types" +import fs from "node:fs" + +interface ClientSession { + ws: WebSocket + pc: RTCPeerConnection + videoTrack: MediaStreamTrack + inputHandler: InputHandler + sessionId: string + bytesRecv: number + bytesSent: number +} + +import type { IncomingMessage } from "node:http" +import type { Duplex } from "node:stream" +import type { EventEmitter } from "node:events" + +export interface SessionSnapshot { + id: string + state: string + createdAt: number + sseViewerCount: number + hasInputConnection: boolean + bytesRecv: number + bytesSent: number +} + +export class WebRTCManager { + private wss: WebSocketServer + private udpSocket: dgram.Socket + private clients = new Map() + private sessionCreatedAt = new Map() + + constructor(server: EventEmitter) { + this.wss = new WebSocketServer({ noServer: true }) + this.udpSocket = dgram.createSocket("udp4") + + server.on( + "upgrade", + (request: IncomingMessage, socket: Duplex, head: Buffer) => { + try { + const url = new URL( + request.url || "", + `http://${request.headers.host || "localhost"}`, + ) + if (url.pathname === "/ws") { + this.wss.handleUpgrade(request, socket, head, (ws) => { + this.wss.emit("connection", ws, request) + }) + } + } catch (_err) { + // Ignore invalid URLs or non-matching upgrades + } + }, + ) + + this.setupUdpSocket() + this.setupWebSocketServer() + } + + private setupUdpSocket() { + this.udpSocket.on("error", (err) => { + logger.error(`UDP socket error:\n${err.stack}`) + this.udpSocket.close() + }) + + this.udpSocket.on("message", (msg) => { + for (const client of this.clients.values()) { + try { + client.videoTrack.writeRtp(msg) + client.bytesRecv += msg.length + } catch (_err) { + // Ignore individual track write errors + } + } + }) + + this.udpSocket.bind(5004, "127.0.0.1", () => { + logger.info("UDP socket listening for RTP packets on 127.0.0.1:5004") + }) + } + + private getInitialConfig(): Partial { + try { + const configPath = "./src/server-config.json" + if (fs.existsSync(configPath)) { + const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8")) + return { + sensitivity: + typeof cfg.sensitivity === "number" ? cfg.sensitivity : 1.0, + invertScroll: + typeof cfg.invertScroll === "boolean" ? cfg.invertScroll : false, + } + } + } catch (e) { + logger.warn( + `Failed to read initial config from server-config.json: ${String(e)}`, + ) + } + return { sensitivity: 1.0, invertScroll: false } + } + + private setupWebSocketServer() { + this.wss.on("connection", async (ws) => { + const sessionId = Math.random().toString(36).substring(7) + this.sessionCreatedAt.set(sessionId, Date.now()) + logger.info(`Viewer connected via WebSocket: ${sessionId}`) + + const pc = new RTCPeerConnection({ + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], + codecs: { + video: [ + new RTCRtpCodecParameters({ + mimeType: "video/H264", + clockRate: 90000, + payloadType: 96, + rtcpFeedback: [ + { type: "nack" }, + { type: "nack", parameter: "pli" }, + { type: "goog-remb" }, + ], + }), + ], + }, + }) + const videoTrack = new MediaStreamTrack({ + kind: "video", + }) + pc.addTrack(videoTrack) + + const inputHandler = new InputHandler( + this.getInitialConfig(), + 8, + (errorType, message) => { + logger.error(`InputHandler error: ${errorType} - ${message}`) + try { + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "error", + errorType, + message, + }), + ) + } + } catch (e) { + logger.error( + `Failed to transmit error frame to client: ${String(e)}`, + ) + } + }, + ) + + const dcUnordered = pc.createDataChannel("input-unordered", { + ordered: false, + maxRetransmits: 0, + }) + const dcOrdered = pc.createDataChannel("input-ordered", { ordered: true }) + + const handleDataMessage = (msg: Buffer | string) => { + try { + const raw = typeof msg === "string" ? msg : msg.toString() + const session = this.clients.get(sessionId) + if (session) session.bytesSent += raw.length + const parsed = JSON.parse(raw) + if (parsed.type === "ping") { + const pong = JSON.stringify({ + type: "pong", + timestamp: parsed.timestamp, + }) + dcUnordered.send(pong) + dcOrdered.send(pong) + return + } + inputHandler.handleMessage(parsed as InputMessage).catch((err) => { + logger.error(`Input handler processing error: ${String(err)}`) + }) + } catch (err) { + logger.error(`Input parse error: ${String(err)}`) + } + } + + dcUnordered.onMessage.subscribe((msg) => handleDataMessage(msg)) + dcOrdered.onMessage.subscribe((msg) => handleDataMessage(msg)) + + const client: ClientSession = { + ws, + pc, + videoTrack, + inputHandler, + sessionId, + bytesRecv: 0, + bytesSent: 0, + } + this.clients.set(sessionId, client) + + pc.iceConnectionStateChange.subscribe((state) => { + logger.info(`WebRTC ICE state for ${sessionId}: ${state}`) + if ( + state === "disconnected" || + state === "failed" || + state === "closed" + ) { + this.cleanupClient(sessionId) + } + }) + + pc.onIceCandidate.subscribe((candidate) => { + if (candidate) { + ws.send( + JSON.stringify({ type: "ice", candidate: candidate.toJSON() }), + ) + } + }) + + ws.on("message", async (data) => { + try { + const msg = JSON.parse(data.toString()) + if (msg.type === "answer") { + await pc.setRemoteDescription(msg.sdp) + } else if (msg.type === "ice" && msg.candidate) { + await pc.addIceCandidate(msg.candidate) + } else if (msg.type === "ping") { + ws.send(JSON.stringify({ type: "pong" })) + } + } catch (err) { + logger.error(`WebSocket message handling error: ${String(err)}`) + } + }) + + ws.on("close", () => { + this.cleanupClient(sessionId) + }) + + try { + const offer = await pc.createOffer() + await pc.setLocalDescription(offer) + console.log(offer.sdp) + ws.send(JSON.stringify({ type: "offer", sdp: offer })) + } catch (err) { + logger.error(`Failed to create offer: ${String(err)}`) + this.cleanupClient(sessionId) + } + }) + } + + private cleanupClient(sessionId: string) { + const client = this.clients.get(sessionId) + if (client) { + logger.info(`Cleaning up client session: ${sessionId}`) + try { + client.pc.close() + } catch (_e) {} + try { + client.inputHandler.destroy() + } catch (_e) {} + this.clients.delete(sessionId) + this.sessionCreatedAt.delete(sessionId) + } + } + public getSessions(): SessionSnapshot[] { + const snapshots: SessionSnapshot[] = [] + for (const [id, client] of this.clients) { + snapshots.push({ + id, + state: client.pc.iceConnectionState ?? "new", + createdAt: this.sessionCreatedAt.get(id) ?? Date.now(), + sseViewerCount: client.ws.readyState === 1 ? 1 : 0, + hasInputConnection: true, + bytesRecv: client.bytesRecv, + bytesSent: client.bytesSent, + }) + } + return snapshots + } + + public updateConfig(config: Partial) { + for (const client of this.clients.values()) { + client.inputHandler.updateConfig(config) + } + } + + public shutdown() { + this.wss.close() + this.udpSocket.close() + for (const sessionId of this.clients.keys()) { + this.cleanupClient(sessionId) + } + } +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts index b4184f37..d1afd798 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -52,8 +52,6 @@ if (process.env.NODE_ENV !== "production") { } // Optional: Intercept standard console.log and redirect to winston -const _originalConsoleLog = console.log -const _originalConsoleError = console.error const serialize = (a: unknown): string => typeof a === "string" ? a : JSON.stringify(a) diff --git a/vite.config.ts b/vite.config.ts index 12314476..501473c5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,14 +17,10 @@ const config = defineConfig({ { name: "rein-server", async configureServer(server) { - const httpServer = server.httpServer - if (!httpServer) return - attachSignalingRoutes(httpServer) + attachSignalingRoutes(server) }, async configurePreviewServer(server) { - const httpServer = server.httpServer - if (!httpServer) return - attachSignalingRoutes(httpServer) + attachSignalingRoutes(server) }, }, devtools(), @@ -37,7 +33,7 @@ const config = defineConfig({ }), ], ssr: { - external: ["node-datachannel", "dbus-next", "eventsource"], + external: ["dbus-next", "eventsource", "werift"], noExternal: ["tailwindcss", "@tailwindcss/postcss"], }, server: { @@ -46,7 +42,7 @@ const config = defineConfig({ }, build: { rollupOptions: { - external: ["node-datachannel"], + external: ["werift"], }, }, }) From c453b436e8b4daf81902825529a49f65b9b92e08 Mon Sep 17 00:00:00 2001 From: pinjinx Date: Wed, 29 Jul 2026 21:46:42 +0530 Subject: [PATCH 2/6] [Architecture] Added welcome and fixed debug --- src/components/Trackpad/ScreenMirror.tsx | 4 ++ src/hooks/useWebRtcStream.ts | 8 ++++ src/routes/debug.tsx | 2 +- src/routes/trackpad.tsx | 15 ++++++-- src/server-config.json | 3 +- src/server/server.ts | 21 +++++++---- src/server/welcome.ts | 48 ++++++++++++++++++++++++ src/utils/i18n.ts | 10 +++++ src/utils/logger.ts | 15 +++++++- vite.config.ts | 18 +++++++++ 10 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 src/server/welcome.ts diff --git a/src/components/Trackpad/ScreenMirror.tsx b/src/components/Trackpad/ScreenMirror.tsx index 1c752ee1..f99d5d22 100644 --- a/src/components/Trackpad/ScreenMirror.tsx +++ b/src/components/Trackpad/ScreenMirror.tsx @@ -11,6 +11,7 @@ interface ScreenMirrorProps { handlers: React.HTMLAttributes videoStream: MediaStream | null trackActive: boolean + connecting: boolean status: "connecting" | "connected" | "disconnected" } @@ -26,6 +27,7 @@ export const ScreenMirror = ({ handlers, videoStream, trackActive, + connecting, status, }: ScreenMirrorProps) => { const videoElementRef = useRef(null) @@ -44,6 +46,7 @@ export const ScreenMirror = ({ }, [videoStream]) const getWaitingText = () => { + if (connecting) return t("screenMirror", "establishingConnection") switch (status) { case "disconnected": return t("screenMirror", "disconnected") @@ -55,6 +58,7 @@ export const ScreenMirror = ({ } const getSubText = () => { + if (connecting) return t("screenMirror", "negotiatingWebRtc") switch (status) { case "disconnected": return t("screenMirror", "checkNetwork") diff --git a/src/hooks/useWebRtcStream.ts b/src/hooks/useWebRtcStream.ts index 9888ca5b..a01e8ed8 100644 --- a/src/hooks/useWebRtcStream.ts +++ b/src/hooks/useWebRtcStream.ts @@ -13,6 +13,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { const [videoStream, setVideoStream] = useState(null) const [error, setError] = useState(null) const [errorHandle, setErrorHandle] = useState(null) + const [connecting, setConnecting] = useState(false) const [reconnectAttempt, setReconnectAttempt] = useState(0) const { registerDataChannel, send: sendInputEvent } = useConnection() @@ -43,6 +44,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { console.warn( `[WebRTC] Max retry attempts (${MAX_RETRIES}) reached. Stopping retries.`, ) + setConnecting(false) setErrorHandle("Connection Failed") setError("Failed to establish stream session after multiple attempts") isRetryingRef.current = false @@ -102,6 +104,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { } setErrorHandle(null) setError(null) + setConnecting(true) setTrackActive(false) setVideoStream(null) retryCountRef.current = 0 @@ -113,6 +116,8 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { let isDisposed = false + setConnecting(true) + if (reconnectAttempt > 0) { console.log( `[WebRTC] Re-establishing WebRTC session (attempt ${reconnectAttempt})...`, @@ -138,6 +143,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { if (event.track.kind === "video" && event.streams[0]) { setVideoStream(event.streams[0]) setTrackActive(true) + setConnecting(false) retryCountRef.current = 0 } } @@ -205,6 +211,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { } } else if (msg.type === "error") { console.error("[WebRTC] Host error received:", msg) + setConnecting(false) setErrorHandle(msg.errorType || "Host Error") setError(msg.message || "Host reported an error") } @@ -279,6 +286,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { videoStream, error, errorHandle, + connecting, reconnect, sendInputEvent, } diff --git a/src/routes/debug.tsx b/src/routes/debug.tsx index 4aa1ae60..42e50c56 100644 --- a/src/routes/debug.tsx +++ b/src/routes/debug.tsx @@ -163,7 +163,7 @@ function DebugScreen() { const [consoleTab, setConsoleTab] = useState<"server" | "client">("server") const [levelFilter, setLevelFilter] = useState("ALL") const [searchQuery, setSearchQuery] = useState("") - const [copiedId, setCopiedId] = useState(null) + const [_copiedId, setCopiedId] = useState(null) const logEndRef = useRef(null) const copyToClipboard = (id: string, text: string) => { const done = () => { diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index 9d062175..86665ce6 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -43,10 +43,16 @@ function TrackpadPage() { const [keyboardOpen, setKeyboardOpen] = useState(false) const [extraKeysVisible, setExtraKeysVisible] = useState(true) const { status, send, sendCombo } = useRemoteConnection() - const { trackActive, videoStream, error, errorHandle, reconnect } = - useWebRtcStream({ - token, - }) + const { + trackActive, + videoStream, + error, + errorHandle, + connecting, + reconnect, + } = useWebRtcStream({ + token, + }) // Send input actions safely over WebRTC DataChannels const broadcastMessage = (payload: unknown) => { @@ -224,6 +230,7 @@ function TrackpadPage() { handlers={handlers} videoStream={videoStream} trackActive={trackActive} + connecting={connecting} status={status} /> )} diff --git a/src/server-config.json b/src/server-config.json index 19946fbb..753968da 100644 --- a/src/server-config.json +++ b/src/server-config.json @@ -4,5 +4,6 @@ "address": "rein.local", "inputThrottleMs": 8, "sensitivity": 2.1, - "invertScroll": false + "invertScroll": false, + "verboseLogs": false } diff --git a/src/server/server.ts b/src/server/server.ts index dc574776..a244c409 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -16,15 +16,13 @@ import type { InputConfig } from "./types" let gstManager: GstManager | null = null let webrtcManager: WebRTCManager | null = null let hostStatus: "stopped" | "starting" | "running" | "error" = "stopped" -/** Latest RTT reported by the connected client (phone). Updated via POST /api/debug/report-latency. */ let lastReportedLatencyMs: number | null = null -// ── SSE log fan-out ─────────────────────────────────────────────────────────── -// A set of active SSE response streams. Each GET /api/debug/logs subscriber -// gets added here and removed on close/error. const sseClients = new Set() -/** Custom Winston transport that pushes every log line to SSE subscribers. */ +const LOG_BUFFER_MAX = 500 +const logBuffer: string[] = [] + class SseTransport extends winston.transports.Stream { constructor() { const passthrough = new Transform({ @@ -41,6 +39,9 @@ class SseTransport extends winston.transports.Stream { level: String(info.level ?? "info").toUpperCase(), message: String(info.message ?? ""), })}\n\n` + // Push to replay buffer + logBuffer.push(payload) + if (logBuffer.length > LOG_BUFFER_MAX) logBuffer.shift() for (const res of sseClients) { try { res.write(payload) @@ -52,7 +53,6 @@ class SseTransport extends winston.transports.Stream { } } -// Attach SSE transport once at module load time so it captures all log output. logger.add(new SseTransport()) function getPrimaryIp(): string { @@ -266,8 +266,15 @@ export function attachSignalingRoutes(server: any): void { Connection: "keep-alive", "X-Accel-Buffering": "no", }) - // Send a comment to establish the connection res.write(": connected\n\n") + // Replay buffered logs so the client sees history from before opening /debug + for (const entry of logBuffer) { + try { + res.write(entry) + } catch { + /* client gone already */ + } + } sseClients.add(res) req.on("close", () => sseClients.delete(res)) req.on("error", () => sseClients.delete(res)) diff --git a/src/server/welcome.ts b/src/server/welcome.ts new file mode 100644 index 00000000..87190c99 --- /dev/null +++ b/src/server/welcome.ts @@ -0,0 +1,48 @@ +import os from "node:os" +import { i18n } from "../utils/i18n" + +const str = i18n.en.server +function getLanIp(): string { + const ifaces = os.networkInterfaces() + for (const name of Object.keys(ifaces)) { + for (const iface of ifaces[name] ?? []) { + if (iface.family === "IPv4" && !iface.internal) { + return iface.address + } + } + } + return "127.0.0.1" +} + +export function printWelcome(port: number): void { + const local = `http://localhost:${port}` + const network = `http://${getLanIp()}:${port}` + const debug = `${local}/debug` + + const bold = (t: string) => `\x1b[1m${t}\x1b[0m` + const cyan = (t: string) => `\x1b[36m${t}\x1b[0m` + const green = (t: string) => `\x1b[32m${t}\x1b[0m` + const gray = (t: string) => `\x1b[90m${t}\x1b[0m` + + const divider = gray("────────────────────────────────────────────────────") + + const row = (label: string, value: string, color = green) => + `${gray(label.padEnd(10))} ${color(value)}` + + process.stdout.write( + [ + "", + ` ${bold(cyan("REIN"))}`, + ` ${divider}`, + "", + ` ${row(str.localLabel, local,cyan)}`, + ` ${row(str.networkLabel, network)}`, + ` ${row(str.debugLabel, debug, cyan)}`, + "", + ` ${divider}`, + ` ${gray("Status")} ${green("Running")}`, + ` ${gray("Port")} ${port}`, + "", + ].join("\n"), + ) +} diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index eae9c07d..51b80610 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -18,11 +18,21 @@ export const i18n = { establishingSecure: "Establishing secure connection", settingUpScreen: "Setting up screen sharing", checkNetwork: "Attempting to connect to the host.", + establishingConnection: "Establishing Connection", + negotiatingWebRtc: "Negotiating WebRTC session, please wait\u2026", }, errorComponent: { unknownError: "Unknown Error", unexpectedNetworkError: "An unexpected network error occurred.", }, + server: { + welcomeTitle: "Welcome to Rein", + localLabel: "Local", + networkLabel: "Network", + debugLabel: "Debug", + settingsLabel: "Settings", + readyLine: "Listening for connections", + }, }, } as const diff --git a/src/utils/logger.ts b/src/utils/logger.ts index d1afd798..79a9af4c 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -19,6 +19,17 @@ try { ) } +// Read verboseLogs flag from server-config.json (defaults to false if missing/unreadable) +let verboseLogs = false +try { + const configPath = path.join(__dirname, "..", "server-config.json") + const raw = fs.readFileSync(configPath, "utf-8") + const cfg = JSON.parse(raw) as { verboseLogs?: boolean } + verboseLogs = cfg.verboseLogs === true +} catch { + // Config unreadable — keep terminal silent +} + // Ensure the logger handles uncaught exceptions and rejections const logger = winston.createLogger({ level: "info", @@ -39,8 +50,8 @@ const logger = winston.createLogger({ rejectionHandlers: [new winston.transports.File({ filename: LOG_FILE })], }) -// If we're not in production then log to the `console` -if (process.env.NODE_ENV !== "production") { +// Only print to terminal when verboseLogs is explicitly enabled in server-config.json +if (verboseLogs) { logger.add( new winston.transports.Console({ format: winston.format.combine( diff --git a/vite.config.ts b/vite.config.ts index 501473c5..6eca4046 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,9 @@ import { nitro } from "nitro/vite" import { defineConfig } from "vite" import serverConfig from "./src/server-config.json" import { attachSignalingRoutes } from "./src/server/server" +import { printWelcome } from "./src/server/welcome" import react from "@vitejs/plugin-react" + const config = defineConfig({ base: "/", resolve: { @@ -18,9 +20,25 @@ const config = defineConfig({ name: "rein-server", async configureServer(server) { attachSignalingRoutes(server) + server.httpServer?.once("listening", () => { + const addr = server.httpServer?.address() + const port = + addr && typeof addr === "object" + ? addr.port + : serverConfig.frontendPort + printWelcome(port) + }) }, async configurePreviewServer(server) { attachSignalingRoutes(server) + server.httpServer?.once("listening", () => { + const addr = server.httpServer?.address() + const port = + addr && typeof addr === "object" + ? addr.port + : serverConfig.frontendPort + printWelcome(port) + }) }, }, devtools(), From 36f6c19a3cdb9e89de3f099d0315b5f8f15d47af Mon Sep 17 00:00:00 2001 From: pinjinx Date: Wed, 29 Jul 2026 21:59:54 +0530 Subject: [PATCH 3/6] [Architecture] CI/CD Fix --- biome.json | 2 +- package-lock.json | 822 ++++++++++++++++++++++-------------------- package.json | 4 +- src/routeTree.gen.ts | 62 ++-- src/server/welcome.ts | 2 +- 5 files changed, 464 insertions(+), 428 deletions(-) diff --git a/biome.json b/biome.json index cea5da6b..d08bc51a 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.5/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/package-lock.json b/package-lock.json index ddb84fc6..54a4211c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -412,9 +412,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.4.tgz", - "integrity": "sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz", + "integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -428,20 +428,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.4", - "@biomejs/cli-darwin-x64": "2.5.4", - "@biomejs/cli-linux-arm64": "2.5.4", - "@biomejs/cli-linux-arm64-musl": "2.5.4", - "@biomejs/cli-linux-x64": "2.5.4", - "@biomejs/cli-linux-x64-musl": "2.5.4", - "@biomejs/cli-win32-arm64": "2.5.4", - "@biomejs/cli-win32-x64": "2.5.4" + "@biomejs/cli-darwin-arm64": "2.5.5", + "@biomejs/cli-darwin-x64": "2.5.5", + "@biomejs/cli-linux-arm64": "2.5.5", + "@biomejs/cli-linux-arm64-musl": "2.5.5", + "@biomejs/cli-linux-x64": "2.5.5", + "@biomejs/cli-linux-x64-musl": "2.5.5", + "@biomejs/cli-win32-arm64": "2.5.5", + "@biomejs/cli-win32-x64": "2.5.5" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.4.tgz", - "integrity": "sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz", + "integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==", "cpu": [ "arm64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.4.tgz", - "integrity": "sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz", + "integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==", "cpu": [ "x64" ], @@ -473,9 +473,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.4.tgz", - "integrity": "sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz", + "integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.4.tgz", - "integrity": "sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz", + "integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==", "cpu": [ "arm64" ], @@ -513,9 +513,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.4.tgz", - "integrity": "sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz", + "integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==", "cpu": [ "x64" ], @@ -533,9 +533,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.4.tgz", - "integrity": "sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz", + "integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==", "cpu": [ "x64" ], @@ -553,9 +553,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.4.tgz", - "integrity": "sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz", + "integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==", "cpu": [ "arm64" ], @@ -570,9 +570,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.4.tgz", - "integrity": "sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz", + "integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==", "cpu": [ "x64" ], @@ -616,9 +616,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -640,9 +640,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", - "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", "dev": true, "funding": [ { @@ -657,7 +657,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.2.1" + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -691,9 +691,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", - "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -1060,9 +1060,9 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", - "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "dependencies": { @@ -1113,9 +1113,9 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", - "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "optional": true, @@ -1616,12 +1616,6 @@ "node": ">= 8" } }, - "node_modules/@fidm/x509/node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -2437,9 +2431,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", "cpu": [ "arm" ], @@ -2450,9 +2444,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", "cpu": [ "arm64" ], @@ -2463,9 +2457,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", "cpu": [ "arm64" ], @@ -2476,9 +2470,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", "cpu": [ "x64" ], @@ -2489,9 +2483,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", "cpu": [ "arm64" ], @@ -2502,9 +2496,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", "cpu": [ "x64" ], @@ -2515,9 +2509,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", "cpu": [ "arm" ], @@ -2531,9 +2525,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", "cpu": [ "arm" ], @@ -2547,9 +2541,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", "cpu": [ "arm64" ], @@ -2563,9 +2557,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", "cpu": [ "arm64" ], @@ -2579,9 +2573,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", "cpu": [ "loong64" ], @@ -2595,9 +2589,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", "cpu": [ "loong64" ], @@ -2611,9 +2605,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", "cpu": [ "ppc64" ], @@ -2627,9 +2621,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", "cpu": [ "ppc64" ], @@ -2643,9 +2637,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", "cpu": [ "riscv64" ], @@ -2659,9 +2653,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", "cpu": [ "riscv64" ], @@ -2675,9 +2669,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", "cpu": [ "s390x" ], @@ -2691,9 +2685,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", "cpu": [ "x64" ], @@ -2707,9 +2701,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", "cpu": [ "x64" ], @@ -2723,9 +2717,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", "cpu": [ "x64" ], @@ -2736,9 +2730,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", "cpu": [ "arm64" ], @@ -2749,9 +2743,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", "cpu": [ "arm64" ], @@ -2762,9 +2756,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", "cpu": [ "ia32" ], @@ -2775,9 +2769,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", "cpu": [ "x64" ], @@ -2788,9 +2782,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", "cpu": [ "x64" ], @@ -3243,9 +3237,9 @@ } }, "node_modules/@tanstack/devtools": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.12.5.tgz", - "integrity": "sha512-JdxTSeVdjJheycgz4c7qbldNKDCEDWlWr1l9dZBhd9sOmRBT5Z70ka9Eb8mb+FUnalcOIB62IDSR/iSxAIUD8Q==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.13.0.tgz", + "integrity": "sha512-p/nOH9bS/OO/u3402zPjoGu+Mz6Fzi/iRqJuYghuuYRUY32kZt+C0/d+pP/bi6/2JTi1FdT6oEXI2lWlA5tXxw==", "dev": true, "license": "MIT", "dependencies": { @@ -3444,9 +3438,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", - "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", "license": "MIT", "peer": true, "funding": { @@ -3455,13 +3449,13 @@ } }, "node_modules/@tanstack/react-devtools": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.10.8.tgz", - "integrity": "sha512-YJV6YttQf9lhhPbPBLULgy1eScEvJUMsCS26mjg9hfBKgAJQA5sF9zvzorDzW9Ob6o/asoXikO81JnRUVuFX0Q==", + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.10.9.tgz", + "integrity": "sha512-lS6mtccEmUaodsWiRORGM/MGKT0jgzcy5v+eY6pzOPxEgzTHUDhca+WGxShFqKxmF4oneRxXjww1gkvMrWq6uw==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/devtools": "0.12.5" + "@tanstack/devtools": "0.13.0" }, "engines": { "node": ">=18" @@ -3478,13 +3472,13 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", - "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", "license": "MIT", "peer": true, "dependencies": { - "@tanstack/query-core": "5.101.2" + "@tanstack/query-core": "5.101.4" }, "funding": { "type": "github", @@ -3569,18 +3563,18 @@ } }, "node_modules/@tanstack/react-start": { - "version": "1.168.28", - "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.168.28.tgz", - "integrity": "sha512-UyJ/OYBw7x8MQclyXKlbg72qzSTK6Do4AugHcx0LoagHNg6tQgvF0l2HZqll2CrxKAlv2Ar8+0gEkHnFYxxI5Q==", + "version": "1.168.32", + "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.168.32.tgz", + "integrity": "sha512-y1WXHo+jPfHxiuuN1m+br06IcriiBQnEWryBdbKdEOS5vw2PmnOj+Cgf1/YcGOqtSougScWFeE2rL1FXWvsLLg==", "license": "MIT", "dependencies": { "@tanstack/react-router": "1.170.18", "@tanstack/react-start-client": "1.168.16", - "@tanstack/react-start-rsc": "0.1.27", + "@tanstack/react-start-rsc": "0.1.31", "@tanstack/react-start-server": "1.167.22", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.14", - "@tanstack/start-plugin-core": "1.171.20", + "@tanstack/start-plugin-core": "1.171.24", "@tanstack/start-server-core": "1.169.17", "pathe": "^2.0.3" }, @@ -3632,9 +3626,9 @@ } }, "node_modules/@tanstack/react-start-rsc": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/@tanstack/react-start-rsc/-/react-start-rsc-0.1.27.tgz", - "integrity": "sha512-4Gt8+XHRhcYJjfw1rTlkW5ZxRWcp6AmvPoBf1p7ysOSVlnfVitQBqgAAFNGy+d745vWfoz1/JWqyFBP8IVVYYg==", + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-rsc/-/react-start-rsc-0.1.31.tgz", + "integrity": "sha512-WxjkXYflq550vTNJpdPyMaPC+Vyh88L5wOL+SiDTjPMGne9ad7FZmoJxqfCFEv1e7HVKMH/mMoE8619TsTNVzQ==", "license": "MIT", "dependencies": { "@tanstack/react-router": "1.170.18", @@ -3642,7 +3636,7 @@ "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.14", "@tanstack/start-fn-stubs": "1.162.0", - "@tanstack/start-plugin-core": "1.171.20", + "@tanstack/start-plugin-core": "1.171.24", "@tanstack/start-server-core": "1.169.17", "@tanstack/start-storage-context": "1.167.17", "pathe": "^2.0.3" @@ -3760,9 +3754,9 @@ } }, "node_modules/@tanstack/router-generator": { - "version": "1.167.19", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.167.19.tgz", - "integrity": "sha512-db3AQGLinf8ZQ8AKMyxLVWwHIFZxtx+zLktvPXv/nFH/B793/Z7lKLl4dUWM3JpAluynDSVVKaTWQVTAgTYmVA==", + "version": "1.167.21", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.167.21.tgz", + "integrity": "sha512-m3oXZyienj8owialdyoZ0txHQrnEx/Ra+D9kWtar5fC2cWZr5Pvxl86VY2mX5RRLC5QLKLeRGT1x4HV95wHVDQ==", "license": "MIT", "dependencies": { "@babel/types": "^7.28.5", @@ -3783,16 +3777,16 @@ } }, "node_modules/@tanstack/router-plugin": { - "version": "1.168.20", - "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.168.20.tgz", - "integrity": "sha512-G5S63xDr2AxADtWP+0tQtJsduijT/Mk85hDh3Od4ct8UV0PHr2f/H1CGl72lYWLgcWRMn6bKJoQhz27QFIgLRQ==", + "version": "1.168.23", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.168.23.tgz", + "integrity": "sha512-0+PIcvnaAimFwjoEIeV3h7LKjzC8zNnp7pH2UamdKwQ9QlY99WU9V0Xl0zbM0i9hrUa/mKgWPDAzELmPUu5fMA==", "license": "MIT", "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", - "@tanstack/router-generator": "1.167.19", + "@tanstack/router-generator": "1.167.21", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", @@ -3903,17 +3897,17 @@ } }, "node_modules/@tanstack/start-plugin-core": { - "version": "1.171.20", - "resolved": "https://registry.npmjs.org/@tanstack/start-plugin-core/-/start-plugin-core-1.171.20.tgz", - "integrity": "sha512-s2n82ykGXGkDSoJwGZP85vC9Dv5vAN6fya5TCSm/cvUn+r/g30iDR1x+Ptizas3fIvcracSOOvRnma4RJ8Orww==", + "version": "1.171.24", + "resolved": "https://registry.npmjs.org/@tanstack/start-plugin-core/-/start-plugin-core-1.171.24.tgz", + "integrity": "sha512-l/tm+T0ntXHeIzr9kJDTJ2IDNZC0yFazjkvbEVeZsDOrJ8F+HiZmY+tXYqI5/nDYkwxY0DVQr+kGsTRVb6y2Jw==", "license": "MIT", "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", - "@tanstack/router-generator": "1.167.19", - "@tanstack/router-plugin": "1.168.20", + "@tanstack/router-generator": "1.167.21", + "@tanstack/router-plugin": "1.168.23", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.17", "exsolve": "^1.0.7", @@ -4726,39 +4720,6 @@ "readable-stream": "^2.0.6" } }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "optional": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/are-we-there-yet/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "optional": true - }, - "node_modules/are-we-there-yet/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4999,9 +4960,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.43", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", - "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", + "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -5020,6 +4981,13 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", + "optional": true + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -5057,22 +5025,22 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "funding": [ { "type": "opencollective", @@ -5089,9 +5057,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, @@ -5463,16 +5431,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone-response/node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5518,9 +5476,9 @@ } }, "node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", "license": "MIT", "engines": { "node": ">=12.20" @@ -5947,9 +5905,9 @@ } }, "node_modules/daisyui": { - "version": "5.6.18", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.6.18.tgz", - "integrity": "sha512-6y9rboRQl8fosnusy/5pg11yp+H0LtK3YdtfS4Vf2QVnaYOKasNdYm6hDxeiT6bwZPdl4WRPzVIlIApdo2ssBg==", + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.4.tgz", + "integrity": "sha512-zKzFIwvEvn2QvE1TbUUtDnYFOqWoxeZh+j4QXfhkZKYAT++pO2ijyT9rkFm0GIPDWR1/oo9hHZzd96AsTK0iZg==", "dev": true, "license": "MIT", "funding": { @@ -6145,6 +6103,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -6396,39 +6367,6 @@ "readable-stream": "^2.0.2" } }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -6665,9 +6603,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.392", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", - "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", "license": "ISC" }, "node_modules/electron-winstaller": { @@ -6770,9 +6708,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.24.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", - "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7097,9 +7035,9 @@ "optional": true }, "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -7684,13 +7622,13 @@ "license": "ISC" }, "node_modules/h3": { - "version": "2.0.1-rc.25", - "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.25.tgz", - "integrity": "sha512-uIK++Up28xDt4n0+bSWfVssRivNXpUmMmA6CCiK6j9+utLcBMcFK97Gn9PlSZwAPW9oLy3CjGJ4ag40CY9YYtQ==", + "version": "2.0.1-rc.26", + "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.26.tgz", + "integrity": "sha512-GDxlvDsKxgjRvG5UBRJYyGJTMWLV30CJ4cV+e7QCTgftDHihrvio1fVPbNembhEr6J4WNm8IWy3fookgyTweLw==", "license": "MIT", "dependencies": { "rou3": "^0.9.1", - "srvx": "^0.11.22" + "srvx": "^0.12.4" }, "bin": { "h3": "bin/h3.mjs" @@ -7738,6 +7676,18 @@ "integrity": "sha512-z/sSmzvtwMDDnxsPVhfWMuG6F6mbmhFDXoVqLmMfbpDD9qfV3GDmSQpf0+W296/ZDIpW2wcMmBfpVFzcnOi/nA==", "license": "MIT" }, + "node_modules/h3/node_modules/srvx": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.12.4.tgz", + "integrity": "sha512-RixzFlMn3dvzDTpKIAXhXrqL4cy6vScNCP0VVwgVbUBU94o+DiXLsFnAZetbyKAEnK6Ox3hzHXWGsyv/Iibv7g==", + "license": "MIT", + "bin": { + "srvx": "bin/srvx.mjs" + }, + "engines": { + "node": ">=20.16.0" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -8387,9 +8337,9 @@ } }, "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.3.tgz", + "integrity": "sha512-E9y1AsgYGlaxMhcZzHr8y96QF2U5XzA12GGVAfbWqIubTwPNMXQarfBzePNXHe0xtIEtNd6ifAv3GAKYGUeBAQ==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -8849,16 +8799,13 @@ } }, "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/minimatch": { @@ -9046,13 +8993,13 @@ } }, "node_modules/nitro-nightly": { - "version": "3.0.1-20260717-080150-bfc2f5ef", - "resolved": "https://registry.npmjs.org/nitro-nightly/-/nitro-nightly-3.0.1-20260717-080150-bfc2f5ef.tgz", - "integrity": "sha512-MImiqT2V4NboxjLBF5A6n0jgu9iMXZ2b6GYfYb+TMZ1DFCN/nlDTRyu0rijCKTFqzSzCa1duvVsyc9jsvWYcyw==", + "version": "3.0.1-20260722-224121-77b77ffb", + "resolved": "https://registry.npmjs.org/nitro-nightly/-/nitro-nightly-3.0.1-20260722-224121-77b77ffb.tgz", + "integrity": "sha512-29DHcZ8+KRozWOq9VrEyBMIzx1jJVv9fAeHDiQkiaS9I9fT5FAYmofwAeOYT6pgg31unbLVGvDkcQ9U3mIxvFA==", "license": "MIT", "dependencies": { "consola": "^3.4.2", - "crossws": "^0.4.9", + "crossws": "^0.4.10", "db0": "^0.3.4", "env-runner": "^0.1.16", "h3": "^2.0.1-rc.25", @@ -9062,7 +9009,7 @@ "ocache": "^0.2.0", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", - "rolldown": "^1.1.5", + "rolldown": "^1.2.0", "rou3": "^0.9.1", "srvx": "^0.11.22", "unenv": "^2.0.0-rc.24", @@ -9079,7 +9026,7 @@ "dotenv": "*", "giget": "*", "jiti": "^2.7.0", - "rollup": "^4.62.0", + "rollup": "^4.62.2", "vite": "^7 || ^8", "xml2js": "^0.6.2", "zephyr-agent": "^1.1.2" @@ -9130,6 +9077,16 @@ } } }, + "node_modules/nitro-nightly/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "extraneous": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/nitro-nightly/node_modules/ocache": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ocache/-/ocache-0.2.0.tgz", @@ -9247,6 +9204,16 @@ } } }, + "node_modules/nitro/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "extraneous": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/nitro/node_modules/unstorage": { "version": "2.0.0-alpha.7", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz", @@ -9457,9 +9424,9 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", - "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "dev": true, "license": "MIT", "engines": { @@ -9849,9 +9816,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "devOptional": true, "funding": [ { @@ -9869,7 +9836,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9929,9 +9896,9 @@ } }, "node_modules/prettier": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", - "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -9958,6 +9925,13 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -10213,24 +10187,24 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-icons": { @@ -10243,10 +10217,11 @@ } }, "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.3.0", @@ -10295,19 +10270,28 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "devOptional": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -10322,9 +10306,9 @@ } }, "node_modules/recharts": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.0.tgz", - "integrity": "sha512-wulMvfncpIlmu2uFtRU/mE5/+NiVtASXkw2KdwJTdHs3WsASX0WxZlX+rpKgyn5BDbIhkPtCpUKkB9XNK5KE0w==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", "license": "MIT", "workspaces": [ "www" @@ -10587,9 +10571,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -10603,31 +10587,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", "fsevents": "~2.3.2" } }, @@ -10699,9 +10683,9 @@ } }, "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -10761,18 +10745,18 @@ } }, "node_modules/seroval": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.5.tgz", - "integrity": "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.6.tgz", + "integrity": "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.5.tgz", - "integrity": "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.6.tgz", + "integrity": "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==", "license": "MIT", "engines": { "node": ">=10" @@ -10973,6 +10957,13 @@ "node": ">=0.10.0" } }, + "node_modules/sshpk/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", + "optional": true + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -11016,14 +11007,20 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -11128,9 +11125,9 @@ } }, "node_modules/tar": { - "version": "7.5.20", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", - "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -11458,11 +11455,10 @@ } }, "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense", - "optional": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" }, "node_modules/type-fest": { "version": "0.13.1", @@ -11499,9 +11495,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "optional": true, @@ -12676,14 +12672,14 @@ } }, "node_modules/wait-on": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz", - "integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz", + "integrity": "sha512-PymrLXHLBM1Ju/Xspb2ADUhbPSMvbnuNvy/mN2hWtpbJ3da0h3Ky1LqwKPG5QSVR57liyO0iUpfipYl/s5qNvA==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.16.0", - "joi": "^18.2.1", + "axios": "^1.18.1", + "joi": "^18.2.3", "lodash": "^4.18.1", "minimist": "^1.2.8", "rxjs": "^7.8.2" @@ -12788,12 +12784,6 @@ "node": ">=16" } }, - "node_modules/werift-dtls/node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, "node_modules/werift-ice": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/werift-ice/-/werift-ice-0.2.2.tgz", @@ -12856,12 +12846,6 @@ } } }, - "node_modules/werift/node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -12971,6 +12955,34 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -13063,6 +13075,30 @@ "node": ">=18" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 0eb809f2..feaa0b79 100644 --- a/package.json +++ b/package.json @@ -101,9 +101,9 @@ } }, "allowScripts": { - "electron@40.10.5": true, "electron-winstaller@5.4.0": true, "esbuild@0.28.1": true, - "koffi@2.16.2": true + "electron@40.10.6": true, + "koffi@2.16.3": true } } diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index cac4c23a..2463a92e 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,19 +9,14 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as TrackpadRouteImport } from './routes/trackpad' -import { Route as SettingsRouteImport } from './routes/settings' -import { Route as DebugRouteImport } from './routes/debug' import { Route as IndexRouteImport } from './routes/index' +import { Route as DebugRouteImport } from './routes/debug' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as TrackpadRouteImport } from './routes/trackpad' -const TrackpadRoute = TrackpadRouteImport.update({ - id: '/trackpad', - path: '/trackpad', - getParentRoute: () => rootRouteImport, -} as any) -const SettingsRoute = SettingsRouteImport.update({ - id: '/settings', - path: '/settings', +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', getParentRoute: () => rootRouteImport, } as any) const DebugRoute = DebugRouteImport.update({ @@ -29,9 +24,14 @@ const DebugRoute = DebugRouteImport.update({ path: '/debug', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const TrackpadRoute = TrackpadRouteImport.update({ + id: '/trackpad', + path: '/trackpad', getParentRoute: () => rootRouteImport, } as any) @@ -71,18 +71,11 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/trackpad': { - id: '/trackpad' - path: '/trackpad' - fullPath: '/trackpad' - preLoaderRoute: typeof TrackpadRouteImport - parentRoute: typeof rootRouteImport - } - '/settings': { - id: '/settings' - path: '/settings' - fullPath: '/settings' - preLoaderRoute: typeof SettingsRouteImport + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } '/debug': { @@ -92,11 +85,18 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DebugRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/trackpad': { + id: '/trackpad' + path: '/trackpad' + fullPath: '/trackpad' + preLoaderRoute: typeof TrackpadRouteImport parentRoute: typeof rootRouteImport } } diff --git a/src/server/welcome.ts b/src/server/welcome.ts index 87190c99..8960d08b 100644 --- a/src/server/welcome.ts +++ b/src/server/welcome.ts @@ -35,7 +35,7 @@ export function printWelcome(port: number): void { ` ${bold(cyan("REIN"))}`, ` ${divider}`, "", - ` ${row(str.localLabel, local,cyan)}`, + ` ${row(str.localLabel, local, cyan)}`, ` ${row(str.networkLabel, network)}`, ` ${row(str.debugLabel, debug, cyan)}`, "", From 69d8cef004b8f5f95cddef0becc48fca5d5ef304 Mon Sep 17 00:00:00 2001 From: pinjinx Date: Thu, 30 Jul 2026 20:46:40 +0530 Subject: [PATCH 4/6] [Architecture] Code Rabbit Fixes --- src/contexts/ConnectionProvider.tsx | 18 ++-- src/contexts/DebugContext.tsx | 18 +++- src/hooks/useWebRtcStream.ts | 40 ++++++++- src/routes/debug.tsx | 36 ++++---- src/server/constants.ts | 2 + src/server/gstreamer/gstManager.ts | 18 +++- src/server/net.ts | 15 ++++ src/server/server.ts | 64 +++++++++----- src/server/webRTC.ts | 124 +++++++++++++++++++--------- src/server/welcome.ts | 17 +--- src/utils/i18n.ts | 3 + src/utils/logger.ts | 22 +++-- vite.config.ts | 6 +- 13 files changed, 266 insertions(+), 117 deletions(-) create mode 100644 src/server/net.ts diff --git a/src/contexts/ConnectionProvider.tsx b/src/contexts/ConnectionProvider.tsx index e17fcc1a..7d348b96 100644 --- a/src/contexts/ConnectionProvider.tsx +++ b/src/contexts/ConnectionProvider.tsx @@ -66,6 +66,8 @@ export function ConnectionProvider({ } }, []) + const lastReportTimeRef = useRef(0) + const registerDataChannel = useCallback( (unorderedDc: RTCDataChannel, orderedDc: RTCDataChannel) => { unorderedDcRef.current = unorderedDc @@ -96,12 +98,16 @@ export function ConnectionProvider({ if (parsed.type === "pong" && parsed.timestamp) { const ms = Date.now() - parsed.timestamp setLatency(ms) - // Report measured RTT to the host debug dashboard (best-effort) - fetch("/api/debug/report-latency", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ latencyMs: ms }), - }).catch(() => {}) + // Report measured RTT to the host debug dashboard (throttled every 10s) + const now = Date.now() + if (now - lastReportTimeRef.current > 10000) { + lastReportTimeRef.current = now + fetch("/api/debug/report-latency", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ latencyMs: ms }), + }).catch(() => {}) + } } } catch {} } diff --git a/src/contexts/DebugContext.tsx b/src/contexts/DebugContext.tsx index 19140c5d..a3733ddb 100644 --- a/src/contexts/DebugContext.tsx +++ b/src/contexts/DebugContext.tsx @@ -4,6 +4,7 @@ import { useCallback, useContext, useEffect, + useMemo, useRef, useState, } from "react" @@ -40,7 +41,15 @@ function nowLabel(): string { } function serialize(args: unknown[]): { message: string; details?: string } { - const parts = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))) + const parts = args.map((a) => { + if (typeof a === "string") return a + if (a instanceof Error) return a.stack || a.message + try { + return JSON.stringify(a) + } catch { + return String(a) + } + }) const [message, ...rest] = parts return { message: message ?? "", @@ -131,8 +140,13 @@ export function DebugProvider({ children }: { children: React.ReactNode }) { } }, [addLog]) + const contextValue = useMemo( + () => ({ clientLogs, clearClientLogs }), + [clientLogs, clearClientLogs], + ) + return ( - + {children} ) diff --git a/src/hooks/useWebRtcStream.ts b/src/hooks/useWebRtcStream.ts index a01e8ed8..63d9d160 100644 --- a/src/hooks/useWebRtcStream.ts +++ b/src/hooks/useWebRtcStream.ts @@ -53,12 +53,20 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { if (wsRef.current) { try { + wsRef.current.onclose = null + wsRef.current.onerror = null + wsRef.current.onmessage = null + wsRef.current.onopen = null wsRef.current.close() } catch {} wsRef.current = null } if (pcRef.current) { try { + pcRef.current.onconnectionstatechange = null + pcRef.current.ontrack = null + pcRef.current.ondatachannel = null + pcRef.current.onicecandidate = null pcRef.current.close() } catch {} pcRef.current = null @@ -92,12 +100,20 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { isRetryingRef.current = false if (wsRef.current) { try { + wsRef.current.onclose = null + wsRef.current.onerror = null + wsRef.current.onmessage = null + wsRef.current.onopen = null wsRef.current.close() } catch {} wsRef.current = null } if (pcRef.current) { try { + pcRef.current.onconnectionstatechange = null + pcRef.current.ontrack = null + pcRef.current.ondatachannel = null + pcRef.current.onicecandidate = null pcRef.current.close() } catch {} pcRef.current = null @@ -125,7 +141,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { } const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" - const wsUrl = `${protocol}//${window.location.host}/ws` + const wsUrl = `${protocol}//${window.location.host}/ws?token=${encodeURIComponent(token)}` const ws = new WebSocket(wsUrl) wsRef.current = ws @@ -186,12 +202,20 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { ws.onmessage = async (event) => { if (isDisposed || isRetryingRef.current) return try { - const msg = JSON.parse(event.data) - if (msg.type === "offer") { + const msg = JSON.parse(event.data) as { + type: string + sdp?: RTCSessionDescriptionInit + candidate?: RTCIceCandidateInit + errorType?: string + message?: string + } + if (msg.type === "offer" && msg.sdp) { await pc.setRemoteDescription(msg.sdp) const answer = await pc.createAnswer() await pc.setLocalDescription(answer) - ws.send(JSON.stringify({ type: "answer", sdp: answer })) + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "answer", sdp: answer })) + } while (iceQueue.length > 0) { const cand = iceQueue.shift() @@ -271,9 +295,17 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { isDisposed = true clearInterval(statsInterval) try { + ws.onclose = null + ws.onerror = null + ws.onmessage = null + ws.onopen = null ws.close() } catch {} try { + pc.onconnectionstatechange = null + pc.ontrack = null + pc.ondatachannel = null + pc.onicecandidate = null pc.close() } catch {} setTrackActive(false) diff --git a/src/routes/debug.tsx b/src/routes/debug.tsx index 42e50c56..36f333bf 100644 --- a/src/routes/debug.tsx +++ b/src/routes/debug.tsx @@ -131,11 +131,17 @@ function DebugScreen() { { t: nowLabel(), recvKBps: 0, sentKBps: 0, latencyMs: 0 }, ]) }, [selectedSessionId]) + const serverStateRef = useRef(serverState) + useEffect(() => { + serverStateRef.current = serverState + }, [serverState]) + useEffect(() => { const id = setInterval(() => { + const current = serverStateRef.current const src = selectedSessionId - ? serverState.sessions.filter((s) => s.id === selectedSessionId) - : serverState.sessions + ? current.sessions.filter((s) => s.id === selectedSessionId) + : current.sessions const totalRecv = src.reduce((sum, s) => sum + (s.bytesRecv ?? 0), 0) const totalSent = src.reduce((sum, s) => sum + (s.bytesSent ?? 0), 0) const deltaRecv = @@ -151,19 +157,19 @@ function DebugScreen() { t: nowLabel(), recvKBps: Math.round(deltaRecv * 10) / 10, sentKBps: Math.round(deltaSent * 10) / 10, - latencyMs: serverState.latencyMs ?? 0, + latencyMs: current.latencyMs ?? 0, }, ] return next.length > 60 ? next.slice(-60) : next }) }, 1000) return () => clearInterval(id) - }, [selectedSessionId, serverState.sessions, serverState.latencyMs]) + }, [selectedSessionId]) const [consoleTab, setConsoleTab] = useState<"server" | "client">("server") const [levelFilter, setLevelFilter] = useState("ALL") const [searchQuery, setSearchQuery] = useState("") - const [_copiedId, setCopiedId] = useState(null) + const [copiedId, setCopiedId] = useState(null) const logEndRef = useRef(null) const copyToClipboard = (id: string, text: string) => { const done = () => { @@ -296,11 +302,7 @@ function DebugScreen() {

Network

- {selectedSessionId - ? selectedSessionId - ? selectedSessionId - : "Invalid Id" - : "all sessions"} + {selectedSessionId ? selectedSessionId : "all sessions"}
{/* Live-value strip */} @@ -475,16 +477,7 @@ function DebugScreen() { return ( )) )} diff --git a/src/server/constants.ts b/src/server/constants.ts index 7a343dba..38c75069 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -22,3 +22,5 @@ export const MAX_KEY_LENGTH = 50 export const ACCEL_THRESHOLD = 1 export const ACCEL_FACTOR = 0.8 export const ACCEL_EXPONENT = 1.2 +export const RTP_HOST = "127.0.0.1" +export const RTP_PORT = 5004 diff --git a/src/server/gstreamer/gstManager.ts b/src/server/gstreamer/gstManager.ts index 1f1318c1..033b3fff 100644 --- a/src/server/gstreamer/gstManager.ts +++ b/src/server/gstreamer/gstManager.ts @@ -5,6 +5,7 @@ import path from "node:path" import logger from "../../utils/logger" import { type CaptureProvider, createCaptureProvider } from "./captureProvider" import { resolveGstPaths } from "./gstPaths" +import { RTP_HOST, RTP_PORT } from "../constants" export class GstManager { private process: ChildProcess | null = null @@ -40,8 +41,8 @@ export class GstManager { "pt=96", "!", "udpsink", - "host=127.0.0.1", - "port=5004", + `host=${RTP_HOST}`, + `port=${RTP_PORT}`, "sync=false", "async=false", ) @@ -66,6 +67,7 @@ export class GstManager { } catch (error) { logger.error(`Capture initialization failed: ${String(error)}`) await this.cleanup() + throw error } } @@ -129,8 +131,18 @@ export class GstManager { public async stop(): Promise { if (this.process) { logger.info("Terminating GStreamer video pipeline") - this.process.kill("SIGTERM") + const proc = this.process this.process = null + proc.kill("SIGTERM") + const killTimer = setTimeout(() => { + if (proc.exitCode === null) { + logger.warn( + "GStreamer process did not exit on SIGTERM, sending SIGKILL", + ) + proc.kill("SIGKILL") + } + }, 2000) + proc.once("close", () => clearTimeout(killTimer)) } await this.cleanup() } diff --git a/src/server/net.ts b/src/server/net.ts new file mode 100644 index 00000000..bef7a1e1 --- /dev/null +++ b/src/server/net.ts @@ -0,0 +1,15 @@ +import os from "node:os" + +export function getLanIp(): string { + const ifaces = os.networkInterfaces() + for (const name of Object.keys(ifaces)) { + const list = ifaces[name] + if (!list) continue + for (const iface of list) { + if (iface.family === "IPv4" && !iface.internal) { + return iface.address + } + } + } + return "127.0.0.1" +} diff --git a/src/server/server.ts b/src/server/server.ts index a244c409..b7bb25ee 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,5 +1,4 @@ import type { IncomingMessage, ServerResponse } from "node:http" -import os from "node:os" import { Transform } from "node:stream" import logger from "../utils/logger" import winston from "winston" @@ -12,6 +11,7 @@ import { import { GstManager } from "./gstreamer/gstManager" import { WebRTCManager } from "./webRTC" import type { InputConfig } from "./types" +import { getLanIp } from "./net" let gstManager: GstManager | null = null let webrtcManager: WebRTCManager | null = null @@ -55,24 +55,19 @@ class SseTransport extends winston.transports.Stream { logger.add(new SseTransport()) -function getPrimaryIp(): string { - const interfaces = os.networkInterfaces() - for (const name of Object.keys(interfaces)) { - const ifaceList = interfaces[name] - if (!ifaceList) continue - for (const iface of ifaceList) { - if (iface.family === "IPv4" && !iface.internal) { - return iface.address - } - } - } - return "127.0.0.1" -} +const MAX_BODY_BYTES = 1024 * 1024 // 1MB limit function parseJsonBody(req: IncomingMessage): Promise { return new Promise((resolve, reject) => { let body = "" + let size = 0 req.on("data", (chunk) => { + size += chunk.length + if (size > MAX_BODY_BYTES) { + req.destroy() + reject(new Error("Payload too large")) + return + } body += chunk }) req.on("end", () => { @@ -180,11 +175,20 @@ export function attachSignalingRoutes(server: any): void { if (pathname === "/api/host/stop" && req.method === "POST") { if (!requireAuth(req, res)) return + hostStatus = "stopped" if (gstManager) { - gstManager.stop() + gstManager + .stop() + .then(() => { + json(res, 200, { status: hostStatus }) + }) + .catch((err) => { + logger.error(`Error stopping GStreamer: ${err}`) + json(res, 500, { error: "Failed to stop host engine" }) + }) + } else { + json(res, 200, { status: hostStatus }) } - hostStatus = "stopped" - json(res, 200, { status: hostStatus }) return } @@ -196,7 +200,7 @@ export function attachSignalingRoutes(server: any): void { if (pathname === "/api/host/ip" && req.method === "GET") { if (!requireAuth(req, res)) return - json(res, 200, { ip: getPrimaryIp() }) + json(res, 200, { ip: getLanIp() }) return } @@ -233,6 +237,7 @@ export function attachSignalingRoutes(server: any): void { } if (pathname === "/api/debug/sessions" && req.method === "GET") { + if (!requireAuth(req, res)) return const sessions = webrtcManager?.getSessions() ?? [] const inputConnectionCount = sessions.filter( (s) => s.hasInputConnection, @@ -248,6 +253,7 @@ export function attachSignalingRoutes(server: any): void { } if (pathname === "/api/debug/report-latency" && req.method === "POST") { + if (!requireAuth(req, res)) return parseJsonBody<{ latencyMs?: number }>(req) .then((body) => { if (typeof body.latencyMs === "number" && body.latencyMs >= 0) { @@ -260,6 +266,7 @@ export function attachSignalingRoutes(server: any): void { } if (pathname === "/api/debug/logs" && req.method === "GET") { + if (!requireAuth(req, res)) return res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", @@ -276,8 +283,21 @@ export function attachSignalingRoutes(server: any): void { } } sseClients.add(res) - req.on("close", () => sseClients.delete(res)) - req.on("error", () => sseClients.delete(res)) + const keepAliveTimer = setInterval(() => { + try { + res.write(": keep-alive\n\n") + } catch { + sseClients.delete(res) + clearInterval(keepAliveTimer) + } + }, 15000) + + const cleanupSse = () => { + sseClients.delete(res) + clearInterval(keepAliveTimer) + } + req.on("close", cleanupSse) + req.on("error", cleanupSse) return } @@ -293,7 +313,7 @@ export function attachSignalingRoutes(server: any): void { logger.info("Signaling HTTP routes and WebSocket attached") } -export function stopServer() { +export async function stopServer() { if (webrtcManager) webrtcManager.shutdown() - if (gstManager) gstManager.stop() + if (gstManager) await gstManager.stop() } diff --git a/src/server/webRTC.ts b/src/server/webRTC.ts index f063509b..6d2c8cdb 100644 --- a/src/server/webRTC.ts +++ b/src/server/webRTC.ts @@ -2,13 +2,18 @@ import { MediaStreamTrack, RTCPeerConnection, RTCRtpCodecParameters, + type RTCDataChannel, } from "werift" import { WebSocketServer, WebSocket } from "ws" import dgram from "node:dgram" +import crypto from "node:crypto" import { InputHandler } from "./InputHandler" import logger from "../utils/logger" import type { InputMessage, InputConfig } from "./types" import fs from "node:fs" +import { fileURLToPath } from "node:url" +import { isKnownToken } from "./tokenStore" +import { RTP_HOST, RTP_PORT } from "./constants" interface ClientSession { ws: WebSocket @@ -18,6 +23,8 @@ interface ClientSession { sessionId: string bytesRecv: number bytesSent: number + dcUnordered: RTCDataChannel + dcOrdered: RTCDataChannel } import type { IncomingMessage } from "node:http" @@ -37,31 +44,52 @@ export interface SessionSnapshot { export class WebRTCManager { private wss: WebSocketServer private udpSocket: dgram.Socket + private serverRef: EventEmitter + private upgradeHandler: ( + request: IncomingMessage, + socket: Duplex, + head: Buffer, + ) => void private clients = new Map() private sessionCreatedAt = new Map() constructor(server: EventEmitter) { this.wss = new WebSocketServer({ noServer: true }) this.udpSocket = dgram.createSocket("udp4") + this.serverRef = server - server.on( - "upgrade", - (request: IncomingMessage, socket: Duplex, head: Buffer) => { - try { - const url = new URL( - request.url || "", - `http://${request.headers.host || "localhost"}`, - ) - if (url.pathname === "/ws") { - this.wss.handleUpgrade(request, socket, head, (ws) => { - this.wss.emit("connection", ws, request) - }) + this.upgradeHandler = ( + request: IncomingMessage, + socket: Duplex, + head: Buffer, + ) => { + try { + const url = new URL( + request.url || "", + `http://${request.headers.host || "localhost"}`, + ) + if (url.pathname === "/ws") { + const addr = request.socket.remoteAddress + const isLocal = + addr === "127.0.0.1" || + addr === "::1" || + addr === "::ffff:127.0.0.1" + const token = url.searchParams.get("token") + if (!isLocal && (!token || !isKnownToken(token))) { + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n") + socket.destroy() + return } - } catch (_err) { - // Ignore invalid URLs or non-matching upgrades + this.wss.handleUpgrade(request, socket, head, (ws) => { + this.wss.emit("connection", ws, request) + }) } - }, - ) + } catch (_err) { + // Ignore invalid URLs or non-matching upgrades + } + } + + this.serverRef.on("upgrade", this.upgradeHandler) this.setupUdpSocket() this.setupWebSocketServer() @@ -70,28 +98,34 @@ export class WebRTCManager { private setupUdpSocket() { this.udpSocket.on("error", (err) => { logger.error(`UDP socket error:\n${err.stack}`) - this.udpSocket.close() + try { + this.udpSocket.close() + } catch {} }) this.udpSocket.on("message", (msg) => { for (const client of this.clients.values()) { try { client.videoTrack.writeRtp(msg) - client.bytesRecv += msg.length + client.bytesSent += msg.length } catch (_err) { // Ignore individual track write errors } } }) - this.udpSocket.bind(5004, "127.0.0.1", () => { - logger.info("UDP socket listening for RTP packets on 127.0.0.1:5004") + this.udpSocket.bind(RTP_PORT, RTP_HOST, () => { + logger.info( + `UDP socket listening for RTP packets on ${RTP_HOST}:${RTP_PORT}`, + ) }) } private getInitialConfig(): Partial { try { - const configPath = "./src/server-config.json" + const configPath = fileURLToPath( + new URL("../server-config.json", import.meta.url), + ) if (fs.existsSync(configPath)) { const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8")) return { @@ -111,7 +145,7 @@ export class WebRTCManager { private setupWebSocketServer() { this.wss.on("connection", async (ws) => { - const sessionId = Math.random().toString(36).substring(7) + const sessionId = crypto.randomUUID() this.sessionCreatedAt.set(sessionId, Date.now()) logger.info(`Viewer connected via WebSocket: ${sessionId}`) @@ -166,19 +200,20 @@ export class WebRTCManager { }) const dcOrdered = pc.createDataChannel("input-ordered", { ordered: true }) - const handleDataMessage = (msg: Buffer | string) => { + const handleDataMessage = (dc: RTCDataChannel, msg: Buffer | string) => { try { const raw = typeof msg === "string" ? msg : msg.toString() const session = this.clients.get(sessionId) - if (session) session.bytesSent += raw.length + if (session) session.bytesRecv += raw.length const parsed = JSON.parse(raw) if (parsed.type === "ping") { const pong = JSON.stringify({ type: "pong", timestamp: parsed.timestamp, }) - dcUnordered.send(pong) - dcOrdered.send(pong) + if (dc.readyState === "open") { + dc.send(pong) + } return } inputHandler.handleMessage(parsed as InputMessage).catch((err) => { @@ -189,8 +224,10 @@ export class WebRTCManager { } } - dcUnordered.onMessage.subscribe((msg) => handleDataMessage(msg)) - dcOrdered.onMessage.subscribe((msg) => handleDataMessage(msg)) + dcUnordered.onMessage.subscribe((msg) => + handleDataMessage(dcUnordered, msg), + ) + dcOrdered.onMessage.subscribe((msg) => handleDataMessage(dcOrdered, msg)) const client: ClientSession = { ws, @@ -200,22 +237,20 @@ export class WebRTCManager { sessionId, bytesRecv: 0, bytesSent: 0, + dcUnordered, + dcOrdered, } this.clients.set(sessionId, client) pc.iceConnectionStateChange.subscribe((state) => { logger.info(`WebRTC ICE state for ${sessionId}: ${state}`) - if ( - state === "disconnected" || - state === "failed" || - state === "closed" - ) { + if (state === "failed" || state === "closed") { this.cleanupClient(sessionId) } }) pc.onIceCandidate.subscribe((candidate) => { - if (candidate) { + if (candidate && ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: "ice", candidate: candidate.toJSON() }), ) @@ -229,7 +264,7 @@ export class WebRTCManager { await pc.setRemoteDescription(msg.sdp) } else if (msg.type === "ice" && msg.candidate) { await pc.addIceCandidate(msg.candidate) - } else if (msg.type === "ping") { + } else if (msg.type === "ping" && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "pong" })) } } catch (err) { @@ -244,8 +279,9 @@ export class WebRTCManager { try { const offer = await pc.createOffer() await pc.setLocalDescription(offer) - console.log(offer.sdp) - ws.send(JSON.stringify({ type: "offer", sdp: offer })) + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "offer", sdp: offer })) + } } catch (err) { logger.error(`Failed to create offer: ${String(err)}`) this.cleanupClient(sessionId) @@ -270,12 +306,15 @@ export class WebRTCManager { public getSessions(): SessionSnapshot[] { const snapshots: SessionSnapshot[] = [] for (const [id, client] of this.clients) { + const hasInputConnection = + client.dcUnordered.readyState === "open" || + client.dcOrdered.readyState === "open" snapshots.push({ id, state: client.pc.iceConnectionState ?? "new", createdAt: this.sessionCreatedAt.get(id) ?? Date.now(), - sseViewerCount: client.ws.readyState === 1 ? 1 : 0, - hasInputConnection: true, + sseViewerCount: client.ws.readyState === WebSocket.OPEN ? 1 : 0, + hasInputConnection, bytesRecv: client.bytesRecv, bytesSent: client.bytesSent, }) @@ -290,8 +329,13 @@ export class WebRTCManager { } public shutdown() { + if (this.serverRef && typeof this.serverRef.off === "function") { + this.serverRef.off("upgrade", this.upgradeHandler) + } this.wss.close() - this.udpSocket.close() + try { + this.udpSocket.close() + } catch {} for (const sessionId of this.clients.keys()) { this.cleanupClient(sessionId) } diff --git a/src/server/welcome.ts b/src/server/welcome.ts index 8960d08b..6a396d9c 100644 --- a/src/server/welcome.ts +++ b/src/server/welcome.ts @@ -1,18 +1,7 @@ -import os from "node:os" import { i18n } from "../utils/i18n" +import { getLanIp } from "./net" const str = i18n.en.server -function getLanIp(): string { - const ifaces = os.networkInterfaces() - for (const name of Object.keys(ifaces)) { - for (const iface of ifaces[name] ?? []) { - if (iface.family === "IPv4" && !iface.internal) { - return iface.address - } - } - } - return "127.0.0.1" -} export function printWelcome(port: number): void { const local = `http://localhost:${port}` @@ -40,8 +29,8 @@ export function printWelcome(port: number): void { ` ${row(str.debugLabel, debug, cyan)}`, "", ` ${divider}`, - ` ${gray("Status")} ${green("Running")}`, - ` ${gray("Port")} ${port}`, + ` ${row(str.statusLabel, str.runningLabel, green)}`, + ` ${row(str.portLabel, String(port), cyan)}`, "", ].join("\n"), ) diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 51b80610..96edbb43 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -32,6 +32,9 @@ export const i18n = { debugLabel: "Debug", settingsLabel: "Settings", readyLine: "Listening for connections", + statusLabel: "Status", + runningLabel: "Running", + portLabel: "Port", }, }, } as const diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 79a9af4c..8fd19503 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -3,6 +3,7 @@ import fs from "node:fs" import os from "node:os" import path from "node:path" +import { fileURLToPath } from "node:url" import winston from "winston" // dynamic log path (similar to logPath() requirement) @@ -22,12 +23,16 @@ try { // Read verboseLogs flag from server-config.json (defaults to false if missing/unreadable) let verboseLogs = false try { - const configPath = path.join(__dirname, "..", "server-config.json") + const configPath = fileURLToPath( + new URL("../server-config.json", import.meta.url), + ) const raw = fs.readFileSync(configPath, "utf-8") const cfg = JSON.parse(raw) as { verboseLogs?: boolean } verboseLogs = cfg.verboseLogs === true -} catch { - // Config unreadable — keep terminal silent +} catch (err: unknown) { + process.stderr.write( + `[logger] Server config read notice: ${err instanceof Error ? err.message : String(err)}\n`, + ) } // Ensure the logger handles uncaught exceptions and rejections @@ -64,8 +69,15 @@ if (verboseLogs) { // Optional: Intercept standard console.log and redirect to winston -const serialize = (a: unknown): string => - typeof a === "string" ? a : JSON.stringify(a) +const serialize = (a: unknown): string => { + if (typeof a === "string") return a + if (a instanceof Error) return a.stack || a.message + try { + return JSON.stringify(a) + } catch { + return String(a) + } +} console.log = (...args: unknown[]) => { logger.info(args.map(serialize).join(" ")) diff --git a/vite.config.ts b/vite.config.ts index 6eca4046..dbaf988f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -30,9 +30,11 @@ const config = defineConfig({ }) }, async configurePreviewServer(server) { + const httpServer = server.httpServer + if (!httpServer) return attachSignalingRoutes(server) - server.httpServer?.once("listening", () => { - const addr = server.httpServer?.address() + httpServer.once("listening", () => { + const addr = httpServer.address() const port = addr && typeof addr === "object" ? addr.port From 7f5fec6be69c023b8ad744002218bb5d51a52453 Mon Sep 17 00:00:00 2001 From: pinjinx Date: Fri, 31 Jul 2026 01:18:11 +0530 Subject: [PATCH 5/6] [Architecture] Code Rabbit fixesv2 --- src/contexts/ConnectionProvider.tsx | 18 +++- src/contexts/DebugContext.tsx | 2 +- src/hooks/useWebRtcStream.ts | 8 +- src/routes/debug.tsx | 135 ++++++++++++++++++---------- src/server/server.ts | 8 +- src/server/welcome.ts | 2 +- src/utils/i18n.ts | 64 +++++++++++-- src/{server => utils}/net.ts | 5 ++ vite.config.ts | 36 +++----- 9 files changed, 185 insertions(+), 93 deletions(-) rename src/{server => utils}/net.ts (66%) diff --git a/src/contexts/ConnectionProvider.tsx b/src/contexts/ConnectionProvider.tsx index 7d348b96..7dba01dc 100644 --- a/src/contexts/ConnectionProvider.tsx +++ b/src/contexts/ConnectionProvider.tsx @@ -102,11 +102,25 @@ export function ConnectionProvider({ const now = Date.now() if (now - lastReportTimeRef.current > 10000) { lastReportTimeRef.current = now + const token = + typeof window !== "undefined" + ? new URLSearchParams(window.location.search).get("token") || + localStorage.getItem("rein_auth_token") + : null fetch("/api/debug/report-latency", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, body: JSON.stringify({ latencyMs: ms }), - }).catch(() => {}) + }) + .then((res) => { + if (!res.ok) { + console.error("Failed to report latency:", res.status) + } + }) + .catch(() => {}) } } } catch {} diff --git a/src/contexts/DebugContext.tsx b/src/contexts/DebugContext.tsx index a3733ddb..942d5f0c 100644 --- a/src/contexts/DebugContext.tsx +++ b/src/contexts/DebugContext.tsx @@ -45,7 +45,7 @@ function serialize(args: unknown[]): { message: string; details?: string } { if (typeof a === "string") return a if (a instanceof Error) return a.stack || a.message try { - return JSON.stringify(a) + return JSON.stringify(a) ?? String(a) } catch { return String(a) } diff --git a/src/hooks/useWebRtcStream.ts b/src/hooks/useWebRtcStream.ts index 63d9d160..19bb15f4 100644 --- a/src/hooks/useWebRtcStream.ts +++ b/src/hooks/useWebRtcStream.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react" import { useConnection } from "../contexts/ConnectionProvider" - +import { t } from "../utils/i18n" interface UseWebRtcStreamOptions { token: string | null } @@ -16,7 +16,6 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { const [connecting, setConnecting] = useState(false) const [reconnectAttempt, setReconnectAttempt] = useState(0) const { registerDataChannel, send: sendInputEvent } = useConnection() - const pcRef = useRef(null) const wsRef = useRef(null) const retryTimerRef = useRef | null>(null) @@ -45,9 +44,8 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { `[WebRTC] Max retry attempts (${MAX_RETRIES}) reached. Stopping retries.`, ) setConnecting(false) - setErrorHandle("Connection Failed") - setError("Failed to establish stream session after multiple attempts") - isRetryingRef.current = false + setErrorHandle(t("errorComponent", "connectionFailedTitle")) + setError(t("errorComponent", "connectionFailedBody")) return } diff --git a/src/routes/debug.tsx b/src/routes/debug.tsx index 36f333bf..d4871108 100644 --- a/src/routes/debug.tsx +++ b/src/routes/debug.tsx @@ -12,6 +12,7 @@ import { ResponsiveContainer, } from "recharts" import { useClientLogs } from "../contexts/DebugContext" +import { t } from "../utils/i18n" export const Route = createFileRoute("/debug")({ component: DebugScreen, @@ -59,9 +60,9 @@ function nowLabel(): string { function fmtAge(createdAt: number): string { const s = Math.floor((Date.now() - createdAt) / 1000) - if (s < 60) return `${s}s ago` - if (s < 3600) return `${Math.floor(s / 60)}m ago` - return `${Math.floor(s / 3600)}h ago` + if (s < 60) return t("debug", "secondsAgo", { s }) + if (s < 3600) return t("debug", "minutesAgo", { m: Math.floor(s / 60) }) + return t("debug", "hoursAgo", { h: Math.floor(s / 3600) }) } function DebugScreen() { @@ -220,6 +221,19 @@ function DebugScreen() { } } + const hostStatusLabels: Record = { + stopped: t("debug", "statusStopped"), + starting: t("debug", "statusStarting"), + running: t("debug", "statusRunning"), + error: t("debug", "statusError"), + } + + const sessionStateLabels: Record = { + connected: t("debug", "sessionConnected"), + answered: t("debug", "sessionAnswered"), + offering: t("debug", "sessionOffering"), + } + const hostStatusCls = { running: "text-success", starting: "text-warning", @@ -257,17 +271,17 @@ function DebugScreen() {
{[ { - label: "GStreamer", - value: serverState.hostStatus, + label: t("debug", "gstreamer"), + value: hostStatusLabels[serverState.hostStatus], cls: hostStatusCls, }, { - label: "Active Sessions", + label: t("debug", "activeSessions"), value: String(serverState.sessionCount), cls: serverState.sessionCount > 0 ? "text-success" : "", }, { - label: "Viewers (SSE)", + label: t("debug", "viewersSse"), value: String( serverState.sessions.reduce((a, s) => a + s.sseViewerCount, 0), ), @@ -276,7 +290,7 @@ function DebugScreen() { : "", }, { - label: "Input Channels", + label: t("debug", "inputChannels"), value: String(serverState.inputConnectionCount), cls: serverState.inputConnectionCount > 0 ? "text-success" : "", }, @@ -300,9 +314,11 @@ function DebugScreen() { {/* Header row */}
-

Network

+

{t("debug", "network")}

- {selectedSessionId ? selectedSessionId : "all sessions"} + {selectedSessionId + ? selectedSessionId + : t("debug", "allSessions")}
{/* Live-value strip */} @@ -311,39 +327,39 @@ function DebugScreen() {
- Latency + {t("debug", "latency")} - {latest?.latencyMs ?? 0} ms + {t("debug", "latencyMs", { ms: latest?.latencyMs ?? 0 })} - peak {maxLat} ms + {t("debug", "peakMs", { ms: maxLat })}
{/* Video recv */}
- Video Recv + {t("debug", "videoRecv")} - {latest?.recvKBps ?? 0} KB/s + {t("debug", "kbpsValue", { val: latest?.recvKBps ?? 0 })} - peak {maxRecv.toFixed(1)} KB/s + {t("debug", "peakKbps", { val: maxRecv.toFixed(1) })}
{/* Input sent */}
- Input Sent + {t("debug", "inputSent")} - {latest?.sentKBps ?? 0} KB/s + {t("debug", "kbpsValue", { val: latest?.sentKBps ?? 0 })} - peak {maxSent.toFixed(1)} KB/s + {t("debug", "peakKbps", { val: maxSent.toFixed(1) })}
@@ -410,13 +426,22 @@ function DebugScreen() { >
{d.t}
- Latency: {d.latencyMs} ms + {t("debug", "latency")}:{" "} + + {t("debug", "latencyMs", { ms: d.latencyMs })} +
- Video Recv: {d.recvKBps} KB/s + {t("debug", "videoRecv")}:{" "} + + {t("debug", "kbpsValue", { val: d.recvKBps })} +
- Input Sent: {d.sentKBps} KB/s + {t("debug", "inputSent")}:{" "} + + {t("debug", "kbpsValue", { val: d.sentKBps })} +
) @@ -426,7 +451,7 @@ function DebugScreen() {
-

Client Sessions

+

+ {t("debug", "clientSessions")} +

{serverState.sessions.length === 0 ? (
- No active sessions + {t("debug", "noActiveSessions")}
) : ( serverState.sessions.map((session) => { @@ -506,12 +533,12 @@ function DebugScreen() { : "bg-neutral-800 text-neutral-300 border-neutral-700" }`} > - {session.state} + {sessionStateLabels[session.state] ?? session.state}
- WS peers:{" "} + {t("debug", "wsPeers")}:{" "} 0 @@ -523,7 +550,7 @@ function DebugScreen() {
- Input DC:{" "} + {t("debug", "inputDc")}:{" "} - {session.hasInputConnection ? "open" : "none"} + {session.hasInputConnection + ? t("debug", "dcOpen") + : t("debug", "dcNone")}
@@ -552,21 +581,23 @@ function DebugScreen() {
- Log Console + + {t("debug", "logConsole")} +
@@ -575,7 +606,7 @@ function DebugScreen() {
setSearchQuery(e.target.value)} className="input input-xs input-bordered pl-8 w-36 sm:w-44 font-mono text-[11px] rounded" @@ -583,16 +614,24 @@ function DebugScreen() {
- {["ALL", "INFO", "WARN", "ERROR"].map((lvl) => ( - - ))} + {(["ALL", "INFO", "WARN", "ERROR"] as const).map((lvl) => { + const levelLabels: Record = { + ALL: t("debug", "filterAll"), + INFO: t("debug", "filterInfo"), + WARN: t("debug", "filterWarn"), + ERROR: t("debug", "filterError"), + } + return ( + + ) + })}
@@ -612,7 +651,7 @@ function DebugScreen() {
{filteredLogs.length === 0 ? (
- No log records for the current filter. + {t("debug", "noLogRecords")}
) : ( filteredLogs.map((log) => ( @@ -653,7 +692,7 @@ function DebugScreen() {
{copiedId === log.id && ( - Copied! + {t("debug", "copied")} )} diff --git a/src/server/server.ts b/src/server/server.ts index b7bb25ee..54b3f744 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -11,7 +11,7 @@ import { import { GstManager } from "./gstreamer/gstManager" import { WebRTCManager } from "./webRTC" import type { InputConfig } from "./types" -import { getLanIp } from "./net" +import { getLanIp, isLoopbackAddress } from "../utils/net" let gstManager: GstManager | null = null let webrtcManager: WebRTCManager | null = null @@ -92,8 +92,7 @@ function json(res: ServerResponse, status: number, body: unknown): void { function requireAuth(req: IncomingMessage, res: ServerResponse): boolean { const addr = req.socket.remoteAddress - const isLocal = - addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" + const isLocal = isLoopbackAddress(addr) if (isLocal) return true const authHeader = req.headers.authorization ?? "" @@ -206,8 +205,7 @@ export function attachSignalingRoutes(server: any): void { if (pathname === "/api/auth/token" && req.method === "POST") { const addr = req.socket.remoteAddress - const isLocal = - addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" + const isLocal = isLoopbackAddress(addr) if (!isLocal) { json(res, 403, { error: "Localhost only" }) return diff --git a/src/server/welcome.ts b/src/server/welcome.ts index 6a396d9c..da7e398f 100644 --- a/src/server/welcome.ts +++ b/src/server/welcome.ts @@ -1,5 +1,5 @@ import { i18n } from "../utils/i18n" -import { getLanIp } from "./net" +import { getLanIp } from "../utils/net" const str = i18n.en.server diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 96edbb43..e0b23041 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -24,6 +24,8 @@ export const i18n = { errorComponent: { unknownError: "Unknown Error", unexpectedNetworkError: "An unexpected network error occurred.", + connectionFailedTitle: "Connection Failed", + connectionFailedBody: "Unable to establish WebRTC stream connection.", }, server: { welcomeTitle: "Welcome to Rein", @@ -36,6 +38,48 @@ export const i18n = { runningLabel: "Running", portLabel: "Port", }, + debug: { + gstreamer: "GStreamer", + activeSessions: "Active Sessions", + viewersSse: "Viewers (SSE)", + inputChannels: "Input Channels", + statusStopped: "stopped", + statusStarting: "starting", + statusRunning: "running", + statusError: "error", + network: "Network", + allSessions: "all sessions", + latency: "Latency", + latencyMs: "{ms} ms", + peakMs: "peak {ms} ms", + videoRecv: "Video Recv", + kbpsValue: "{val} KB/s", + peakKbps: "peak {val} KB/s", + inputSent: "Input Sent", + clientSessions: "Client Sessions", + noActiveSessions: "No active sessions", + sessionConnected: "connected", + sessionAnswered: "answered", + sessionOffering: "offering", + wsPeers: "WS peers", + inputDc: "Input DC", + dcOpen: "open", + dcNone: "none", + logConsole: "Log Console", + serverTab: "Server ({count})", + clientTab: "Client ({count})", + filterPlaceholder: "Filter", + filterAll: "ALL", + filterInfo: "INFO", + filterWarn: "WARN", + filterError: "ERROR", + clear: "Clear", + noLogRecords: "No log records for the current filter.", + copied: "Copied!", + secondsAgo: "{s}s ago", + minutesAgo: "{m}m ago", + hoursAgo: "{h}h ago", + }, }, } as const @@ -50,12 +94,18 @@ const currentLocale: Locale = "en" export function t< K1 extends keyof TranslationKeys, K2 extends keyof TranslationKeys[K1], ->(category: K1, key: K2): string { - return ( - (i18n[currentLocale][category] as Record)[ - key as unknown as string - ] ?? +>(category: K1, key: K2, params?: Record): string { + let str = ((i18n[currentLocale][category] as Record)[ + key as unknown as string + ] ?? (i18n.en[category] as Record)[key as unknown as string] ?? - "" - ) + "") as string + + if (params) { + for (const [pKey, pVal] of Object.entries(params)) { + str = str.replace(new RegExp(`\\{${pKey}\\}`, "g"), String(pVal)) + } + } + + return str } diff --git a/src/server/net.ts b/src/utils/net.ts similarity index 66% rename from src/server/net.ts rename to src/utils/net.ts index bef7a1e1..be0dbcff 100644 --- a/src/server/net.ts +++ b/src/utils/net.ts @@ -13,3 +13,8 @@ export function getLanIp(): string { } return "127.0.0.1" } + +export function isLoopbackAddress(addr?: string | null): boolean { + if (!addr) return false + return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" +} diff --git a/vite.config.ts b/vite.config.ts index dbaf988f..38b7f128 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,16 @@ import serverConfig from "./src/server-config.json" import { attachSignalingRoutes } from "./src/server/server" import { printWelcome } from "./src/server/welcome" import react from "@vitejs/plugin-react" +// biome-ignore lint/suspicious/noExplicitAny: Vite server instance +const wireServer = (server: any) => { + attachSignalingRoutes(server) + server.httpServer?.once("listening", () => { + const addr = server.httpServer?.address() + const port = + addr && typeof addr === "object" ? addr.port : serverConfig.frontendPort + printWelcome(port) + }) +} const config = defineConfig({ base: "/", @@ -18,30 +28,8 @@ const config = defineConfig({ plugins: [ { name: "rein-server", - async configureServer(server) { - attachSignalingRoutes(server) - server.httpServer?.once("listening", () => { - const addr = server.httpServer?.address() - const port = - addr && typeof addr === "object" - ? addr.port - : serverConfig.frontendPort - printWelcome(port) - }) - }, - async configurePreviewServer(server) { - const httpServer = server.httpServer - if (!httpServer) return - attachSignalingRoutes(server) - httpServer.once("listening", () => { - const addr = httpServer.address() - const port = - addr && typeof addr === "object" - ? addr.port - : serverConfig.frontendPort - printWelcome(port) - }) - }, + configureServer: wireServer, + configurePreviewServer: wireServer, }, devtools(), nitro(), From cacb3c2707c85e23ac3dbf9dc59049cf46f14530 Mon Sep 17 00:00:00 2001 From: pinjinx Date: Fri, 31 Jul 2026 02:10:59 +0530 Subject: [PATCH 6/6] [Architecture] Code Rabbit fixes v3 --- src/hooks/useWebRtcStream.ts | 4 +- src/server/gstreamer/gstManager.ts | 31 +++++--- src/server/server.ts | 19 ++++- src/server/webRTC.ts | 116 +++++++++++++++++++++++------ src/{server => utils}/welcome.ts | 19 +++-- vite.config.ts | 2 +- vitest.config.ts | 14 ++++ 7 files changed, 159 insertions(+), 46 deletions(-) rename src/{server => utils}/welcome.ts (69%) create mode 100644 vitest.config.ts diff --git a/src/hooks/useWebRtcStream.ts b/src/hooks/useWebRtcStream.ts index 19bb15f4..47fb51cf 100644 --- a/src/hooks/useWebRtcStream.ts +++ b/src/hooks/useWebRtcStream.ts @@ -90,7 +90,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { triggerRetry() }, [triggerRetry]) - const reconnect = () => { + const reconnect = useCallback(() => { if (retryTimerRef.current) { clearTimeout(retryTimerRef.current) retryTimerRef.current = null @@ -123,7 +123,7 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { setVideoStream(null) retryCountRef.current = 0 setReconnectAttempt((prev) => prev + 1) - } + }, []) useEffect(() => { if (!token) return diff --git a/src/server/gstreamer/gstManager.ts b/src/server/gstreamer/gstManager.ts index 033b3fff..56cbe771 100644 --- a/src/server/gstreamer/gstManager.ts +++ b/src/server/gstreamer/gstManager.ts @@ -10,7 +10,7 @@ import { RTP_HOST, RTP_PORT } from "../constants" export class GstManager { private process: ChildProcess | null = null private provider: CaptureProvider | null = null - + private stopping = false private buildPipelineArgs(sourceBlocks: string[]): string[] { const args = [...sourceBlocks] @@ -51,7 +51,7 @@ export class GstManager { } public async start(): Promise { - if (this.process) return + if (this.process || this.stopping) return logger.info("Spawning GStreamer UDP engine") @@ -132,17 +132,24 @@ export class GstManager { if (this.process) { logger.info("Terminating GStreamer video pipeline") const proc = this.process + this.stopping = true + await new Promise((resolve) => { + const killTimer = setTimeout(() => { + if (proc.exitCode === null) { + logger.warn( + "GStreamer process did not exit on SIGTERM, sending SIGKILL", + ) + proc.kill("SIGKILL") + } + }, 2000) + proc.once("close", () => { + clearTimeout(killTimer) + resolve() + }) + proc.kill("SIGTERM") + }) this.process = null - proc.kill("SIGTERM") - const killTimer = setTimeout(() => { - if (proc.exitCode === null) { - logger.warn( - "GStreamer process did not exit on SIGTERM, sending SIGKILL", - ) - proc.kill("SIGKILL") - } - }, 2000) - proc.once("close", () => clearTimeout(killTimer)) + this.stopping = false } await this.cleanup() } diff --git a/src/server/server.ts b/src/server/server.ts index 54b3f744..a1ab4a10 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -112,6 +112,17 @@ function requireAuth(req: IncomingMessage, res: ServerResponse): boolean { return true } +function getEffectiveHostStatus(): + | "stopped" + | "starting" + | "running" + | "error" { + if (hostStatus === "running" && webrtcManager?.hasError()) { + return "error" + } + return hostStatus +} + // biome-ignore lint/suspicious/noExplicitAny: Vite server instance export function attachSignalingRoutes(server: any): void { const httpServer = server.httpServer || server @@ -153,7 +164,7 @@ export function attachSignalingRoutes(server: any): void { if (pathname === "/api/host/start" && req.method === "POST") { if (!requireAuth(req, res)) return if (hostStatus === "running") { - json(res, 200, { status: hostStatus }) + json(res, 200, { status: getEffectiveHostStatus() }) return } hostStatus = "starting" @@ -168,7 +179,7 @@ export function attachSignalingRoutes(server: any): void { hostStatus = "error" }) - json(res, 200, { status: hostStatus }) + json(res, 200, { status: getEffectiveHostStatus() }) return } @@ -193,7 +204,7 @@ export function attachSignalingRoutes(server: any): void { if (pathname === "/api/host/status" && req.method === "GET") { if (!requireAuth(req, res)) return - json(res, 200, { status: hostStatus }) + json(res, 200, { status: getEffectiveHostStatus() }) return } @@ -241,7 +252,7 @@ export function attachSignalingRoutes(server: any): void { (s) => s.hasInputConnection, ).length json(res, 200, { - hostStatus, + hostStatus: getEffectiveHostStatus(), sessionCount: sessions.length, sessions, inputConnectionCount, diff --git a/src/server/webRTC.ts b/src/server/webRTC.ts index 6d2c8cdb..75abeeb5 100644 --- a/src/server/webRTC.ts +++ b/src/server/webRTC.ts @@ -43,7 +43,7 @@ export interface SessionSnapshot { export class WebRTCManager { private wss: WebSocketServer - private udpSocket: dgram.Socket + private udpSocket: dgram.Socket | null = null private serverRef: EventEmitter private upgradeHandler: ( request: IncomingMessage, @@ -52,10 +52,15 @@ export class WebRTCManager { ) => void private clients = new Map() private sessionCreatedAt = new Map() + private isUdpBound = false + private udpError = false + private isShutdown = false + private rebindTimer: NodeJS.Timeout | null = null + private rebindBackoffMs = 500 + private readonly maxBackoffMs = 10000 constructor(server: EventEmitter) { this.wss = new WebSocketServer({ noServer: true }) - this.udpSocket = dgram.createSocket("udp4") this.serverRef = server this.upgradeHandler = ( @@ -96,29 +101,86 @@ export class WebRTCManager { } private setupUdpSocket() { - this.udpSocket.on("error", (err) => { - logger.error(`UDP socket error:\n${err.stack}`) + if (this.isShutdown) return + + if (this.rebindTimer) { + clearTimeout(this.rebindTimer) + this.rebindTimer = null + } + + if (this.udpSocket) { try { + this.udpSocket.removeAllListeners() this.udpSocket.close() } catch {} - }) + this.udpSocket = null + } - this.udpSocket.on("message", (msg) => { - for (const client of this.clients.values()) { - try { - client.videoTrack.writeRtp(msg) - client.bytesSent += msg.length - } catch (_err) { - // Ignore individual track write errors + try { + const socket = dgram.createSocket("udp4") + this.udpSocket = socket + + socket.on("error", (err) => { + logger.error(`UDP socket error:\n${err.stack ?? err}`) + this.handleUdpFailure() + }) + + socket.on("message", (msg) => { + for (const client of this.clients.values()) { + try { + client.videoTrack.writeRtp(msg) + client.bytesSent += msg.length + } catch (_err) { + // Ignore individual track write errors + } } - } - }) + }) + + socket.bind(RTP_PORT, RTP_HOST, () => { + logger.info( + `UDP socket listening for RTP packets on ${RTP_HOST}:${RTP_PORT}`, + ) + this.isUdpBound = true + this.udpError = false + this.rebindBackoffMs = 500 + }) + } catch (err) { + logger.error(`Failed to create/bind UDP socket: ${String(err)}`) + this.handleUdpFailure() + } + } - this.udpSocket.bind(RTP_PORT, RTP_HOST, () => { - logger.info( - `UDP socket listening for RTP packets on ${RTP_HOST}:${RTP_PORT}`, + private handleUdpFailure() { + this.isUdpBound = false + this.udpError = true + + if (this.udpSocket) { + try { + this.udpSocket.removeAllListeners() + this.udpSocket.close() + } catch {} + this.udpSocket = null + } + + if (this.isShutdown) return + + if (!this.rebindTimer) { + const delay = this.rebindBackoffMs + logger.warn(`Scheduling UDP socket rebind in ${delay}ms`) + this.rebindTimer = setTimeout(() => { + this.rebindTimer = null + this.setupUdpSocket() + }, delay) + + this.rebindBackoffMs = Math.min( + this.rebindBackoffMs * 2, + this.maxBackoffMs, ) - }) + } + } + + public hasError(): boolean { + return this.udpError || !this.isUdpBound } private getInitialConfig(): Partial { @@ -299,6 +361,9 @@ export class WebRTCManager { try { client.inputHandler.destroy() } catch (_e) {} + try { + client.ws.close() + } catch (_e) {} this.clients.delete(sessionId) this.sessionCreatedAt.delete(sessionId) } @@ -329,13 +394,22 @@ export class WebRTCManager { } public shutdown() { + this.isShutdown = true + if (this.rebindTimer) { + clearTimeout(this.rebindTimer) + this.rebindTimer = null + } if (this.serverRef && typeof this.serverRef.off === "function") { this.serverRef.off("upgrade", this.upgradeHandler) } this.wss.close() - try { - this.udpSocket.close() - } catch {} + if (this.udpSocket) { + try { + this.udpSocket.removeAllListeners() + this.udpSocket.close() + } catch {} + this.udpSocket = null + } for (const sessionId of this.clients.keys()) { this.cleanupClient(sessionId) } diff --git a/src/server/welcome.ts b/src/utils/welcome.ts similarity index 69% rename from src/server/welcome.ts rename to src/utils/welcome.ts index da7e398f..94f02f5c 100644 --- a/src/server/welcome.ts +++ b/src/utils/welcome.ts @@ -1,23 +1,30 @@ -import { i18n } from "../utils/i18n" -import { getLanIp } from "../utils/net" - +import { i18n } from "./i18n" +import { getLanIp } from "./net" +import fs from "node:fs" +import { fileURLToPath } from "node:url" const str = i18n.en.server export function printWelcome(port: number): void { const local = `http://localhost:${port}` const network = `http://${getLanIp()}:${port}` const debug = `${local}/debug` - const bold = (t: string) => `\x1b[1m${t}\x1b[0m` const cyan = (t: string) => `\x1b[36m${t}\x1b[0m` const green = (t: string) => `\x1b[32m${t}\x1b[0m` const gray = (t: string) => `\x1b[90m${t}\x1b[0m` - const divider = gray("────────────────────────────────────────────────────") - const row = (label: string, value: string, color = green) => `${gray(label.padEnd(10))} ${color(value)}` + const configPath = fileURLToPath( + new URL("../server-config.json", import.meta.url), + ) + //If logs enabled dont print welcomescreen + const raw = fs.readFileSync(configPath, "utf-8") + const cfg = JSON.parse(raw) as { verboseLogs?: boolean } + let verboseLogs = false + verboseLogs = cfg.verboseLogs === true + if (verboseLogs) return process.stdout.write( [ "", diff --git a/vite.config.ts b/vite.config.ts index 38b7f128..a8ef0511 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,7 @@ import { nitro } from "nitro/vite" import { defineConfig } from "vite" import serverConfig from "./src/server-config.json" import { attachSignalingRoutes } from "./src/server/server" -import { printWelcome } from "./src/server/welcome" +import { printWelcome } from "./src/utils/welcome" import react from "@vitejs/plugin-react" // biome-ignore lint/suspicious/noExplicitAny: Vite server instance const wireServer = (server: any) => { diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..217f7ddf --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config" +import path from "node:path" + +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +})