diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 1b66c511..27a23fbc 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -6,26 +6,6 @@ import globals from 'globals'; export default [ { ignores: ['node_modules/**', '../web/dist/**'] }, js.configs.recommended, - { - files: ['**/*.js'], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - }, - }, - rules: { - 'no-unused-vars': ['warn', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - }], - 'no-empty': ['error', { allowEmptyCatch: true }], - 'no-case-declarations': 'off', - }, - }, ...tseslint.configs.recommended.map((config) => ({ ...config, files: ['**/*.{ts,tsx}'], @@ -50,16 +30,6 @@ export default [ caughtErrorsIgnorePattern: '^_', }], 'no-empty': ['error', { allowEmptyCatch: true }], - - // react-hooks v7 introduces several new rules driven by the React - // Compiler. The migration uses several legitimate patterns those - // rules flag (initial-fetch in useEffect, dirty-check derived - // state, `Date.now()` inside derive helpers, inline arrow event - // handlers, in-place mutation of imported Outbound class - // instances in the OutboundFormModal). We're not running the - // compiler, so the memoization-preservation warnings have no - // effect on runtime — turning them off until the codebase - // stabilises. 'react-hooks/set-state-in-effect': 'off', 'react-hooks/purity': 'off', 'react-hooks/react-compiler': 'off', diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ef3736c9..2042a04f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,6 +25,7 @@ "react-dom": "^19.2.6", "react-i18next": "^17.0.8", "react-router-dom": "^7.15.1", + "recharts": "^3.8.1", "swagger-ui-react": "^5.32.6" }, "devDependencies": { @@ -1571,6 +1572,42 @@ "react-dom": ">=18.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/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", @@ -1860,6 +1897,18 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@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/@swagger-api/apidom-ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.11.1.tgz", @@ -2583,6 +2632,69 @@ "tslib": "^2.4.0" } }, + "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/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -3456,6 +3568,127 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "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": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "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": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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": ">=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/dayjs": { "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", @@ -3479,6 +3712,12 @@ } } }, + "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/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", @@ -3637,6 +3876,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3832,6 +4081,12 @@ "node": ">=0.10.0" } }, + "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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4302,6 +4557,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz", @@ -4327,6 +4592,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "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/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -5537,6 +5811,42 @@ "react": ">= 0.14.0" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "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": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "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/recharts/node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -5552,6 +5862,15 @@ "immutable": "^3.8.1 || ^4.0.0-rc.1" } }, + "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/refractor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", @@ -6028,6 +6347,12 @@ "node": ">=12.22" } }, + "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/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6280,6 +6605,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "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.0.13", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index a62b16f5..905bb63b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "react-dom": "^19.2.6", "react-i18next": "^17.0.8", "react-router-dom": "^7.15.1", + "recharts": "^3.8.1", "swagger-ui-react": "^5.32.6" }, "devDependencies": { diff --git a/frontend/src/api/axios-init.js b/frontend/src/api/axios-init.ts similarity index 67% rename from frontend/src/api/axios-init.js rename to frontend/src/api/axios-init.ts index 258c26ee..c8769b05 100644 --- a/frontend/src/api/axios-init.js +++ b/frontend/src/api/axios-init.ts @@ -1,18 +1,21 @@ import axios from 'axios'; +import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import qs from 'qs'; const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']); const CSRF_TOKEN_PATH = '/csrf-token'; -let csrfToken = null; -let csrfFetchPromise = null; +let csrfToken: string | null = null; +let csrfFetchPromise: Promise | null = null; let sessionExpired = false; -function readMetaToken() { +type CsrfAwareConfig = InternalAxiosRequestConfig & { __csrfRetried?: boolean }; + +function readMetaToken(): string | null { return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || null; } -async function fetchCsrfToken() { +async function fetchCsrfToken(): Promise { try { const basePath = window.X_UI_BASE_PATH; const url = (typeof basePath === 'string' && basePath !== '' && basePath !== '/' @@ -24,14 +27,14 @@ async function fetchCsrfToken() { headers: { 'X-Requested-With': 'XMLHttpRequest' }, }); if (!res.ok) return null; - const json = await res.json(); + const json = (await res.json()) as { success?: boolean; obj?: unknown } | null; return json?.success && typeof json.obj === 'string' ? json.obj : null; - } catch (_e) { + } catch { return null; } } -async function ensureCsrfToken() { +async function ensureCsrfToken(): Promise { if (csrfToken) return csrfToken; const meta = readMetaToken(); if (meta) { @@ -45,14 +48,11 @@ async function ensureCsrfToken() { return csrfToken; } -// Apply the panel's axios defaults + interceptors. Call once at app -// startup before any HTTP call goes out. -export function setupAxios() { +export function setupAxios(): void { axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - // Read base path from window object or fallback to meta tag (for Cloudflare Rocket Loader compatibility) - let basePath = window.X_UI_BASE_PATH; + let basePath: string | null | undefined = window.X_UI_BASE_PATH; if (!basePath) { const metaTag = document.querySelector('meta[name="base-path"]'); basePath = metaTag ? metaTag.getAttribute('content') : null; @@ -61,22 +61,19 @@ export function setupAxios() { axios.defaults.baseURL = basePath; } - // Seed the cache from the meta tag if a server-rendered page injected - // one — saves a round trip on legacy templates that still embed it. csrfToken = readMetaToken(); axios.interceptors.request.use( - async (config) => { - config.headers = config.headers || {}; + async (config: InternalAxiosRequestConfig) => { const method = (config.method || 'get').toUpperCase(); if (!SAFE_METHODS.has(method)) { const token = await ensureCsrfToken(); - if (token) config.headers['X-CSRF-Token'] = token; + if (token) config.headers.set('X-CSRF-Token', token); } if (config.data instanceof FormData) { - config.headers['Content-Type'] = 'multipart/form-data'; + config.headers.set('Content-Type', 'multipart/form-data'); } else { - const declaredType = String(config.headers['Content-Type'] || config.headers['content-type'] || ''); + const declaredType = String(config.headers.get('Content-Type') || config.headers.get('content-type') || ''); if (declaredType.toLowerCase().startsWith('application/json')) { if (config.data !== undefined && typeof config.data !== 'string') { config.data = JSON.stringify(config.data); @@ -87,12 +84,12 @@ export function setupAxios() { } return config; }, - (error) => Promise.reject(error), + (error: unknown) => Promise.reject(error), ); axios.interceptors.response.use( - (response) => response, - async (error) => { + (response: AxiosResponse) => response, + async (error: AxiosError) => { const status = error.response?.status; if (status === 401) { if (!sessionExpired) { @@ -100,21 +97,19 @@ export function setupAxios() { const basePath = window.X_UI_BASE_PATH || '/'; window.location.replace(basePath); } - return new Promise(() => { }); + return new Promise(() => {}); } - // 403 with a stale/missing CSRF token: drop the cache, re-fetch, retry once. - const cfg = error.config; + const cfg = error.config as CsrfAwareConfig | undefined; if (status === 403 && cfg && !cfg.__csrfRetried) { csrfToken = null; cfg.__csrfRetried = true; const token = await ensureCsrfToken(); if (token) { - cfg.headers = cfg.headers || {}; - cfg.headers['X-CSRF-Token'] = token; - const declaredType = String(cfg.headers['Content-Type'] || cfg.headers['content-type'] || ''); + cfg.headers.set('X-CSRF-Token', token); + const declaredType = String(cfg.headers.get('Content-Type') || cfg.headers.get('content-type') || ''); if (typeof cfg.data === 'string') { if (declaredType.toLowerCase().startsWith('application/json')) { - try { cfg.data = JSON.parse(cfg.data); } catch (_e) { /* keep as-is */ } + try { cfg.data = JSON.parse(cfg.data); } catch {} } else { cfg.data = qs.parse(cfg.data); } diff --git a/frontend/src/api/websocket.js b/frontend/src/api/websocket.js deleted file mode 100644 index b45eed92..00000000 --- a/frontend/src/api/websocket.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * WebSocket client for real-time panel updates. - * - * Public API (kept stable for index.html / inbounds.html / xray.html): - * - connect() — open the connection (idempotent) - * - disconnect() — close and stop reconnecting - * - on(event, callback) — subscribe to event - * - off(event, callback) — unsubscribe - * - send(data) — send JSON to the server - * - isConnected — boolean, current state - * - reconnectAttempts — number, attempts since last success - * - maxReconnectAttempts — number, give-up threshold - * - * Built-in events: - * 'connected', 'disconnected', 'error', 'message', - * plus any server-emitted message type (status, traffic, client_stats, ...). - */ -export class WebSocketClient { - static #MAX_PAYLOAD_BYTES = 10 * 1024 * 1024; // 10 MB, mirrors hub maxMessageSize. - static #BASE_RECONNECT_MS = 1000; - static #MAX_RECONNECT_MS = 30_000; - // After exhausting maxReconnectAttempts we switch to a polite slow-retry - // cadence rather than giving up forever — a panel that recovers an hour - // later should reconnect without a manual page reload. - static #SLOW_RETRY_MS = 60_000; - - constructor(basePath = '') { - this.basePath = basePath; - this.maxReconnectAttempts = 10; - this.reconnectAttempts = 0; - this.isConnected = false; - - this.ws = null; - this.shouldReconnect = true; - this.reconnectTimer = null; - this.listeners = new Map(); // event → Set - } - - // Open the connection. Safe to call repeatedly — no-op if already - // open/connecting. Re-enables reconnects if previously disabled. Cancels - // any pending reconnect timer so an external connect() can't race a - // delayed retry into spawning a second socket. - connect() { - if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { - return; - } - this.shouldReconnect = true; - this.#cancelReconnect(); - this.#openSocket(); - } - - // Close the connection and stop any pending reconnect attempt. Resets the - // attempt counter so a future connect() starts fresh from the small backoff. - disconnect() { - this.shouldReconnect = false; - this.#cancelReconnect(); - this.reconnectAttempts = 0; - if (this.ws) { - try { this.ws.close(1000, 'client disconnect'); } catch { /* ignore */ } - this.ws = null; - } - this.isConnected = false; - } - - // Subscribe to an event. Re-subscribing the same callback is a no-op. - on(event, callback) { - if (typeof callback !== 'function') return; - let set = this.listeners.get(event); - if (!set) { - set = new Set(); - this.listeners.set(event, set); - } - set.add(callback); - } - - // Unsubscribe from an event. - off(event, callback) { - const set = this.listeners.get(event); - if (!set) return; - set.delete(callback); - if (set.size === 0) this.listeners.delete(event); - } - - // Send JSON to the server. Drops silently if not connected — callers - // should rely on connect()/server pushes rather than client-initiated sends. - send(data) { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify(data)); - } - } - - // ───── internals ───── - - #openSocket() { - const url = this.#buildUrl(); - let socket; - try { - socket = new WebSocket(url); - } catch (err) { - console.error('WebSocket: failed to construct connection', err); - this.#emit('error', err); - this.#scheduleReconnect(); - return; - } - this.ws = socket; - - // Every handler must check `this.ws !== socket` first. A previous socket - // can still fire events (especially `close`) after we've moved on to a - // new one — e.g. connect() called while the old socket is in CLOSING - // state. Without the guard, a stale close would null out the freshly - // opened socket and silently break send(). - socket.addEventListener('open', () => { - if (this.ws !== socket) return; - this.isConnected = true; - this.reconnectAttempts = 0; - this.#emit('connected'); - }); - - socket.addEventListener('message', (event) => { - if (this.ws !== socket) return; - this.#onMessage(event); - }); - - socket.addEventListener('error', (event) => { - if (this.ws !== socket) return; - // Browsers fire 'error' before 'close' on failure. We surface it for - // consumers (so polling fallbacks can engage) but don't log every blip - // — bad networks would flood the console otherwise. - this.#emit('error', event); - }); - - socket.addEventListener('close', () => { - if (this.ws !== socket) return; - this.isConnected = false; - this.ws = null; - this.#emit('disconnected'); - if (this.shouldReconnect) this.#scheduleReconnect(); - }); - } - - #buildUrl() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - // basePath comes from window.X_UI_BASE_PATH which is only injected - // by the Go binary in production. In dev (Vite serves directly) the - // global is missing and basePath would be '' — without the fallback to - // '/' we'd build `ws://host:portws` (no separator) and the WebSocket - // constructor throws a SyntaxError. - let basePath = this.basePath || '/'; - if (!basePath.startsWith('/')) basePath = '/' + basePath; - if (!basePath.endsWith('/')) basePath += '/'; - return `${protocol}//${window.location.host}${basePath}ws`; - } - - #onMessage(event) { - const data = event.data; - // Reject oversized payloads up front. We compare actual UTF-8 byte - // length (via Blob.size) against the limit — string.length counts - // UTF-16 code units, which can undercount real bytes by up to 4× for - // payloads with non-ASCII characters and bypass the cap. - if (typeof data === 'string') { - const byteLen = new Blob([data]).size; - if (byteLen > WebSocketClient.#MAX_PAYLOAD_BYTES) { - console.error(`WebSocket: payload too large (${byteLen} bytes), closing`); - try { this.ws?.close(1009, 'message too big'); } catch { /* ignore */ } - return; - } - } - let message; - try { - message = JSON.parse(data); - } catch (err) { - console.error('WebSocket: invalid JSON message', err); - return; - } - if (!message || typeof message !== 'object' || typeof message.type !== 'string') { - console.error('WebSocket: malformed message envelope'); - return; - } - this.#emit(message.type, message.payload, message.time); - this.#emit('message', message); - } - - #emit(event, ...args) { - const set = this.listeners.get(event); - if (!set) return; - for (const callback of set) { - try { - callback(...args); - } catch (err) { - console.error(`WebSocket: handler for "${event}" threw`, err); - } - } - } - - #scheduleReconnect() { - if (!this.shouldReconnect) return; - this.#cancelReconnect(); - - let base; - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts += 1; - // Exponential backoff inside the active window. - const exp = WebSocketClient.#BASE_RECONNECT_MS * 2 ** (this.reconnectAttempts - 1); - base = Math.min(WebSocketClient.#MAX_RECONNECT_MS, exp); - } else { - // Active window exhausted — keep trying once a minute. The page-level - // polling fallback runs in parallel; this just brings WS back when the - // network recovers. - base = WebSocketClient.#SLOW_RETRY_MS; - } - // ±25% jitter so reloads after a panel restart don't reconnect in lockstep. - const delay = base * (0.75 + Math.random() * 0.5); - - this.reconnectTimer = setTimeout(() => { - this.reconnectTimer = null; - // clearTimeout doesn't cancel a callback that has already fired but - // whose macrotask hasn't run yet — re-check shouldReconnect here so - // disconnect() called in that window can't be overridden. - if (!this.shouldReconnect) return; - this.#openSocket(); - }, delay); - } - - #cancelReconnect() { - if (this.reconnectTimer !== null) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } -} - diff --git a/frontend/src/api/websocket.ts b/frontend/src/api/websocket.ts new file mode 100644 index 00000000..3db7cdfa --- /dev/null +++ b/frontend/src/api/websocket.ts @@ -0,0 +1,192 @@ +type WebSocketListener = (...args: unknown[]) => void; + +interface WebSocketMessage { + type: string; + payload?: unknown; + time?: unknown; +} + +export class WebSocketClient { + static #MAX_PAYLOAD_BYTES = 10 * 1024 * 1024; + static #BASE_RECONNECT_MS = 1000; + static #MAX_RECONNECT_MS = 30_000; + static #SLOW_RETRY_MS = 60_000; + + basePath: string; + maxReconnectAttempts: number; + reconnectAttempts: number; + isConnected: boolean; + + private ws: WebSocket | null; + private shouldReconnect: boolean; + private reconnectTimer: ReturnType | null; + private listeners: Map>; + + constructor(basePath = '') { + this.basePath = basePath; + this.maxReconnectAttempts = 10; + this.reconnectAttempts = 0; + this.isConnected = false; + + this.ws = null; + this.shouldReconnect = true; + this.reconnectTimer = null; + this.listeners = new Map(); + } + + connect(): void { + if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { + return; + } + this.shouldReconnect = true; + this.#cancelReconnect(); + this.#openSocket(); + } + + disconnect(): void { + this.shouldReconnect = false; + this.#cancelReconnect(); + this.reconnectAttempts = 0; + if (this.ws) { + try { this.ws.close(1000, 'client disconnect'); } catch {} + this.ws = null; + } + this.isConnected = false; + } + + on(event: string, callback: WebSocketListener): void { + if (typeof callback !== 'function') return; + let set = this.listeners.get(event); + if (!set) { + set = new Set(); + this.listeners.set(event, set); + } + set.add(callback); + } + + off(event: string, callback: WebSocketListener): void { + const set = this.listeners.get(event); + if (!set) return; + set.delete(callback); + if (set.size === 0) this.listeners.delete(event); + } + + send(data: unknown): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(data)); + } + } + + #openSocket(): void { + const url = this.#buildUrl(); + let socket: WebSocket; + try { + socket = new WebSocket(url); + } catch (err) { + console.error('WebSocket: failed to construct connection', err); + this.#emit('error', err); + this.#scheduleReconnect(); + return; + } + this.ws = socket; + + socket.addEventListener('open', () => { + if (this.ws !== socket) return; + this.isConnected = true; + this.reconnectAttempts = 0; + this.#emit('connected'); + }); + + socket.addEventListener('message', (event) => { + if (this.ws !== socket) return; + this.#onMessage(event); + }); + + socket.addEventListener('error', (event) => { + if (this.ws !== socket) return; + this.#emit('error', event); + }); + + socket.addEventListener('close', () => { + if (this.ws !== socket) return; + this.isConnected = false; + this.ws = null; + this.#emit('disconnected'); + if (this.shouldReconnect) this.#scheduleReconnect(); + }); + } + + #buildUrl(): string { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + let basePath = this.basePath || '/'; + if (!basePath.startsWith('/')) basePath = '/' + basePath; + if (!basePath.endsWith('/')) basePath += '/'; + return `${protocol}//${window.location.host}${basePath}ws`; + } + + #onMessage(event: MessageEvent): void { + const data = event.data; + if (typeof data === 'string') { + const byteLen = new Blob([data]).size; + if (byteLen > WebSocketClient.#MAX_PAYLOAD_BYTES) { + console.error(`WebSocket: payload too large (${byteLen} bytes), closing`); + try { this.ws?.close(1009, 'message too big'); } catch {} + return; + } + } + let message: unknown; + try { + message = JSON.parse(typeof data === 'string' ? data : ''); + } catch (err) { + console.error('WebSocket: invalid JSON message', err); + return; + } + if (!message || typeof message !== 'object' || typeof (message as { type?: unknown }).type !== 'string') { + console.error('WebSocket: malformed message envelope'); + return; + } + const msg = message as WebSocketMessage; + this.#emit(msg.type, msg.payload, msg.time); + this.#emit('message', msg); + } + + #emit(event: string, ...args: unknown[]): void { + const set = this.listeners.get(event); + if (!set) return; + for (const callback of set) { + try { + callback(...args); + } catch (err) { + console.error(`WebSocket: handler for "${event}" threw`, err); + } + } + } + + #scheduleReconnect(): void { + if (!this.shouldReconnect) return; + this.#cancelReconnect(); + + let base: number; + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts += 1; + const exp = WebSocketClient.#BASE_RECONNECT_MS * 2 ** (this.reconnectAttempts - 1); + base = Math.min(WebSocketClient.#MAX_RECONNECT_MS, exp); + } else { + base = WebSocketClient.#SLOW_RETRY_MS; + } + const delay = base * (0.75 + Math.random() * 0.5); + + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + if (!this.shouldReconnect) return; + this.#openSocket(); + }, delay); + } + + #cancelReconnect(): void { + if (this.reconnectTimer !== null) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } +} diff --git a/frontend/src/api/websocketBridge.ts b/frontend/src/api/websocketBridge.ts index 114e74d2..a5c99031 100644 --- a/frontend/src/api/websocketBridge.ts +++ b/frontend/src/api/websocketBridge.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import { WebSocketClient } from '@/api/websocket.js'; +import { WebSocketClient } from '@/api/websocket'; import { keys } from '@/api/queryKeys'; type Handler = (payload: unknown) => void; diff --git a/frontend/src/components/AppSidebar.css b/frontend/src/components/AppSidebar.css index 85f66d6f..39a595c4 100644 --- a/frontend/src/components/AppSidebar.css +++ b/frontend/src/components/AppSidebar.css @@ -10,7 +10,7 @@ font-weight: 600; font-size: 18px; letter-spacing: 0.5px; - color: rgba(0, 0, 0, 0.88); + color: var(--ant-color-text); } .sider-brand { @@ -19,7 +19,7 @@ justify-content: space-between; gap: 8px; padding: 14px 14px; - border-bottom: 1px solid rgba(128, 128, 128, 0.15); + border-bottom: 1px solid var(--ant-color-border-secondary); user-select: none; } @@ -74,7 +74,7 @@ display: inline-flex; align-items: center; justify-content: center; - color: rgba(0, 0, 0, 0.75); + color: var(--ant-color-text-secondary); text-decoration: none; flex-shrink: 0; transition: background-color 0.2s, transform 0.15s, color 0.2s; @@ -102,7 +102,7 @@ align-items: center; justify-content: center; cursor: pointer; - color: rgba(0, 0, 0, 0.75); + color: var(--ant-color-text-secondary); padding: 0; flex-shrink: 0; transition: background-color 0.2s, transform 0.15s, color 0.2s; @@ -110,15 +110,14 @@ .sidebar-theme-cycle:hover, .sidebar-theme-cycle:focus-visible { - background-color: rgba(64, 150, 255, 0.1); - color: #4096ff; + background-color: color-mix(in srgb, var(--ant-color-primary) 12%, transparent); + color: var(--ant-color-primary); transform: scale(1.08); outline: none; } -.sidebar-theme-cycle svg { - width: 16px; - height: 16px; +.sidebar-theme-cycle .anticon { + font-size: 16px; } .drawer-header-actions { @@ -151,7 +150,7 @@ align-items: center; justify-content: space-between; padding: 14px 16px; - border-bottom: 1px solid rgba(128, 128, 128, 0.15); + border-bottom: 1px solid var(--ant-color-border-secondary); } .drawer-close { @@ -165,12 +164,12 @@ justify-content: center; cursor: pointer; font-size: 16px; - color: rgba(0, 0, 0, 0.65); + color: var(--ant-color-text-secondary); } .drawer-close:hover, .drawer-close:focus-visible { - background: rgba(128, 128, 128, 0.18); + background: var(--ant-color-fill-tertiary); } .drawer-menu .ant-menu-item { @@ -186,7 +185,7 @@ .drawer-utility { margin-top: auto; - border-top: 1px solid rgba(128, 128, 128, 0.15); + border-top: 1px solid var(--ant-color-border-secondary); } .ant-sidebar > .ant-layout-sider .ant-layout-sider-children { @@ -204,7 +203,7 @@ .sider-utility { flex: 0 0 auto; - border-top: 1px solid rgba(128, 128, 128, 0.15); + border-top: 1px solid var(--ant-color-border-secondary); } @media (max-width: 768px) { @@ -225,55 +224,11 @@ } } -body.dark .drawer-brand, -body.dark .sider-brand { - color: rgba(255, 255, 255, 0.92); -} - -html[data-theme='ultra-dark'] .drawer-brand, -html[data-theme='ultra-dark'] .sider-brand { - color: #ffffff; -} - -body.dark .drawer-close { - color: rgba(255, 255, 255, 0.75); -} - -html[data-theme='ultra-dark'] .drawer-close { - color: rgba(255, 255, 255, 0.85); -} - -body.dark .sidebar-theme-cycle { - color: rgba(255, 255, 255, 0.85); -} - -html[data-theme='ultra-dark'] .sidebar-theme-cycle { - color: rgba(255, 255, 255, 0.92); -} - -body.dark .sidebar-donate { - color: rgba(255, 255, 255, 0.85); -} - -html[data-theme='ultra-dark'] .sidebar-donate { - color: rgba(255, 255, 255, 0.92); -} - -body.dark .ant-drawer .ant-drawer-content, -body.dark .ant-drawer .ant-drawer-body { - background: #252526 !important; -} - -html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-content, -html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-body { - background: #0a0a0a !important; -} - .sider-nav .ant-menu-item-selected, .sider-utility .ant-menu-item-selected, .drawer-menu .ant-menu-item-selected { - background-color: rgba(64, 150, 255, 0.2) !important; - color: #4096ff !important; + background-color: color-mix(in srgb, var(--ant-color-primary) 20%, transparent) !important; + color: var(--ant-color-primary) !important; } .sider-nav .ant-menu-item-active:not(.ant-menu-item-selected), @@ -282,6 +237,6 @@ html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-body { .sider-nav .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover, .sider-utility .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover, .drawer-menu .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover { - background-color: rgba(64, 150, 255, 0.1) !important; - color: #4096ff !important; + background-color: color-mix(in srgb, var(--ant-color-primary) 10%, transparent) !important; + color: var(--ant-color-primary) !important; } diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index 0c19b773..03c11b45 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -12,7 +12,10 @@ import { HeartOutlined, LogoutOutlined, MenuOutlined, + MoonFilled, + MoonOutlined, SettingOutlined, + SunOutlined, TeamOutlined, ToolOutlined, UserOutlined, @@ -69,6 +72,7 @@ function ThemeCycleButton({ id, isDark, isUltra, onCycle, ariaLabel }: { onCycle: () => void; ariaLabel: string; }) { + const icon = !isDark ? : !isUltra ? : ; return ( ); } diff --git a/frontend/src/components/CustomStatistic.css b/frontend/src/components/CustomStatistic.css deleted file mode 100644 index 9cb3e065..00000000 --- a/frontend/src/components/CustomStatistic.css +++ /dev/null @@ -1,52 +0,0 @@ -.ant-statistic-content { - font-size: 17px !important; - line-height: 1.4 !important; - font-weight: 600; -} - -.ant-statistic-content-value, -.ant-statistic-content-prefix, -.ant-statistic-content-suffix { - font-size: 17px !important; -} - -.ant-statistic-content-prefix { - margin-inline-end: 8px !important; - opacity: 0.7; -} - -.ant-statistic-content-prefix .anticon { - font-size: 17px !important; -} - -.ant-statistic-content-suffix { - font-size: 12px !important; - opacity: 0.55; - margin-inline-start: 4px; - font-weight: 500; -} - -.ant-statistic-title { - font-size: 11px !important; - margin-bottom: 6px !important; - letter-spacing: 0.6px; - text-transform: uppercase; - color: rgba(0, 0, 0, 0.55); - font-weight: 500; -} - -body.dark .ant-statistic-content { - color: rgba(255, 255, 255, 0.92); -} - -body.dark .ant-statistic-title { - color: rgba(255, 255, 255, 0.72); -} - -html[data-theme='ultra-dark'] .ant-statistic-content { - color: rgba(255, 255, 255, 0.95); -} - -html[data-theme='ultra-dark'] .ant-statistic-title { - color: rgba(255, 255, 255, 0.70); -} diff --git a/frontend/src/components/CustomStatistic.tsx b/frontend/src/components/CustomStatistic.tsx deleted file mode 100644 index 6089637f..00000000 --- a/frontend/src/components/CustomStatistic.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { ReactNode } from 'react'; -import { Statistic } from 'antd'; -import './CustomStatistic.css'; - -interface CustomStatisticProps { - title?: string; - value?: string | number; - prefix?: ReactNode; - suffix?: ReactNode; -} - -export default function CustomStatistic({ title = '', value = '', prefix, suffix }: CustomStatisticProps) { - return ; -} diff --git a/frontend/src/components/FinalMaskForm.tsx b/frontend/src/components/FinalMaskForm.tsx index 07039cca..b66709da 100644 --- a/frontend/src/components/FinalMaskForm.tsx +++ b/frontend/src/components/FinalMaskForm.tsx @@ -3,7 +3,7 @@ import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd' import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { RandomUtil } from '@/utils'; -import { Protocols } from '@/models/outbound.js'; +import { Protocols } from '@/models/outbound'; interface StreamShape { network?: string; @@ -138,7 +138,7 @@ export default function FinalMaskForm({ stream, protocol, onChange }: FinalMaskF TCP Mask {mIdx + 1} { stream.delTcpMask(mIdx); notify(); @@ -238,7 +238,7 @@ export default function FinalMaskForm({ stream, protocol, onChange }: FinalMaskF UDP Mask {mIdx + 1} { stream.delUdpMask(mIdx); notify(); @@ -403,7 +403,7 @@ function HeaderCustomGroups({ {groupKey === 'clients' ? 'Clients' : 'Servers'} Group {gi + 1} { (settings[groupKey] as ItemRow[][]).splice(gi, 1); onChange(); @@ -445,7 +445,7 @@ function UdpHeaderCustom({ mask, onChange }: { mask: MaskRow; onChange: () => vo {groupKey === 'client' ? 'Client' : 'Server'} {ci + 1} { (settings[groupKey] as ItemRow[]).splice(ci, 1); onChange(); @@ -493,7 +493,7 @@ function NoiseItems({ mask, onChange }: { mask: MaskRow; onChange: () => void }) Noise {ni + 1} { (settings.noise as ItemRow[]).splice(ni, 1); onChange(); diff --git a/frontend/src/components/InputAddon.css b/frontend/src/components/InputAddon.css index e8544941..10a6dca0 100644 --- a/frontend/src/components/InputAddon.css +++ b/frontend/src/components/InputAddon.css @@ -5,22 +5,15 @@ height: 32px; font-size: 14px; line-height: 30px; - background-color: rgba(0, 0, 0, 0.02); - border: 1px solid #d9d9d9; + background-color: var(--ant-color-fill-tertiary); + border: 1px solid var(--ant-color-border); border-radius: 6px; position: relative; z-index: 1; - color: rgba(0, 0, 0, 0.88); + color: var(--ant-color-text); white-space: nowrap; } -body.dark .input-addon, -html[data-theme='ultra-dark'] .input-addon { - background-color: rgba(255, 255, 255, 0.04); - border-color: #424242; - color: rgba(255, 255, 255, 0.85); -} - .ant-space-compact > .input-addon:not(:first-child) { margin-inline-start: -1px; } diff --git a/frontend/src/components/JsonEditor.css b/frontend/src/components/JsonEditor.css index e7e0c320..c449c0ab 100644 --- a/frontend/src/components/JsonEditor.css +++ b/frontend/src/components/JsonEditor.css @@ -1,8 +1,8 @@ .json-editor-host { - border: 1px solid var(--ant-color-border, #d9d9d9); + border: 1px solid var(--ant-color-border); border-radius: 6px; overflow: hidden; - background: var(--ant-color-bg-container, #fff); + background: var(--ant-color-bg-container); } .json-editor-host .cm-editor, @@ -11,16 +11,6 @@ } .json-editor-host:focus-within { - border-color: var(--ant-color-primary, #1677ff); - box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1); -} - -body.dark .json-editor-host { - border-color: #3a3a3c; - background: #1e1e1e; -} - -html[data-theme="ultra-dark"] .json-editor-host { - border-color: #1f1f1f; - background: #0a0a0a; + border-color: var(--ant-color-primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ant-color-primary) 10%, transparent); } diff --git a/frontend/src/components/SettingListItem.css b/frontend/src/components/SettingListItem.css index 2f024eda..df9b4dfe 100644 --- a/frontend/src/components/SettingListItem.css +++ b/frontend/src/components/SettingListItem.css @@ -2,18 +2,13 @@ display: flex; align-items: center; justify-content: space-between; - border-bottom: 1px solid rgba(5, 5, 5, 0.06); + border-bottom: 1px solid var(--ant-color-border-secondary); } .setting-list-item:last-child { border-bottom: 0; } -body.dark .setting-list-item, -html[data-theme='ultra-dark'] .setting-list-item { - border-bottom-color: rgba(255, 255, 255, 0.08); -} - .setting-list-meta { display: flex; flex-direction: column; @@ -22,22 +17,12 @@ html[data-theme='ultra-dark'] .setting-list-item { .setting-list-title { font-size: 14px; - color: rgba(0, 0, 0, 0.88); + color: var(--ant-color-text); font-weight: 500; } .setting-list-description { font-size: 14px; - color: rgba(0, 0, 0, 0.45); + color: var(--ant-color-text-tertiary); line-height: 1.5715; } - -body.dark .setting-list-title, -html[data-theme='ultra-dark'] .setting-list-title { - color: rgba(255, 255, 255, 0.85); -} - -body.dark .setting-list-description, -html[data-theme='ultra-dark'] .setting-list-description { - color: rgba(255, 255, 255, 0.45); -} diff --git a/frontend/src/components/Sparkline.css b/frontend/src/components/Sparkline.css index ad0e012c..2aece7ca 100644 --- a/frontend/src/components/Sparkline.css +++ b/frontend/src/components/Sparkline.css @@ -2,43 +2,3 @@ display: block; width: 100%; } - -.sparkline-svg .cpu-grid-y-text, -.sparkline-svg .cpu-grid-x-text { - fill: rgba(0, 0, 0, 0.55); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - letter-spacing: 0.2px; -} - -.sparkline-svg .cpu-grid-text { - fill: rgba(0, 0, 0, 0.88); -} - -.sparkline-svg .cpu-grid-line { - stroke: rgba(0, 0, 0, 0.08); -} - -.sparkline-svg .cpu-tooltip-text { - pointer-events: none; -} - -.sparkline-svg .cpu-tooltip-pill { - filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.18)); -} - -body.dark .sparkline-svg .cpu-grid-y-text, -body.dark .sparkline-svg .cpu-grid-x-text { - fill: rgba(255, 255, 255, 0.7); -} - -body.dark .sparkline-svg .cpu-grid-text { - fill: rgba(255, 255, 255, 0.95); -} - -body.dark .sparkline-svg .cpu-grid-line { - stroke: rgba(255, 255, 255, 0.10); -} - -body.dark .sparkline-svg .cpu-tooltip-pill { - filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.6)); -} diff --git a/frontend/src/components/Sparkline.tsx b/frontend/src/components/Sparkline.tsx index c49db262..15a9055f 100644 --- a/frontend/src/components/Sparkline.tsx +++ b/frontend/src/components/Sparkline.tsx @@ -1,27 +1,29 @@ -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; -import type { MouseEvent } from 'react'; +import { useId, useMemo } from 'react'; +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; import './Sparkline.css'; interface SparklineProps { data: number[]; labels?: (string | number)[]; - vbWidth?: number; height?: number; stroke?: string; strokeWidth?: number; maxPoints?: number; showGrid?: boolean; - gridColor?: string; fillOpacity?: number; showMarker?: boolean; markerRadius?: number; showAxes?: boolean; yTickStep?: number; tickCountX?: number; - paddingLeft?: number; - paddingRight?: number; - paddingTop?: number; - paddingBottom?: number; showTooltip?: boolean; valueMin?: number; valueMax?: number | null; @@ -29,340 +31,136 @@ interface SparklineProps { tooltipFormatter?: ((v: number) => string) | null; } +interface ChartPoint { + index: number; + value: number; + label: string; +} + export default function Sparkline({ data, labels = [], - vbWidth = 320, height = 80, stroke = '#008771', strokeWidth = 2, maxPoints = 120, showGrid = true, - gridColor = 'rgba(0,0,0,0.08)', fillOpacity = 0.22, showMarker = true, markerRadius = 3, showAxes = false, yTickStep = 25, tickCountX = 4, - paddingLeft = 56, - paddingRight = 6, - paddingTop = 6, - paddingBottom = 20, showTooltip = false, valueMin = 0, valueMax = 100, yFormatter = (v: number) => `${Math.round(v)}%`, tooltipFormatter = null, }: SparklineProps) { - const svgRef = useRef(null); - const [measuredWidth, setMeasuredWidth] = useState(0); - const [hoverIdx, setHoverIdx] = useState(-1); - const reactId = useId(); const safeId = reactId.replace(/[^a-zA-Z0-9]/g, ''); const gradId = `spkGrad-${safeId}`; - const shadowId = `spkShadow-${safeId}`; - const glowId = `spkGlow-${safeId}`; - useEffect(() => { - const el = svgRef.current; - if (!el) return; - const measure = () => { - const w = el.getBoundingClientRect?.().width || 0; - if (w > 0) setMeasuredWidth(Math.round(w)); - }; - measure(); - if (typeof ResizeObserver !== 'undefined') { - const ro = new ResizeObserver(measure); - ro.observe(el); - return () => ro.disconnect(); + const points = useMemo(() => { + const n = Math.min(data.length, maxPoints); + if (n === 0) return []; + const sliceStart = data.length - n; + const labelStart = Math.max(0, labels.length - n); + return data.slice(sliceStart).map((value, i) => ({ + index: i, + value: Number(value) || 0, + label: String(labels[labelStart + i] ?? i + 1), + })); + }, [data, labels, maxPoints]); + + const yDomain = useMemo<[number, number]>(() => { + if (valueMax != null) return [valueMin, valueMax]; + let max = valueMin; + for (const p of points) { + if (Number.isFinite(p.value) && p.value > max) max = p.value; } - window.addEventListener('resize', measure); - return () => window.removeEventListener('resize', measure); - }, []); - - const effectiveVbWidth = measuredWidth > 0 ? measuredWidth : vbWidth; - const drawWidth = Math.max(1, effectiveVbWidth - paddingLeft - paddingRight); - const drawHeight = Math.max(1, height - paddingTop - paddingBottom); - const nPoints = Math.min(data.length, maxPoints); - - const dataSlice = useMemo( - () => (nPoints === 0 ? [] : data.slice(data.length - nPoints)), - [data, nPoints], - ); - - const labelsSlice = useMemo(() => { - if (!labels?.length || nPoints === 0) return [] as (string | number)[]; - const start = Math.max(0, labels.length - nPoints); - return labels.slice(start); - }, [labels, nPoints]); - - const yDomain = useMemo(() => { - const min = valueMin; - if (valueMax != null) return { min, max: valueMax }; - let max = min; - for (const v of dataSlice) { - const n = Number(v); - if (Number.isFinite(n) && n > max) max = n; - } - if (max <= min) max = min + 1; - return { min, max: max * 1.1 }; - }, [dataSlice, valueMin, valueMax]); - - const project = useCallback( - (v: number) => { - const { min, max } = yDomain; - const span = max - min; - if (span <= 0) return paddingTop + drawHeight; - const clipped = Math.max(min, Math.min(max, Number(v) || 0)); - const ratio = (clipped - min) / span; - return Math.round(paddingTop + (drawHeight - ratio * drawHeight)); - }, - [yDomain, paddingTop, drawHeight], - ); - - const pointsArr = useMemo<[number, number][]>(() => { - if (nPoints === 0) return []; - const w = drawWidth; - const dx = nPoints > 1 ? w / (nPoints - 1) : 0; - return dataSlice.map((v, i) => { - const x = Math.round(paddingLeft + i * dx); - return [x, project(v)]; - }); - }, [dataSlice, nPoints, drawWidth, paddingLeft, project]); - - const pointsStr = useMemo(() => pointsArr.map((p) => `${p[0]},${p[1]}`).join(' '), [pointsArr]); - - const areaPath = useMemo(() => { - if (pointsArr.length === 0) return ''; - const first = pointsArr[0]; - const last = pointsArr[pointsArr.length - 1]; - const baseY = paddingTop + drawHeight; - const line = pointsStr.replace(/ /g, ' L '); - return `M ${first[0]},${baseY} L ${line} L ${last[0]},${baseY} Z`; - }, [pointsArr, pointsStr, paddingTop, drawHeight]); - - const gridLines = useMemo(() => { - if (!showGrid) return []; - const h = drawHeight; - const w = drawWidth; - return [0, 0.25, 0.5, 0.75, 1].map((r) => { - const y = Math.round(paddingTop + h * r); - return { x1: paddingLeft, y1: y, x2: paddingLeft + w, y2: y }; - }); - }, [showGrid, drawHeight, drawWidth, paddingTop, paddingLeft]); - - const lastPoint = pointsArr.length === 0 ? null : pointsArr[pointsArr.length - 1]; + if (max <= valueMin) max = valueMin + 1; + return [valueMin, max * 1.1]; + }, [points, valueMin, valueMax]); const yTicks = useMemo(() => { - if (!showAxes) return []; - const { min, max } = yDomain; - const out: { y: number; label: string }[] = []; + if (!showAxes) return undefined; + const [min, max] = yDomain; if (valueMax === 100 && valueMin === 0 && yTickStep > 0) { - for (let p = min; p <= max; p += yTickStep) { - out.push({ y: project(p), label: yFormatter(p) }); - } + const out: number[] = []; + for (let v = min; v <= max; v += yTickStep) out.push(v); return out; } - const ticks = 5; - for (let i = 0; i < ticks; i++) { - const v = min + ((max - min) * i) / (ticks - 1); - out.push({ y: project(v), label: yFormatter(v) }); - } - return out; - }, [showAxes, yDomain, valueMax, valueMin, yTickStep, project, yFormatter]); + const n = 5; + return Array.from({ length: n }, (_, i) => min + ((max - min) * i) / (n - 1)); + }, [showAxes, yDomain, valueMin, valueMax, yTickStep]); - const xTicks = useMemo(() => { - if (!showAxes) return []; - if (nPoints === 0) return []; + const xTickIndexes = useMemo(() => { + if (!showAxes || points.length === 0) return undefined; const m = Math.max(2, tickCountX); - const w = drawWidth; - const dx = nPoints > 1 ? w / (nPoints - 1) : 0; - const out: { x: number; label: string }[] = []; - for (let i = 0; i < m; i++) { - const idx = Math.round((i * (nPoints - 1)) / (m - 1)); - const label = labelsSlice[idx] != null ? String(labelsSlice[idx]) : String(idx); - const x = Math.round(paddingLeft + idx * dx); - out.push({ x, label }); - } - return out; - }, [showAxes, labelsSlice, nPoints, tickCountX, drawWidth, paddingLeft]); + return Array.from({ length: m }, (_, i) => Math.round((i * (points.length - 1)) / (m - 1))); + }, [showAxes, tickCountX, points.length]); - const onMouseMove = useCallback( - (evt: MouseEvent) => { - if (!showTooltip || pointsArr.length === 0) return; - const rect = evt.currentTarget.getBoundingClientRect(); - const px = evt.clientX - rect.left; - const x = (px / rect.width) * effectiveVbWidth; - const dx = nPoints > 1 ? drawWidth / (nPoints - 1) : 0; - const idx = Math.max(0, Math.min(nPoints - 1, Math.round((x - paddingLeft) / (dx || 1)))); - setHoverIdx(idx); - }, - [showTooltip, pointsArr.length, effectiveVbWidth, nPoints, drawWidth, paddingLeft], - ); - - const onMouseLeave = useCallback(() => setHoverIdx(-1), []); - - const hoverText = useMemo(() => { - const idx = hoverIdx; - if (idx < 0 || idx >= dataSlice.length) return ''; - const raw = Number(dataSlice[idx] || 0); - const fmt = tooltipFormatter || yFormatter; - const val = fmt(Number.isFinite(raw) ? raw : 0); - const lab = labelsSlice[idx] != null ? labelsSlice[idx] : ''; - return `${val}${lab ? ' • ' + lab : ''}`; - }, [hoverIdx, dataSlice, labelsSlice, tooltipFormatter, yFormatter]); - - const tooltipPillWidth = Math.max(48, hoverText.length * 6.2 + 14); - const hoverPoint = hoverIdx >= 0 ? pointsArr[hoverIdx] : null; - const tooltipX = hoverPoint - ? Math.max( - paddingLeft + 2, - Math.min(effectiveVbWidth - paddingRight - tooltipPillWidth - 2, hoverPoint[0] - tooltipPillWidth / 2), - ) - : 0; + const fmtTooltip = tooltipFormatter ?? yFormatter; return ( - - - - - - - - - - - - - - - - - - - - - - - - - {showGrid && ( - - {gridLines.map((g, i) => ( - - ))} - - )} - - {showAxes && ( - - {yTicks.map((tk, i) => ( - - {tk.label} - - ))} - {xTicks.map((tk, i) => ( - - {tk.label} - - ))} - - )} - - {areaPath && } - - {showMarker && lastPoint && ( - <> - - - - - - - )} - - {showTooltip && hoverIdx >= 0 && pointsArr[hoverIdx] && ( - - + + + + + + + + {showGrid && ( + + )} + points[i]?.label).filter(Boolean) as string[] | undefined} + /> + + {showTooltip && ( + [fmtTooltip(Number(v) || 0), '']} + separator="" /> - - - - - {hoverText} - - - )} - + )} + + + ); } diff --git a/frontend/src/entries/login.tsx b/frontend/src/entries/login.tsx index eecec4bd..24b28cd2 100644 --- a/frontend/src/entries/login.tsx +++ b/frontend/src/entries/login.tsx @@ -2,7 +2,7 @@ import { createRoot } from 'react-dom/client'; import { message } from 'antd'; import 'antd/dist/reset.css'; -import { setupAxios } from '@/api/axios-init.js'; +import { setupAxios } from '@/api/axios-init'; import { applyDocumentTitle } from '@/utils'; import { readyI18n } from '@/i18n/react'; import { ThemeProvider } from '@/hooks/useTheme'; diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index eb3c1cf7..6a2d2216 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -28,6 +28,28 @@ interface Window { __SUB_PAGE_DATA__?: SubPageData; } +declare module 'qs' { + interface StringifyOptions { + arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma'; + encode?: boolean; + encoder?: (str: unknown, defaultEncoder: (s: unknown) => string, charset: string, type: 'key' | 'value') => string; + allowDots?: boolean; + skipNulls?: boolean; + addQueryPrefix?: boolean; + } + interface ParseOptions { + depth?: number; + arrayLimit?: number; + allowDots?: boolean; + parseArrays?: boolean; + ignoreQueryPrefix?: boolean; + } + export function stringify(obj: unknown, options?: StringifyOptions): string; + export function parse(str: string, options?: ParseOptions): Record; + const qs: { stringify: typeof stringify; parse: typeof parse }; + export default qs; +} + declare module 'persian-calendar-suite' { import type { ComponentType, ReactNode } from 'react'; diff --git a/frontend/src/hooks/useTheme.tsx b/frontend/src/hooks/useTheme.tsx index 12c33252..665e46f1 100644 --- a/frontend/src/hooks/useTheme.tsx +++ b/frontend/src/hooks/useTheme.tsx @@ -68,10 +68,25 @@ const ULTRA_DARK_MENU_TOKENS = { darkSubMenuItemBg: '#000', darkPopupBg: '#101013', }; +const DARK_CARD_TOKENS = { + colorBorderSecondary: 'rgba(255, 255, 255, 0.06)', +}; +const ULTRA_DARK_CARD_TOKENS = { + colorBorderSecondary: 'rgba(255, 255, 255, 0.04)', +}; +const STATISTIC_TOKENS = { + contentFontSize: 17, + titleFontSize: 11, +}; export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig { if (!isDark) { - return { algorithm: antdTheme.defaultAlgorithm }; + return { + algorithm: antdTheme.defaultAlgorithm, + components: { + Statistic: STATISTIC_TOKENS, + }, + }; } return { algorithm: antdTheme.darkAlgorithm, @@ -79,6 +94,8 @@ export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeCo components: { Layout: isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS, Menu: isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS, + Card: isUltra ? ULTRA_DARK_CARD_TOKENS : DARK_CARD_TOKENS, + Statistic: STATISTIC_TOKENS, }, }; } diff --git a/frontend/src/hooks/useWebSocket.ts b/frontend/src/hooks/useWebSocket.ts index 02ddd0be..4a5e036e 100644 --- a/frontend/src/hooks/useWebSocket.ts +++ b/frontend/src/hooks/useWebSocket.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { WebSocketClient } from '@/api/websocket.js'; +import { WebSocketClient } from '@/api/websocket'; type Handler = (payload: unknown) => void; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a4da5109..91131eed 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,8 +2,11 @@ import { createRoot } from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { message } from 'antd'; import 'antd/dist/reset.css'; +import '@/styles/utils.css'; +import '@/styles/page-shell.css'; +import '@/styles/page-cards.css'; -import { setupAxios } from '@/api/axios-init.js'; +import { setupAxios } from '@/api/axios-init'; import { readyI18n } from '@/i18n/react'; import { ThemeProvider } from '@/hooks/useTheme'; import { QueryProvider } from '@/api/QueryProvider'; diff --git a/frontend/src/models/dbinbound.js b/frontend/src/models/dbinbound.ts similarity index 59% rename from frontend/src/models/dbinbound.js rename to frontend/src/models/dbinbound.ts index 49c19eaf..391838c2 100644 --- a/frontend/src/models/dbinbound.js +++ b/frontend/src/models/dbinbound.ts @@ -1,23 +1,94 @@ -import dayjs from 'dayjs'; +import dayjs, { type Dayjs } from 'dayjs'; import { ObjectUtil, NumberFormatter, SizeFormatter } from '@/utils'; -import { Inbound, Protocols } from './inbound.js'; +import { Inbound, Protocols } from './inbound'; -export function coerceInboundJsonField(value) { +export type RawJsonField = string | Record | unknown[]; + +export interface ClientStats { + email: string; + up: number; + down: number; + total: number; + expiryTime: number; + enable?: boolean; + inboundId?: number; + reset?: number; +} + +export interface FallbackParentRef { + masterId: number; + path: string; +} + +export type DBInboundInit = Partial<{ + id: number; + userId: number; + up: number; + down: number; + total: number; + remark: string; + enable: boolean; + expiryTime: number; + trafficReset: string; + lastTrafficResetTime: number; + listen: string; + port: number; + protocol: string; + settings: RawJsonField; + streamSettings: RawJsonField; + tag: string; + sniffing: RawJsonField; + clientStats: ClientStats[]; + nodeId: number | null; + fallbackParent: FallbackParentRef | null; +}>; + +export function coerceInboundJsonField(value: unknown): Record { if (value == null) return {}; - if (typeof value === 'object') return value; + if (typeof value === 'object' && !Array.isArray(value)) { + return value as Record; + } if (typeof value !== 'string') return {}; const trimmed = value.trim(); if (trimmed === '') return {}; try { - return JSON.parse(trimmed); - } catch (_e) { + const parsed = JSON.parse(trimmed); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as Record; + } + return {}; + } catch { return {}; } } export class DBInbound { + id: number; + userId: number; + up: number; + down: number; + total: number; + remark: string; + enable: boolean; + expiryTime: number; + trafficReset: string; + lastTrafficResetTime: number; - constructor(data) { + listen: string; + port: number; + protocol: string; + settings: RawJsonField; + streamSettings: RawJsonField; + tag: string; + sniffing: RawJsonField; + clientStats: ClientStats[]; + nodeId: number | null; + fallbackParent: FallbackParentRef | null; + + private _cachedInbound: Inbound | null = null; + private _clientStatsMap: Map | null = null; + + constructor(data?: DBInboundInit) { this.id = 0; this.userId = 0; this.up = 0; @@ -36,12 +107,8 @@ export class DBInbound { this.streamSettings = ""; this.tag = ""; this.sniffing = ""; - this.clientStats = "" - // Optional FK to web/runtime registered Node. null/undefined = - // local panel; otherwise the inbound lives on the named node. + this.clientStats = []; this.nodeId = null; - // Populated by the API when this inbound is a fallback child of - // a VLESS/Trojan TCP-TLS master. Shape: { masterId, path }. this.fallbackParent = null; if (data == null) { return; @@ -49,11 +116,11 @@ export class DBInbound { ObjectUtil.cloneProps(this, data); } - get totalGB() { + get totalGB(): number { return NumberFormatter.toFixed(this.total / SizeFormatter.ONE_GB, 2); } - set totalGB(gb) { + set totalGB(gb: number) { this.total = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0); } @@ -89,7 +156,7 @@ export class DBInbound { return this.protocol === Protocols.HYSTERIA; } - get address() { + get address(): string { let address = location.hostname; if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") { address = this.listen; @@ -97,14 +164,14 @@ export class DBInbound { return address; } - get _expiryTime() { + get _expiryTime(): Dayjs | null { if (this.expiryTime === 0) { return null; } return dayjs(this.expiryTime); } - set _expiryTime(t) { + set _expiryTime(t: Dayjs | null | undefined) { if (t == null) { this.expiryTime = 0; } else { @@ -112,16 +179,16 @@ export class DBInbound { } } - get isExpiry() { + get isExpiry(): boolean { return this.expiryTime < new Date().getTime(); } - invalidateCache() { + invalidateCache(): void { this._cachedInbound = null; this._clientStatsMap = null; } - toInbound() { + toInbound(): Inbound { if (this._cachedInbound) { return this._cachedInbound; } @@ -145,19 +212,21 @@ export class DBInbound { return this._cachedInbound; } - getClientStats(email) { + getClientStats(email: string): ClientStats | undefined { if (!this._clientStatsMap) { this._clientStatsMap = new Map(); - if (this.clientStats && Array.isArray(this.clientStats)) { + if (Array.isArray(this.clientStats)) { for (const stats of this.clientStats) { - this._clientStatsMap.set(stats.email, stats); + if (stats && stats.email) { + this._clientStatsMap.set(stats.email, stats); + } } } } return this._clientStatsMap.get(email); } - isMultiUser() { + isMultiUser(): boolean { switch (this.protocol) { case Protocols.VMESS: case Protocols.VLESS: @@ -171,7 +240,7 @@ export class DBInbound { } } - hasLink() { + hasLink(): boolean { switch (this.protocol) { case Protocols.VMESS: case Protocols.VLESS: @@ -184,8 +253,8 @@ export class DBInbound { } } - genInboundLinks(remarkModel, hostOverride = '') { + genInboundLinks(remarkModel: string, hostOverride: string = ''): string { const inbound = this.toInbound(); return inbound.genInboundLinks(this.remark, remarkModel, hostOverride); } -} \ No newline at end of file +} diff --git a/frontend/src/models/inbound.js b/frontend/src/models/inbound.ts similarity index 85% rename from frontend/src/models/inbound.js rename to frontend/src/models/inbound.ts index d98dd8ba..e1342faa 100644 --- a/frontend/src/models/inbound.js +++ b/frontend/src/models/inbound.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import dayjs from 'dayjs'; import { ObjectUtil, RandomUtil, Base64, NumberFormatter, SizeFormatter, Wireguard } from '@/utils'; import { getRandomRealityTarget } from '@/models/reality-targets'; @@ -136,25 +137,33 @@ Object.freeze(TCP_CONGESTION_OPTION); Object.freeze(USERS_SECURITY); Object.freeze(MODE_OPTION); -export class XrayCommonClass { +export type JsonObject = Record; +export interface HeaderEntry { name: string; value: string } +export interface FallbackEntry { + dest?: string | number; + name?: string; + alpn?: string; + path?: string; + xver?: number | string; +} - static toJsonArray(arr) { - return arr.map(obj => obj.toJson()); +export class XrayCommonClass { + [key: string]: any; + + static toJsonArray(arr: T[]): unknown[] { + return arr.map((obj) => obj.toJson()); } - static fromJson() { + static fromJson(..._args: unknown[]): XrayCommonClass | undefined { return new XrayCommonClass(); } - toJson() { + toJson(): unknown { return this; } - // Build a clean Xray fallback entry. Per docs, name/alpn/path empty = "any", - // and xver=0 means PROXY protocol off — omit them so the generated config - // stays minimal and readable. dest is required and always emitted. - static fallbackToJson(fb) { - const out = { dest: fb.dest }; + static fallbackToJson(fb: FallbackEntry): JsonObject { + const out: JsonObject = { dest: fb.dest }; if (fb.name) out.name = fb.name; if (fb.alpn) out.alpn = fb.alpn; if (fb.path) out.path = fb.path; @@ -163,18 +172,19 @@ export class XrayCommonClass { return out; } - toString(format = true) { + toString(format: boolean = true): string { return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson()); } - static toHeaders(v2Headers) { - let newHeaders = []; - if (v2Headers) { - Object.keys(v2Headers).forEach(key => { - let values = v2Headers[key]; - if (typeof (values) === 'string') { + static toHeaders(v2Headers: unknown): HeaderEntry[] { + const newHeaders: HeaderEntry[] = []; + if (v2Headers && typeof v2Headers === 'object') { + const map = v2Headers as Record; + Object.keys(map).forEach((key: string) => { + const values = map[key]; + if (typeof values === 'string') { newHeaders.push({ name: key, value: values }); - } else { + } else if (Array.isArray(values)) { for (let i = 0; i < values.length; ++i) { newHeaders.push({ name: key, value: values[i] }); } @@ -184,19 +194,20 @@ export class XrayCommonClass { return newHeaders; } - static toV2Headers(headers, arr = true) { - let v2Headers = {}; + static toV2Headers(headers: HeaderEntry[], arr: boolean = true): Record { + const v2Headers: Record = {}; for (let i = 0; i < headers.length; ++i) { - let name = headers[i].name; - let value = headers[i].value; + const name = headers[i].name; + const value = headers[i].value; if (ObjectUtil.isEmpty(name) || ObjectUtil.isEmpty(value)) { continue; } if (!(name in v2Headers)) { v2Headers[name] = arr ? [value] : value; } else { - if (arr) { - v2Headers[name].push(value); + const existing = v2Headers[name]; + if (arr && Array.isArray(existing)) { + existing.push(value); } else { v2Headers[name] = value; } @@ -207,10 +218,13 @@ export class XrayCommonClass { } export class TcpStreamSettings extends XrayCommonClass { + static TcpRequest: any; + static TcpResponse: any; + constructor( - acceptProxyProtocol = false, - type = 'none', - request = new TcpStreamSettings.TcpRequest(), + acceptProxyProtocol: any = false, + type: any = 'none', + request: any = new TcpStreamSettings.TcpRequest(), response = new TcpStreamSettings.TcpResponse(), ) { super(); @@ -220,7 +234,7 @@ export class TcpStreamSettings extends XrayCommonClass { this.response = response; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { let header = json.header; if (!header) { header = {}; @@ -233,7 +247,7 @@ export class TcpStreamSettings extends XrayCommonClass { } toJson() { - const json = {}; + const json: any = {}; if (this.acceptProxyProtocol) { json.acceptProxyProtocol = true; } @@ -255,7 +269,7 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass { version = '1.1', method = 'GET', path = ['/'], - headers = [], + headers: any[] = [], ) { super(); this.version = version; @@ -264,23 +278,23 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass { this.headers = headers; } - addPath(path) { + addPath(path: any) { this.path.push(path); } - removePath(index) { + removePath(index: number) { this.path.splice(index, 1); } - addHeader(name, value) { + addHeader(name: any, value: any) { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number) { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new TcpStreamSettings.TcpRequest( json.version, json.method, @@ -304,7 +318,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass { version = '1.1', status = '200', reason = 'OK', - headers = [], + headers: any[] = [], ) { super(); this.version = version; @@ -313,15 +327,15 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass { this.headers = headers; } - addHeader(name, value) { + addHeader(name: any, value: any) { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number) { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new TcpStreamSettings.TcpResponse( json.version, json.status, @@ -358,7 +372,7 @@ export class KcpStreamSettings extends XrayCommonClass { this.maxSendingWindow = maxSendingWindow; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new KcpStreamSettings( json.mtu, json.tti, @@ -383,10 +397,10 @@ export class KcpStreamSettings extends XrayCommonClass { export class WsStreamSettings extends XrayCommonClass { constructor( - acceptProxyProtocol = false, + acceptProxyProtocol: any = false, path = '/', host = '', - headers = [], + headers: any[] = [], heartbeatPeriod = 0, ) { super(); @@ -397,15 +411,15 @@ export class WsStreamSettings extends XrayCommonClass { this.heartbeatPeriod = heartbeatPeriod; } - addHeader(name, value) { + addHeader(name: any, value: any) { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number) { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new WsStreamSettings( json.acceptProxyProtocol, json.path, @@ -438,7 +452,7 @@ export class GrpcStreamSettings extends XrayCommonClass { this.multiMode = multiMode; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new GrpcStreamSettings( json.serviceName, json.authority, @@ -457,10 +471,10 @@ export class GrpcStreamSettings extends XrayCommonClass { export class HTTPUpgradeStreamSettings extends XrayCommonClass { constructor( - acceptProxyProtocol = false, + acceptProxyProtocol: any = false, path = '/', host = '', - headers = [] + headers: any[] = [] ) { super(); this.acceptProxyProtocol = acceptProxyProtocol; @@ -469,15 +483,15 @@ export class HTTPUpgradeStreamSettings extends XrayCommonClass { this.headers = headers; } - addHeader(name, value) { + addHeader(name: any, value: any) { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number) { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new HTTPUpgradeStreamSettings( json.acceptProxyProtocol, json.path, @@ -533,7 +547,7 @@ export class xHTTPStreamSettings extends XrayCommonClass { // URL-share only — embedded in the link's `extra` blob so clients // pick them up; xray's listener ignores them at runtime. uplinkHTTPMethod = '', - headers = [], + headers: any[] = [], ) { super(); this.path = path; @@ -560,15 +574,15 @@ export class xHTTPStreamSettings extends XrayCommonClass { this.headers = headers; } - addHeader(name, value) { + addHeader(name: any, value: any) { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number) { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new xHTTPStreamSettings( json.path, json.host, @@ -625,20 +639,21 @@ export class xHTTPStreamSettings extends XrayCommonClass { export class HysteriaStreamSettings extends XrayCommonClass { constructor( - protocol, - version = 2, - auth = '', - udpIdleTimeout = 60, - masquerade, + protocol?: any, + version: any = 2, + auth: any = '', + udpIdleTimeout: any = 60, + masquerade?: any, ) { - super(protocol); + super(); + this.protocol = protocol; this.version = version; this.auth = auth; this.udpIdleTimeout = udpIdleTimeout; this.masquerade = masquerade; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new HysteriaStreamSettings( json.protocol, json.version ?? 2, @@ -675,7 +690,7 @@ export class HysteriaMasquerade extends XrayCommonClass { rewriteHost = false, insecure = false, content = '', - headers = [], + headers: any[] = [], statusCode = 0, ) { super(); @@ -689,15 +704,15 @@ export class HysteriaMasquerade extends XrayCommonClass { this.statusCode = statusCode; } - addHeader(name, value) { + addHeader(name: any, value: any) { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number) { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { const type = ['proxy', 'file', 'string'].includes(json.type) ? json.type : 'proxy'; return new HysteriaMasquerade( type, @@ -725,8 +740,11 @@ export class HysteriaMasquerade extends XrayCommonClass { } }; export class TlsStreamSettings extends XrayCommonClass { + static Cert: any; + static Settings: any; + constructor( - serverName = '', + serverName: any = '', minVersion = TLS_VERSION_OPTION.TLS12, maxVersion = TLS_VERSION_OPTION.TLS13, cipherSuites = '', @@ -756,15 +774,15 @@ export class TlsStreamSettings extends XrayCommonClass { this.certs.push(new TlsStreamSettings.Cert()); } - removeCert(index) { + removeCert(index: number) { this.certs.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { let certs; let settings; if (!ObjectUtil.isEmpty(json.certificates)) { - certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert)); + certs = json.certificates.map((cert: any) => TlsStreamSettings.Cert.fromJson(cert)); } if (!ObjectUtil.isEmpty(json.settings)) { @@ -824,7 +842,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass { this.buildChain = buildChain } - static fromJson(json = {}) { + static fromJson(json: any = {}) { if ('certificateFile' in json && 'keyFile' in json) { return new TlsStreamSettings.Cert( true, @@ -876,7 +894,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass { this.fingerprint = fingerprint; this.echConfigList = echConfigList; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new TlsStreamSettings.Settings( json.fingerprint, json.echConfigList, @@ -892,8 +910,10 @@ TlsStreamSettings.Settings = class extends XrayCommonClass { export class RealityStreamSettings extends XrayCommonClass { + static Settings: any; + constructor( - show = false, + show: any = false, xver = 0, target = '', serverNames = '', @@ -925,7 +945,7 @@ export class RealityStreamSettings extends XrayCommonClass { this.settings = settings; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { let settings; if (!ObjectUtil.isEmpty(json.settings)) { settings = new RealityStreamSettings.Settings( @@ -983,7 +1003,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass { this.spiderX = spiderX; this.mldsa65Verify = mldsa65Verify; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new RealityStreamSettings.Settings( json.publicKey, json.fingerprint, @@ -1005,7 +1025,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass { export class SockoptStreamSettings extends XrayCommonClass { constructor( - acceptProxyProtocol = false, + acceptProxyProtocol: any = false, tcpFastOpen = false, mark = 0, tproxy = "off", @@ -1043,7 +1063,7 @@ export class SockoptStreamSettings extends XrayCommonClass { this.trustedXForwardedFor = trustedXForwardedFor; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { if (Object.keys(json).length === 0) return undefined; return new SockoptStreamSettings( json.acceptProxyProtocol, @@ -1067,7 +1087,7 @@ export class SockoptStreamSettings extends XrayCommonClass { } toJson() { - const result = { + const result: any = { acceptProxyProtocol: this.acceptProxyProtocol, tcpFastOpen: this.tcpFastOpen, mark: this.mark, @@ -1093,13 +1113,13 @@ export class SockoptStreamSettings extends XrayCommonClass { } export class UdpMask extends XrayCommonClass { - constructor(type = 'salamander', settings = {}) { + constructor(type: any = 'salamander', settings: any = {}) { super(); this.type = type; this.settings = this._getDefaultSettings(type, settings); } - _getDefaultSettings(type, settings = {}) { + _getDefaultSettings(type: any, settings: any = {}): any { switch (type) { case 'salamander': case 'mkcp-aes128gcm': @@ -1132,7 +1152,7 @@ export class UdpMask extends XrayCommonClass { } } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new UdpMask( json.type || 'salamander', json.settings || {} @@ -1140,7 +1160,7 @@ export class UdpMask extends XrayCommonClass { } toJson() { - const cleanItem = item => { + const cleanItem = (item: any) => { const out = { ...item }; if (out.type === 'array') { delete out.packet; @@ -1170,13 +1190,13 @@ export class UdpMask extends XrayCommonClass { } export class TcpMask extends XrayCommonClass { - constructor(type = 'fragment', settings = {}) { + constructor(type: any = 'fragment', settings: any = {}) { super(); this.type = type; this.settings = this._getDefaultSettings(type, settings); } - _getDefaultSettings(type, settings = {}) { + _getDefaultSettings(type: any, settings: any = {}): any { switch (type) { case 'fragment': return { @@ -1204,7 +1224,7 @@ export class TcpMask extends XrayCommonClass { } } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new TcpMask( json.type || 'fragment', json.settings || {} @@ -1212,7 +1232,7 @@ export class TcpMask extends XrayCommonClass { } toJson() { - const cleanItem = item => { + const cleanItem = (item: any) => { const out = { ...item }; if (out.type === 'array') { delete out.packet; @@ -1225,7 +1245,7 @@ export class TcpMask extends XrayCommonClass { let settings = this.settings; if (this.type === 'header-custom' && settings) { - const cleanGroup = group => Array.isArray(group) ? group.map(cleanItem) : group; + const cleanGroup = (group: any) => Array.isArray(group) ? group.map(cleanItem) : group; settings = { ...settings, clients: Array.isArray(settings.clients) ? settings.clients.map(cleanGroup) : settings.clients, @@ -1242,18 +1262,18 @@ export class TcpMask extends XrayCommonClass { export class QuicParams extends XrayCommonClass { constructor( - congestion = 'bbr', - debug = false, - brutalUp = 65537, - brutalDown = 65537, - udpHop = undefined, - initStreamReceiveWindow = 8388608, - maxStreamReceiveWindow = 8388608, - initConnectionReceiveWindow = 20971520, - maxConnectionReceiveWindow = 20971520, - maxIdleTimeout = 30, - keepAlivePeriod = 5, - disablePathMTUDiscovery = false, + congestion: any = 'bbr', + debug: any = false, + brutalUp: any = 65537, + brutalDown: any = 65537, + udpHop: any = undefined, + initStreamReceiveWindow: any = 8388608, + maxStreamReceiveWindow: any = 8388608, + initConnectionReceiveWindow: any = 20971520, + maxConnectionReceiveWindow: any = 20971520, + maxIdleTimeout: any = 30, + keepAlivePeriod: any = 5, + disablePathMTUDiscovery: any = false, maxIncomingStreams = 1024, ) { super(); @@ -1280,7 +1300,7 @@ export class QuicParams extends XrayCommonClass { this.udpHop = value ? (this.udpHop || { ports: '20000-50000', interval: '5-10' }) : undefined; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { if (!json || Object.keys(json).length === 0) return undefined; return new QuicParams( json.congestion, @@ -1300,7 +1320,7 @@ export class QuicParams extends XrayCommonClass { } toJson() { - const result = { congestion: this.congestion }; + const result: any = { congestion: this.congestion }; if (this.debug) result.debug = this.debug; if (['brutal', 'force-brutal'].includes(this.congestion)) { if (this.brutalUp) result.brutalUp = this.brutalUp; @@ -1320,10 +1340,10 @@ export class QuicParams extends XrayCommonClass { } export class FinalMaskStreamSettings extends XrayCommonClass { - constructor(tcp = [], udp = [], quicParams = undefined) { + constructor(tcp: any[] = [], udp: any[] = [], quicParams: any = undefined) { super(); - this.tcp = Array.isArray(tcp) ? tcp.map(t => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : []; - this.udp = Array.isArray(udp) ? udp.map(u => new UdpMask(u.type, u.settings)) : [new UdpMask(udp.type, udp.settings)]; + this.tcp = Array.isArray(tcp) ? tcp.map((t: any) => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : []; + this.udp = Array.isArray(udp) ? udp.map((u: any) => new UdpMask(u.type, u.settings)) : [new UdpMask((udp as any).type, (udp as any).settings)]; this.quicParams = quicParams instanceof QuicParams ? quicParams : (quicParams ? QuicParams.fromJson(quicParams) : undefined); } @@ -1335,7 +1355,7 @@ export class FinalMaskStreamSettings extends XrayCommonClass { this.quicParams = value ? (this.quicParams || new QuicParams()) : undefined; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new FinalMaskStreamSettings( json.tcp || [], json.udp || [], @@ -1344,12 +1364,12 @@ export class FinalMaskStreamSettings extends XrayCommonClass { } toJson() { - const result = {}; + const result: any = {} as any; if (this.tcp && this.tcp.length > 0) { - result.tcp = this.tcp.map(t => t.toJson()); + result.tcp = this.tcp.map((t: any) => t.toJson()); } if (this.udp && this.udp.length > 0) { - result.udp = this.udp.map(udp => udp.toJson()); + result.udp = this.udp.map((udp: any) => udp.toJson()); } if (this.quicParams) { result.quicParams = this.quicParams.toJson(); @@ -1372,7 +1392,7 @@ export class StreamSettings extends XrayCommonClass { xhttpSettings = new xHTTPStreamSettings(), hysteriaSettings = new HysteriaStreamSettings(), finalmask = new FinalMaskStreamSettings(), - sockopt = undefined, + sockopt: any = undefined, ) { super(); this.network = network; @@ -1395,7 +1415,7 @@ export class StreamSettings extends XrayCommonClass { this.finalmask.tcp.push(new TcpMask(type)); } - delTcpMask(index) { + delTcpMask(index: number) { if (this.finalmask.tcp) { this.finalmask.tcp.splice(index, 1); } @@ -1405,7 +1425,7 @@ export class StreamSettings extends XrayCommonClass { this.finalmask.udp.push(new UdpMask(type)); } - delUdpMask(index) { + delUdpMask(index: number) { if (this.finalmask.udp) { this.finalmask.udp.splice(index, 1); } @@ -1451,7 +1471,7 @@ export class StreamSettings extends XrayCommonClass { this.sockopt = value ? new SockoptStreamSettings() : undefined; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new StreamSettings( json.network, json.security, @@ -1510,7 +1530,7 @@ export class Sniffing extends XrayCommonClass { this.domainsExcluded = Array.isArray(domainsExcluded) ? domainsExcluded : []; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { let destOverride = ObjectUtil.clone(json.destOverride); if (ObjectUtil.isEmpty(destOverride) || ObjectUtil.isArrEmpty(destOverride) || ObjectUtil.isEmpty(destOverride[0])) { destOverride = ['http', 'tls', 'quic', 'fakedns']; @@ -1541,8 +1561,21 @@ export class Sniffing extends XrayCommonClass { } export class Inbound extends XrayCommonClass { + static Settings: any; + static ClientBase: any; + static VmessSettings: any; + static VLESSSettings: any; + static TrojanSettings: any; + static ShadowsocksSettings: any; + static HysteriaSettings: any; + static TunnelSettings: any; + static MixedSettings: any; + static HttpSettings: any; + static WireguardSettings: any; + static TunSettings: any; + constructor( - port = RandomUtil.randomInteger(10000, 60000), + port: any = RandomUtil.randomInteger(10000, 60000), listen = '', protocol = Protocols.VLESS, settings = null, @@ -1568,7 +1601,7 @@ export class Inbound extends XrayCommonClass { // Looks for a "host"-named entry in xhttp.headers and returns its value, // or '' if not found. Used as a fallback when xhttp.host is empty so the // share URL still carries a usable Host hint. - static xhttpHostFallback(xhttp) { + static xhttpHostFallback(xhttp: any): string { if (!xhttp || !Array.isArray(xhttp.headers)) return ''; for (const h of xhttp.headers) { if (h && typeof h.name === 'string' && h.name.toLowerCase() === 'host') { @@ -1593,16 +1626,16 @@ export class Inbound extends XrayCommonClass { // // Truthy-only guards keep default inbounds emitting the same compact // URL they did before this helper grew. - static buildXhttpExtra(xhttp) { + static buildXhttpExtra(xhttp: any): any { if (!xhttp) return null; - const extra = {}; + const extra: any = {}; if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) { extra.xPaddingBytes = xhttp.xPaddingBytes; } if (xhttp.xPaddingObfsMode === true) { extra.xPaddingObfsMode = true; - ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => { + ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => { if (typeof xhttp[k] === 'string' && xhttp[k].length > 0) { extra[k] = xhttp[k]; } @@ -1642,7 +1675,7 @@ export class Inbound extends XrayCommonClass { // expects. The server runtime ignores this field, but the client // (consuming the share link) honors it. if (Array.isArray(xhttp.headers) && xhttp.headers.length > 0) { - const headersMap = {}; + const headersMap: any = {}; for (const h of xhttp.headers) { if (h && h.name && h.name.toLowerCase() !== 'host') { headersMap[h.name] = h.value || ''; @@ -1663,7 +1696,7 @@ export class Inbound extends XrayCommonClass { // Without this, the admin's custom xPaddingBytes / sessionKey / etc. // never reach the client and handshakes are silently rejected with // `invalid padding (...) length: 0`. - static applyXhttpExtraToParams(xhttp, params) { + static applyXhttpExtraToParams(xhttp: any, params: any): void { if (!xhttp) return; params.set("path", xhttp.path); const host = xhttp.host?.length > 0 ? xhttp.host : Inbound.xhttpHostFallback(xhttp); @@ -1684,7 +1717,7 @@ export class Inbound extends XrayCommonClass { // of building a query string. (The base VMess link generator already // sets net/type/path/host, so we only contribute the SplitHTTPConfig // extra side here.) - static applyXhttpExtraToObj(xhttp, obj) { + static applyXhttpExtraToObj(xhttp: any, obj: any): void { if (!xhttp || !obj) return; if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) { obj.x_padding_bytes = xhttp.xPaddingBytes; @@ -1696,12 +1729,12 @@ export class Inbound extends XrayCommonClass { } } - static externalProxyAlpn(value) { + static externalProxyAlpn(value: any): any { if (Array.isArray(value)) return value.filter(Boolean).join(','); return typeof value === 'string' ? value : ''; } - static applyExternalProxyTLSParams(externalProxy, params, security) { + static applyExternalProxyTLSParams(externalProxy: any, params: any, security: any): void { if (!externalProxy || security !== 'tls') return; const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest; if (sni?.length > 0) params.set("sni", sni); @@ -1710,7 +1743,7 @@ export class Inbound extends XrayCommonClass { if (alpn.length > 0) params.set("alpn", alpn); } - static applyExternalProxyTLSObj(externalProxy, obj, security) { + static applyExternalProxyTLSObj(externalProxy: any, obj: any, security: any): void { if (!externalProxy || !obj || security !== 'tls') return; const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest; if (sni?.length > 0) obj.sni = sni; @@ -1719,15 +1752,15 @@ export class Inbound extends XrayCommonClass { if (alpn.length > 0) obj.alpn = alpn; } - static hasShareableFinalMaskValue(value) { + static hasShareableFinalMaskValue(value: any): boolean { if (value == null) { return false; } if (Array.isArray(value)) { - return value.some(item => Inbound.hasShareableFinalMaskValue(item)); + return value.some((item: any) => Inbound.hasShareableFinalMaskValue(item)); } if (typeof value === 'object') { - return Object.values(value).some(item => Inbound.hasShareableFinalMaskValue(item)); + return Object.values(value).some((item: any) => Inbound.hasShareableFinalMaskValue(item)); } if (typeof value === 'string') { return value.length > 0; @@ -1735,7 +1768,7 @@ export class Inbound extends XrayCommonClass { return true; } - static serializeFinalMask(finalmask) { + static serializeFinalMask(finalmask: any): any { if (!finalmask) { return ''; } @@ -1745,7 +1778,7 @@ export class Inbound extends XrayCommonClass { // Export finalmask with the same compact JSON payload shape that // v2rayN-compatible share links use: fm=. - static applyFinalMaskToParams(finalmask, params) { + static applyFinalMaskToParams(finalmask: any, params: any): void { if (!params) return; const payload = Inbound.serializeFinalMask(finalmask); if (payload.length > 0) { @@ -1755,7 +1788,7 @@ export class Inbound extends XrayCommonClass { // VMess links are a base64 JSON object, so keep the same fm payload // under a flat property instead of a URL query string. - static applyFinalMaskToObj(finalmask, obj) { + static applyFinalMaskToObj(finalmask: any, obj: any): void { if (!obj) return; const payload = Inbound.serializeFinalMask(finalmask); if (payload.length > 0) { @@ -1847,7 +1880,7 @@ export class Inbound extends XrayCommonClass { return ""; } - getHeader(obj, name) { + getHeader(obj: any, name: any) { for (const header of obj.headers) { if (header.name.toLowerCase() === name.toLowerCase()) { return header.value; @@ -1886,8 +1919,8 @@ export class Inbound extends XrayCommonClass { return this.stream.grpc.serviceName; } - isExpiry(index) { - let exp = this.clients[index].expiryTime; + isExpiry(index: number) { + const exp = this.clients[index].expiryTime; return exp > 0 ? exp < new Date().getTime() : false; } @@ -1911,7 +1944,7 @@ export class Inbound extends XrayCommonClass { if (!this.canEnableTlsFlow()) return false; const clients = this.settings?.vlesses; if (!Array.isArray(clients)) return false; - return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION); + return clients.some((c: any) => c?.flow === TLS_FLOW_CONTROL.VISION); } canEnableReality() { @@ -1933,12 +1966,12 @@ export class Inbound extends XrayCommonClass { this.sniffing = new Sniffing(); } - genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security, externalProxy = null) { + genVmessLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientId?: any, security?: any, externalProxy: any = null) { if (this.protocol !== Protocols.VMESS) { return ''; } const tls = forceTls == 'same' ? this.stream.security : forceTls; - let obj = { + const obj: any = { v: '2', ps: remark, add: address, @@ -2002,7 +2035,7 @@ export class Inbound extends XrayCommonClass { return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2)); } - genVLESSLink(address = '', port = this.port, forceTls, remark = '', clientId, flow, externalProxy = null) { + genVLESSLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientId?: any, flow?: any, externalProxy: any = null) { const uuid = clientId; const type = this.stream.network; const security = forceTls == 'same' ? this.stream.security : forceTls; @@ -2010,12 +2043,12 @@ export class Inbound extends XrayCommonClass { params.set("type", this.stream.network); params.set("encryption", this.settings.encryption); switch (type) { - case "tcp": + case "tcp": { const tcp = this.stream.tcp; if (tcp.type === 'http') { const request = tcp.request; params.set("path", request.path.join(',')); - const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host'); + const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host'); if (index >= 0) { const host = request.headers[index].value; params.set("host", host); @@ -2023,17 +2056,20 @@ export class Inbound extends XrayCommonClass { params.set("headerType", 'http'); } break; - case "kcp": + } + case "kcp": { const kcp = this.stream.kcp; params.set("mtu", kcp.mtu); params.set("tti", kcp.tti); break; - case "ws": + } + case "ws": { const ws = this.stream.ws; params.set("path", ws.path); params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host')); break; - case "grpc": + } + case "grpc": { const grpc = this.stream.grpc; params.set("serviceName", grpc.serviceName); params.set("authority", grpc.authority); @@ -2041,11 +2077,13 @@ export class Inbound extends XrayCommonClass { params.set("mode", "multi"); } break; - case "httpupgrade": + } + case "httpupgrade": { const httpupgrade = this.stream.httpupgrade; params.set("path", httpupgrade.path); params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host')); break; + } case "xhttp": Inbound.applyXhttpExtraToParams(this.stream.xhttp, params); break; @@ -2105,19 +2143,19 @@ export class Inbound extends XrayCommonClass { return url.toString(); } - genSSLink(address = '', port = this.port, forceTls, remark = '', clientPassword, externalProxy = null) { - let settings = this.settings; + genSSLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientPassword?: any, externalProxy: any = null) { + const settings = this.settings; const type = this.stream.network; const security = forceTls == 'same' ? this.stream.security : forceTls; const params = new Map(); params.set("type", this.stream.network); switch (type) { - case "tcp": + case "tcp": { const tcp = this.stream.tcp; if (tcp.type === 'http') { const request = tcp.request; params.set("path", request.path.join(',')); - const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host'); + const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host'); if (index >= 0) { const host = request.headers[index].value; params.set("host", host); @@ -2125,17 +2163,20 @@ export class Inbound extends XrayCommonClass { params.set("headerType", 'http'); } break; - case "kcp": + } + case "kcp": { const kcp = this.stream.kcp; params.set("mtu", kcp.mtu); params.set("tti", kcp.tti); break; - case "ws": + } + case "ws": { const ws = this.stream.ws; params.set("path", ws.path); params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host')); break; - case "grpc": + } + case "grpc": { const grpc = this.stream.grpc; params.set("serviceName", grpc.serviceName); params.set("authority", grpc.authority); @@ -2143,11 +2184,13 @@ export class Inbound extends XrayCommonClass { params.set("mode", "multi"); } break; - case "httpupgrade": + } + case "httpupgrade": { const httpupgrade = this.stream.httpupgrade; params.set("path", httpupgrade.path); params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host')); break; + } case "xhttp": Inbound.applyXhttpExtraToParams(this.stream.xhttp, params); break; @@ -2171,11 +2214,11 @@ export class Inbound extends XrayCommonClass { } - let password = new Array(); + const password: string[] = []; if (this.isSS2022) password.push(settings.password); if (this.isSSMultiUser) password.push(clientPassword); - let link = `ss://${Base64.encode(`${settings.method}:${password.join(':')}`, true)}@${address}:${port}`; + const link = `ss://${Base64.encode(`${settings.method}:${password.join(':')}`, true)}@${address}:${port}`; const url = new URL(link); for (const [key, value] of params) { url.searchParams.set(key, value) @@ -2184,18 +2227,18 @@ export class Inbound extends XrayCommonClass { return url.toString(); } - genTrojanLink(address = '', port = this.port, forceTls, remark = '', clientPassword, externalProxy = null) { + genTrojanLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientPassword?: any, externalProxy: any = null) { const security = forceTls == 'same' ? this.stream.security : forceTls; const type = this.stream.network; const params = new Map(); params.set("type", this.stream.network); switch (type) { - case "tcp": + case "tcp": { const tcp = this.stream.tcp; if (tcp.type === 'http') { const request = tcp.request; params.set("path", request.path.join(',')); - const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host'); + const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host'); if (index >= 0) { const host = request.headers[index].value; params.set("host", host); @@ -2203,17 +2246,20 @@ export class Inbound extends XrayCommonClass { params.set("headerType", 'http'); } break; - case "kcp": + } + case "kcp": { const kcp = this.stream.kcp; params.set("mtu", kcp.mtu); params.set("tti", kcp.tti); break; - case "ws": + } + case "ws": { const ws = this.stream.ws; params.set("path", ws.path); params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host')); break; - case "grpc": + } + case "grpc": { const grpc = this.stream.grpc; params.set("serviceName", grpc.serviceName); params.set("authority", grpc.authority); @@ -2221,11 +2267,13 @@ export class Inbound extends XrayCommonClass { params.set("mode", "multi"); } break; - case "httpupgrade": + } + case "httpupgrade": { const httpupgrade = this.stream.httpupgrade; params.set("path", httpupgrade.path); params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host')); break; + } case "xhttp": Inbound.applyXhttpExtraToParams(this.stream.xhttp, params); break; @@ -2279,7 +2327,7 @@ export class Inbound extends XrayCommonClass { return url.toString(); } - genHysteriaLink(address = '', port = this.port, remark = '', clientAuth) { + genHysteriaLink(address: any = '', port: any = this.port, remark: any = '', clientAuth?: any) { const protocol = this.settings.version == 2 ? "hysteria2" : "hysteria"; const link = `${protocol}://${clientAuth}@${address}:${port}`; @@ -2293,7 +2341,7 @@ export class Inbound extends XrayCommonClass { const udpMasks = this.stream?.finalmask?.udp; if (Array.isArray(udpMasks)) { - const salamanderMask = udpMasks.find(mask => mask?.type === 'salamander'); + const salamanderMask = udpMasks.find((mask: any) => mask?.type === 'salamander'); const obfsPassword = salamanderMask?.settings?.password; if (typeof obfsPassword === 'string' && obfsPassword.length > 0) { params.set("obfs", "salamander"); @@ -2311,7 +2359,7 @@ export class Inbound extends XrayCommonClass { return url.toString(); } - getWireguardTxt(address, port, remark, peerId) { + getWireguardTxt(address: any, port: any, remark: any, peerId: any) { let txt = `[Interface]\n` txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n` txt += `Address = ${this.settings.peers[peerId].allowedIPs[0]}\n` @@ -2333,7 +2381,7 @@ export class Inbound extends XrayCommonClass { return txt; } - getWireguardLink(address, port, remark, peerId) { + getWireguardLink(address: any, port: any, remark: any, peerId: any) { const peer = this.settings?.peers?.[peerId]; if (!peer) return ''; @@ -2370,8 +2418,8 @@ export class Inbound extends XrayCommonClass { genWireguardLinks(remark = '', remarkModel = '-ieo', hostOverride = '') { const addr = this._resolveAddr(hostOverride); const separationChar = remarkModel.charAt(0); - let links = []; - this.settings.peers.forEach((p, index) => { + const links: any[] = []; + this.settings.peers.forEach((_p: any, index: number) => { links.push(this.getWireguardLink(addr, this.port, remark + separationChar + (index + 1), index)); }); return links.join('\r\n'); @@ -2380,14 +2428,14 @@ export class Inbound extends XrayCommonClass { genWireguardConfigs(remark = '', remarkModel = '-ieo', hostOverride = '') { const addr = this._resolveAddr(hostOverride); const separationChar = remarkModel.charAt(0); - let links = []; - this.settings.peers.forEach((p, index) => { + const links: any[] = []; + this.settings.peers.forEach((_p: any, index: number) => { links.push(this.getWireguardTxt(addr, this.port, remark + separationChar + (index + 1), index)); }); return links.join('\r\n'); } - genLink(address = '', port = this.port, forceTls = 'same', remark = '', client, externalProxy = null) { + genLink(address: any = '', port: any = this.port, forceTls: any = 'same', remark: any = '', client?: any, externalProxy: any = null) { switch (this.protocol) { case Protocols.VMESS: return this.genVmessLink(address, port, forceTls, remark, client.id, client.security, externalProxy); @@ -2403,28 +2451,28 @@ export class Inbound extends XrayCommonClass { } } - genAllLinks(remark = '', remarkModel = '-ieo', client, hostOverride = '') { - let result = []; - let email = client ? client.email : ''; - let addr = this._resolveAddr(hostOverride); - let port = this.port; + genAllLinks(remark: any = '', remarkModel: any = '-ieo', client?: any, hostOverride: any = '') { + const result: any[] = []; + const email = client ? client.email : ''; + const addr = this._resolveAddr(hostOverride); + const port = this.port; const separationChar = remarkModel.charAt(0); const orderChars = remarkModel.slice(1); - let orders = { + const orders: any = { 'i': remark, 'e': email, 'o': '', }; if (ObjectUtil.isArrEmpty(this.stream.externalProxy)) { - let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar); + const r = orderChars.split('').map((char: string) => orders[char]).filter((x: any) => x.length > 0).join(separationChar); result.push({ remark: r, link: this.genLink(addr, port, 'same', r, client) }); } else { - this.stream.externalProxy.forEach((ep) => { + this.stream.externalProxy.forEach((ep: any) => { orders['o'] = ep.remark; - let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar); + const r = orderChars.split('').map((char: string) => orders[char]).filter((x: any) => x.length > 0).join(separationChar); result.push({ remark: r, link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client, ep) @@ -2435,11 +2483,11 @@ export class Inbound extends XrayCommonClass { } genInboundLinks(remark = '', remarkModel = '-ieo', hostOverride = '') { - let addr = this._resolveAddr(hostOverride); + const addr = this._resolveAddr(hostOverride); if (this.clients) { - let links = []; - this.clients.forEach((client) => { - this.genAllLinks(remark, remarkModel, client, hostOverride).forEach(l => { + const links: any[] = []; + this.clients.forEach((client: any) => { + this.genAllLinks(remark, remarkModel, client, hostOverride).forEach((l: any) => { links.push(l.link); }) }); @@ -2453,7 +2501,7 @@ export class Inbound extends XrayCommonClass { } } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound( json.port, json.listen, @@ -2470,7 +2518,7 @@ export class Inbound extends XrayCommonClass { // Only these protocols use streamSettings const streamProtocols = [Protocols.VLESS, Protocols.VMESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA]; - const result = { + const result: any = { port: this.port, listen: this.listen, protocol: this.protocol, @@ -2490,12 +2538,12 @@ export class Inbound extends XrayCommonClass { } Inbound.Settings = class extends XrayCommonClass { - constructor(protocol) { + constructor(protocol: any) { super(); this.protocol = protocol; } - static getSettings(protocol) { + static getSettings(protocol: any): any { switch (protocol) { case Protocols.VMESS: return new Inbound.VmessSettings(protocol); case Protocols.VLESS: return new Inbound.VLESSSettings(protocol); @@ -2511,7 +2559,7 @@ Inbound.Settings = class extends XrayCommonClass { } } - static fromJson(protocol, json) { + static fromJson(protocol: any, json: any): any { switch (protocol) { case Protocols.VMESS: return Inbound.VmessSettings.fromJson(json); case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json); @@ -2535,17 +2583,17 @@ Inbound.Settings = class extends XrayCommonClass { /** Shared user-quota fields and UI helpers for multi-user protocol clients. */ Inbound.ClientBase = class extends XrayCommonClass { constructor( - email = RandomUtil.randomLowerAndNum(8), - limitIp = 0, - totalGB = 0, - expiryTime = 0, - enable = true, - tgId = '', - subId = RandomUtil.randomLowerAndNum(16), - comment = '', - reset = 0, - created_at = undefined, - updated_at = undefined, + email: any = RandomUtil.randomLowerAndNum(8), + limitIp: any = 0, + totalGB: any = 0, + expiryTime: any = 0, + enable: any = true, + tgId: any = '', + subId: any = RandomUtil.randomLowerAndNum(16), + comment: any = '', + reset: any = 0, + created_at: any = undefined, + updated_at: any = undefined, ) { super(); this.email = email; @@ -2561,7 +2609,7 @@ Inbound.ClientBase = class extends XrayCommonClass { this.updated_at = updated_at; } - static commonArgsFromJson(json = {}) { + static commonArgsFromJson(json: any = {}) { return [ json.email, json.limitIp, @@ -2603,7 +2651,7 @@ Inbound.ClientBase = class extends XrayCommonClass { return dayjs(this.expiryTime); } - set _expiryTime(t) { + set _expiryTime(t: any) { if (t == null || t === '') { this.expiryTime = 0; } else { @@ -2621,34 +2669,34 @@ Inbound.ClientBase = class extends XrayCommonClass { }; Inbound.VmessSettings = class extends Inbound.Settings { - constructor(protocol, - vmesses = []) { + constructor(protocol: any, + vmesses: any[] = []) { super(protocol); this.vmesses = vmesses; } - indexOfVmessById(id) { - return this.vmesses.findIndex(VMESS => VMESS.id === id); + indexOfVmessById(id: any) { + return this.vmesses.findIndex((VMESS: any) => VMESS.id === id); } - addVmess(VMESS) { + addVmess(VMESS: any) { if (this.indexOfVmessById(VMESS.id) >= 0) { return false; } this.vmesses.push(VMESS); } - delVmess(VMESS) { + delVmess(VMESS: any) { const i = this.indexOfVmessById(VMESS.id); if (i >= 0) { this.vmesses.splice(i, 1); } } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.VmessSettings( Protocols.VMESS, - (json.clients || []).map(client => Inbound.VmessSettings.VMESS.fromJson(client)), + (json.clients || []).map((client: any) => Inbound.VmessSettings.VMESS.fromJson(client)), ); } @@ -2661,16 +2709,16 @@ Inbound.VmessSettings = class extends Inbound.Settings { Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase { constructor( - id = RandomUtil.randomUUID(), - security = USERS_SECURITY.AUTO, - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + id: any = RandomUtil.randomUUID(), + security: any = USERS_SECURITY.AUTO, + email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any, ) { super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); this.id = id; this.security = security; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.VmessSettings.VMESS( json.id, json.security, @@ -2689,12 +2737,12 @@ Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase { Inbound.VLESSSettings = class extends Inbound.Settings { constructor( - protocol, - vlesses = [], - decryption = "none", - encryption = "none", - fallbacks = [], - testseed = [], + protocol: any, + vlesses: any[] = [], + decryption: any = "none", + encryption: any = "none", + fallbacks: any[] = [], + testseed: any[] = [], ) { super(protocol); this.vlesses = vlesses; @@ -2708,31 +2756,31 @@ Inbound.VLESSSettings = class extends Inbound.Settings { this.fallbacks.push(new Inbound.VLESSSettings.Fallback()); } - delFallback(index) { + delFallback(index: number) { this.fallbacks.splice(index, 1); } // Empty array means "use server defaults" (won't be sent). // Anything else must be exactly 4 positive integers. - static isValidTestseed(arr) { + static isValidTestseed(arr: any): boolean { if (!Array.isArray(arr) || arr.length === 0) return true; if (arr.length !== 4) return false; - return arr.every(v => Number.isInteger(v) && v > 0); + return arr.every((v: any) => Number.isInteger(v) && v > 0); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { // Preserve a saved testseed only if it's a valid 4-positive-int array; otherwise leave empty // so toJson omits it and the form falls back to placeholder defaults. const saved = json.testseed; const testseed = (Array.isArray(saved) && saved.length === 4 - && saved.every(v => Number.isInteger(v) && v > 0)) + && saved.every((v: any) => Number.isInteger(v) && v > 0)) ? saved : []; const obj = new Inbound.VLESSSettings( Protocols.VLESS, - (json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)), + (json.clients || []).map((client: any) => Inbound.VLESSSettings.VLESS.fromJson(client)), json.decryption, json.encryption, Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []), @@ -2743,7 +2791,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings { toJson() { - const json = { + const json: any = { clients: Inbound.VLESSSettings.toJsonArray(this.vlesses), }; @@ -2762,11 +2810,11 @@ Inbound.VLESSSettings = class extends Inbound.Settings { // testseed is only meaningful for the exact xtls-rprx-vision flow, and only when // the user supplied a complete 4-positive-int array. Otherwise omit and let the // backend fall back to its safe defaults. - const hasVisionFlow = this.vlesses && this.vlesses.some(v => v.flow === TLS_FLOW_CONTROL.VISION); + const hasVisionFlow = this.vlesses && this.vlesses.some((v: any) => v.flow === TLS_FLOW_CONTROL.VISION); if (hasVisionFlow && Array.isArray(this.testseed) && this.testseed.length === 4 - && this.testseed.every(v => Number.isInteger(v) && v > 0)) { + && this.testseed.every((v: any) => Number.isInteger(v) && v > 0)) { json.testseed = this.testseed; } @@ -2776,11 +2824,11 @@ Inbound.VLESSSettings = class extends Inbound.Settings { Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase { constructor( - id = RandomUtil.randomUUID(), - flow = '', - reverseTag = '', - reverseSniffing = new Sniffing(), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + id: any = RandomUtil.randomUUID(), + flow: any = '', + reverseTag: any = '', + reverseSniffing: any = new Sniffing(), + email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any, ) { super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); this.id = id; @@ -2789,7 +2837,7 @@ Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase { this.reverseSniffing = reverseSniffing; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.VLESSSettings.VLESS( json.id, json.flow, @@ -2800,7 +2848,7 @@ Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase { } toJson() { - const json = { + const json: any = { id: this.id, flow: this.flow, ...this._clientBaseToJson(), @@ -2825,20 +2873,20 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass { } toJson() { - return XrayCommonClass.fallbackToJson(this); + return XrayCommonClass.fallbackToJson(this as unknown as FallbackEntry); } - static fromJson(json = []) { - return (json || []).map(f => new Inbound.VLESSSettings.Fallback( + static fromJson(json: any = []) { + return (json || []).map((f: any) => new Inbound.VLESSSettings.Fallback( f.name, f.alpn, f.path, f.dest, f.xver, )); } }; Inbound.TrojanSettings = class extends Inbound.Settings { - constructor(protocol, - trojans = [], - fallbacks = [],) { + constructor(protocol: any, + trojans: any[] = [], + fallbacks: any[] = [],) { super(protocol); this.trojans = trojans; this.fallbacks = fallbacks; @@ -2848,19 +2896,19 @@ Inbound.TrojanSettings = class extends Inbound.Settings { this.fallbacks.push(new Inbound.TrojanSettings.Fallback()); } - delFallback(index) { + delFallback(index: number) { this.fallbacks.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.TrojanSettings( Protocols.TROJAN, - (json.clients || []).map(client => Inbound.TrojanSettings.Trojan.fromJson(client)), + (json.clients || []).map((client: any) => Inbound.TrojanSettings.Trojan.fromJson(client)), Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),); } toJson() { - const json = { + const json: any = { clients: Inbound.TrojanSettings.toJsonArray(this.trojans), }; if (this.fallbacks && this.fallbacks.length > 0) { @@ -2873,7 +2921,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings { Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase { constructor( password = RandomUtil.randomSeq(10), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any, ) { super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); this.password = password; @@ -2886,7 +2934,7 @@ Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase { }; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.TrojanSettings.Trojan( json.password, ...Inbound.ClientBase.commonArgsFromJson(json), @@ -2905,22 +2953,22 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass { } toJson() { - return XrayCommonClass.fallbackToJson(this); + return XrayCommonClass.fallbackToJson(this as unknown as FallbackEntry); } - static fromJson(json = []) { - return (json || []).map(f => new Inbound.TrojanSettings.Fallback( + static fromJson(json: any = []) { + return (json || []).map((f: any) => new Inbound.TrojanSettings.Fallback( f.name, f.alpn, f.path, f.dest, f.xver, )); } }; Inbound.ShadowsocksSettings = class extends Inbound.Settings { - constructor(protocol, - method = SSMethods.BLAKE3_AES_256_GCM, - password = RandomUtil.randomShadowsocksPassword(), - network = 'tcp', - shadowsockses = [], + constructor(protocol: any, + method: any = SSMethods.BLAKE3_AES_256_GCM, + password: any = RandomUtil.randomShadowsocksPassword(), + network: any = 'tcp', + shadowsockses: any[] = [], ivCheck = false, ) { super(protocol); @@ -2931,13 +2979,13 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings { this.ivCheck = ivCheck; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.ShadowsocksSettings( Protocols.SHADOWSOCKS, json.method, json.password, json.network, - (json.clients || []).map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)), + (json.clients || []).map((client: any) => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)), json.ivCheck, ); } @@ -2957,7 +3005,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase { constructor( method = '', password = RandomUtil.randomShadowsocksPassword(), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any, ) { super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); this.method = method; @@ -2972,7 +3020,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase { }; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.ShadowsocksSettings.Shadowsocks( json.method, json.password, @@ -2982,17 +3030,17 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase { }; Inbound.HysteriaSettings = class extends Inbound.Settings { - constructor(protocol, version = 2, hysterias = []) { + constructor(protocol: any, version: any = 2, hysterias: any[] = []) { super(protocol); this.version = version; this.hysterias = hysterias; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.HysteriaSettings( Protocols.HYSTERIA, json.version ?? 2, - (json.clients || []).map(client => Inbound.HysteriaSettings.Hysteria.fromJson(client)), + (json.clients || []).map((client: any) => Inbound.HysteriaSettings.Hysteria.fromJson(client)), ); } @@ -3007,7 +3055,7 @@ Inbound.HysteriaSettings = class extends Inbound.Settings { Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase { constructor( auth = RandomUtil.randomSeq(10), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any, ) { super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); this.auth = auth; @@ -3020,7 +3068,7 @@ Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase { }; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.HysteriaSettings.Hysteria( json.auth, ...Inbound.ClientBase.commonArgsFromJson(json), @@ -3030,12 +3078,12 @@ Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase { Inbound.TunnelSettings = class extends Inbound.Settings { constructor( - protocol, - rewriteAddress, - rewritePort, - portMap = [], - allowedNetwork = 'tcp,udp', - followRedirect = false + protocol: any, + rewriteAddress?: any, + rewritePort?: any, + portMap: any[] = [], + allowedNetwork: any = 'tcp,udp', + followRedirect: any = false ) { super(protocol); this.rewriteAddress = rewriteAddress; @@ -3049,11 +3097,11 @@ Inbound.TunnelSettings = class extends Inbound.Settings { this.portMap.push({ name: port, value: target }); } - removePortMap(index) { + removePortMap(index: number) { this.portMap.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.TunnelSettings( Protocols.TUNNEL, json.rewriteAddress, @@ -3076,7 +3124,7 @@ Inbound.TunnelSettings = class extends Inbound.Settings { }; Inbound.MixedSettings = class extends Inbound.Settings { - constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') { + constructor(protocol: any, auth: any = 'password', accounts: any[] = [new Inbound.MixedSettings.SocksAccount()], udp: any = false, ip: any = '127.0.0.1') { super(protocol); this.auth = auth; this.accounts = accounts; @@ -3084,19 +3132,19 @@ Inbound.MixedSettings = class extends Inbound.Settings { this.ip = ip; } - addAccount(account) { + addAccount(account: any) { this.accounts.push(account); } - delAccount(index) { + delAccount(index: number) { this.accounts.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { let accounts; if (json.auth === 'password') { accounts = json.accounts.map( - account => Inbound.MixedSettings.SocksAccount.fromJson(account) + (account: any) => Inbound.MixedSettings.SocksAccount.fromJson(account) ) } return new Inbound.MixedSettings( @@ -3111,7 +3159,7 @@ Inbound.MixedSettings = class extends Inbound.Settings { toJson() { return { auth: this.auth, - accounts: this.auth === 'password' ? this.accounts.map(account => account.toJson()) : undefined, + accounts: this.auth === 'password' ? this.accounts.map((account: any) => account.toJson()) : undefined, udp: this.udp, ip: this.ip, }; @@ -3124,34 +3172,34 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass { this.pass = pass; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.MixedSettings.SocksAccount(json.user, json.pass); } }; Inbound.HttpSettings = class extends Inbound.Settings { constructor( - protocol, - accounts = [new Inbound.HttpSettings.HttpAccount()], - allowTransparent = false, + protocol: any, + accounts: any[] = [new Inbound.HttpSettings.HttpAccount()], + allowTransparent: any = false, ) { super(protocol); this.accounts = accounts; this.allowTransparent = allowTransparent; } - addAccount(account) { + addAccount(account: any) { this.accounts.push(account); } - delAccount(index) { + delAccount(index: number) { this.accounts.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.HttpSettings( Protocols.HTTP, - json.accounts.map(account => Inbound.HttpSettings.HttpAccount.fromJson(account)), + json.accounts.map((account: any) => Inbound.HttpSettings.HttpAccount.fromJson(account)), json.allowTransparent, ); } @@ -3171,20 +3219,21 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass { this.pass = pass; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.HttpSettings.HttpAccount(json.user, json.pass); } }; Inbound.WireguardSettings = class extends XrayCommonClass { constructor( - protocol, - mtu = 1420, - secretKey = Wireguard.generateKeypair().privateKey, - peers = [new Inbound.WireguardSettings.Peer()], - noKernelTun = false + protocol?: any, + mtu: any = 1420, + secretKey: any = Wireguard.generateKeypair().privateKey, + peers: any[] = [new Inbound.WireguardSettings.Peer()], + noKernelTun: any = false ) { - super(protocol); + super(); + this.protocol = protocol; this.mtu = mtu; this.secretKey = secretKey; this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : ''; @@ -3196,16 +3245,16 @@ Inbound.WireguardSettings = class extends XrayCommonClass { this.peers.push(new Inbound.WireguardSettings.Peer(null, null, '', ['10.0.0.' + (this.peers.length + 2)])); } - delPeer(index) { + delPeer(index: number) { this.peers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.WireguardSettings( Protocols.WIREGUARD, json.mtu, json.secretKey, - json.peers.map(peer => Inbound.WireguardSettings.Peer.fromJson(peer)), + json.peers.map((peer: any) => Inbound.WireguardSettings.Peer.fromJson(peer)), json.noKernelTun, ); } @@ -3221,7 +3270,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass { }; Inbound.WireguardSettings.Peer = class extends XrayCommonClass { - constructor(privateKey, publicKey, psk = '', allowedIPs = ['10.0.0.2/32'], keepAlive = 0) { + constructor(privateKey?: any, publicKey?: any, psk: any = '', allowedIPs: any[] = ['10.0.0.2/32'], keepAlive: any = 0) { super(); this.privateKey = privateKey this.publicKey = publicKey; @@ -3229,14 +3278,14 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass { [this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair()) } this.psk = psk; - allowedIPs.forEach((a, index) => { + allowedIPs.forEach((a: any, index: number) => { if (a.length > 0 && !a.includes('/')) allowedIPs[index] += '/32'; }) this.allowedIPs = allowedIPs; this.keepAlive = keepAlive; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { return new Inbound.WireguardSettings.Peer( json.privateKey, json.publicKey, @@ -3247,7 +3296,7 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass { } toJson() { - this.allowedIPs.forEach((a, index) => { + this.allowedIPs.forEach((a: any, index: number) => { if (a.length > 0 && !a.includes('/')) this.allowedIPs[index] += '/32'; }); return { @@ -3262,13 +3311,13 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass { Inbound.TunSettings = class extends Inbound.Settings { constructor( - protocol, - name = 'xray0', - mtu = 1500, - gateway = [], - dns = [], - userLevel = 0, - autoSystemRoutingTable = [], + protocol: any, + name: any = 'xray0', + mtu: any = 1500, + gateway: any[] = [], + dns: any[] = [], + userLevel: any = 0, + autoSystemRoutingTable: any[] = [], autoOutboundsInterface = 'auto' ) { super(protocol); @@ -3281,7 +3330,7 @@ Inbound.TunSettings = class extends Inbound.Settings { this.autoOutboundsInterface = autoOutboundsInterface; } - static fromJson(json = {}) { + static fromJson(json: any = {}) { const rawMtu = json.mtu ?? json.MTU; const mtu = Array.isArray(rawMtu) ? rawMtu[0] : rawMtu; return new Inbound.TunSettings( diff --git a/frontend/src/models/outbound.js b/frontend/src/models/outbound.ts similarity index 88% rename from frontend/src/models/outbound.js rename to frontend/src/models/outbound.ts index 79af2682..5a8ab442 100644 --- a/frontend/src/models/outbound.js +++ b/frontend/src/models/outbound.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { ObjectUtil, Base64, Wireguard } from '@/utils'; export const Protocols = { @@ -109,30 +110,30 @@ export const Address_Port_Strategy = { export const DNSRuleActions = ['direct', 'drop', 'reject', 'hijack']; -export function normalizeDNSRuleField(value) { +export function normalizeDNSRuleField(value: any): string { if (value === null || value === undefined) { return ''; } if (Array.isArray(value)) { - return value.map(item => item.toString().trim()).filter(item => item.length > 0).join(','); + return value.map((item: any) => item.toString().trim()).filter((item: any) => item.length > 0).join(','); } return value.toString().trim(); } -export function normalizeDNSRuleAction(action) { +export function normalizeDNSRuleAction(action: any): string { action = ObjectUtil.isEmpty(action) ? 'direct' : action.toString().toLowerCase().trim(); return DNSRuleActions.includes(action) ? action : 'direct'; } -export function parseLegacyDNSBlockTypes(blockTypes) { +export function parseLegacyDNSBlockTypes(blockTypes: any): number[] { if (blockTypes === null || blockTypes === undefined || blockTypes === '') { return []; } if (Array.isArray(blockTypes)) { return blockTypes - .map(item => Number(item)) - .filter(item => Number.isInteger(item) && item >= 0 && item <= 65535); + .map((item: any) => Number(item)) + .filter((item: any) => Number.isInteger(item) && item >= 0 && item <= 65535); } if (typeof blockTypes === 'number') { @@ -142,13 +143,13 @@ export function parseLegacyDNSBlockTypes(blockTypes) { return blockTypes .toString() .split(',') - .map(item => item.trim()) - .filter(item => /^\d+$/.test(item)) - .map(item => Number(item)) - .filter(item => item >= 0 && item <= 65535); + .map((item: any) => item.trim()) + .filter((item: any) => /^\d+$/.test(item)) + .map((item: any) => Number(item)) + .filter((item: any) => item >= 0 && item <= 65535); } -export function buildLegacyDNSRules(nonIPQuery, blockTypes) { +export function buildLegacyDNSRules(nonIPQuery: any, blockTypes: any): any[] { const mode = ['reject', 'drop', 'skip'].includes(nonIPQuery) ? nonIPQuery : 'reject'; const rules = []; const parsedBlockTypes = parseLegacyDNSBlockTypes(blockTypes); @@ -163,9 +164,9 @@ export function buildLegacyDNSRules(nonIPQuery, blockTypes) { return rules; } -export function getDNSRulesFromJson(json = {}) { +export function getDNSRulesFromJson(json: any = {}): any[] { if (Array.isArray(json.rules) && json.rules.length > 0) { - return json.rules.map(rule => Outbound.DNSRule.fromJson(rule)); + return json.rules.map((rule: any) => Outbound.DNSRule.fromJson(rule)); } if (json.nonIPQuery !== undefined || json.blockTypes !== undefined) { @@ -189,20 +190,21 @@ Object.freeze(Address_Port_Strategy); Object.freeze(DNSRuleActions); export class CommonClass { + [key: string]: any; - static toJsonArray(arr) { + static toJsonArray(arr: any[]): any[] { return arr.map(obj => obj.toJson()); } - static fromJson() { + static fromJson(..._args: any[]): any { return new CommonClass(); } - toJson() { + toJson(): any { return this; } - toString(format = true) { + toString(format: boolean = true): string { return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson()); } } @@ -225,7 +227,7 @@ export class ReverseSniffing extends CommonClass { this.domainsExcluded = Array.isArray(domainsExcluded) ? domainsExcluded : []; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (!json || Object.keys(json).length === 0) { return new ReverseSniffing(); } @@ -252,15 +254,15 @@ export class ReverseSniffing extends CommonClass { } export class TcpStreamSettings extends CommonClass { - constructor(type = 'none', host, path) { + constructor(type: any = 'none', host?: any, path?: any) { super(); this.type = type; this.host = host; this.path = path; } - static fromJson(json = {}) { - let header = json.header; + static fromJson(json: any = {}): any { + const header = json.header; if (!header) return new TcpStreamSettings(); if (header.type == 'http' && header.request) { return new TcpStreamSettings( @@ -305,7 +307,7 @@ export class KcpStreamSettings extends CommonClass { this.maxSendingWindow = maxSendingWindow; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new KcpStreamSettings( json.mtu, json.tti, @@ -341,7 +343,7 @@ export class WsStreamSettings extends CommonClass { this.heartbeatPeriod = heartbeatPeriod; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new WsStreamSettings( json.path, json.host, @@ -370,7 +372,7 @@ export class GrpcStreamSettings extends CommonClass { this.multiMode = multiMode; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new GrpcStreamSettings(json.serviceName, json.authority, json.multiMode); } @@ -390,7 +392,7 @@ export class HttpUpgradeStreamSettings extends CommonClass { this.host = host; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new HttpUpgradeStreamSettings( json.path, json.host, @@ -414,10 +416,10 @@ export class HttpUpgradeStreamSettings extends CommonClass { export class xHTTPStreamSettings extends CommonClass { constructor( // Bidirectional — must match the inbound side - path = '/', - host = '', - mode = '', - xPaddingBytes = "100-1000", + path: any = '/', + host: any = '', + mode: any = '', + xPaddingBytes: any = "100-1000", xPaddingObfsMode = false, xPaddingKey = '', xPaddingHeader = '', @@ -429,9 +431,9 @@ export class xHTTPStreamSettings extends CommonClass { seqKey = '', uplinkDataPlacement = '', uplinkDataKey = '', - scMaxEachPostBytes = "1000000", + scMaxEachPostBytes: any = "1000000", // Client-side only - headers = [], + headers: any[] = [], uplinkHTTPMethod = '', uplinkChunkSize = 0, noGRPCHeader = false, @@ -475,17 +477,17 @@ export class xHTTPStreamSettings extends CommonClass { this.enableXmux = enableXmux; } - addHeader(name, value) { + addHeader(name: any, value: any): void { this.headers.push({ name: name, value: value }); } - removeHeader(index) { + removeHeader(index: number): void { this.headers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { const headersInput = json.headers; - let headers = []; + let headers: any[] = []; if (Array.isArray(headersInput)) { headers = headersInput; } else if (headersInput && typeof headersInput === 'object') { @@ -524,7 +526,7 @@ export class xHTTPStreamSettings extends CommonClass { toJson() { // Upstream expects headers as a {name: value} map, not a list of entries. - const headersMap = {}; + const headersMap: any = {}; if (Array.isArray(this.headers)) { for (const h of this.headers) { if (h && h.name) headersMap[h.name] = h.value || ''; @@ -566,9 +568,9 @@ export class xHTTPStreamSettings extends CommonClass { export class TlsStreamSettings extends CommonClass { constructor( - serverName = '', - alpn = [], - fingerprint = '', + serverName: any = '', + alpn: any[] = [], + fingerprint: any = '', echConfigList = '', verifyPeerCertByName = '', pinnedPeerCertSha256 = '', @@ -582,7 +584,7 @@ export class TlsStreamSettings extends CommonClass { this.pinnedPeerCertSha256 = pinnedPeerCertSha256; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new TlsStreamSettings( json.serverName, json.alpn, @@ -607,12 +609,12 @@ export class TlsStreamSettings extends CommonClass { export class RealityStreamSettings extends CommonClass { constructor( - publicKey = '', - fingerprint = '', - serverName = '', - shortId = '', - spiderX = '', - mldsa65Verify = '' + publicKey: any = '', + fingerprint: any = '', + serverName: any = '', + shortId: any = '', + spiderX: any = '', + mldsa65Verify: any = '' ) { super(); this.publicKey = publicKey; @@ -622,7 +624,7 @@ export class RealityStreamSettings extends CommonClass { this.spiderX = spiderX; this.mldsa65Verify = mldsa65Verify; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new RealityStreamSettings( json.publicKey, json.fingerprint, @@ -680,7 +682,7 @@ export class HysteriaStreamSettings extends CommonClass { this.disablePathMTUDiscovery = disablePathMTUDiscovery; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { let udphopPort = ''; let udphopIntervalMin = 30; let udphopIntervalMax = 30; @@ -715,7 +717,7 @@ export class HysteriaStreamSettings extends CommonClass { } toJson() { - const result = { + const result: any = { version: this.version, auth: this.auth, congestion: this.congestion, @@ -765,7 +767,7 @@ export class SockoptStreamSettings extends CommonClass { } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (Object.keys(json).length === 0) return undefined; return new SockoptStreamSettings( json.dialerProxy, @@ -781,7 +783,7 @@ export class SockoptStreamSettings extends CommonClass { } toJson() { - const result = { + const result: any = { dialerProxy: this.dialerProxy, tcpFastOpen: this.tcpFastOpen, tcpKeepAliveInterval: this.tcpKeepAliveInterval, @@ -799,13 +801,13 @@ export class SockoptStreamSettings extends CommonClass { } export class UdpMask extends CommonClass { - constructor(type = 'salamander', settings = {}) { + constructor(type: any = 'salamander', settings: any = {}) { super(); this.type = type; this.settings = this._getDefaultSettings(type, settings); } - _getDefaultSettings(type, settings = {}) { + _getDefaultSettings(type: any, settings: any = {}): any { switch (type) { case 'salamander': case 'mkcp-aes128gcm': @@ -846,7 +848,7 @@ export class UdpMask extends CommonClass { } } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new UdpMask( json.type || 'salamander', json.settings || {} @@ -854,7 +856,7 @@ export class UdpMask extends CommonClass { } toJson() { - const cleanItem = item => { + const cleanItem = (item: any) => { const out = { ...item }; if (out.type === 'array') { delete out.packet; @@ -884,13 +886,13 @@ export class UdpMask extends CommonClass { } export class TcpMask extends CommonClass { - constructor(type = 'fragment', settings = {}) { + constructor(type: any = 'fragment', settings: any = {}) { super(); this.type = type; this.settings = this._getDefaultSettings(type, settings); } - _getDefaultSettings(type, settings = {}) { + _getDefaultSettings(type: any, settings: any = {}): any { switch (type) { case 'fragment': return { @@ -918,7 +920,7 @@ export class TcpMask extends CommonClass { } } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new TcpMask( json.type || 'fragment', json.settings || {} @@ -926,7 +928,7 @@ export class TcpMask extends CommonClass { } toJson() { - const cleanItem = item => { + const cleanItem = (item: any) => { const out = { ...item }; if (out.type === 'array') { delete out.packet; @@ -939,7 +941,7 @@ export class TcpMask extends CommonClass { let settings = this.settings; if (this.type === 'header-custom' && settings) { - const cleanGroup = group => Array.isArray(group) ? group.map(cleanItem) : group; + const cleanGroup = (group: any) => Array.isArray(group) ? group.map(cleanItem) : group; settings = { ...settings, clients: Array.isArray(settings.clients) ? settings.clients.map(cleanGroup) : settings.clients, @@ -956,11 +958,11 @@ export class TcpMask extends CommonClass { export class QuicParams extends CommonClass { constructor( - congestion = 'bbr', - debug = false, - brutalUp = 65537, - brutalDown = 65537, - udpHop = undefined, + congestion: any = 'bbr', + debug: any = false, + brutalUp: any = 65537, + brutalDown: any = 65537, + udpHop: any = undefined, initStreamReceiveWindow = 8388608, maxStreamReceiveWindow = 8388608, initConnectionReceiveWindow = 20971520, @@ -994,7 +996,7 @@ export class QuicParams extends CommonClass { this.udpHop = value ? (this.udpHop || { ports: '20000-50000', interval: '5-10' }) : undefined; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (!json || Object.keys(json).length === 0) return undefined; return new QuicParams( json.congestion, @@ -1014,7 +1016,7 @@ export class QuicParams extends CommonClass { } toJson() { - const result = { congestion: this.congestion }; + const result: any = { congestion: this.congestion } as any; if (this.debug) result.debug = this.debug; if (['brutal', 'force-brutal'].includes(this.congestion)) { if (this.brutalUp) result.brutalUp = this.brutalUp; @@ -1034,10 +1036,10 @@ export class QuicParams extends CommonClass { } export class FinalMaskStreamSettings extends CommonClass { - constructor(tcp = [], udp = [], quicParams = undefined) { + constructor(tcp: any[] = [], udp: any[] = [], quicParams: any = undefined) { super(); - this.tcp = Array.isArray(tcp) ? tcp.map(t => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : []; - this.udp = Array.isArray(udp) ? udp.map(u => new UdpMask(u.type, u.settings)) : [new UdpMask(udp.type, udp.settings)]; + this.tcp = Array.isArray(tcp) ? tcp.map((t: any) => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : []; + this.udp = Array.isArray(udp) ? udp.map((u: any) => new UdpMask(u.type, u.settings)) : [new UdpMask((udp as any).type, (udp as any).settings)]; this.quicParams = quicParams instanceof QuicParams ? quicParams : (quicParams ? QuicParams.fromJson(quicParams) : undefined); } @@ -1049,7 +1051,7 @@ export class FinalMaskStreamSettings extends CommonClass { this.quicParams = value ? (this.quicParams || new QuicParams()) : undefined; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new FinalMaskStreamSettings( json.tcp || [], json.udp || [], @@ -1058,12 +1060,12 @@ export class FinalMaskStreamSettings extends CommonClass { } toJson() { - const result = {}; + const result: any = {} as any; if (this.tcp && this.tcp.length > 0) { - result.tcp = this.tcp.map(t => t.toJson()); + result.tcp = this.tcp.map((t: any) => t.toJson()); } if (this.udp && this.udp.length > 0) { - result.udp = this.udp.map(udp => udp.toJson()); + result.udp = this.udp.map((udp: any) => udp.toJson()); } if (this.quicParams) { result.quicParams = this.quicParams.toJson(); @@ -1108,7 +1110,7 @@ export class StreamSettings extends CommonClass { this.finalmask.tcp.push(new TcpMask(type)); } - delTcpMask(index) { + delTcpMask(index: number) { if (this.finalmask.tcp) { this.finalmask.tcp.splice(index, 1); } @@ -1118,7 +1120,7 @@ export class StreamSettings extends CommonClass { this.finalmask.udp.push(new UdpMask(type)); } - delUdpMask(index) { + delUdpMask(index: number) { if (this.finalmask.udp) { this.finalmask.udp.splice(index, 1); } @@ -1147,7 +1149,7 @@ export class StreamSettings extends CommonClass { this.sockopt = value ? new SockoptStreamSettings() : undefined; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { // Xray-core supports both "xhttpSettings" and "splithttpSettings" (backward-compat alias) const xhttpJson = json.xhttpSettings ?? json.splithttpSettings; // Normalize "splithttp" network name to "xhttp" for internal consistency @@ -1198,7 +1200,7 @@ export class Mux extends CommonClass { this.xudpProxyUDP443 = xudpProxyUDP443; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (Object.keys(json).length === 0) return undefined; return new Mux( json.enabled, @@ -1219,13 +1221,28 @@ export class Mux extends CommonClass { } export class Outbound extends CommonClass { + static Settings: any; + static FreedomSettings: any; + static BlackholeSettings: any; + static LoopbackSettings: any; + static DNSRule: any; + static DNSSettings: any; + static VmessSettings: any; + static VLESSSettings: any; + static TrojanSettings: any; + static ShadowsocksSettings: any; + static SocksSettings: any; + static HttpSettings: any; + static WireguardSettings: any; + static HysteriaSettings: any; + constructor( - tag = '', - protocol = Protocols.VLESS, - settings = null, - streamSettings = new StreamSettings(), - sendThrough, - mux = new Mux(), + tag: any = '', + protocol: any = Protocols.VLESS, + settings: any = null, + streamSettings: any = new StreamSettings(), + sendThrough?: any, + mux: any = new Mux(), ) { super(); this.tag = tag; @@ -1320,7 +1337,7 @@ export class Outbound extends CommonClass { return [Protocols.Socks, Protocols.HTTP].includes(this.protocol); } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound( json.tag, json.protocol, @@ -1332,14 +1349,14 @@ export class Outbound extends CommonClass { } toJson() { - var stream; + let stream; if (this.canEnableStream()) { stream = this.stream.toJson(); } else { if (this.stream?.sockopt) stream = { sockopt: this.stream.sockopt.toJson() }; } - let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings; + const settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings; return { protocol: this.protocol, settings: settingsOut, @@ -1351,7 +1368,7 @@ export class Outbound extends CommonClass { }; } - static fromLink(link) { + static fromLink(link: any) { const data = link.split('://'); if (data.length != 2) return null; switch (data[0].toLowerCase()) { @@ -1369,10 +1386,10 @@ export class Outbound extends CommonClass { } } - static fromVmessLink(json = {}) { - let stream = new StreamSettings(json.net, json.tls); + static fromVmessLink(json: any = {}) { + const stream = new StreamSettings(json.net, json.tls); - let network = json.net; + const network = json.net; if (network === 'tcp') { stream.tcp = new TcpStreamSettings( json.type, @@ -1402,7 +1419,7 @@ export class Outbound extends CommonClass { if (typeof json.xPaddingBytes === 'string' && json.xPaddingBytes) xh.xPaddingBytes = json.xPaddingBytes; if (json.xPaddingObfsMode === true) { xh.xPaddingObfsMode = true; - ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => { + ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => { if (typeof json[k] === 'string' && json[k]) xh[k] = json[k]; }); } @@ -1414,7 +1431,7 @@ export class Outbound extends CommonClass { "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes", "scMinPostsIntervalMs", ]; - xFields.forEach(k => { + xFields.forEach((k: string) => { if (typeof json[k] === 'string' && json[k]) xh[k] = json[k]; }); if (typeof json.uplinkChunkSize === 'number' && json.uplinkChunkSize !== 0) xh.uplinkChunkSize = json.uplinkChunkSize; @@ -1451,17 +1468,17 @@ export class Outbound extends CommonClass { return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, port, json.id, json.scy), stream); } - static fromParamLink(link) { + static fromParamLink(link: any) { const url = new URL(link); - let type = url.searchParams.get('type') ?? 'tcp'; - let security = url.searchParams.get('security') ?? 'none'; - let stream = new StreamSettings(type, security); + const type = url.searchParams.get('type') ?? 'tcp'; + const security = url.searchParams.get('security') ?? 'none'; + const stream = new StreamSettings(type, security); - let headerType = url.searchParams.get('headerType') ?? undefined; - let host = url.searchParams.get('host') ?? undefined; - let path = url.searchParams.get('path') ?? undefined; - let seed = url.searchParams.get('seed') ?? path ?? undefined; - let mode = url.searchParams.get('mode') ?? undefined; + const headerType = url.searchParams.get('headerType') ?? undefined; + const host = url.searchParams.get('host') ?? undefined; + const path = url.searchParams.get('path') ?? undefined; + const seed = url.searchParams.get('seed') ?? path ?? undefined; + const mode = url.searchParams.get('mode') ?? undefined; if (type === 'tcp' || type === 'none') { stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path); @@ -1496,7 +1513,7 @@ export class Outbound extends CommonClass { const extra = JSON.parse(extraRaw); if (typeof extra.xPaddingBytes === 'string' && extra.xPaddingBytes) xh.xPaddingBytes = extra.xPaddingBytes; if (extra.xPaddingObfsMode === true) xh.xPaddingObfsMode = true; - ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => { + ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => { if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k]; }); if (!xh.mode && typeof extra.mode === 'string' && extra.mode) xh.mode = extra.mode; @@ -1508,7 +1525,7 @@ export class Outbound extends CommonClass { "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes", "scMinPostsIntervalMs", ]; - xFields.forEach(k => { + xFields.forEach((k: string) => { if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k]; }); if (typeof extra.uplinkChunkSize === 'number' && extra.uplinkChunkSize !== 0) xh.uplinkChunkSize = extra.uplinkChunkSize; @@ -1529,20 +1546,20 @@ export class Outbound extends CommonClass { } if (security == 'tls') { - let fp = url.searchParams.get('fp') ?? 'none'; - let alpn = url.searchParams.get('alpn'); - let sni = url.searchParams.get('sni') ?? ''; - let ech = url.searchParams.get('ech') ?? ''; + const fp = url.searchParams.get('fp') ?? 'none'; + const alpn = url.searchParams.get('alpn'); + const sni = url.searchParams.get('sni') ?? ''; + const ech = url.searchParams.get('ech') ?? ''; stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech); } if (security == 'reality') { - let pbk = url.searchParams.get('pbk'); - let fp = url.searchParams.get('fp'); - let sni = url.searchParams.get('sni') ?? ''; - let sid = url.searchParams.get('sid') ?? ''; - let spx = url.searchParams.get('spx') ?? ''; - let pqv = url.searchParams.get('pqv') ?? ''; + const pbk = url.searchParams.get('pbk'); + const fp = url.searchParams.get('fp'); + const sni = url.searchParams.get('sni') ?? ''; + const sid = url.searchParams.get('sid') ?? ''; + const spx = url.searchParams.get('spx') ?? ''; + const pqv = url.searchParams.get('pqv') ?? ''; stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx, pqv); } @@ -1550,13 +1567,16 @@ export class Outbound extends CommonClass { const match = link.match(regex); if (!match) return null; - let [, protocol, userData, address, port,] = match; + const address = match[3]; + let protocol = match[1]; + let userData: any = match[2]; + let port: any = match[4]; port *= 1; if (protocol == 'ss') { protocol = 'shadowsocks'; userData = atob(userData).split(':'); } - var settings; + let settings; switch (protocol) { case Protocols.VLESS: settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none'); @@ -1564,10 +1584,11 @@ export class Outbound extends CommonClass { case Protocols.Trojan: settings = new Outbound.TrojanSettings(address, port, userData); break; - case Protocols.Shadowsocks: - let method = userData.splice(0, 1)[0]; + case Protocols.Shadowsocks: { + const method = userData.splice(0, 1)[0]; settings = new Outbound.ShadowsocksSettings(address, port, userData.join(":"), method, true); break; + } default: return null; } @@ -1585,29 +1606,30 @@ export class Outbound extends CommonClass { return new Outbound(remark, protocol, settings, stream); } - static fromHysteriaLink(link) { + static fromHysteriaLink(link: any) { // Parse hysteria2://password@address:port[?param1=value1¶m2=value2...][#remarks] const regex = /^hysteria2?:\/\/([^@]+)@([^:?#]+):(\d+)([^#]*)(#.*)?$/; const match = link.match(regex); if (!match) return null; - let [, password, address, port, params, hash] = match; + const password = match[1]; + const address = match[2]; + let port: any = match[3]; + const params = match[4]; + const hash = match[5]; port = parseInt(port); - // Parse URL parameters if present - let urlParams = new URLSearchParams(params); + const urlParams = new URLSearchParams(params); - // Create stream settings with hysteria network - let security = urlParams.get('security') ?? 'none'; - let stream = new StreamSettings('hysteria', security); + const security = urlParams.get('security') ?? 'none'; + const stream = new StreamSettings('hysteria', security); - // Parse TLS settings when security=tls if (security === 'tls') { - let fp = urlParams.get('fp') ?? 'none'; - let alpn = urlParams.get('alpn'); - let sni = urlParams.get('sni') ?? ''; - let ech = urlParams.get('ech') ?? ''; + const fp = urlParams.get('fp') ?? 'none'; + const alpn = urlParams.get('alpn'); + const sni = urlParams.get('sni') ?? ''; + const ech = urlParams.get('ech') ?? ''; stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech); } @@ -1619,7 +1641,7 @@ export class Outbound extends CommonClass { stream.hysteria.udphopPort = urlParams.get('udphopPort') ?? ''; // Support both old single interval and new min/max range if (urlParams.has('udphopInterval')) { - const interval = parseInt(urlParams.get('udphopInterval')); + const interval = parseInt(urlParams.get('udphopInterval')!); stream.hysteria.udphopIntervalMin = interval; stream.hysteria.udphopIntervalMax = interval; } else { @@ -1629,22 +1651,22 @@ export class Outbound extends CommonClass { // Optional QUIC parameters for FinalMask support and hysteria2 share links if (urlParams.has('initStreamReceiveWindow')) { - stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow')); + stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow')!); } if (urlParams.has('maxStreamReceiveWindow')) { - stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow')); + stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow')!); } if (urlParams.has('initConnectionReceiveWindow')) { - stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow')); + stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow')!); } if (urlParams.has('maxConnectionReceiveWindow')) { - stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow')); + stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow')!); } if (urlParams.has('maxIdleTimeout')) { - stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout')); + stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout')!); } if (urlParams.has('keepAlivePeriod')) { - stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod')); + stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod')!); } if (urlParams.has('disablePathMTUDiscovery')) { stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true'; @@ -1682,23 +1704,21 @@ export class Outbound extends CommonClass { } catch (_) { /* ignore malformed fm */ } } - // Create settings - let settings = new Outbound.HysteriaSettings(address, port, 2); + const settings = new Outbound.HysteriaSettings(address, port, 2); - // Extract remark from hash - let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`; + const remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`; return new Outbound(remark, Protocols.Hysteria, settings, stream); } } Outbound.Settings = class extends CommonClass { - constructor(protocol) { + constructor(protocol: any) { super(); this.protocol = protocol; } - static getSettings(protocol) { + static getSettings(protocol: any): any { switch (protocol) { case Protocols.Freedom: return new Outbound.FreedomSettings(); case Protocols.Blackhole: return new Outbound.BlackholeSettings(); @@ -1716,7 +1736,7 @@ Outbound.Settings = class extends CommonClass { } } - static fromJson(protocol, json) { + static fromJson(protocol: any, json: any): any { switch (protocol) { case Protocols.Freedom: return Outbound.FreedomSettings.fromJson(json); case Protocols.Blackhole: return Outbound.BlackholeSettings.fromJson(json); @@ -1752,7 +1772,7 @@ Outbound.FreedomSettings = class extends CommonClass { this.fragment = fragment || {}; this.noises = Array.isArray(noises) ? noises : []; this.finalRules = Array.isArray(finalRules) - ? finalRules.map(rule => rule instanceof Outbound.FreedomSettings.FinalRule ? rule : Outbound.FreedomSettings.FinalRule.fromJson(rule)) + ? finalRules.map((rule: any) => rule instanceof Outbound.FreedomSettings.FinalRule ? rule : Outbound.FreedomSettings.FinalRule.fromJson(rule)) : []; } @@ -1760,7 +1780,7 @@ Outbound.FreedomSettings = class extends CommonClass { this.noises.push(new Outbound.FreedomSettings.Noise()); } - delNoise(index) { + delNoise(index: number) { this.noises.splice(index, 1); } @@ -1768,13 +1788,13 @@ Outbound.FreedomSettings = class extends CommonClass { this.finalRules.push(new Outbound.FreedomSettings.FinalRule(action)); } - delFinalRule(index) { + delFinalRule(index: number) { this.finalRules.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { const finalRules = Array.isArray(json.finalRules) - ? json.finalRules.map(rule => Outbound.FreedomSettings.FinalRule.fromJson(rule)) + ? json.finalRules.map((rule: any) => Outbound.FreedomSettings.FinalRule.fromJson(rule)) : []; // Backward compatibility: map legacy ipsBlocked entries to blocking finalRules. @@ -1786,7 +1806,7 @@ Outbound.FreedomSettings = class extends CommonClass { json.domainStrategy, json.redirect, json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : {}, - json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : [], + json.noises ? json.noises.map((noise: any) => Outbound.FreedomSettings.Noise.fromJson(noise)) : [], finalRules, ); } @@ -1816,7 +1836,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass { this.maxSplit = maxSplit; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.FreedomSettings.Fragment( json.packets, json.length, @@ -1840,7 +1860,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass { this.applyTo = applyTo; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.FreedomSettings.Noise( json.type, json.packet, @@ -1869,7 +1889,7 @@ Outbound.FreedomSettings.FinalRule = class extends CommonClass { this.blockDelay = blockDelay; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.FreedomSettings.FinalRule( json.action, Array.isArray(json.network) ? json.network.join(',') : json.network, @@ -1891,12 +1911,12 @@ Outbound.FreedomSettings.FinalRule = class extends CommonClass { }; Outbound.BlackholeSettings = class extends CommonClass { - constructor(type) { + constructor(type?: any) { super(); this.type = type; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.BlackholeSettings( json.response ? json.response.type : undefined, ); @@ -1915,7 +1935,7 @@ Outbound.LoopbackSettings = class extends CommonClass { this.inboundTag = inboundTag; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.LoopbackSettings(json.inboundTag || ''); } @@ -1934,7 +1954,7 @@ Outbound.DNSRule = class extends CommonClass { this.domain = domain; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.DNSRule( json.action, normalizeDNSRuleField(json.qtype), @@ -1943,7 +1963,7 @@ Outbound.DNSRule = class extends CommonClass { } toJson() { - const rule = { + const rule: any = { action: normalizeDNSRuleAction(this.action), }; @@ -1981,18 +2001,18 @@ Outbound.DNSSettings = class extends CommonClass { this.rewriteAddress = rewriteAddress; this.rewritePort = rewritePort; this.userLevel = userLevel; - this.rules = Array.isArray(rules) ? rules.map(rule => rule instanceof Outbound.DNSRule ? rule : Outbound.DNSRule.fromJson(rule)) : []; + this.rules = Array.isArray(rules) ? rules.map((rule: any) => rule instanceof Outbound.DNSRule ? rule : Outbound.DNSRule.fromJson(rule)) : []; } addRule(action = 'direct') { this.rules.push(new Outbound.DNSRule(action)); } - delRule(index) { + delRule(index: number) { this.rules.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { // Spec uses rewrite{Network,Address,Port}; older configs used the // bare network/address/port keys — accept both so existing saved // configs keep working after the migration. @@ -2006,7 +2026,7 @@ Outbound.DNSSettings = class extends CommonClass { } toJson() { - const json = {}; + const json: any = {}; if (!ObjectUtil.isEmpty(this.rewriteNetwork)) json.rewriteNetwork = this.rewriteNetwork; if (!ObjectUtil.isEmpty(this.rewriteAddress)) json.rewriteAddress = this.rewriteAddress; if (this.rewritePort > 0) json.rewritePort = this.rewritePort; @@ -2016,7 +2036,7 @@ Outbound.DNSSettings = class extends CommonClass { } }; Outbound.VmessSettings = class extends CommonClass { - constructor(address, port, id, security) { + constructor(address?: any, port?: any, id?: any, security?: any) { super(); this.address = address; this.port = port; @@ -2024,7 +2044,7 @@ Outbound.VmessSettings = class extends CommonClass { this.security = security; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (!ObjectUtil.isArrEmpty(json.vnext)) { const v = json.vnext[0] || {}; const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0]; @@ -2051,7 +2071,7 @@ Outbound.VmessSettings = class extends CommonClass { } }; Outbound.VLESSSettings = class extends CommonClass { - constructor(address, port, id, flow, encryption = 'none', reverseTag = '', reverseSniffing = new ReverseSniffing(), testpre = 0, testseed = []) { + constructor(address?: any, port?: any, id?: any, flow?: any, encryption: any = 'none', reverseTag: any = '', reverseSniffing: any = new ReverseSniffing(), testpre: any = 0, testseed: any[] = []) { super(); this.address = address; this.port = port; @@ -2064,7 +2084,7 @@ Outbound.VLESSSettings = class extends CommonClass { this.testseed = testseed; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { // Handle v2rayN-style nested vnext array (standard Xray JSON format) if (!ObjectUtil.isArrEmpty(json.vnext)) { const v = json.vnext[0] || {}; @@ -2072,7 +2092,7 @@ Outbound.VLESSSettings = class extends CommonClass { const saved = json.testseed; const testseed = (Array.isArray(saved) && saved.length === 4 - && saved.every(v => Number.isInteger(v) && v > 0)) + && saved.every((v: any) => Number.isInteger(v) && v > 0)) ? saved : []; return new Outbound.VLESSSettings( @@ -2091,7 +2111,7 @@ Outbound.VLESSSettings = class extends CommonClass { const saved = json.testseed; const testseed = (Array.isArray(saved) && saved.length === 4 - && saved.every(v => Number.isInteger(v) && v > 0)) + && saved.every((v: any) => Number.isInteger(v) && v > 0)) ? saved : []; return new Outbound.VLESSSettings( @@ -2108,7 +2128,7 @@ Outbound.VLESSSettings = class extends CommonClass { } toJson() { - const result = { + const result: any = { address: this.address, port: this.port, id: this.id, @@ -2130,7 +2150,7 @@ Outbound.VLESSSettings = class extends CommonClass { } if (Array.isArray(this.testseed) && this.testseed.length === 4 - && this.testseed.every(v => Number.isInteger(v) && v > 0)) { + && this.testseed.every((v: any) => Number.isInteger(v) && v > 0)) { result.testseed = this.testseed; } } @@ -2138,14 +2158,14 @@ Outbound.VLESSSettings = class extends CommonClass { } }; Outbound.TrojanSettings = class extends CommonClass { - constructor(address, port, password) { + constructor(address?: any, port?: any, password?: any) { super(); this.address = address; this.port = port; this.password = password; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings(); return new Outbound.TrojanSettings( json.servers[0].address, @@ -2165,7 +2185,7 @@ Outbound.TrojanSettings = class extends CommonClass { } }; Outbound.ShadowsocksSettings = class extends CommonClass { - constructor(address, port, password, method, uot, UoTVersion) { + constructor(address?: any, port?: any, password?: any, method?: any, uot?: any, UoTVersion?: any) { super(); this.address = address; this.port = port; @@ -2175,7 +2195,7 @@ Outbound.ShadowsocksSettings = class extends CommonClass { this.UoTVersion = UoTVersion; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { let servers = json.servers; if (ObjectUtil.isArrEmpty(servers)) servers = [{}]; return new Outbound.ShadowsocksSettings( @@ -2203,7 +2223,7 @@ Outbound.ShadowsocksSettings = class extends CommonClass { }; Outbound.SocksSettings = class extends CommonClass { - constructor(address, port, user, pass) { + constructor(address?: any, port?: any, user?: any, pass?: any) { super(); this.address = address; this.port = port; @@ -2211,7 +2231,7 @@ Outbound.SocksSettings = class extends CommonClass { this.pass = pass; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { let servers = json.servers; if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }]; return new Outbound.SocksSettings( @@ -2233,7 +2253,7 @@ Outbound.SocksSettings = class extends CommonClass { } }; Outbound.HttpSettings = class extends CommonClass { - constructor(address, port, user, pass) { + constructor(address?: any, port?: any, user?: any, pass?: any) { super(); this.address = address; this.port = port; @@ -2241,7 +2261,7 @@ Outbound.HttpSettings = class extends CommonClass { this.pass = pass; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { let servers = json.servers; if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }]; return new Outbound.HttpSettings( @@ -2290,11 +2310,11 @@ Outbound.WireguardSettings = class extends CommonClass { this.peers.push(new Outbound.WireguardSettings.Peer()); } - delPeer(index) { + delPeer(index: number) { this.peers.splice(index, 1); } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.WireguardSettings( json.mtu, json.secretKey, @@ -2302,7 +2322,7 @@ Outbound.WireguardSettings = class extends CommonClass { json.workers, json.domainStrategy, json.reserved, - json.peers.map(peer => Outbound.WireguardSettings.Peer.fromJson(peer)), + json.peers.map((peer: any) => Outbound.WireguardSettings.Peer.fromJson(peer)), json.noKernelTun, ); } @@ -2337,7 +2357,7 @@ Outbound.WireguardSettings.Peer = class extends CommonClass { this.keepAlive = keepAlive; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { return new Outbound.WireguardSettings.Peer( json.publicKey, json.preSharedKey, @@ -2366,7 +2386,7 @@ Outbound.HysteriaSettings = class extends CommonClass { this.version = version; } - static fromJson(json = {}) { + static fromJson(json: any = {}): any { if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings(); return new Outbound.HysteriaSettings( json.address, diff --git a/frontend/src/models/reality-targets.js b/frontend/src/models/reality-targets.js deleted file mode 100644 index 85a3e881..00000000 --- a/frontend/src/models/reality-targets.js +++ /dev/null @@ -1,24 +0,0 @@ -// List of popular services for VLESS Reality Target/SNI randomization -export const REALITY_TARGETS = [ - { target: 'www.amazon.com:443', sni: 'www.amazon.com' }, - { target: 'aws.amazon.com:443', sni: 'aws.amazon.com' }, - { target: 'www.oracle.com:443', sni: 'www.oracle.com' }, - { target: 'www.nvidia.com:443', sni: 'www.nvidia.com' }, - { target: 'www.amd.com:443', sni: 'www.amd.com' }, - { target: 'www.intel.com:443', sni: 'www.intel.com' }, - { target: 'www.sony.com:443', sni: 'www.sony.com' } -]; - -/** - * Returns a random Reality target configuration from the predefined list - * @returns {Object} Object with target and sni properties - */ -export function getRandomRealityTarget() { - const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length); - const selected = REALITY_TARGETS[randomIndex]; - // Return a copy to avoid reference issues - return { - target: selected.target, - sni: selected.sni - }; -} diff --git a/frontend/src/models/reality-targets.ts b/frontend/src/models/reality-targets.ts new file mode 100644 index 00000000..518c836e --- /dev/null +++ b/frontend/src/models/reality-targets.ts @@ -0,0 +1,23 @@ +export interface RealityTarget { + target: string; + sni: string; +} + +export const REALITY_TARGETS: readonly RealityTarget[] = [ + { target: 'www.amazon.com:443', sni: 'www.amazon.com' }, + { target: 'aws.amazon.com:443', sni: 'aws.amazon.com' }, + { target: 'www.oracle.com:443', sni: 'www.oracle.com' }, + { target: 'www.nvidia.com:443', sni: 'www.nvidia.com' }, + { target: 'www.amd.com:443', sni: 'www.amd.com' }, + { target: 'www.intel.com:443', sni: 'www.intel.com' }, + { target: 'www.sony.com:443', sni: 'www.sony.com' }, +]; + +export function getRandomRealityTarget(): RealityTarget { + const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length); + const selected = REALITY_TARGETS[randomIndex]; + return { + target: selected.target, + sni: selected.sni, + }; +} diff --git a/frontend/src/pages/api-docs/ApiDocsPage.css b/frontend/src/pages/api-docs/ApiDocsPage.css index f6ac4f7b..c6cb6daf 100644 --- a/frontend/src/pages/api-docs/ApiDocsPage.css +++ b/frontend/src/pages/api-docs/ApiDocsPage.css @@ -1,13 +1,4 @@ -.api-docs-page { - --bg-page: #e6e8ec; - --bg-card: #ffffff; - min-height: 100vh; - background: var(--bg-page); -} - .api-docs-page.is-dark { - --bg-page: #1a1b1f; - --bg-card: #23252b; --sw-bg: #1f2026; --sw-bg-soft: #25272e; --sw-bg-input: #15161a; @@ -22,8 +13,6 @@ } .api-docs-page.is-dark.is-ultra { - --bg-page: #000; - --bg-card: #101013; --sw-bg: #0a0a0d; --sw-bg-soft: #131316; --sw-bg-input: #050507; @@ -51,7 +40,7 @@ .api-docs-page .docs-wrapper { background: var(--bg-card); border-radius: 8px; - border: 1px solid rgba(128, 128, 128, 0.12); + border: 1px solid var(--ant-color-border-secondary); overflow: hidden; } diff --git a/frontend/src/pages/api-docs/ApiDocsPage.tsx b/frontend/src/pages/api-docs/ApiDocsPage.tsx index e708b45c..8841c707 100644 --- a/frontend/src/pages/api-docs/ApiDocsPage.tsx +++ b/frontend/src/pages/api-docs/ApiDocsPage.tsx @@ -5,7 +5,6 @@ import 'swagger-ui-react/swagger-ui.css'; import { useTheme } from '@/hooks/useTheme'; import AppSidebar from '@/components/AppSidebar'; -import '@/styles/page-cards.css'; import './ApiDocsPage.css'; const basePath = window.X_UI_BASE_PATH || ''; diff --git a/frontend/src/pages/clients/ClientBulkAddModal.css b/frontend/src/pages/clients/ClientBulkAddModal.css deleted file mode 100644 index e49ef577..00000000 --- a/frontend/src/pages/clients/ClientBulkAddModal.css +++ /dev/null @@ -1,5 +0,0 @@ -.random-icon { - margin-left: 4px; - cursor: pointer; - color: var(--ant-color-primary, #1677ff); -} diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx index 1b15267f..264b3229 100644 --- a/frontend/src/pages/clients/ClientBulkAddModal.tsx +++ b/frontend/src/pages/clients/ClientBulkAddModal.tsx @@ -9,7 +9,6 @@ import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils'; import { TLS_FLOW_CONTROL } from '@/models/inbound'; import DateTimePicker from '@/components/DateTimePicker'; import type { InboundOption } from '@/hooks/useClients'; -import './ClientBulkAddModal.css'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const; diff --git a/frontend/src/pages/clients/ClientInfoModal.css b/frontend/src/pages/clients/ClientInfoModal.css index 9cf9c9bc..1320fd98 100644 --- a/frontend/src/pages/clients/ClientInfoModal.css +++ b/frontend/src/pages/clients/ClientInfoModal.css @@ -40,7 +40,7 @@ } .link-panel { - border: 1px solid rgba(128, 128, 128, 0.2); + border: 1px solid var(--ant-color-border); border-radius: 8px; padding: 10px; margin-bottom: 10px; @@ -62,37 +62,25 @@ word-break: break-all; white-space: pre-wrap; padding: 6px 8px; - background: rgba(0, 0, 0, 0.04); + background: var(--ant-color-fill-tertiary); border-radius: 4px; user-select: all; } -body.dark .link-panel-text { - background: rgba(255, 255, 255, 0.05); -} - .link-panel-anchor { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; word-break: break-all; padding: 6px 8px; - background: rgba(0, 0, 0, 0.04); + background: var(--ant-color-fill-tertiary); border-radius: 4px; - color: var(--ant-color-primary, #1677ff); + color: var(--ant-color-primary); text-decoration: underline; - text-decoration-color: rgba(22, 119, 255, 0.4); + text-decoration-color: color-mix(in srgb, var(--ant-color-primary) 40%, transparent); transition: background 120ms ease, text-decoration-color 120ms ease; } .link-panel-anchor:hover { - background: rgba(22, 119, 255, 0.08); - text-decoration-color: var(--ant-color-primary, #1677ff); -} - -body.dark .link-panel-anchor { - background: rgba(255, 255, 255, 0.05); -} - -body.dark .link-panel-anchor:hover { - background: rgba(22, 119, 255, 0.16); + background: color-mix(in srgb, var(--ant-color-primary) 12%, transparent); + text-decoration-color: var(--ant-color-primary); } diff --git a/frontend/src/pages/clients/ClientsPage.css b/frontend/src/pages/clients/ClientsPage.css index ab1ce76c..8153b279 100644 --- a/frontend/src/pages/clients/ClientsPage.css +++ b/frontend/src/pages/clients/ClientsPage.css @@ -1,56 +1,6 @@ -.clients-page { - --bg-page: #e6e8ec; - --bg-card: #ffffff; - min-height: 100vh; - background: var(--bg-page); -} - -.clients-page.is-dark { - --bg-page: #1a1b1f; - --bg-card: #23252b; -} - -.clients-page.is-dark.is-ultra { - --bg-page: #000; - --bg-card: #101013; -} - -.clients-page .ant-layout, -.clients-page .ant-layout-content { - background: transparent; -} - -.clients-page .content-shell { - background: transparent; -} - -.clients-page .content-area { - padding: 24px; -} - -@media (max-width: 768px) { - .clients-page .content-area { - padding: 8px; - } -} - .clients-page .ant-pagination-options-size-changer, .clients-page .ant-pagination-options-size-changer .ant-select-selector { - min-width: 100px !important; -} - -.clients-page .loading-spacer { - min-height: calc(100vh - 120px); -} - -.clients-page .summary-card { - padding: 16px; -} - -@media (max-width: 768px) { - .clients-page .summary-card { - padding: 8px; - } + min-width: 100px; } .client-email-list { @@ -92,11 +42,11 @@ vertical-align: middle; } -.dot-green { background: #52c41a; } -.dot-blue { background: #1677ff; } -.dot-red { background: #ff4d4f; } -.dot-orange { background: #fa8c16; } -.dot-gray { background: rgba(128, 128, 128, 0.6); } +.dot-green { background: var(--ant-color-success); } +.dot-blue { background: var(--ant-color-primary); } +.dot-red { background: var(--ant-color-error); } +.dot-orange { background: var(--ant-color-warning); } +.dot-gray { background: var(--ant-color-text-quaternary); } .status-tag { margin: 0 0 0 4px; @@ -154,32 +104,27 @@ .card-pagination .ant-pagination-options-size-changer, .card-pagination .ant-pagination-options-size-changer .ant-select-selector { - min-width: 88px !important; + min-width: 88px; } .bulk-count { font-size: 12px; - background: rgba(22, 119, 255, 0.12); - color: var(--ant-color-primary, #1677ff); + background: color-mix(in srgb, var(--ant-color-primary) 12%, transparent); + color: var(--ant-color-primary); padding: 1px 8px; border-radius: 10px; } .client-card { - border: 1px solid rgba(128, 128, 128, 0.2); + border: 1px solid var(--ant-color-border-secondary); border-radius: 10px; padding: 10px 12px; - background: rgba(255, 255, 255, 0.02); + background: var(--ant-color-fill-quaternary); } .client-card.is-selected { - border-color: var(--ant-color-primary, #1677ff); - background: rgba(22, 119, 255, 0.06); -} - -body.dark .client-card { - background: rgba(255, 255, 255, 0.03); - border-color: rgba(255, 255, 255, 0.1); + border-color: var(--ant-color-primary); + background: color-mix(in srgb, var(--ant-color-primary) 6%, transparent); } .card-head { diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx index 0a024536..872f87b4 100644 --- a/frontend/src/pages/clients/ClientsPage.tsx +++ b/frontend/src/pages/clients/ClientsPage.tsx @@ -18,6 +18,7 @@ import { Select, Space, Spin, + Statistic, Switch, Table, Tag, @@ -49,7 +50,6 @@ import { useClients } from '@/hooks/useClients'; import { useDatepicker } from '@/hooks/useDatepicker'; import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import AppSidebar from '@/components/AppSidebar'; -import CustomStatistic from '@/components/CustomStatistic'; import { IntlUtil, SizeFormatter } from '@/utils'; import { setMessageInstance } from '@/utils/messageBus'; import LazyMount from '@/components/LazyMount'; @@ -58,7 +58,6 @@ const ClientInfoModal = lazy(() => import('./ClientInfoModal')); const ClientQrModal = lazy(() => import('./ClientQrModal')); const ClientBulkAddModal = lazy(() => import('./ClientBulkAddModal')); const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal')); -import '@/styles/page-cards.css'; import './ClientsPage.css'; const FILTER_STATE_KEY = 'clientsFilterState'; @@ -216,13 +215,12 @@ export default function ClientsPage() { return 'active'; }, [expireDiff, trafficDiff]); - function bucketBadgeColor(bucket: Bucket | null): string { + function bucketBadgeStatus(bucket: Bucket | null): 'success' | 'warning' | 'error' | 'default' { switch (bucket) { - case 'depleted': return '#ff4d4f'; - case 'expiring': return '#fa8c16'; - case 'deactive': return 'rgba(128,128,128,0.6)'; - case 'active': return '#52c41a'; - default: return 'rgba(128,128,128,0.6)'; + case 'depleted': return 'error'; + case 'expiring': return 'warning'; + case 'active': return 'success'; + default: return 'default'; } } @@ -624,7 +622,7 @@ export default function ClientsPage() { - } /> + } /> {summary.online.map((e) =>
{e}
)}} > - } /> + } />
@@ -641,7 +639,7 @@ export default function ClientsPage() { open={summary.depleted.length ? undefined : false} content={
{summary.depleted.map((e) =>
{e}
)}
} > - } /> + } /> @@ -650,7 +648,7 @@ export default function ClientsPage() { open={summary.expiring.length ? undefined : false} content={
{summary.expiring.map((e) =>
{e}
)}
} > - } /> + } /> @@ -659,11 +657,11 @@ export default function ClientsPage() { open={summary.deactive.length ? undefined : false} content={
{summary.deactive.map((e) =>
{e}
)}
} > - } /> + } /> - } /> + } />
@@ -838,7 +836,7 @@ export default function ClientsPage() { checked={selectedRowKeys.includes(row.email)} onChange={(e) => toggleSelect(row.email, e.target.checked)} /> - + {row.email} {bucket === 'depleted' && {t('depleted')}} {bucket === 'expiring' && {t('depletingSoon')}} diff --git a/frontend/src/pages/inbounds/InboundFormModal.css b/frontend/src/pages/inbounds/InboundFormModal.css index 505ef12f..ab149e81 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.css +++ b/frontend/src/pages/inbounds/InboundFormModal.css @@ -1,22 +1,3 @@ -.mt-4 { margin-top: 4px; } -.mt-8 { margin-top: 8px; } -.mt-12 { margin-top: 12px; } -.mb-4 { margin-bottom: 4px; } -.mb-8 { margin-bottom: 8px; } -.mb-12 { margin-bottom: 12px; } - -.random-icon { - margin-left: 4px; - cursor: pointer; - color: var(--ant-color-primary, #1890ff); -} - -.danger-icon { - margin-left: 6px; - cursor: pointer; - color: #ff4d4f; -} - .vless-auth-state { display: block; margin-top: 6px; @@ -34,9 +15,9 @@ .advanced-panel { padding: 14px; - border: 1px solid rgba(128, 128, 128, 0.18); + border: 1px solid var(--ant-color-border-secondary); border-radius: 12px; - background: rgba(128, 128, 128, 0.04); + background: var(--ant-color-fill-quaternary); } .advanced-panel__header { @@ -79,9 +60,3 @@ padding-inline: 10px; } } - -body.dark .advanced-panel, -html[data-theme='ultra-dark'] .advanced-panel { - border-color: rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.03); -} diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx index 369a1945..dd1c43c4 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/InboundFormModal.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import dayjs, { type Dayjs } from 'dayjs'; @@ -55,8 +54,8 @@ import { DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION, MODE_OPTION, -} from '@/models/inbound.js'; -import { DBInbound } from '@/models/dbinbound.js'; +} from '@/models/inbound'; +import { DBInbound } from '@/models/dbinbound'; import FinalMaskForm from '@/components/FinalMaskForm'; import DateTimePicker from '@/components/DateTimePicker'; import JsonEditor from '@/components/JsonEditor'; @@ -71,11 +70,75 @@ interface InboundFormModalProps { onClose: () => void; onSaved: () => void; mode: 'add' | 'edit'; - dbInbound: any; - dbInbounds: any[]; + dbInbound: DBInbound | null; + dbInbounds: DBInbound[]; availableNodes?: NodeRecord[]; } +interface StreamLike { + network?: string; + tcp?: { type?: string; request?: { path?: string[] }; acceptProxyProtocol?: boolean }; + ws?: { path?: string; acceptProxyProtocol?: boolean }; + grpc?: { serviceName?: string; multiMode?: boolean }; + httpupgrade?: { path?: string; acceptProxyProtocol?: boolean }; + xhttp?: { path?: string }; + security?: string; + tls?: { certs?: TlsCert[] }; + reality?: unknown; + externalProxy?: unknown; +} + +interface TlsCert { + useFile?: boolean; + certFile?: string; + keyFile?: string; + cert?: string; + key?: string; + ocspStapling?: number; + oneTimeLoading?: boolean; + usage?: string; + buildChain?: boolean; +} + +interface VlessClient { + id?: string; + email?: string; + flow?: string; + enable?: boolean; + subId?: string; + totalGB?: number; + expiryTime?: number; + limitIp?: number; + comment?: string; + tgId?: string; +} + +interface ShadowsocksClient { + email?: string; + password?: string; + method?: string; + enable?: boolean; + subId?: string; + totalGB?: number; + expiryTime?: number; + limitIp?: number; + comment?: string; + tgId?: string; +} + +interface HttpAccount { + user?: string; + pass?: string; +} + +interface WireguardPeer { + privateKey?: string; + publicKey?: string; + psk?: string; + allowedIPs: string[]; + keepAlive?: number; +} + const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly']; const PROTOCOLS = Object.values(Protocols) as string[]; const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION) as string[]; @@ -107,12 +170,12 @@ interface FallbackRow { xver: number; } -function deriveFallbackDefaults(childDb: any): Omit { +function deriveFallbackDefaults(childDb: DBInbound | null | undefined): Omit { const out = { name: '', alpn: '', path: '', xver: 0 }; if (!childDb) return out; - let stream: any; + let stream: StreamLike | undefined; try { - stream = childDb.toInbound()?.stream; + stream = childDb.toInbound()?.stream as StreamLike | undefined; } catch { return out; } @@ -166,7 +229,9 @@ export default function InboundFormModal({ [availableNodes], ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const inboundRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const dbFormRef = useRef(null); const fallbackKeyRef = useRef(0); const advancedTextRef = useRef({ stream: '', sniffing: '', settings: '' }); @@ -279,9 +344,9 @@ export default function InboundFormModal({ if (!open) return; setFallbackEditing(new Set()); if (mode === 'edit' && dbInbound) { - const parsed = (Inbound as any).fromJson(dbInbound.toInbound().toJson()); + const parsed = Inbound.fromJson(dbInbound.toInbound().toJson()); inboundRef.current = parsed; - dbFormRef.current = new (DBInbound as any)(dbInbound); + dbFormRef.current = new DBInbound(dbInbound); primeAdvancedJson(); if (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) { loadFallbacks(dbInbound.id); @@ -289,12 +354,12 @@ export default function InboundFormModal({ setFallbacks([]); } } else { - const ib = new (Inbound as any)(); + const ib = new Inbound(); ib.protocol = Protocols.VLESS; - ib.settings = (Inbound as any).Settings.getSettings(Protocols.VLESS); + ib.settings = Inbound.Settings.getSettings(Protocols.VLESS); ib.port = RandomUtil.randomInteger(10000, 60000); inboundRef.current = ib; - const form = new (DBInbound as any)(); + const form = new DBInbound(); form.enable = true; form.remark = ''; form.total = 0; @@ -333,7 +398,7 @@ export default function InboundFormModal({ const ib = inboundRef.current; if (mode === 'edit' || !ib) return; ib.protocol = next; - ib.settings = (Inbound as any).Settings.getSettings(next); + ib.settings = Inbound.Settings.getSettings(next); if (!NODE_ELIGIBLE_PROTOCOLS.has(next) && dbFormRef.current) { dbFormRef.current.nodeId = null; } @@ -352,7 +417,7 @@ export default function InboundFormModal({ && !ib.canEnableTlsFlow() && Array.isArray(ib.settings.vlesses) ) { - ib.settings.vlesses.forEach((c: any) => { c.flow = ''; }); + ib.settings.vlesses.forEach((c: VlessClient) => { c.flow = ''; }); } if (next !== 'kcp' && ib.stream.finalmask) { ib.stream.finalmask.udp = []; @@ -379,7 +444,7 @@ export default function InboundFormModal({ xver: 0, }; if (childId) { - const child = (dbInbounds || []).find((ib: any) => ib.id === childId); + const child = (dbInbounds || []).find((ib) => ib.id === childId); Object.assign(row, deriveFallbackDefaults(child)); } setFallbacks((prev) => [...prev, row]); @@ -402,7 +467,7 @@ export default function InboundFormModal({ const onFallbackChildPicked = useCallback((rowKey: string, childId: number) => { setFallbacks((prev) => prev.map((row) => { if (row.rowKey !== rowKey) return row; - const child = (dbInbounds || []).find((ib: any) => ib.id === childId); + const child = (dbInbounds || []).find((ib) => ib.id === childId); const defaults = deriveFallbackDefaults(child); return { ...row, childId, ...defaults }; })); @@ -415,7 +480,7 @@ export default function InboundFormModal({ const rederiveFallback = useCallback((rowKey: string) => { setFallbacks((prev) => prev.map((row) => { if (row.rowKey !== rowKey || !row.childId) return row; - const child = (dbInbounds || []).find((ib: any) => ib.id === row.childId); + const child = (dbInbounds || []).find((ib) => ib.id === row.childId); const defaults = deriveFallbackDefaults(child); return { ...row, ...defaults }; })); @@ -432,9 +497,9 @@ export default function InboundFormModal({ for (const ib of list) { if (ib.id === masterId) continue; if (existing.has(ib.id)) continue; - let stream: any; - try { stream = ib.toInbound()?.stream; } catch { continue; } - if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network)) continue; + let stream: StreamLike | undefined; + try { stream = ib.toInbound()?.stream as StreamLike | undefined; } catch { continue; } + if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network ?? '')) continue; const row: FallbackRow = { rowKey: `fb-${++fallbackKeyRef.current}`, childId: ib.id, @@ -456,8 +521,8 @@ export default function InboundFormModal({ const list = dbInbounds || []; const masterId = dbInbound?.id; return list - .filter((ib: any) => ib.id !== masterId) - .map((ib: any) => ({ + .filter((ib) => ib.id !== masterId) + .map((ib) => ({ label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`, value: ib.id, })); @@ -488,22 +553,22 @@ export default function InboundFormModal({ try { return await fn(); } finally { setSaving(false); } }, []); - const randomSSPassword = useCallback((target: any) => { + const randomSSPassword = useCallback((target: ShadowsocksClient) => { if (target) { - target.password = (RandomUtil as any).randomShadowsocksPassword(inboundRef.current.settings.method); + target.password = RandomUtil.randomShadowsocksPassword(inboundRef.current.settings.method); refresh(); } }, [refresh]); - const regenWgKeypair = useCallback((target: any) => { - const kp = (Wireguard as any).generateKeypair(); + const regenWgKeypair = useCallback((target: WireguardPeer) => { + const kp = Wireguard.generateKeypair(); target.publicKey = kp.publicKey; target.privateKey = kp.privateKey; refresh(); }, [refresh]); const regenInboundWg = useCallback(() => { - const kp = (Wireguard as any).generateKeypair(); + const kp = Wireguard.generateKeypair(); inboundRef.current.settings.pubKey = kp.publicKey; inboundRef.current.settings.secretKey = kp.privateKey; refresh(); @@ -557,7 +622,7 @@ export default function InboundFormModal({ const randomizeShortIds = useCallback(() => { if (!inboundRef.current?.stream?.reality) return; - inboundRef.current.stream.reality.shortIds = (RandomUtil as any).randomShortIds(); + inboundRef.current.stream.reality.shortIds = RandomUtil.randomShortIds(); refresh(); }, [refresh]); @@ -590,7 +655,7 @@ export default function InboundFormModal({ refresh(); }, [defaultCert, defaultKey, refresh]); - const matchesVlessAuth = useCallback((block: any, authId: string) => { + const matchesVlessAuth = useCallback((block: { id?: string; label?: string } | undefined | null, authId: string) => { if (block?.id === authId) return true; const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, ''); if (authId === 'mlkem768') return label.includes('mlkem768'); @@ -633,11 +698,11 @@ export default function InboundFormModal({ const onSSMethodChange = useCallback(() => { const ib = inboundRef.current; - ib.settings.password = (RandomUtil as any).randomShadowsocksPassword(ib.settings.method); + ib.settings.password = RandomUtil.randomShadowsocksPassword(ib.settings.method); if (ib.isSSMultiUser) { - ib.settings.shadowsockses.forEach((c: any) => { + ib.settings.shadowsockses.forEach((c: ShadowsocksClient) => { c.method = ib.isSS2022 ? '' : ib.settings.method; - c.password = (RandomUtil as any).randomShadowsocksPassword(ib.settings.method); + c.password = RandomUtil.randomShadowsocksPassword(ib.settings.method); }); } else { ib.settings.shadowsockses = []; @@ -686,7 +751,7 @@ export default function InboundFormModal({ return false; } try { - inboundRef.current = (Inbound as any).fromJson({ + inboundRef.current = Inbound.fromJson({ port: ib.port, listen: ib.listen, protocol: ib.protocol, @@ -781,17 +846,26 @@ export default function InboundFormModal({ })(); const setAdvancedAllValue = (next: string) => { - let parsed: any; + let parsedRaw: unknown; try { - parsed = JSON.parse(next); + parsedRaw = JSON.parse(next); } catch (e) { messageApi.error(`All JSON invalid: ${(e as Error).message}`); return; } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + if (!parsedRaw || typeof parsedRaw !== 'object' || Array.isArray(parsedRaw)) { messageApi.error('All JSON must be an inbound object.'); return; } + const parsed = parsedRaw as { + listen?: string; + port?: number | string; + protocol?: string; + tag?: string; + settings?: unknown; + sniffing?: unknown; + streamSettings?: unknown; + }; const ib = inboundRef.current; try { if (typeof parsed.listen === 'string') ib.listen = parsed.listen; @@ -857,7 +931,7 @@ export default function InboundFormModal({ settings = compactAdvancedJson(advancedTextRef.current.settings, ib.settings.toString(), t('pages.inbounds.advanced.settings')); } catch { return; } - const payload: any = { + const payload: Record = { up: form.up || 0, down: form.down || 0, total: form.total, @@ -876,14 +950,15 @@ export default function InboundFormModal({ if (form.nodeId != null) payload.nodeId = form.nodeId; const url = mode === 'edit' - ? `/panel/api/inbounds/update/${dbInbound.id}` + ? `/panel/api/inbounds/update/${dbInbound!.id}` : '/panel/api/inbounds/add'; const msg = await HttpUtil.post(url, payload); if (msg?.success) { if (isFallbackHost) { + const obj = msg.obj as { id?: number; Id?: number } | null; const masterId = mode === 'edit' - ? dbInbound.id - : ((msg.obj as any)?.id || (msg.obj as any)?.Id); + ? dbInbound!.id + : (obj?.id || obj?.Id); if (masterId) await saveFallbacks(masterId); } onSaved(); @@ -1155,8 +1230,8 @@ export default function InboundFormModal({ - {(ib.settings.accounts || []).map((account: any, idx: number) => ( + {(ib.settings.accounts || []).map((account: HttpAccount, idx: number) => ( {String(idx + 1)} Add peer - {(ib.settings.peers || []).map((peer: any, idx: number) => ( + {(ib.settings.peers || []).map((peer: WireguardPeer, idx: number) => (
Peer {idx + 1} @@ -1906,7 +1981,7 @@ export default function InboundFormModal({ { ib.stream.tls.disableSystemRoot = v; refresh(); }} /> { ib.stream.tls.enableSessionResumption = v; refresh(); }} /> - {(ib.stream.tls.certs || []).map((cert: any, idx: number) => ( + {(ib.stream.tls.certs || []).map((cert: TlsCert, idx: number) => (
{ cert.useFile = e.target.value; refresh(); }}> diff --git a/frontend/src/pages/inbounds/InboundInfoModal.css b/frontend/src/pages/inbounds/InboundInfoModal.css index 44dce4e2..2240375b 100644 --- a/frontend/src/pages/inbounds/InboundInfoModal.css +++ b/frontend/src/pages/inbounds/InboundInfoModal.css @@ -39,7 +39,7 @@ align-items: center; gap: 12px; padding: 6px 0; - border-bottom: 1px solid rgba(128, 128, 128, 0.12); + border-bottom: 1px solid var(--ant-color-border-secondary); } .info-row:last-child { @@ -95,16 +95,12 @@ word-break: break-all; white-space: pre-wrap; padding: 4px 8px; - background: rgba(0, 0, 0, 0.04); + background: var(--ant-color-fill-tertiary); border-radius: 4px; user-select: all; min-width: 0; } -body.dark .value-code { - background: rgba(255, 255, 255, 0.05); -} - .value-copy { flex-shrink: 0; } @@ -112,7 +108,7 @@ body.dark .value-code { .share-buttons { margin-inline-start: 4px; padding-inline-start: 8px; - border-inline-start: 1px solid rgba(128, 128, 128, 0.25); + border-inline-start: 1px solid var(--ant-color-border); } .summary-table { @@ -157,7 +153,7 @@ body.dark .value-code { } .link-panel { - border: 1px solid rgba(128, 128, 128, 0.2); + border: 1px solid var(--ant-color-border); border-radius: 8px; padding: 10px; margin-bottom: 10px; @@ -179,37 +175,25 @@ body.dark .value-code { word-break: break-all; white-space: pre-wrap; padding: 6px 8px; - background: rgba(0, 0, 0, 0.04); + background: var(--ant-color-fill-tertiary); border-radius: 4px; user-select: all; } -body.dark .link-panel-text { - background: rgba(255, 255, 255, 0.05); -} - .link-panel-anchor { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; word-break: break-all; padding: 6px 8px; - background: rgba(0, 0, 0, 0.04); + background: var(--ant-color-fill-tertiary); border-radius: 4px; - color: var(--ant-color-primary, #1677ff); + color: var(--ant-color-primary); text-decoration: underline; - text-decoration-color: rgba(22, 119, 255, 0.4); + text-decoration-color: color-mix(in srgb, var(--ant-color-primary) 40%, transparent); transition: background 120ms ease, text-decoration-color 120ms ease; } .link-panel-anchor:hover { - background: rgba(22, 119, 255, 0.08); - text-decoration-color: var(--ant-color-primary, #1677ff); -} - -body.dark .link-panel-anchor { - background: rgba(255, 255, 255, 0.05); -} - -body.dark .link-panel-anchor:hover { - background: rgba(22, 119, 255, 0.16); + background: color-mix(in srgb, var(--ant-color-primary) 12%, transparent); + text-decoration-color: var(--ant-color-primary); } diff --git a/frontend/src/pages/inbounds/InboundInfoModal.tsx b/frontend/src/pages/inbounds/InboundInfoModal.tsx index f8d8f059..6f3a0599 100644 --- a/frontend/src/pages/inbounds/InboundInfoModal.tsx +++ b/frontend/src/pages/inbounds/InboundInfoModal.tsx @@ -12,7 +12,7 @@ import { ClipboardManager, FileManager, } from '@/utils'; -import { Protocols } from '@/models/inbound.js'; +import { Protocols } from '@/models/inbound'; import InfinityIcon from '@/components/InfinityIcon'; import { useDatepicker } from '@/hooks/useDatepicker'; import type { SubSettings } from './useInbounds'; diff --git a/frontend/src/pages/inbounds/InboundList.css b/frontend/src/pages/inbounds/InboundList.css index 2a9f28ba..7246719e 100644 --- a/frontend/src/pages/inbounds/InboundList.css +++ b/frontend/src/pages/inbounds/InboundList.css @@ -32,29 +32,29 @@ font-size: 12px; } -.ant-table { +.inbounds-page .ant-table { border-radius: 8px; overflow: hidden; } -.ant-table-container { +.inbounds-page .ant-table-container { border-radius: 8px; overflow: hidden; } -.ant-table-thead > tr:first-child > *:first-child { +.inbounds-page .ant-table-thead > tr:first-child > *:first-child { border-start-start-radius: 8px; } -.ant-table-thead > tr:first-child > *:last-child { +.inbounds-page .ant-table-thead > tr:first-child > *:last-child { border-start-end-radius: 8px; } -.ant-table-tbody > tr:last-child > *:first-child { +.inbounds-page .ant-table-tbody > tr:last-child > *:first-child { border-end-start-radius: 8px; } -.ant-table-tbody > tr:last-child > *:last-child { +.inbounds-page .ant-table-tbody > tr:last-child > *:last-child { border-end-end-radius: 8px; } @@ -66,20 +66,15 @@ } .inbound-card { - border: 1px solid rgba(128, 128, 128, 0.2); + border: 1px solid var(--ant-color-border-secondary); border-radius: 10px; padding: 12px; - background: rgba(255, 255, 255, 0.02); + background: var(--ant-color-fill-quaternary); display: flex; flex-direction: column; gap: 8px; } -body.dark .inbound-card { - background: rgba(255, 255, 255, 0.03); - border-color: rgba(255, 255, 255, 0.1); -} - .card-head { display: flex; align-items: center; @@ -142,21 +137,21 @@ body.dark .inbound-card { } @media (max-width: 768px) { - .ant-card-head { + .inbounds-page .ant-card-head { padding: 0 12px; min-height: 44px; } - .ant-card-head-title, - .ant-card-extra { + .inbounds-page .ant-card-head-title, + .inbounds-page .ant-card-extra { padding: 8px 0; } - .ant-card-body { + .inbounds-page .ant-card-body { padding: 8px; } - .row-action-trigger { + .inbounds-page .row-action-trigger { font-size: 22px; padding: 4px; } diff --git a/frontend/src/pages/inbounds/InboundList.tsx b/frontend/src/pages/inbounds/InboundList.tsx index c73db4d3..690b204f 100644 --- a/frontend/src/pages/inbounds/InboundList.tsx +++ b/frontend/src/pages/inbounds/InboundList.tsx @@ -57,7 +57,7 @@ interface DBInboundRecord extends ProtocolFlags { down: number; total: number; expiryTime: number; - _expiryTime: unknown; + _expiryTime: { valueOf(): number } | null; nodeId?: number | null; toInbound: () => { stream?: { network?: string; isTls?: boolean; isReality?: boolean }; diff --git a/frontend/src/pages/inbounds/InboundsPage.css b/frontend/src/pages/inbounds/InboundsPage.css deleted file mode 100644 index 2f3bc461..00000000 --- a/frontend/src/pages/inbounds/InboundsPage.css +++ /dev/null @@ -1,50 +0,0 @@ -.inbounds-page { - --bg-page: #e6e8ec; - --bg-card: #ffffff; - - min-height: 100vh; - background: var(--bg-page); -} - -.inbounds-page.is-dark { - --bg-page: #1a1b1f; - --bg-card: #23252b; -} - -.inbounds-page.is-dark.is-ultra { - --bg-page: #000; - --bg-card: #101013; -} - -.inbounds-page .ant-layout, -.inbounds-page .ant-layout-content { - background: transparent; -} - -.content-shell { - background: transparent; -} - -.content-area { - padding: 24px; -} - -@media (max-width: 768px) { - .content-area { - padding: 8px; - } -} - -.loading-spacer { - min-height: calc(100vh - 120px); -} - -.summary-card { - padding: 16px; -} - -@media (max-width: 768px) { - .summary-card { - padding: 8px; - } -} diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx index 7cd570c5..c72b078b 100644 --- a/frontend/src/pages/inbounds/InboundsPage.tsx +++ b/frontend/src/pages/inbounds/InboundsPage.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { lazy, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -9,6 +8,7 @@ import { Modal, Row, Spin, + Statistic, message, } from 'antd'; @@ -20,14 +20,13 @@ import { } from '@ant-design/icons'; import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils'; -import { Inbound } from '@/models/inbound.js'; -import { coerceInboundJsonField } from '@/models/dbinbound.js'; +import { Inbound } from '@/models/inbound'; +import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound'; import { useTheme } from '@/hooks/useTheme'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useWebSocket } from '@/hooks/useWebSocket'; import { useNodesQuery } from '@/api/queries/useNodesQuery'; import AppSidebar from '@/components/AppSidebar'; -import CustomStatistic from '@/components/CustomStatistic'; const TextModal = lazy(() => import('@/components/TextModal')); const PromptModal = lazy(() => import('@/components/PromptModal')); @@ -37,8 +36,6 @@ import LazyMount from '@/components/LazyMount'; const InboundFormModal = lazy(() => import('./InboundFormModal')); const InboundInfoModal = lazy(() => import('./InboundInfoModal')); const QrCodeModal = lazy(() => import('./QrCodeModal')); -import '@/styles/page-cards.css'; -import './InboundsPage.css'; type RowAction = | 'edit' @@ -53,6 +50,12 @@ type RowAction = type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds'; +interface ClientMatchTarget { + id?: string; + email?: string; + password?: string; +} + export default function InboundsPage() { const { t } = useTranslation(); const { isDark, isUltra, antdThemeConfig } = useTheme(); @@ -94,7 +97,7 @@ export default function InboundsPage() { [nodesList], ); const hasNodeAttachedInbound = useMemo( - () => (dbInbounds || []).some((ib: any) => ib?.nodeId != null), + () => (dbInbounds || []).some((ib) => ib?.nodeId != null), [dbInbounds], ); const showNodeInfo = hasNodeAttachedInbound || hasActiveNode; @@ -106,14 +109,14 @@ export default function InboundsPage() { const [formOpen, setFormOpen] = useState(false); const [formMode, setFormMode] = useState<'add' | 'edit'>('add'); - const [formDbInbound, setFormDbInbound] = useState(null); + const [formDbInbound, setFormDbInbound] = useState(null); const [infoOpen, setInfoOpen] = useState(false); - const [infoDbInbound, setInfoDbInbound] = useState(null); + const [infoDbInbound, setInfoDbInbound] = useState(null); const [infoClientIndex, setInfoClientIndex] = useState(0); const [qrOpen, setQrOpen] = useState(false); - const [qrDbInbound, setQrDbInbound] = useState(null); + const [qrDbInbound, setQrDbInbound] = useState(null); const [textOpen, setTextOpen] = useState(false); const [textTitle, setTextTitle] = useState(''); @@ -128,7 +131,7 @@ export default function InboundsPage() { const [promptLoading, setPromptLoading] = useState(false); const [promptHandler, setPromptHandler] = useState<((value: string) => Promise | boolean | void) | null>(null); - const hostOverrideFor = useCallback((dbInbound: any) => { + const hostOverrideFor = useCallback((dbInbound: DBInbound | null) => { if (!dbInbound || dbInbound.nodeId == null) return ''; return nodesById.get(dbInbound.nodeId)?.address || ''; }, [nodesById]); @@ -172,8 +175,8 @@ export default function InboundsPage() { } }, [promptHandler]); - const projectChildThroughMaster = useCallback((child: any, master: any) => { - const projected = JSON.parse(JSON.stringify(child)); + const projectChildThroughMaster = useCallback((child: DBInbound, master: DBInbound): DBInbound => { + const projected = JSON.parse(JSON.stringify(child)) as DBInbound; projected.listen = master.listen; projected.port = master.port; const masterStream = master.toInbound().stream; @@ -183,17 +186,18 @@ export default function InboundsPage() { childInbound.stream.reality = masterStream.reality; childInbound.stream.externalProxy = masterStream.externalProxy; projected.streamSettings = childInbound.stream.toString(); - return new child.constructor(projected); + const Ctor = child.constructor as new (data: DBInbound) => DBInbound; + return new Ctor(projected); }, []); - const checkFallback = useCallback((dbInbound: any) => { + const checkFallback = useCallback((dbInbound: DBInbound): DBInbound => { const parent = dbInbound?.fallbackParent; if (parent?.masterId) { - const master = (dbInbounds as any[]).find((ib: any) => ib.id === parent.masterId); + const master = dbInbounds.find((ib) => ib.id === parent.masterId); if (master) return projectChildThroughMaster(dbInbound, master); } - if (!(dbInbound?.listen as string | undefined)?.startsWith?.('@')) return dbInbound; - for (const candidate of dbInbounds as any[]) { + if (!dbInbound?.listen?.startsWith?.('@')) return dbInbound; + for (const candidate of dbInbounds) { if (candidate.id === dbInbound.id) continue; const parsed = candidate.toInbound(); if (!parsed.isTcp) continue; @@ -205,11 +209,11 @@ export default function InboundsPage() { return dbInbound; }, [dbInbounds, projectChildThroughMaster]); - const findClientIndex = useCallback((dbInbound: any, client: any) => { + const findClientIndex = useCallback((dbInbound: DBInbound, client: ClientMatchTarget | null) => { if (!client) return 0; const inbound = dbInbound.toInbound(); - const clients = inbound?.clients || []; - const idx = clients.findIndex((c: any) => { + const clients = (inbound?.clients || []) as ClientMatchTarget[]; + const idx = clients.findIndex((c) => { if (!c) return false; switch (dbInbound.protocol) { case 'trojan': @@ -222,7 +226,7 @@ export default function InboundsPage() { return idx >= 0 ? idx : 0; }, []); - const exportInboundLinks = useCallback((dbInbound: any) => { + const exportInboundLinks = useCallback((dbInbound: DBInbound) => { const projected = checkFallback(dbInbound); openText({ title: t('pages.inbounds.exportLinksTitle'), @@ -231,13 +235,13 @@ export default function InboundsPage() { }); }, [checkFallback, remarkModel, hostOverrideFor, openText, t]); - const exportInboundClipboard = useCallback((dbInbound: any) => { + const exportInboundClipboard = useCallback((dbInbound: DBInbound) => { openText({ title: t('pages.inbounds.inboundJsonTitle'), content: JSON.stringify(dbInbound, null, 2) }); }, [openText, t]); - const exportInboundSubs = useCallback((dbInbound: any) => { + const exportInboundSubs = useCallback((dbInbound: DBInbound) => { const inbound = dbInbound.toInbound(); - const clients = inbound?.clients || []; + const clients = (inbound?.clients || []) as { subId?: string }[]; const subLinks: string[] = []; for (const c of clients) { if (c.subId && subSettings.subURI) { @@ -253,7 +257,7 @@ export default function InboundsPage() { const exportAllLinks = useCallback(async () => { const hydrated = await Promise.all( - (dbInbounds as any[]).map((ib) => hydrateInbound(ib.id).then((r) => r ?? ib)), + dbInbounds.map((ib) => hydrateInbound(ib.id).then((r) => r ?? ib)), ); const out: string[] = []; for (const ib of hydrated) { @@ -265,12 +269,12 @@ export default function InboundsPage() { const exportAllSubs = useCallback(async () => { const hydrated = await Promise.all( - (dbInbounds as any[]).map((ib) => hydrateInbound(ib.id).then((r) => r ?? ib)), + dbInbounds.map((ib) => hydrateInbound(ib.id).then((r) => r ?? ib)), ); const out: string[] = []; for (const ib of hydrated) { const inbound = ib.toInbound(); - const clients = inbound?.clients || []; + const clients = (inbound?.clients || []) as { subId?: string }[]; for (const c of clients) { if (c.subId && subSettings.subURI) { out.push(subSettings.subURI + c.subId); @@ -303,13 +307,13 @@ export default function InboundsPage() { setFormOpen(true); }, []); - const openEdit = useCallback((dbInbound: any) => { + const openEdit = useCallback((dbInbound: DBInbound) => { setFormMode('edit'); setFormDbInbound(dbInbound); setFormOpen(true); }, []); - const confirmDelete = useCallback((dbInbound: any) => { + const confirmDelete = useCallback((dbInbound: DBInbound) => { modal.confirm({ title: t('pages.inbounds.deleteConfirmTitle', { remark: dbInbound.remark }), content: t('pages.inbounds.deleteConfirmContent'), @@ -323,7 +327,7 @@ export default function InboundsPage() { }); }, [modal, refresh, t]); - const confirmResetTraffic = useCallback((dbInbound: any) => { + const confirmResetTraffic = useCallback((dbInbound: DBInbound) => { modal.confirm({ title: t('pages.inbounds.resetConfirmTitle', { remark: dbInbound.remark }), content: t('pages.inbounds.resetConfirmContent'), @@ -336,7 +340,7 @@ export default function InboundsPage() { }); }, [modal, refresh, t]); - const confirmClone = useCallback((dbInbound: any) => { + const confirmClone = useCallback((dbInbound: DBInbound) => { modal.confirm({ title: t('pages.inbounds.cloneConfirmTitle', { remark: dbInbound.remark }), content: t('pages.inbounds.cloneConfirmContent'), @@ -350,7 +354,7 @@ export default function InboundsPage() { raw.clients = []; clonedSettings = JSON.stringify(raw); } catch { - clonedSettings = (Inbound as any).Settings.getSettings(baseInbound.protocol).toString(); + clonedSettings = Inbound.Settings.getSettings(baseInbound.protocol).toString(); } const data = { up: 0, @@ -393,7 +397,7 @@ export default function InboundsPage() { } }, [modal, importInbound, exportAllLinks, exportAllSubs, refresh, messageApi]); - const onRowAction = useCallback(async ({ key, dbInbound }: { key: RowAction; dbInbound: any }) => { + const onRowAction = useCallback(async ({ key, dbInbound }: { key: RowAction; dbInbound: DBInbound }) => { // Actions that touch per-client secrets (uuid, password, flow, ...) need // the full payload that the slim list view does not ship. Hydrate first // and then operate on the rehydrated record. @@ -457,21 +461,21 @@ export default function InboundsPage() { - } /> - } /> - } @@ -483,7 +487,7 @@ export default function InboundsPage() { onRowAction({ key, dbInbound: dbInbound as unknown as DBInbound })} /> @@ -512,7 +516,7 @@ export default function InboundsPage() { onSaved={refresh} mode={formMode} dbInbound={formDbInbound} - dbInbounds={dbInbounds as any[]} + dbInbounds={dbInbounds} availableNodes={nodesList} /> diff --git a/frontend/src/pages/inbounds/QrCodeModal.tsx b/frontend/src/pages/inbounds/QrCodeModal.tsx index f4fae390..720944e5 100644 --- a/frontend/src/pages/inbounds/QrCodeModal.tsx +++ b/frontend/src/pages/inbounds/QrCodeModal.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Collapse, Modal } from 'antd'; import type { CollapseProps } from 'antd'; -import { Protocols } from '@/models/inbound.js'; +import { Protocols } from '@/models/inbound'; import QrPanel from './QrPanel'; import type { SubSettings } from './useInbounds'; diff --git a/frontend/src/pages/inbounds/QrPanel.css b/frontend/src/pages/inbounds/QrPanel.css index 8f739c9c..b2e2c1d8 100644 --- a/frontend/src/pages/inbounds/QrPanel.css +++ b/frontend/src/pages/inbounds/QrPanel.css @@ -1,5 +1,5 @@ .qr-panel { - border: 1px solid rgba(128, 128, 128, 0.2); + border: 1px solid var(--ant-color-border-secondary); border-radius: 8px; padding: 10px; margin-bottom: 10px; diff --git a/frontend/src/pages/inbounds/useInbounds.ts b/frontend/src/pages/inbounds/useInbounds.ts index 6a5ff89a..37e18088 100644 --- a/frontend/src/pages/inbounds/useInbounds.ts +++ b/frontend/src/pages/inbounds/useInbounds.ts @@ -2,8 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { HttpUtil } from '@/utils'; -import { DBInbound } from '@/models/dbinbound.js'; -import { Protocols } from '@/models/inbound.js'; +import { DBInbound } from '@/models/dbinbound'; +import { Protocols } from '@/models/inbound'; import { setDatepicker } from '@/hooks/useDatepicker'; import { keys } from '@/api/queryKeys'; diff --git a/frontend/src/pages/index/BackupModal.css b/frontend/src/pages/index/BackupModal.css index e5d8147b..838aa85d 100644 --- a/frontend/src/pages/index/BackupModal.css +++ b/frontend/src/pages/index/BackupModal.css @@ -1,32 +1,22 @@ .backup-list { width: 100%; - border: 1px solid rgba(5, 5, 5, 0.06); + border: 1px solid var(--ant-color-border-secondary); border-radius: 8px; overflow: hidden; } -body.dark .backup-list, -html[data-theme='ultra-dark'] .backup-list { - border-color: rgba(255, 255, 255, 0.12); -} - .backup-item { display: flex; align-items: center; gap: 16px; padding: 12px 24px; - border-bottom: 1px solid rgba(5, 5, 5, 0.06); + border-bottom: 1px solid var(--ant-color-border-secondary); } .backup-item:last-child { border-bottom: 0; } -body.dark .backup-item, -html[data-theme='ultra-dark'] .backup-item { - border-bottom-color: rgba(255, 255, 255, 0.08); -} - .backup-meta { flex: 1; display: flex; @@ -37,21 +27,11 @@ html[data-theme='ultra-dark'] .backup-item { .backup-title { font-size: 14px; font-weight: 500; - color: rgba(0, 0, 0, 0.88); + color: var(--ant-color-text); } .backup-description { font-size: 14px; - color: rgba(0, 0, 0, 0.45); + color: var(--ant-color-text-tertiary); line-height: 1.5715; } - -body.dark .backup-title, -html[data-theme='ultra-dark'] .backup-title { - color: rgba(255, 255, 255, 0.85); -} - -body.dark .backup-description, -html[data-theme='ultra-dark'] .backup-description { - color: rgba(255, 255, 255, 0.45); -} diff --git a/frontend/src/pages/index/CustomGeoSection.css b/frontend/src/pages/index/CustomGeoSection.css index 3b2bbc44..dc58cce7 100644 --- a/frontend/src/pages/index/CustomGeoSection.css +++ b/frontend/src/pages/index/CustomGeoSection.css @@ -1,7 +1,3 @@ -.mb-10 { - margin-bottom: 10px; -} - .toolbar { display: flex; align-items: center; @@ -14,15 +10,11 @@ margin-left: 4px; padding: 2px 8px; border-radius: 10px; - background: rgba(0, 0, 0, 0.05); + background: var(--ant-color-fill-tertiary); font-size: 12px; opacity: 0.75; } -body.dark .custom-geo-count { - background: rgba(255, 255, 255, 0.08); -} - .custom-geo-alias-cell { display: flex; align-items: center; @@ -48,20 +40,12 @@ body.dark .custom-geo-count { font-size: 12px; padding: 2px 6px; border-radius: 4px; - background: rgba(0, 0, 0, 0.05); + background: var(--ant-color-fill-tertiary); user-select: all; } .custom-geo-copyable:hover { - background: rgba(0, 0, 0, 0.1); -} - -body.dark .custom-geo-ext-code { - background: rgba(255, 255, 255, 0.08); -} - -body.dark .custom-geo-copyable:hover { - background: rgba(255, 255, 255, 0.14); + background: var(--ant-color-fill-secondary); } .custom-geo-muted { diff --git a/frontend/src/pages/index/IndexPage.css b/frontend/src/pages/index/IndexPage.css index 889ed959..5efca336 100644 --- a/frontend/src/pages/index/IndexPage.css +++ b/frontend/src/pages/index/IndexPage.css @@ -1,34 +1,3 @@ -.index-page { - --bg-page: #e6e8ec; - --bg-card: #ffffff; - - min-height: 100vh; - background: var(--bg-page); -} - -.index-page.is-dark { - --bg-page: #1a1b1f; - --bg-card: #23252b; -} - -.index-page.is-dark.is-ultra { - --bg-page: #000; - --bg-card: #101013; -} - -.index-page .ant-layout, -.index-page .ant-layout-content { - background: transparent; -} - -.index-page .content-shell { - background: transparent; -} - -.index-page .content-area { - padding: 24px; -} - @media (max-width: 768px) { .index-page .content-area { padding: 12px; @@ -36,156 +5,11 @@ } } -.index-page .loading-spacer { - min-height: calc(100vh - 120px); -} - -.index-page .ant-card { - border-radius: 12px; - border: 1px solid rgba(0, 0, 0, 0.06); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); - transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.2s ease; -} - -body.dark .index-page .ant-card { - border-color: rgba(255, 255, 255, 0.06); - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.03); -} - -html[data-theme='ultra-dark'] .index-page .ant-card { - border-color: rgba(255, 255, 255, 0.04); - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.6), - inset 0 1px 0 rgba(255, 255, 255, 0.025); -} - -.index-page .ant-card.ant-card-hoverable:hover { - transform: translateY(-2px); - border-color: rgba(0, 0, 0, 0.10); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); -} - -body.dark .index-page .ant-card.ant-card-hoverable:hover { - border-color: rgba(255, 255, 255, 0.12); - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.5), - inset 0 1px 0 rgba(255, 255, 255, 0.04); -} - -html[data-theme='ultra-dark'] .index-page .ant-card.ant-card-hoverable:hover { - border-color: rgba(255, 255, 255, 0.08); - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.75), - inset 0 1px 0 rgba(255, 255, 255, 0.03); -} - -.index-page .ant-card .ant-card-head { - min-height: 44px; - padding-inline: 16px; -} - -.index-page .ant-card .ant-card-head-title { - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; - opacity: 0.75; -} - -.index-page .ant-card .ant-card-body { - padding: 18px 20px; -} - -.index-page .ant-card .ant-card-body > .ant-row > .ant-col { - position: relative; - padding: 4px 6px; -} - -@media (min-width: 769px) { - .index-page .ant-card .ant-card-body > .ant-row > .ant-col + .ant-col::before { - content: ''; - position: absolute; - left: 0; - top: 10%; - bottom: 10%; - width: 1px; - background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.10), transparent); - pointer-events: none; - } -} - -body.dark .index-page .ant-card .ant-card-body > .ant-row > .ant-col + .ant-col::before { - background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.12), transparent); -} - -.index-page .ant-card .ant-card-head { - border-bottom-color: rgba(0, 0, 0, 0.06); -} - -.index-page .ant-card .ant-card-actions { - border-top-color: rgba(0, 0, 0, 0.06); - background: transparent; -} - -.index-page .ant-card .ant-card-actions > li { - border-inline-end-color: rgba(0, 0, 0, 0.06); -} - -body.dark .index-page .ant-card .ant-card-head { - border-bottom-color: rgba(255, 255, 255, 0.06); -} - -body.dark .index-page .ant-card .ant-card-actions { - border-top-color: rgba(255, 255, 255, 0.06); -} - -body.dark .index-page .ant-card .ant-card-actions > li { - border-inline-end-color: rgba(255, 255, 255, 0.06); -} - -html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-head { - border-bottom-color: rgba(255, 255, 255, 0.04); -} - -html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-actions { - border-top-color: rgba(255, 255, 255, 0.04); -} - -html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-actions > li { - border-inline-end-color: rgba(255, 255, 255, 0.04); -} - .index-page .action { cursor: pointer; justify-content: center; max-width: 100%; - padding: 0 8px; flex-wrap: nowrap; - color: rgba(0, 0, 0, 0.78); - font-weight: 500; - transition: opacity 0.15s ease, transform 0.15s ease, color 0.2s ease; -} - -.index-page .action .anticon { - color: rgba(0, 0, 0, 0.72); -} - -body.dark .index-page .action { - color: rgba(255, 255, 255, 0.82); -} - -body.dark .index-page .action .anticon { - color: rgba(255, 255, 255, 0.75); -} - -html[data-theme='ultra-dark'] .index-page .action { - color: rgba(255, 255, 255, 0.86); -} - -html[data-theme='ultra-dark'] .index-page .action .anticon { - color: rgba(255, 255, 255, 0.78); } .index-page .action > span:not(.anticon):not(.tg-icon) { @@ -195,23 +19,13 @@ html[data-theme='ultra-dark'] .index-page .action .anticon { min-width: 0; } -.index-page .action:hover { - opacity: 0.75; - transform: translateY(-1px); -} - -.index-page .ant-card-actions > li { - margin: 8px 0; - min-width: 0; -} - .index-page .action-update { - color: #fa8c16; + color: var(--ant-color-warning); font-weight: 600; } .index-page .action-update .anticon { - color: #fa8c16; + color: var(--ant-color-warning); } .index-page .history-tag { diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx index 21bfcb3a..c3a58105 100644 --- a/frontend/src/pages/index/IndexPage.tsx +++ b/frontend/src/pages/index/IndexPage.tsx @@ -11,6 +11,7 @@ import { Row, Space, Spin, + Statistic, Tag, Tooltip, } from 'antd'; @@ -39,7 +40,6 @@ import { useTheme } from '@/hooks/useTheme'; import { useStatusQuery } from '@/api/queries/useStatusQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import AppSidebar from '@/components/AppSidebar'; -import CustomStatistic from '@/components/CustomStatistic'; import LazyMount from '@/components/LazyMount'; import { setMessageInstance } from '@/utils/messageBus'; import StatusCard from './StatusCard'; @@ -53,7 +53,6 @@ const SystemHistoryModal = lazy(() => import('./SystemHistoryModal')); const XrayMetricsModal = lazy(() => import('./XrayMetricsModal')); const XrayLogModal = lazy(() => import('./XrayLogModal')); const VersionModal = lazy(() => import('./VersionModal')); -import '@/styles/page-cards.css'; import './IndexPage.css'; export default function IndexPage() { @@ -285,14 +284,14 @@ export default function IndexPage() { - } /> - } @@ -306,14 +305,14 @@ export default function IndexPage() { - } /> - } @@ -327,7 +326,7 @@ export default function IndexPage() { - } @@ -335,7 +334,7 @@ export default function IndexPage() { /> - } @@ -350,14 +349,14 @@ export default function IndexPage() { - } /> - } @@ -392,14 +391,14 @@ export default function IndexPage() { > - } /> - } @@ -413,14 +412,14 @@ export default function IndexPage() { - } /> - } diff --git a/frontend/src/pages/index/LogModal.css b/frontend/src/pages/index/LogModal.css index 1fc31322..86858812 100644 --- a/frontend/src/pages/index/LogModal.css +++ b/frontend/src/pages/index/LogModal.css @@ -32,9 +32,10 @@ word-break: break-word; max-height: 60vh; overflow-y: auto; - border: 1px solid rgba(128, 128, 128, 0.25); + border: 1px solid var(--ant-color-border); border-radius: 6px; - background: rgba(0, 0, 0, 0.04); + background: var(--ant-color-fill-tertiary); + color: var(--ant-color-text); } .log-stamp { @@ -140,10 +141,6 @@ } body.dark .log-container { - background: rgba(255, 255, 255, 0.03); - border-color: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.88); - --log-stamp: #6aa6ee; --log-debug: #6aa6ee; --log-info: #4ed3a6; @@ -165,12 +162,6 @@ html[data-theme="ultra-dark"] .log-container { --log-divider: rgba(255, 255, 255, 0.12); } -.logmodal-mobile { - top: 0 !important; - padding-bottom: 0 !important; - max-width: 100vw !important; -} - .logmodal-mobile .ant-modal-content { border-radius: 0; height: 100vh; diff --git a/frontend/src/pages/index/LogModal.tsx b/frontend/src/pages/index/LogModal.tsx index ef31ad34..1f074017 100644 --- a/frontend/src/pages/index/LogModal.tsx +++ b/frontend/src/pages/index/LogModal.tsx @@ -109,6 +109,7 @@ export default function LogModal({ open, onClose }: LogModalProps) { open={open} footer={null} width={isMobile ? '100vw' : 800} + style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined} className={isMobile ? 'logmodal-mobile' : undefined} onCancel={onClose} title={titleNode} diff --git a/frontend/src/pages/index/PanelUpdateModal.css b/frontend/src/pages/index/PanelUpdateModal.css index 4c676c74..9c8190dc 100644 --- a/frontend/src/pages/index/PanelUpdateModal.css +++ b/frontend/src/pages/index/PanelUpdateModal.css @@ -1,36 +1,22 @@ -.mb-12 { - margin-bottom: 12px; -} - .version-list { width: 100%; - border: 1px solid rgba(5, 5, 5, 0.06); + border: 1px solid var(--ant-color-border-secondary); border-radius: 8px; overflow: hidden; } -body.dark .version-list, -html[data-theme='ultra-dark'] .version-list { - border-color: rgba(255, 255, 255, 0.12); -} - .version-list-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; - border-bottom: 1px solid rgba(5, 5, 5, 0.06); + border-bottom: 1px solid var(--ant-color-border-secondary); } .version-list-item:last-child { border-bottom: 0; } -body.dark .version-list-item, -html[data-theme='ultra-dark'] .version-list-item { - border-bottom-color: rgba(255, 255, 255, 0.08); -} - .actions-row { display: flex; justify-content: flex-end; diff --git a/frontend/src/pages/index/SystemHistoryModal.css b/frontend/src/pages/index/SystemHistoryModal.css index fc5a11e1..445a8ea3 100644 --- a/frontend/src/pages/index/SystemHistoryModal.css +++ b/frontend/src/pages/index/SystemHistoryModal.css @@ -11,20 +11,9 @@ margin: 8px 8px 16px; padding: 16px 18px 18px; border-radius: 14px; - background: linear-gradient(180deg, rgba(99, 102, 241, 0.05), rgba(99, 102, 241, 0)); - border: 1px solid rgba(99, 102, 241, 0.12); - box-shadow: 0 2px 12px rgba(99, 102, 241, 0.06); -} - -body.dark .cpu-chart-wrap { - background: linear-gradient(180deg, rgba(129, 140, 248, 0.08), rgba(129, 140, 248, 0)); - border-color: rgba(129, 140, 248, 0.16); - box-shadow: 0 2px 16px rgba(0, 0, 0, 0.25); -} - -html[data-theme='ultra-dark'] .cpu-chart-wrap { - background: linear-gradient(180deg, rgba(129, 140, 248, 0.05), rgba(129, 140, 248, 0)); - border-color: rgba(129, 140, 248, 0.10); + background: linear-gradient(180deg, color-mix(in srgb, var(--ant-color-primary) 6%, transparent), transparent); + border: 1px solid var(--ant-color-border-secondary); + box-shadow: 0 2px 12px var(--ant-color-fill-quaternary); } .cpu-chart-meta { diff --git a/frontend/src/pages/index/SystemHistoryModal.tsx b/frontend/src/pages/index/SystemHistoryModal.tsx index 84b00643..69592550 100644 --- a/frontend/src/pages/index/SystemHistoryModal.tsx +++ b/frontend/src/pages/index/SystemHistoryModal.tsx @@ -142,7 +142,6 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist = { error: 'pages.index.xrayStatusError', }; -function badgeAnimationClass(color: string): string { - if (color === 'green') return 'xray-running-animation'; - if (color === 'orange') return 'xray-stop-animation'; - if (color === 'red') return 'xray-error-animation'; - return 'xray-processing-animation'; -} - export default function XrayStatusCard({ status, isMobile, @@ -65,12 +58,7 @@ export default function XrayStatusCard({ const extra = status.xray.state !== 'error' ? ( - + ) : ( } > - + ); diff --git a/frontend/src/pages/login/LoginPage.css b/frontend/src/pages/login/LoginPage.css index ed2b7e77..d78ffae8 100644 --- a/frontend/src/pages/login/LoginPage.css +++ b/frontend/src/pages/login/LoginPage.css @@ -228,36 +228,6 @@ font-size: 18px; } -.theme-cycle { - width: 40px; - height: 40px; - border-radius: 50%; - border: 1px solid var(--color-border); - background: var(--bg-card); - color: var(--color-text); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - padding: 0; - -webkit-backdrop-filter: blur(20px); - backdrop-filter: blur(20px); - transition: background-color 0.2s, transform 0.15s, color 0.2s; -} - -.theme-cycle:hover, -.theme-cycle:focus-visible { - background-color: rgba(99, 102, 241, 0.15); - color: var(--color-accent); - transform: scale(1.05); - outline: none; -} - -.theme-cycle svg { - width: 18px; - height: 18px; -} - .login-wrapper { position: relative; min-height: 100vh; @@ -402,44 +372,3 @@ margin-bottom: 0; } -.lang-list { - list-style: none; - margin: 0; - padding: 0; - min-width: 160px; - display: flex; - flex-direction: column; - gap: 2px; -} - -.lang-item { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 8px 12px; - border: none; - border-radius: 8px; - background: transparent; - color: inherit; - font: inherit; - text-align: start; - cursor: pointer; - transition: background-color 0.15s, color 0.15s; -} - -.lang-item:hover, -.lang-item:focus-visible { - background-color: rgba(99, 102, 241, 0.12); - outline: none; -} - -.lang-item.is-active { - color: var(--color-accent); - font-weight: 600; -} - -.lang-item-icon { - font-size: 16px; - line-height: 1; -} diff --git a/frontend/src/pages/login/LoginPage.tsx b/frontend/src/pages/login/LoginPage.tsx index 46d8c7a3..fe2ab6d9 100644 --- a/frontend/src/pages/login/LoginPage.tsx +++ b/frontend/src/pages/login/LoginPage.tsx @@ -6,13 +6,18 @@ import { Form, Input, Layout, + Menu, Popover, + Space, Spin, message, } from 'antd'; import { KeyOutlined, LockOutlined, + MoonFilled, + MoonOutlined, + SunOutlined, TranslationOutlined, UserOutlined, } from '@ant-design/icons'; @@ -105,26 +110,20 @@ export default function LoginPage() { return classes.join(' '); }, [isDark, isUltra]); - const langList = useMemo( - () => LanguageManager.supportedLanguages as { value: string; name: string; icon: string }[], + const langMenuItems = useMemo( + () => (LanguageManager.supportedLanguages as { value: string; name: string; icon: string }[]).map((l) => ({ + key: l.value, + label: ( + + + {l.name} + + ), + })), [], ); - const themeIcon = !isDark ? ( - - ) : !isUltra ? ( - - ) : ( - - ); + const themeIcon = !isDark ? : !isUltra ? : ; return ( @@ -132,35 +131,30 @@ export default function LoginPage() {
- + /> - {langList.map((l) => ( -
  • - -
  • - ))} - + onLangChange(key)} + style={{ border: 'none', minWidth: 160 }} + /> } >
    diff --git a/frontend/src/pages/nodes/NodesPage.css b/frontend/src/pages/nodes/NodesPage.css deleted file mode 100644 index e8800311..00000000 --- a/frontend/src/pages/nodes/NodesPage.css +++ /dev/null @@ -1,49 +0,0 @@ -.nodes-page { - --bg-page: #e6e8ec; - --bg-card: #ffffff; - min-height: 100vh; - background: var(--bg-page); -} - -.nodes-page.is-dark { - --bg-page: #1a1b1f; - --bg-card: #23252b; -} - -.nodes-page.is-dark.is-ultra { - --bg-page: #000; - --bg-card: #101013; -} - -.nodes-page .ant-layout, -.nodes-page .ant-layout-content { - background: transparent; -} - -.nodes-page .content-shell { - background: transparent; -} - -.nodes-page .content-area { - padding: 24px; -} - -@media (max-width: 768px) { - .nodes-page .content-area { - padding: 8px; - } -} - -.nodes-page .loading-spacer { - min-height: calc(100vh - 120px); -} - -.nodes-page .summary-card { - padding: 16px; -} - -@media (max-width: 768px) { - .nodes-page .summary-card { - padding: 8px; - } -} diff --git a/frontend/src/pages/nodes/NodesPage.tsx b/frontend/src/pages/nodes/NodesPage.tsx index 2f5b7a79..966b9649 100644 --- a/frontend/src/pages/nodes/NodesPage.tsx +++ b/frontend/src/pages/nodes/NodesPage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Card, Col, ConfigProvider, Layout, Modal, Row, Spin, message } from 'antd'; +import { Card, Col, ConfigProvider, Layout, Modal, Row, Spin, Statistic, message } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, @@ -14,12 +14,9 @@ import { useNodesQuery } from '@/api/queries/useNodesQuery'; import type { NodeRecord } from '@/api/queries/useNodesQuery'; import { useNodeMutations } from '@/api/queries/useNodeMutations'; import AppSidebar from '@/components/AppSidebar'; -import CustomStatistic from '@/components/CustomStatistic'; import NodeList from './NodeList'; import NodeFormModal from './NodeFormModal'; import { setMessageInstance } from '@/utils/messageBus'; -import '@/styles/page-cards.css'; -import './NodesPage.css'; export default function NodesPage() { const { t } = useTranslation(); @@ -109,28 +106,28 @@ export default function NodesPage() { - } /> - } + prefix={} /> - } + prefix={} /> - 0 ? `${totals.avgLatency} ms` : '-'} prefix={} diff --git a/frontend/src/pages/settings/SecurityTab.css b/frontend/src/pages/settings/SecurityTab.css index 158f45ed..e078c081 100644 --- a/frontend/src/pages/settings/SecurityTab.css +++ b/frontend/src/pages/settings/SecurityTab.css @@ -22,7 +22,7 @@ } .api-token-row { - border: 1px solid rgba(128, 128, 128, 0.18); + border: 1px solid var(--ant-color-border-secondary); border-radius: 8px; padding: 10px 12px; display: flex; @@ -78,7 +78,7 @@ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; padding: 4px 8px; - background: rgba(128, 128, 128, 0.08); + background: var(--ant-color-fill-tertiary); border-radius: 4px; word-break: break-all; } diff --git a/frontend/src/pages/settings/SettingsPage.css b/frontend/src/pages/settings/SettingsPage.css index d5d638ab..7bacdcbe 100644 --- a/frontend/src/pages/settings/SettingsPage.css +++ b/frontend/src/pages/settings/SettingsPage.css @@ -1,87 +1,9 @@ -.settings-page { - --bg-page: #e6e8ec; - --bg-card: #ffffff; - min-height: 100vh; - background: var(--bg-page); -} - -.settings-page.is-dark { - --bg-page: #1a1b1f; - --bg-card: #23252b; -} - -.settings-page.is-dark.is-ultra { - --bg-page: #000; - --bg-card: #101013; -} - -.settings-page .ant-layout, -.settings-page .ant-layout-content { - background: transparent; -} - -.settings-page .content-shell { - background: transparent; -} - -.settings-page .content-area { - padding: 24px; -} - -.settings-page .loading-spacer { - min-height: calc(100vh - 120px); -} - .settings-page .conf-alert { margin-bottom: 10px; } -.settings-page .header-row { - display: flex; - flex-wrap: wrap; - align-items: center; -} - -.settings-page .header-actions { - padding: 4px; -} - -.settings-page .header-info { - display: flex; - justify-content: flex-end; -} - -.icons-only .ant-tabs-nav { - margin-bottom: 8px; -} - -.icons-only .ant-tabs-nav-wrap { - width: 100%; -} - -.icons-only .ant-tabs-nav-list { - display: flex; - width: 100%; -} - -.icons-only .ant-tabs-tab { - flex: 1 1 0; - justify-content: center; - margin: 0; - padding: 10px 0; -} - -.icons-only .ant-tabs-tab .anticon { - margin: 0; - font-size: 18px; -} - -.icons-only .ant-tabs-nav-operations { - display: none; -} - .ldap-no-inbounds { margin-top: 6px; - color: #999; + color: var(--ant-color-text-tertiary); font-size: 12px; } diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index 8442f27b..af77f6b1 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -35,7 +35,6 @@ import SecurityTab from './SecurityTab'; import TelegramTab from './TelegramTab'; import SubscriptionGeneralTab from './SubscriptionGeneralTab'; import SubscriptionFormatsTab from './SubscriptionFormatsTab'; -import '@/styles/page-cards.css'; import './SettingsPage.css'; interface ApiMsg { diff --git a/frontend/src/pages/settings/SubscriptionFormatsTab.css b/frontend/src/pages/settings/SubscriptionFormatsTab.css index 318f341f..730aeb68 100644 --- a/frontend/src/pages/settings/SubscriptionFormatsTab.css +++ b/frontend/src/pages/settings/SubscriptionFormatsTab.css @@ -1,4 +1,3 @@ .nested-block { padding: 10px 20px; - display: block !important; } diff --git a/frontend/src/pages/settings/TwoFactorModal.css b/frontend/src/pages/settings/TwoFactorModal.css index e72f272f..23a401d9 100644 --- a/frontend/src/pages/settings/TwoFactorModal.css +++ b/frontend/src/pages/settings/TwoFactorModal.css @@ -7,9 +7,6 @@ .qr-code { cursor: pointer; - padding: 0 !important; - background: #fff; - border-radius: 6px; } .qr-token { diff --git a/frontend/src/pages/sub/SubPage.css b/frontend/src/pages/sub/SubPage.css index 62292db4..fabeaac2 100644 --- a/frontend/src/pages/sub/SubPage.css +++ b/frontend/src/pages/sub/SubPage.css @@ -53,49 +53,12 @@ .qr-code { cursor: pointer; - padding: 0 !important; - background: #fff; - border-radius: 4px; } .info-table { margin-top: 12px; } -.info-table .ant-descriptions-view, -.info-table .ant-descriptions-view table, -.info-table .ant-descriptions-view th, -.info-table .ant-descriptions-view td { - border-color: rgba(0, 0, 0, 0.18) !important; -} - -.info-table tbody > tr > th, -.info-table tbody > tr > td { - border-bottom: 1px solid rgba(0, 0, 0, 0.18) !important; -} - -.info-table tbody > tr:last-child > th, -.info-table tbody > tr:last-child > td { - border-bottom: none !important; -} - -.is-dark .info-table .ant-descriptions-view, -.is-dark .info-table .ant-descriptions-view table, -.is-dark .info-table .ant-descriptions-view th, -.is-dark .info-table .ant-descriptions-view td { - border-color: rgba(255, 255, 255, 0.18) !important; -} - -.is-dark .info-table tbody > tr > th, -.is-dark .info-table tbody > tr > td { - border-bottom: 1px solid rgba(255, 255, 255, 0.18) !important; -} - -.is-dark .info-table tbody > tr:last-child > th, -.is-dark .info-table tbody > tr:last-child > td { - border-bottom: none !important; -} - .links-section { margin-top: 16px; } @@ -158,49 +121,15 @@ text-align: center; } -.settings-popover { - min-width: 220px; -} - -.theme-cycle { - width: 32px; - height: 32px; +.toolbar-btn { + width: 40px; + height: 40px; + min-width: 40px; border-radius: 50%; - border: 1px solid rgba(0, 0, 0, 0.08); - background: var(--bg-card); - color: rgba(0, 0, 0, 0.65); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; padding: 0; - transition: background-color 0.2s, transform 0.15s, color 0.2s; } -.theme-cycle:hover, -.theme-cycle:focus-visible { - background-color: rgba(64, 150, 255, 0.1); - color: #4096ff; - transform: scale(1.05); - outline: none; +.toolbar-btn .anticon { + font-size: 18px; } -.theme-cycle svg { - width: 16px; - height: 16px; -} - -.is-dark .theme-cycle { - border-color: rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.85); -} - -.is-dark .theme-cycle:hover, -.is-dark .theme-cycle:focus-visible { - background-color: rgba(64, 150, 255, 0.1); - color: #4096ff; -} - -.lang-select { - width: 100%; -} diff --git a/frontend/src/pages/sub/SubPage.tsx b/frontend/src/pages/sub/SubPage.tsx index b0eeabc6..ebea13fd 100644 --- a/frontend/src/pages/sub/SubPage.tsx +++ b/frontend/src/pages/sub/SubPage.tsx @@ -8,11 +8,11 @@ import { Descriptions, Dropdown, Layout, + Menu, message, Popover, QRCode, Row, - Select, Space, Tag, } from 'antd'; @@ -21,7 +21,10 @@ import { AppleOutlined, CopyOutlined, DownOutlined, - SettingOutlined, + MoonFilled, + MoonOutlined, + SunOutlined, + TranslationOutlined, } from '@ant-design/icons'; import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils'; @@ -206,34 +209,20 @@ export default function SubPage() { { key: 'ios-happ', label: 'Happ', onClick: () => open(happUrl) }, ], [copy, open, shadowrocketUrl, v2boxUrl, streisandUrl, happUrl]); - const langOptions = useMemo( - () => LanguageManager.supportedLanguages.map((l: { value: string; name: string; icon: string }) => ({ - value: l.value, + const langMenuItems = useMemo( + () => (LanguageManager.supportedLanguages as { value: string; name: string; icon: string }[]).map((l) => ({ + key: l.value, label: ( - <> - {l.icon} -   {l.name} - + + + {l.name} + ), })), [], ); - const themeIcon = !isDark ? ( - - ) : !isUltra ? ( - - ) : ( - - ); + const themeIcon = !isDark ? : !isUltra ? : ; const cardTitle = ( @@ -244,32 +233,38 @@ export default function SubPage() { const cardExtra = ( - + /> -