diff --git a/components.json b/components.json index 8bfc737f..26b31d57 100644 --- a/components.json +++ b/components.json @@ -1,15 +1,17 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", + "style": "radix-lyra", "rsc": false, "tsx": true, "tailwind": { "config": "", "css": "src/index.css", - "baseColor": "zinc", + "baseColor": "neutral", "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", + "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -17,5 +19,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide" + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} } diff --git a/docker/compose.dev.yml b/docker/compose-dev.yml similarity index 100% rename from docker/compose.dev.yml rename to docker/compose-dev.yml diff --git a/package-lock.json b/package-lock.json index 87c0a961..a2021ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@electron/notarize": "^3.1.1", "@electron/rebuild": "^4.0.4", "@eslint/js": "^9.0.0", + "@fontsource-variable/jetbrains-mono": "^5.2.8", "@hookform/resolvers": "^5.2.2", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.12", @@ -111,6 +112,7 @@ "lucide-react": "^1.11.0", "next-themes": "^0.4.6", "prettier": "3.8.3", + "radix-ui": "^1.4.3", "react": "^19.2.5", "react-cytoscapejs": "^2.0.0", "react-dom": "^19.2.5", @@ -128,12 +130,16 @@ "react-xtermjs": "^1.0.10", "recharts": "^3.8.1", "remark-gfm": "^4.0.1", + "shadcn": "^4.7.0", + "sharp": "^0.34.5", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", + "tw-animate-css": "^1.4.0", "typescript": "~6.0.3", "typescript-eslint": "^8.59.0", "vite": "^8.0.10", + "vite-plugin-svgr": "^5.2.0", "zod": "^4.3.6" }, "engines": { @@ -276,6 +282,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", @@ -303,6 +322,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -313,6 +364,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -345,6 +410,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -405,6 +525,95 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -1225,6 +1434,188 @@ "dev": true, "license": "MIT" }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.65.0.tgz", + "integrity": "sha512-v4FA/Lw3pTEloLxBqTOaYDX6MNo0Jo7lGBsPZhwnJBqRJp0AzQg1ZZNxrFsh6HVC6QWeWrfIKLn0y2eyIXaVDg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^17.2.1", + "eciesjs": "^0.4.10", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "picomatch": "^4.0.4", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@dotenvx/dotenvx/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, "node_modules/@electron/asar": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.0.tgz", @@ -1835,6 +2226,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -1937,6 +2351,631 @@ "dev": true, "license": "MIT" }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", + "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", @@ -2266,6 +3305,47 @@ "dev": true, "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@monaco-editor/loader": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", @@ -2291,6 +3371,31 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.8.tgz", + "integrity": "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/canvas": { "version": "0.1.99", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.99.tgz", @@ -2572,6 +3677,111 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-project/types": { "version": "0.127.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", @@ -2596,6 +3806,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", @@ -2700,6 +3934,58 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-checkbox": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", @@ -2840,6 +4126,35 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -3012,6 +4327,91 @@ } } }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -3139,6 +4539,142 @@ } } }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", @@ -3387,6 +4923,39 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -3676,6 +5245,151 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -3804,6 +5518,25 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -4256,6 +5989,36 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@simple-libs/child-process-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", @@ -4285,6 +6048,19 @@ "url": "https://ko-fi.com/dangreen" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4299,6 +6075,271 @@ "dev": true, "license": "MIT" }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", @@ -4571,6 +6612,18 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@ts-morph/common": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -4937,6 +6990,16 @@ "@types/node": "*" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/speakeasy": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/speakeasy/-/speakeasy-2.0.10.tgz", @@ -4974,6 +7037,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -4988,6 +7058,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -5573,6 +7650,24 @@ } } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -5814,6 +7909,19 @@ "node": ">=0.8" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -6113,6 +8221,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -6273,6 +8394,22 @@ "node": ">=8" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -6498,6 +8635,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-truncate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", @@ -6516,6 +8666,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -6599,6 +8759,13 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -7012,6 +9179,19 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -7161,6 +9341,16 @@ "node": ">=12" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7223,6 +9413,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7239,6 +9444,59 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7297,6 +9555,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -7441,6 +9709,17 @@ "node": ">=6.0.0" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -7643,6 +9922,24 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/eciesjs": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.5", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7832,6 +10129,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -8209,6 +10519,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -8256,6 +10580,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8291,6 +10622,73 @@ "bare-events": "^2.7.0" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -8350,6 +10748,25 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -8418,6 +10835,36 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -8432,6 +10879,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -8449,6 +10913,26 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fault": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", @@ -8491,6 +10975,46 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8550,6 +11074,19 @@ "node": ">=10" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -8692,6 +11229,19 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8776,6 +11326,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8842,6 +11399,19 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -8979,6 +11549,16 @@ "dev": true, "license": "ISC" }, + "node_modules/graphql": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/guacamole-common-js": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/guacamole-common-js/-/guacamole-common-js-1.5.0.tgz", @@ -9122,6 +11702,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -9156,6 +11747,16 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -9257,6 +11858,16 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -9477,9 +12088,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -9538,6 +12149,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9581,6 +12208,68 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -9610,6 +12299,61 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9749,6 +12493,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -9847,6 +12598,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -10381,6 +13142,36 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -10471,6 +13262,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -10906,6 +13707,23 @@ } } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -11497,6 +14315,33 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -11535,6 +14380,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -11661,6 +14516,72 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.14.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.5.tgz", + "integrity": "sha512-X6G05oX4x0e+CNI55KMdhMmwHCBKf2iwazGr+iwsdoJ94JA1ED7wSXb6V+lLPdqFkmIlPiGYvayqnaNcOzobDA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/multer": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", @@ -11723,6 +14644,16 @@ "node": ">= 0.6" } }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nan": { "version": "2.26.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", @@ -11781,6 +14712,17 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-abi": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", @@ -11812,6 +14754,46 @@ "semver": "^7.3.5" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", @@ -11896,6 +14878,36 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11917,6 +14929,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -11954,6 +14976,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -11972,6 +15015,62 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12085,6 +15184,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12094,6 +15206,13 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12150,6 +15269,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pdfjs-dist": { "version": "5.4.296", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", @@ -12205,6 +15334,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -12258,6 +15397,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -12277,6 +15430,19 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prebuild-install": { "name": "@mmomtchev/prebuild-install", "version": "1.0.2", @@ -12342,6 +15508,22 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -12392,6 +15574,30 @@ "node": ">=10" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -12654,6 +15860,197 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -13116,6 +16513,23 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/recharts": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", @@ -13345,6 +16759,24 @@ "node": ">= 4" } }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -13409,6 +16841,43 @@ "node": ">= 18" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -13535,6 +17004,13 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -13547,6 +17023,122 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shadcn": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.7.0.tgz", + "integrity": "sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "@modelcontextprotocol/sdk": "^1.26.0", + "@types/validate-npm-package-name": "^4.0.2", + "browserslist": "^4.26.2", + "commander": "^14.0.0", + "cosmiconfig": "^9.0.0", + "dedent": "^1.6.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "https-proxy-agent": "^7.0.6", + "kleur": "^4.1.5", + "msw": "^2.10.4", + "node-fetch": "^3.3.2", + "open": "^11.0.0", + "ora": "^8.2.0", + "postcss": "^8.5.6", + "postcss-selector-parser": "^7.1.0", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "stringify-object": "^5.0.0", + "tailwind-merge": "^3.0.1", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "validate-npm-package-name": "^7.0.1", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.6" + }, + "bin": { + "shadcn": "dist/index.js" + } + }, + "node_modules/shadcn/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/shadcn/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13726,6 +17318,13 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -13752,6 +17351,17 @@ "npm": ">= 3.0.0" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -13874,6 +17484,19 @@ "node": ">= 0.8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -13893,6 +17516,13 @@ "text-decoder": "^1.1.0" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13968,6 +17598,37 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stringify-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" + } + }, + "node_modules/stringify-object/node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -13984,6 +17645,29 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -14049,6 +17733,26 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -14217,6 +17921,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -14237,6 +17961,19 @@ "tmp": "^0.2.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -14246,6 +17983,19 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -14301,6 +18051,32 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-morph": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -14319,6 +18095,16 @@ "node": "*" } }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -14338,6 +18124,22 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -14412,6 +18214,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -14524,6 +18339,16 @@ "node": ">= 0.8" } }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -14633,6 +18458,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -14797,6 +18632,21 @@ } } }, + "node_modules/vite-plugin-svgr": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz", + "integrity": "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=3.0.0" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -14824,6 +18674,16 @@ "loose-envify": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14939,6 +18799,23 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -15035,6 +18912,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yocto-spinner": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.0.tgz", + "integrity": "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", @@ -15045,6 +18951,16 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "node_modules/zod-validation-error": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", diff --git a/package.json b/package.json index aee21c22..17e1f3e4 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@electron/notarize": "^3.1.1", "@electron/rebuild": "^4.0.4", "@eslint/js": "^9.0.0", + "@fontsource-variable/jetbrains-mono": "^5.2.8", "@hookform/resolvers": "^5.2.2", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.12", @@ -140,6 +141,7 @@ "lucide-react": "^1.11.0", "next-themes": "^0.4.6", "prettier": "3.8.3", + "radix-ui": "^1.4.3", "react": "^19.2.5", "react-cytoscapejs": "^2.0.0", "react-dom": "^19.2.5", @@ -157,12 +159,16 @@ "react-xtermjs": "^1.0.10", "recharts": "^3.8.1", "remark-gfm": "^4.0.1", + "shadcn": "^4.7.0", + "sharp": "^0.34.5", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", + "tw-animate-css": "^1.4.0", "typescript": "~6.0.3", "typescript-eslint": "^8.59.0", "vite": "^8.0.10", + "vite-plugin-svgr": "^5.2.0", "zod": "^4.3.6" }, "lint-staged": { diff --git a/public/favicon.ico b/public/favicon.ico index 9b270324..438484bb 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/full-icon.png b/public/full-icon.png index bac3267a..574578cc 100644 Binary files a/public/full-icon.png and b/public/full-icon.png differ diff --git a/public/icon-mac.png b/public/icon-mac.png index 19f7681e..56ae3e4a 100644 Binary files a/public/icon-mac.png and b/public/icon-mac.png differ diff --git a/public/icon.icns b/public/icon.icns index 7e6b7329..1a3cc893 100644 Binary files a/public/icon.icns and b/public/icon.icns differ diff --git a/public/icon.ico b/public/icon.ico index 9b270324..00594ef7 100644 Binary files a/public/icon.ico and b/public/icon.ico differ diff --git a/public/icon.png b/public/icon.png index ecc08b71..574578cc 100644 Binary files a/public/icon.png and b/public/icon.png differ diff --git a/public/icon.svg b/public/icon.svg index 8ffb60cf..4ee301a7 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1,29 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/public/icons/1024x1024.png b/public/icons/1024x1024.png index 1cfe9ddc..574578cc 100644 Binary files a/public/icons/1024x1024.png and b/public/icons/1024x1024.png differ diff --git a/public/icons/128x128.png b/public/icons/128x128.png index 250df98e..ba734af5 100644 Binary files a/public/icons/128x128.png and b/public/icons/128x128.png differ diff --git a/public/icons/16x16.png b/public/icons/16x16.png index be540109..553660bb 100644 Binary files a/public/icons/16x16.png and b/public/icons/16x16.png differ diff --git a/public/icons/24x24.png b/public/icons/24x24.png index d3489969..6c083f00 100644 Binary files a/public/icons/24x24.png and b/public/icons/24x24.png differ diff --git a/public/icons/256x256.png b/public/icons/256x256.png index f65773cc..75666110 100644 Binary files a/public/icons/256x256.png and b/public/icons/256x256.png differ diff --git a/public/icons/32x32.png b/public/icons/32x32.png index b112caf3..87ac371a 100644 Binary files a/public/icons/32x32.png and b/public/icons/32x32.png differ diff --git a/public/icons/48x48.png b/public/icons/48x48.png index a8d6242b..9730d5c0 100644 Binary files a/public/icons/48x48.png and b/public/icons/48x48.png differ diff --git a/public/icons/512x512.png b/public/icons/512x512.png index cc795299..56ae3e4a 100644 Binary files a/public/icons/512x512.png and b/public/icons/512x512.png differ diff --git a/public/icons/64x64.png b/public/icons/64x64.png index 9fcfda6b..5a5a1e86 100644 Binary files a/public/icons/64x64.png and b/public/icons/64x64.png differ diff --git a/public/icons/icon.icns b/public/icons/icon.icns index b82c7ae7..1a3cc893 100644 Binary files a/public/icons/icon.icns and b/public/icons/icon.icns differ diff --git a/public/icons/icon.ico b/public/icons/icon.ico index 9b270324..00594ef7 100644 Binary files a/public/icons/icon.ico and b/public/icons/icon.ico differ diff --git a/repo-images/HeaderImage.png b/repo-images/HeaderImage.png deleted file mode 100644 index 74892b03..00000000 Binary files a/repo-images/HeaderImage.png and /dev/null differ diff --git a/repo-images/Image 1.png b/repo-images/Image 1.png deleted file mode 100644 index fea7eb76..00000000 Binary files a/repo-images/Image 1.png and /dev/null differ diff --git a/repo-images/Image 10.png b/repo-images/Image 10.png deleted file mode 100644 index efc0c012..00000000 Binary files a/repo-images/Image 10.png and /dev/null differ diff --git a/repo-images/Image 11.png b/repo-images/Image 11.png deleted file mode 100644 index 3db821d8..00000000 Binary files a/repo-images/Image 11.png and /dev/null differ diff --git a/repo-images/Image 12.png b/repo-images/Image 12.png deleted file mode 100644 index b79011b5..00000000 Binary files a/repo-images/Image 12.png and /dev/null differ diff --git a/repo-images/Image 2.png b/repo-images/Image 2.png deleted file mode 100644 index 9a47b331..00000000 Binary files a/repo-images/Image 2.png and /dev/null differ diff --git a/repo-images/Image 3.png b/repo-images/Image 3.png deleted file mode 100644 index 4179a57a..00000000 Binary files a/repo-images/Image 3.png and /dev/null differ diff --git a/repo-images/Image 4.png b/repo-images/Image 4.png deleted file mode 100644 index a47da14f..00000000 Binary files a/repo-images/Image 4.png and /dev/null differ diff --git a/repo-images/Image 5.png b/repo-images/Image 5.png deleted file mode 100644 index e44011e0..00000000 Binary files a/repo-images/Image 5.png and /dev/null differ diff --git a/repo-images/Image 6.png b/repo-images/Image 6.png deleted file mode 100644 index f6f70ba1..00000000 Binary files a/repo-images/Image 6.png and /dev/null differ diff --git a/repo-images/Image 7.png b/repo-images/Image 7.png deleted file mode 100644 index f77d750e..00000000 Binary files a/repo-images/Image 7.png and /dev/null differ diff --git a/repo-images/Image 8.png b/repo-images/Image 8.png deleted file mode 100644 index 03bf9daf..00000000 Binary files a/repo-images/Image 8.png and /dev/null differ diff --git a/repo-images/Image 9.png b/repo-images/Image 9.png deleted file mode 100644 index 28d9e70f..00000000 Binary files a/repo-images/Image 9.png and /dev/null differ diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs new file mode 100644 index 00000000..0930b78e --- /dev/null +++ b/scripts/generate-icons.mjs @@ -0,0 +1,117 @@ +import sharp from 'sharp'; +import { readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, '..'); +const publicDir = join(root, 'public'); +const iconsDir = join(publicDir, 'icons'); + +mkdirSync(iconsDir, { recursive: true }); + +const svgBuffer = readFileSync(join(publicDir, 'icon.svg')); + +const pngSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; + +console.log('Generating PNG icons...'); +await Promise.all( + pngSizes.map(size => + sharp(svgBuffer) + .resize(size, size) + .png() + .toFile(join(iconsDir, `${size}x${size}.png`)) + .then(() => console.log(` ✓ icons/${size}x${size}.png`)) + ) +); + +// icon.png (1024x1024) for Linux electron-builder +await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(publicDir, 'icon.png')); +console.log(' ✓ icon.png'); + +// icon-mac.png (512x512) for macOS +await sharp(svgBuffer).resize(512, 512).png().toFile(join(publicDir, 'icon-mac.png')); +console.log(' ✓ icon-mac.png'); + +// full-icon.png (1024x1024) used in app UI +await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(publicDir, 'full-icon.png')); +console.log(' ✓ full-icon.png'); + +// favicon.ico — embed 16, 32, 48 px layers +console.log('Generating favicon.ico...'); +const icoSizes = [16, 32, 48]; +const icoBuffers = await Promise.all( + icoSizes.map(size => + sharp(svgBuffer).resize(size, size).png().toBuffer() + ) +); +writeFileSync(join(publicDir, 'favicon.ico'), buildIco(icoBuffers, icoSizes)); +console.log(' ✓ favicon.ico'); + +// icon.ico — embed 16, 32, 48, 64, 128, 256 px layers +console.log('Generating icon.ico...'); +const winSizes = [16, 32, 48, 64, 128, 256]; +const winBuffers = await Promise.all( + winSizes.map(size => + sharp(svgBuffer).resize(size, size).png().toBuffer() + ) +); +writeFileSync(join(publicDir, 'icon.ico'), buildIco(winBuffers, winSizes)); +console.log(' ✓ icon.ico'); + +// icons/icon.ico and icons/icon.icns placeholders (stubs pointing to source) +// electron-builder generates .icns; copy the 1024 PNG as icons/icon.png for reference +await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(iconsDir, 'icon.ico').replace('icon.ico', '1024x1024.png')); +// Copy icon.ico and icon.icns into icons/ as well +import { copyFileSync } from 'fs'; +copyFileSync(join(publicDir, 'icon.ico'), join(iconsDir, 'icon.ico')); +console.log(' ✓ icons/icon.ico'); + +console.log('\nDone! Note: icon.icns requires macOS tools (iconutil). Use electron-builder on macOS to generate it.'); + +/** + * Builds a minimal ICO file from PNG buffers. + * @param {Buffer[]} pngBuffers + * @param {number[]} sizes + * @returns {Buffer} + */ +function buildIco(pngBuffers, sizes) { + const count = pngBuffers.length; + const headerSize = 6; + const dirEntrySize = 16; + const dirSize = headerSize + count * dirEntrySize; + + let offset = dirSize; + const entries = pngBuffers.map((buf, i) => { + const size = sizes[i]; + const entry = { size, width: size > 255 ? 0 : size, height: size > 255 ? 0 : size, buf, offset }; + offset += buf.length; + return entry; + }); + + const totalSize = offset; + const ico = Buffer.alloc(totalSize); + + // ICO header + ico.writeUInt16LE(0, 0); // reserved + ico.writeUInt16LE(1, 2); // type: 1 = ICO + ico.writeUInt16LE(count, 4); // image count + + // Directory entries + entries.forEach((e, i) => { + const base = headerSize + i * dirEntrySize; + ico.writeUInt8(e.width, base); // width (0 = 256) + ico.writeUInt8(e.height, base + 1); // height (0 = 256) + ico.writeUInt8(0, base + 2); // color count + ico.writeUInt8(0, base + 3); // reserved + ico.writeUInt16LE(1, base + 4); // planes + ico.writeUInt16LE(32, base + 6); // bit count + ico.writeUInt32LE(e.buf.length, base + 8); // size of image data + ico.writeUInt32LE(e.offset, base + 12); // offset of image data + }); + + // Image data + entries.forEach(e => e.buf.copy(ico, e.offset)); + + return ico; +} diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 5acce909..e158bc7a 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -1044,6 +1044,23 @@ const migrateSchema = () => { { column: "host_key_changed_count", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_changed_count INTEGER NOT NULL DEFAULT 0" }, { column: "mac_address", sql: "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" }, { column: "port_knock_sequence", sql: "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" }, + { column: "enable_ssh", sql: "ALTER TABLE ssh_data ADD COLUMN enable_ssh INTEGER NOT NULL DEFAULT 1" }, + { column: "enable_rdp", sql: "ALTER TABLE ssh_data ADD COLUMN enable_rdp INTEGER NOT NULL DEFAULT 0" }, + { column: "enable_vnc", sql: "ALTER TABLE ssh_data ADD COLUMN enable_vnc INTEGER NOT NULL DEFAULT 0" }, + { column: "enable_telnet", sql: "ALTER TABLE ssh_data ADD COLUMN enable_telnet INTEGER NOT NULL DEFAULT 0" }, + { column: "ssh_port", sql: "ALTER TABLE ssh_data ADD COLUMN ssh_port INTEGER DEFAULT 22" }, + { column: "rdp_port", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_port INTEGER DEFAULT 3389" }, + { column: "vnc_port", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_port INTEGER DEFAULT 5900" }, + { column: "telnet_port", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_port INTEGER DEFAULT 23" }, + { column: "rdp_user", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_user TEXT" }, + { column: "rdp_password", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_password TEXT" }, + { column: "rdp_domain", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_domain TEXT" }, + { column: "rdp_security", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_security TEXT" }, + { column: "rdp_ignore_cert", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_ignore_cert INTEGER DEFAULT 0" }, + { column: "vnc_password", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_password TEXT" }, + { column: "vnc_user", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_user TEXT" }, + { column: "telnet_user", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_user TEXT" }, + { column: "telnet_password", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_password TEXT" }, ]; for (const migration of sshDataMigrations) { diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 0e0d89d9..072d9619 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -129,6 +129,28 @@ export const hosts = sqliteTable("ssh_data", { terminalConfig: text("terminal_config"), quickActions: text("quick_actions"), notes: text("notes"), + enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true), + enableRdp: integer("enable_rdp", { mode: "boolean" }).notNull().default(false), + enableVnc: integer("enable_vnc", { mode: "boolean" }).notNull().default(false), + enableTelnet: integer("enable_telnet", { mode: "boolean" }).notNull().default(false), + + sshPort: integer("ssh_port").default(22), + rdpPort: integer("rdp_port").default(3389), + vncPort: integer("vnc_port").default(5900), + telnetPort: integer("telnet_port").default(23), + + rdpUser: text("rdp_user"), + rdpPassword: text("rdp_password"), + rdpDomain: text("rdp_domain"), + rdpSecurity: text("rdp_security"), + rdpIgnoreCert: integer("rdp_ignore_cert", { mode: "boolean" }).default(false), + + vncPassword: text("vnc_password"), + vncUser: text("vnc_user"), + + telnetUser: text("telnet_user"), + telnetPassword: text("telnet_password"), + domain: text("domain"), security: text("security"), ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false), diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index 5a5b371c..8efb3088 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -252,6 +252,9 @@ const SENSITIVE_FIELDS = [ "autostartKey", "autostartKeyPassword", "socks5Password", + "rdpPassword", + "vncPassword", + "telnetPassword", ]; function stripSensitiveFields( @@ -288,6 +291,23 @@ function transformHostResponse( showTunnelInSidebar: !!host.showTunnelInSidebar, showDockerInSidebar: !!host.showDockerInSidebar, showServerStatsInSidebar: !!host.showServerStatsInSidebar, + enableSsh: host.enableSsh !== undefined ? !!host.enableSsh : true, + enableRdp: !!host.enableRdp, + enableVnc: !!host.enableVnc, + enableTelnet: !!host.enableTelnet, + sshPort: host.sshPort ?? host.port ?? 22, + rdpPort: host.rdpPort ?? 3389, + vncPort: host.vncPort ?? 5900, + telnetPort: host.telnetPort ?? 23, + rdpUser: host.rdpUser || undefined, + rdpDomain: host.rdpDomain || undefined, + rdpSecurity: host.rdpSecurity || undefined, + rdpIgnoreCert: !!host.rdpIgnoreCert, + vncUser: host.vncUser || undefined, + telnetUser: host.telnetUser || undefined, + hasRdpPassword: !!host.rdpPassword, + hasVncPassword: !!host.vncPassword, + hasTelnetPassword: !!host.telnetPassword, tunnelConnections: host.tunnelConnections ? JSON.parse(host.tunnelConnections as string) : [], @@ -593,6 +613,23 @@ router.post( portKnockSequence, overrideCredentialUsername, macAddress, + enableSsh, + enableRdp, + enableVnc, + enableTelnet, + sshPort, + rdpPort, + vncPort, + telnetPort, + rdpUser, + rdpPassword, + rdpDomain, + rdpSecurity, + rdpIgnoreCert, + vncPassword, + vncUser, + telnetUser, + telnetPassword, } = hostData; databaseLogger.info("Creating SSH host", { operation: "host_create", @@ -685,6 +722,20 @@ router.post( portKnockSequence: portKnockSequence ? JSON.stringify(portKnockSequence) : null, + enableSsh: enableSsh !== false ? 1 : 0, + enableRdp: enableRdp ? 1 : 0, + enableVnc: enableVnc ? 1 : 0, + enableTelnet: enableTelnet ? 1 : 0, + sshPort: sshPort || port || 22, + rdpPort: rdpPort || 3389, + vncPort: vncPort || 5900, + telnetPort: telnetPort || 23, + rdpUser: rdpUser || null, + rdpDomain: rdpDomain || null, + rdpSecurity: rdpSecurity || null, + rdpIgnoreCert: rdpIgnoreCert ? 1 : 0, + vncUser: vncUser || null, + telnetUser: telnetUser || null, }; // For non-SSH hosts (RDP, VNC, Telnet), always save password if provided @@ -743,6 +794,10 @@ router.post( sshDataObj.keyType = null; } + sshDataObj.rdpPassword = rdpPassword || null; + sshDataObj.vncPassword = vncPassword || null; + sshDataObj.telnetPassword = telnetPassword || null; + try { const result = await SimpleDBOps.insert( hosts, @@ -1092,6 +1147,23 @@ router.put( portKnockSequence, overrideCredentialUsername, macAddress, + enableSsh, + enableRdp, + enableVnc, + enableTelnet, + sshPort, + rdpPort, + vncPort, + telnetPort, + rdpUser, + rdpPassword, + rdpDomain, + rdpSecurity, + rdpIgnoreCert, + vncPassword, + vncUser, + telnetUser, + telnetPassword, } = hostData; databaseLogger.info("Updating SSH host", { operation: "host_update", @@ -1181,6 +1253,20 @@ router.put( portKnockSequence: portKnockSequence ? JSON.stringify(portKnockSequence) : null, + enableSsh: enableSsh !== false ? 1 : 0, + enableRdp: enableRdp ? 1 : 0, + enableVnc: enableVnc ? 1 : 0, + enableTelnet: enableTelnet ? 1 : 0, + sshPort: sshPort || port || 22, + rdpPort: rdpPort || 3389, + vncPort: vncPort || 5900, + telnetPort: telnetPort || 23, + rdpUser: rdpUser || null, + rdpDomain: rdpDomain || null, + rdpSecurity: rdpSecurity || null, + rdpIgnoreCert: rdpIgnoreCert ? 1 : 0, + vncUser: vncUser || null, + telnetUser: telnetUser || null, }; // For non-SSH hosts (RDP, VNC, Telnet), always save password if provided @@ -1249,6 +1335,11 @@ router.put( sshDataObj.keyType = null; } + if (rdpPassword !== undefined) sshDataObj.rdpPassword = rdpPassword || null; + if (vncPassword !== undefined) sshDataObj.vncPassword = vncPassword || null; + if (telnetPassword !== undefined) + sshDataObj.telnetPassword = telnetPassword || null; + try { const accessInfo = await permissionManager.canAccessHost( userId, diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index c0bf3805..8700f476 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -676,7 +676,18 @@ interface StatsConfig { } const DEFAULT_STATS_CONFIG: StatsConfig = { - enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"], + enabledWidgets: [ + "cpu", + "memory", + "disk", + "network", + "uptime", + "system", + "login_stats", + "processes", + "ports", + "firewall", + ], statusCheckEnabled: true, statusCheckInterval: 60, metricsEnabled: true, diff --git a/src/backend/swagger.ts b/src/backend/swagger.ts deleted file mode 100644 index 8ed87a03..00000000 --- a/src/backend/swagger.ts +++ /dev/null @@ -1,161 +0,0 @@ -import swaggerJSDoc from "@deadendjs/swagger-jsdoc"; -import path from "path"; -import { fileURLToPath } from "url"; -import { promises as fs } from "fs"; -import { systemLogger } from "./utils/logger.js"; - -interface SwaggerOptions { - definition: object; - apis: string[]; -} - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const projectRoot = path.join(__dirname, "..", "..", ".."); - -const swaggerOptions: SwaggerOptions = { - definition: { - openapi: "3.0.3", - info: { - title: "Termix API", - version: "0.0.0", - description: "Termix Backend API Reference", - }, - servers: [ - { - url: "http://localhost:30001", - description: "Main database and authentication server", - }, - { - url: "http://localhost:30003", - description: "SSH tunnel management server", - }, - { - url: "http://localhost:30004", - description: "SSH file manager server", - }, - { - url: "http://localhost:30005", - description: "Server statistics and monitoring server", - }, - { - url: "http://localhost:30006", - description: "Dashboard server", - }, - { - url: "http://localhost:30007", - description: "Docker management server", - }, - ], - components: { - securitySchemes: { - bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - }, - }, - schemas: { - Error: { - type: "object", - properties: { - error: { type: "string" }, - details: { type: "string" }, - }, - }, - }, - }, - security: [ - { - bearerAuth: [], - }, - ], - tags: [ - { - name: "Alerts", - description: "System alerts and notifications management", - }, - { - name: "Credentials", - description: "SSH credential management", - }, - { - name: "Network Topology", - description: "Network topology visualization and management", - }, - { - name: "RBAC", - description: "Role-based access control for host sharing", - }, - { - name: "Snippets", - description: "Command snippet management", - }, - { - name: "Terminal", - description: "Terminal command history", - }, - { - name: "Users", - description: "User management and authentication", - }, - { - name: "Dashboard", - description: "Dashboard statistics and activity", - }, - { - name: "Docker", - description: "Docker container management", - }, - { - name: "SSH Tunnels", - description: "SSH tunnel connection management", - }, - { - name: "Server Stats", - description: "Server status monitoring and metrics collection", - }, - { - name: "File Manager", - description: "SSH file management operations", - }, - ], - }, - apis: [ - path.join(projectRoot, "src", "backend", "database", "routes", "*.ts"), - path.join(projectRoot, "src", "backend", "dashboard.ts"), - path.join(projectRoot, "src", "backend", "ssh", "*.ts"), - ], -}; - -async function generateOpenAPISpec() { - try { - systemLogger.info("Generating OpenAPI specification", { - operation: "openapi_generate_start", - }); - - const swaggerSpec = await swaggerJSDoc(swaggerOptions); - - const outputPath = path.join(projectRoot, "openapi.json"); - - await fs.writeFile( - outputPath, - JSON.stringify(swaggerSpec, null, 2), - "utf-8", - ); - - systemLogger.success("OpenAPI specification generated", { - operation: "openapi_generate_success", - }); - } catch (error) { - systemLogger.error("Failed to generate OpenAPI specification", error, { - operation: "openapi_generation", - }); - process.exit(1); - } -} - -generateOpenAPISpec(); - -export { swaggerOptions, generateOpenAPISpec }; diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts index 52527597..b3fbf50d 100644 --- a/src/backend/utils/field-crypto.ts +++ b/src/backend/utils/field-crypto.ts @@ -30,6 +30,10 @@ class FieldCrypto { "autostartPassword", "autostartKey", "autostartKeyPassword", + "socks5Password", + "rdpPassword", + "vncPassword", + "telnetPassword", ]), ssh_credentials: new Set([ "password", diff --git a/src/backend/utils/lazy-field-encryption.ts b/src/backend/utils/lazy-field-encryption.ts index 5d59ba96..3103ad79 100644 --- a/src/backend/utils/lazy-field-encryption.ts +++ b/src/backend/utils/lazy-field-encryption.ts @@ -267,6 +267,10 @@ export class LazyFieldEncryption { autostartPassword: "autostart_password", autostartKey: "autostart_key", autostartKeyPassword: "autostart_key_password", + socks5Password: "socks5_password", + rdpPassword: "rdp_password", + vncPassword: "vnc_password", + telnetPassword: "telnet_password", totpSecret: "totp_secret", totpBackupCodes: "totp_backup_codes", clientSecret: "client_secret", diff --git a/src/backend/utils/user-data-import.ts b/src/backend/utils/user-data-import.ts deleted file mode 100644 index 7ee07c49..00000000 --- a/src/backend/utils/user-data-import.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { getDb } from "../database/db/index.js"; -import { - users, - hosts, - sshCredentials, - fileManagerRecent, - fileManagerPinned, - fileManagerShortcuts, - dismissedAlerts, -} from "../database/db/schema.js"; -import { eq, and } from "drizzle-orm"; -import { DataCrypto } from "./data-crypto.js"; -import { UserDataExport, type UserExportData } from "./user-data-export.js"; -import { databaseLogger } from "./logger.js"; - -interface ImportOptions { - replaceExisting?: boolean; - skipCredentials?: boolean; - skipFileManagerData?: boolean; - dryRun?: boolean; -} - -interface ImportResult { - success: boolean; - summary: { - sshHostsImported: number; - sshCredentialsImported: number; - fileManagerItemsImported: number; - dismissedAlertsImported: number; - skippedItems: number; - errors: string[]; - }; - dryRun: boolean; -} - -class UserDataImport { - static async importUserData( - targetUserId: string, - exportData: UserExportData, - options: ImportOptions = {}, - ): Promise { - const { - replaceExisting = false, - skipCredentials = false, - skipFileManagerData = false, - dryRun = false, - } = options; - - try { - const targetUser = await getDb() - .select() - .from(users) - .where(eq(users.id, targetUserId)); - if (!targetUser || targetUser.length === 0) { - throw new Error(`Target user not found: ${targetUserId}`); - } - - const validation = UserDataExport.validateExportData(exportData); - if (!validation.valid) { - throw new Error(`Invalid export data: ${validation.errors.join(", ")}`); - } - - let userDataKey: Buffer | null = null; - if (exportData.metadata.encrypted) { - userDataKey = DataCrypto.getUserDataKey(targetUserId); - if (!userDataKey) { - throw new Error( - "Target user data not unlocked - password required for encrypted import", - ); - } - } - - const result: ImportResult = { - success: false, - summary: { - sshHostsImported: 0, - sshCredentialsImported: 0, - fileManagerItemsImported: 0, - dismissedAlertsImported: 0, - skippedItems: 0, - errors: [], - }, - dryRun, - }; - - if ( - exportData.userData.sshHosts && - exportData.userData.sshHosts.length > 0 - ) { - const importStats = await this.importSshHosts( - targetUserId, - exportData.userData.sshHosts as Record[], - { replaceExisting, dryRun, userDataKey }, - ); - result.summary.sshHostsImported = importStats.imported; - result.summary.skippedItems += importStats.skipped; - result.summary.errors.push(...importStats.errors); - } - - if ( - !skipCredentials && - exportData.userData.sshCredentials && - exportData.userData.sshCredentials.length > 0 - ) { - const importStats = await this.importSshCredentials( - targetUserId, - exportData.userData.sshCredentials as Record[], - { replaceExisting, dryRun, userDataKey }, - ); - result.summary.sshCredentialsImported = importStats.imported; - result.summary.skippedItems += importStats.skipped; - result.summary.errors.push(...importStats.errors); - } - - if (!skipFileManagerData && exportData.userData.fileManagerData) { - const importStats = await this.importFileManagerData( - targetUserId, - exportData.userData.fileManagerData, - { replaceExisting, dryRun }, - ); - result.summary.fileManagerItemsImported = importStats.imported; - result.summary.skippedItems += importStats.skipped; - result.summary.errors.push(...importStats.errors); - } - - if ( - exportData.userData.dismissedAlerts && - exportData.userData.dismissedAlerts.length > 0 - ) { - const importStats = await this.importDismissedAlerts( - targetUserId, - exportData.userData.dismissedAlerts as Record[], - { replaceExisting, dryRun }, - ); - result.summary.dismissedAlertsImported = importStats.imported; - result.summary.skippedItems += importStats.skipped; - result.summary.errors.push(...importStats.errors); - } - - result.success = result.summary.errors.length === 0; - - databaseLogger.success("User data import completed", { - operation: "user_data_import_complete", - targetUserId, - dryRun, - ...result.summary, - }); - - return result; - } catch (error) { - databaseLogger.error("User data import failed", error, { - operation: "user_data_import_failed", - targetUserId, - dryRun, - }); - throw error; - } - } - - private static async importSshHosts( - targetUserId: string, - sshHosts: Record[], - options: { - replaceExisting: boolean; - dryRun: boolean; - userDataKey: Buffer | null; - }, - ) { - let imported = 0; - let skipped = 0; - const errors: string[] = []; - - for (const host of sshHosts) { - try { - if (options.dryRun) { - imported++; - continue; - } - - const existing = await getDb() - .select() - .from(hosts) - .where( - and( - eq(hosts.userId, targetUserId), - eq(hosts.ip, host.ip as string), - eq(hosts.port, host.port as number), - eq(hosts.username, host.username as string), - ), - ); - - if (existing.length > 0 && !options.replaceExisting) { - skipped++; - continue; - } - - const newHostData: Record = { - ...host, - userId: targetUserId, - updatedAt: new Date().toISOString(), - }; - - if (existing.length === 0) { - newHostData.createdAt = new Date().toISOString(); - } - - let processedHostData: Record = newHostData; - if (options.userDataKey) { - processedHostData = DataCrypto.encryptRecord( - "ssh_data", - newHostData, - targetUserId, - options.userDataKey, - ) as Record; - } - - delete processedHostData.id; - - if (existing.length > 0 && options.replaceExisting) { - await getDb() - .update(hosts) - .set(processedHostData as unknown as typeof hosts.$inferInsert) - .where(eq(hosts.id, existing[0].id)); - } else { - await getDb() - .insert(hosts) - .values(processedHostData as unknown as typeof hosts.$inferInsert); - } - imported++; - } catch (error) { - errors.push( - `SSH host import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - skipped++; - } - } - - return { imported, skipped, errors }; - } - - private static async importSshCredentials( - targetUserId: string, - credentials: Record[], - options: { - replaceExisting: boolean; - dryRun: boolean; - userDataKey: Buffer | null; - }, - ) { - let imported = 0; - let skipped = 0; - const errors: string[] = []; - - for (const credential of credentials) { - try { - if (options.dryRun) { - imported++; - continue; - } - - const existing = await getDb() - .select() - .from(sshCredentials) - .where( - and( - eq(sshCredentials.userId, targetUserId), - eq(sshCredentials.name, credential.name as string), - ), - ); - - if (existing.length > 0 && !options.replaceExisting) { - skipped++; - continue; - } - - const newCredentialData: Record = { - ...credential, - userId: targetUserId, - updatedAt: new Date().toISOString(), - }; - - if (existing.length === 0) { - newCredentialData.usageCount = 0; - newCredentialData.lastUsed = null; - newCredentialData.createdAt = new Date().toISOString(); - } - - let processedCredentialData: Record = - newCredentialData; - if (options.userDataKey) { - processedCredentialData = DataCrypto.encryptRecord( - "ssh_credentials", - newCredentialData, - targetUserId, - options.userDataKey, - ) as Record; - } - - delete processedCredentialData.id; - - if (existing.length > 0 && options.replaceExisting) { - await getDb() - .update(sshCredentials) - .set( - processedCredentialData as unknown as typeof sshCredentials.$inferInsert, - ) - .where(eq(sshCredentials.id, existing[0].id)); - } else { - await getDb() - .insert(sshCredentials) - .values( - processedCredentialData as unknown as typeof sshCredentials.$inferInsert, - ); - } - imported++; - } catch (error) { - errors.push( - `SSH credential import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - skipped++; - } - } - - return { imported, skipped, errors }; - } - - private static async importFileManagerData( - targetUserId: string, - fileManagerData: Record, - options: { replaceExisting: boolean; dryRun: boolean }, - ) { - let imported = 0; - let skipped = 0; - const errors: string[] = []; - - try { - if (fileManagerData.recent && Array.isArray(fileManagerData.recent)) { - for (const item of fileManagerData.recent) { - try { - if (!options.dryRun) { - const newItem = { - ...item, - id: undefined, - userId: targetUserId, - lastOpened: new Date().toISOString(), - }; - await getDb().insert(fileManagerRecent).values(newItem); - } - imported++; - } catch (error) { - errors.push( - `Recent file import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - skipped++; - } - } - } - - if (fileManagerData.pinned && Array.isArray(fileManagerData.pinned)) { - for (const item of fileManagerData.pinned) { - try { - if (!options.dryRun) { - const newItem = { - ...item, - id: undefined, - userId: targetUserId, - pinnedAt: new Date().toISOString(), - }; - await getDb().insert(fileManagerPinned).values(newItem); - } - imported++; - } catch (error) { - errors.push( - `Pinned file import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - skipped++; - } - } - } - - if ( - fileManagerData.shortcuts && - Array.isArray(fileManagerData.shortcuts) - ) { - for (const item of fileManagerData.shortcuts) { - try { - if (!options.dryRun) { - const newItem = { - ...item, - id: undefined, - userId: targetUserId, - createdAt: new Date().toISOString(), - }; - await getDb().insert(fileManagerShortcuts).values(newItem); - } - imported++; - } catch (error) { - errors.push( - `Shortcut import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - skipped++; - } - } - } - } catch (error) { - errors.push( - `File manager data import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } - - return { imported, skipped, errors }; - } - - private static async importDismissedAlerts( - targetUserId: string, - alerts: Record[], - options: { replaceExisting: boolean; dryRun: boolean }, - ) { - let imported = 0; - let skipped = 0; - const errors: string[] = []; - - for (const alert of alerts) { - try { - if (options.dryRun) { - imported++; - continue; - } - - const existing = await getDb() - .select() - .from(dismissedAlerts) - .where( - and( - eq(dismissedAlerts.userId, targetUserId), - eq(dismissedAlerts.alertId, alert.alertId as string), - ), - ); - - if (existing.length > 0 && !options.replaceExisting) { - skipped++; - continue; - } - - const newAlert = { - ...alert, - id: undefined, - userId: targetUserId, - dismissedAt: new Date().toISOString(), - }; - - if (existing.length > 0 && options.replaceExisting) { - await getDb() - .update(dismissedAlerts) - .set(newAlert as typeof dismissedAlerts.$inferInsert) - .where(eq(dismissedAlerts.id, existing[0].id)); - } else { - await getDb() - .insert(dismissedAlerts) - .values(newAlert as typeof dismissedAlerts.$inferInsert); - } - - imported++; - } catch (error) { - errors.push( - `Dismissed alert import failed: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - skipped++; - } - } - - return { imported, skipped, errors }; - } - - static async importUserDataFromJSON( - targetUserId: string, - jsonData: string, - options: ImportOptions = {}, - ): Promise { - try { - const exportData: UserExportData = JSON.parse(jsonData); - return await this.importUserData(targetUserId, exportData, options); - } catch (error) { - if (error instanceof SyntaxError) { - throw new Error("Invalid JSON format in import data", { cause: error }); - } - throw error; - } - } -} - -export { UserDataImport, type ImportOptions, type ImportResult }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx deleted file mode 100644 index aeaadfce..00000000 --- a/src/components/ui/button.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable react-refresh/only-export-components */ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-95", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export interface ButtonProps - extends React.ComponentProps<"button">, VariantProps { - asChild?: boolean; -} - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: ButtonProps) { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -} - -export { Button, buttonVariants, type ButtonProps }; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx deleted file mode 100644 index c0479b5a..00000000 --- a/src/components/ui/chart.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from "react"; -import * as RechartsPrimitive from "recharts"; - -import { cn } from "@/lib/utils"; - -// Chart Container -const ChartContainer = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -ChartContainer.displayName = "ChartContainer"; - -export { ChartContainer, RechartsPrimitive }; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx deleted file mode 100644 index fb265605..00000000 --- a/src/components/ui/command.tsx +++ /dev/null @@ -1,184 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Command as CommandPrimitive } from "cmdk"; -import { SearchIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; - -function Command({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandDialog({ - title = "Command Palette", - description = "Search for a command to run...", - children, - className, - showCloseButton = true, - ...props -}: React.ComponentProps & { - title?: string; - description?: string; - className?: string; - showCloseButton?: boolean; -}) { - return ( - - - {title} - {description} - - - - {children} - - - - ); -} - -function CommandInput({ - className, - ...props -}: React.ComponentProps) { - return ( -
- - -
- ); -} - -function CommandList({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandEmpty({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandGroup({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { - return ( - - ); -} - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index 61ab08e9..00000000 --- a/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { CheckIcon, ChevronRightIcon, Circle } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)); -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ); -}; -DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx deleted file mode 100644 index f7fb44e5..00000000 --- a/src/components/ui/input.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ); -} - -export { Input }; diff --git a/src/components/ui/kbd.tsx b/src/components/ui/kbd.tsx deleted file mode 100644 index ab78efd3..00000000 --- a/src/components/ui/kbd.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { type ComponentProps, type ReactNode } from "react"; -import { cn } from "@/lib/utils"; - -export type KbdProps = ComponentProps<"span"> & { - children: ReactNode; -}; - -export const Kbd = ({ className, children, ...props }: KbdProps) => ( - - {children} - -); - -export type KbdKeyProps = ComponentProps<"span"> & { - "aria-label"?: string; - className?: string; -}; - -export const KbdKey = ({ className, children, ...props }: KbdKeyProps) => ( - - {children} - -); - -export type KbdSeparatorProps = ComponentProps<"span"> & { - children?: ReactNode; - className?: string; -}; - -export const KbdSeparator = ({ - className, - children = "+", - ...props -}: KbdSeparatorProps) => ( - - {children} - -); diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx deleted file mode 100644 index d683666e..00000000 --- a/src/components/ui/progress.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import * as ProgressPrimitive from "@radix-ui/react-progress"; - -import { cn } from "@/lib/utils"; - -function Progress({ - className, - value, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -export { Progress }; diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx deleted file mode 100644 index 6287ded1..00000000 --- a/src/components/ui/resizable.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from "react"; -import { GripVerticalIcon } from "lucide-react"; -import { - Group as ResizableGroup, - Panel as ResizablePrimitivePanel, - Separator as ResizableSeparator, -} from "react-resizable-panels"; - -import { cn } from "@/lib/utils"; - -function ResizablePanelGroup({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function ResizablePanel({ - ...props -}: React.ComponentProps) { - return ; -} - -function ResizableHandle({ - withHandle, - className, - ...props -}: React.ComponentProps & { - withHandle?: boolean; -}) { - return ( - div]:rotate-90 bg-edge-hover hover:bg-interact active:bg-pressed transition-colors duration-150", - className, - )} - {...props} - > - {withHandle && ( -
- -
- )} -
- ); -} - -export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx deleted file mode 100644 index 4c7e3c57..00000000 --- a/src/components/ui/sonner.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useTheme } from "@/components/theme-provider"; -import { Toaster as Sonner, type ToasterProps, toast } from "sonner"; -import { useRef } from "react"; - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); - const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null); - - const originalToast = toast; - - const rateLimitedToast = ( - message: string, - options?: Record, - ) => { - const now = Date.now(); - const lastToast = lastToastRef.current; - - if ( - lastToast && - lastToast.text === message && - now - lastToast.timestamp < 1000 - ) { - return; - } - - lastToastRef.current = { text: message, timestamp: now }; - return originalToast(message, options); - }; - - Object.assign(toast, { - success: (message: string, options?: Record) => - rateLimitedToast(message, { ...options, type: "success" }), - error: (message: string, options?: Record) => - rateLimitedToast(message, { ...options, type: "error" }), - warning: (message: string, options?: Record) => - rateLimitedToast(message, { ...options, type: "warning" }), - info: (message: string, options?: Record) => - rateLimitedToast(message, { ...options, type: "info" }), - message: rateLimitedToast, - }); - - const darkCustomThemes = [ - "dracula", - "gentlemansChoice", - "midnightEspresso", - "catppuccinMocha", - ]; - const sonnerTheme: ToasterProps["theme"] = darkCustomThemes.includes(theme) - ? "dark" - : (theme as ToasterProps["theme"]); - - return ( - - ); -}; - -export { Toaster }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx deleted file mode 100644 index 2ddbbdd7..00000000 --- a/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "@/lib/utils"; - -function TooltipProvider({ - delayDuration = 0, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function Tooltip({ - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function TooltipContent({ - className, - sideOffset = 4, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - ); -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 2e5cd2d2..00000000 --- a/src/index.css +++ /dev/null @@ -1,559 +0,0 @@ -@import "tailwindcss"; - -@custom-variant dark (&:is(.dark *)); - -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: var(--foreground); - background-color: var(--bg-base); - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - --radius: 0.625rem; - --background: #ffffff; - --foreground: #18181b; - --card: #ffffff; - --card-foreground: #18181b; - --popover: #ffffff; - --popover-foreground: #18181b; - --primary: #27272a; - --primary-foreground: #fafafa; - --secondary: #f4f4f5; - --secondary-foreground: #27272a; - --muted: #f4f4f5; - --muted-foreground: #71717a; - --accent: #f4f4f5; - --accent-foreground: #27272a; - --destructive: #dc2626; - --border: #e4e4e7; - --input: #e4e4e7; - --ring: #a1a1aa; - --chart-1: #e76e50; - --chart-2: #2a9d8f; - --chart-3: #264653; - --chart-4: #e9c46a; - --chart-5: #f4a261; - --sidebar: #f9f9f9; - --sidebar-foreground: #18181b; - --sidebar-primary: #27272a; - --sidebar-primary-foreground: #fafafa; - --sidebar-accent: #f4f4f5; - --sidebar-accent-foreground: #27272a; - --sidebar-border: #e4e4e7; - --sidebar-ring: #a1a1aa; - - --bg-base: #fcfcfc; - --bg-elevated: #ffffff; - --bg-surface: #f3f4f6; - --bg-surface-hover: #e5e7eb; - --bg-input: #ffffff; - --bg-deepest: #e5e7eb; - --bg-header: #eeeeef; - --bg-button: #f3f4f6; - --bg-active: #e5e7eb; - --bg-light: #fafafa; - --bg-subtle: #f5f5f5; - --bg-interact: #d1d5db; - --border-base: #e5e7eb; - --border-panel: #d1d5db; - --border-subtle: #f3f4f6; - --border-medium: #d1d5db; - --bg-hover: #f3f4f6; - --bg-hover-alt: #e5e7eb; - --bg-pressed: #d1d5db; - --border-hover: #d1d5db; - --border-active: #9ca3af; - - --foreground-secondary: #334155; - --foreground-subtle: #94a3b8; - - --scrollbar-thumb: #c1c1c3; - --scrollbar-thumb-hover: #a1a1a3; - --scrollbar-track: #f3f4f6; - - --bg-overlay: rgba(0, 0, 0, 0.5); -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - - --color-dark-bg: #18181b; - --color-dark-bg-darker: #0e0e10; - --color-dark-bg-darkest: #09090b; - --color-dark-bg-input: #222225; - --color-dark-bg-button: #23232a; - --color-dark-bg-active: #1d1d1f; - --color-dark-bg-header: #131316; - --color-dark-border: #303032; - --color-dark-border-active: #2d2d30; - --color-dark-border-hover: #434345; - --color-dark-hover: #2d2d30; - --color-dark-active: #2a2a2c; - --color-dark-pressed: #1a1a1c; - --color-dark-hover-alt: #2a2a2d; - --color-dark-border-light: #5a5a5d; - --color-dark-bg-light: #141416; - --color-dark-border-medium: #373739; - --color-dark-bg-very-light: #101014; - --color-dark-bg-panel: #1b1b1e; - --color-dark-border-panel: #222224; - --color-dark-bg-panel-hover: #232327; - - --color-canvas: var(--bg-base); - --color-elevated: var(--bg-elevated); - --color-surface: var(--bg-surface); - --color-surface-hover: var(--bg-surface-hover); - --color-field: var(--bg-input); - --color-deepest: var(--bg-deepest); - --color-header: var(--bg-header); - --color-button: var(--bg-button); - --color-active: var(--bg-active); - --color-light: var(--bg-light); - --color-subtle: var(--bg-subtle); - --color-interact: var(--bg-interact); - --color-hover: var(--bg-hover); - --color-hover-alt: var(--bg-hover-alt); - --color-pressed: var(--bg-pressed); - - --color-edge: var(--border-base); - --color-edge-panel: var(--border-panel); - --color-edge-subtle: var(--border-subtle); - --color-edge-medium: var(--border-medium); - --color-edge-hover: var(--border-hover); - --color-edge-active: var(--border-active); - - --color-foreground-secondary: var(--foreground-secondary); - --color-foreground-subtle: var(--foreground-subtle); - - --color-overlay: var(--bg-overlay); -} - -.dark { - --background: #09090b; - --foreground: #fafafa; - --card: #18181b; - --card-foreground: #fafafa; - --popover: #27272a; - --popover-foreground: #fafafa; - --primary: #e4e4e7; - --primary-foreground: #27272a; - --secondary: #3f3f46; - --secondary-foreground: #fafafa; - --muted: #27272a; - --muted-foreground: #9ca3af; - --accent: #3f3f46; - --accent-foreground: #fafafa; - --destructive: #f87171; - --border: #ffffff1a; - --input: #ffffff26; - --ring: #71717a; - --chart-1: #3b82f6; - --chart-2: #34d399; - --chart-3: #f4a261; - --chart-4: #a855f7; - --chart-5: #f43f5e; - --sidebar: #18181b; - --sidebar-foreground: #fafafa; - --sidebar-primary: #3b82f6; - --sidebar-primary-foreground: #fafafa; - --sidebar-accent: #3f3f46; - --sidebar-accent-foreground: #fafafa; - --sidebar-border: #ffffff1a; - --sidebar-ring: #71717a; - - --bg-base: #18181b; - --bg-elevated: #0e0e10; - --bg-surface: #1b1b1e; - --bg-surface-hover: #232327; - --bg-input: #222225; - --bg-deepest: #09090b; - --bg-header: #131316; - --bg-button: #23232a; - --bg-active: #1d1d1f; - --bg-light: #141416; - --bg-subtle: #101014; - --bg-interact: #2a2a2c; - --border-base: #303032; - --border-panel: #222224; - --border-subtle: #5a5a5d; - --border-medium: #373739; - --bg-hover: #2d2d30; - --bg-hover-alt: #2a2a2d; - --bg-pressed: #1a1a1c; - --border-hover: #434345; - --border-active: #2d2d30; - - --foreground-secondary: #d1d5db; - --foreground-subtle: #6b7280; - - --scrollbar-thumb: #434345; - --scrollbar-thumb-hover: #5a5a5d; - --scrollbar-track: #18181b; - - --bg-overlay: rgba(0, 0, 0, 0.7); -} - -.dracula { - --background: #282a36; - --foreground: #ffffff; - --card: #343746; - --card-foreground: #ffffff; - --popover: #343746; - --popover-foreground: #ffffff; - --primary: #ffffff; - --primary-foreground: #282a36; - --secondary: #44475a; - --secondary-foreground: #ffffff; - --muted: #343746; - --muted-foreground: #a6accd; - --accent: #44475a; - --accent-foreground: #ffffff; - --destructive: #ff5555; - --border: #44475a; - --input: #343746; - --ring: #8be9fd; - --chart-1: #8be9fd; - --chart-2: #50fa7b; - --chart-3: #ffb86c; - --chart-4: #bd93f9; - --chart-5: #ff79c6; - --sidebar: #1e1f29; - --sidebar-foreground: #ffffff; - --sidebar-primary: #8be9fd; - --sidebar-primary-foreground: #1e1f29; - --sidebar-accent: #44475a; - --sidebar-accent-foreground: #ffffff; - --sidebar-border: #44475a; - --sidebar-ring: #8be9fd; - - --bg-base: #282a36; - --bg-elevated: #343746; - --bg-surface: #343746; - --bg-surface-hover: #44475a; - --bg-input: #343746; - --bg-deepest: #1e1f29; - --bg-header: #1e1f29; - --bg-button: #343746; - --bg-active: #44475a; - --bg-light: #343746; - --bg-subtle: #1e1f29; - --bg-interact: #44475a; - --border-base: #44475a; - --border-panel: #1e1f29; - --border-subtle: #44475a; - --border-medium: #44475a; - --bg-hover: #44475a; - --bg-hover-alt: #44475a; - --bg-pressed: #1e1f29; - --border-hover: #6272a4; - --border-active: #8be9fd; - - --foreground-secondary: #a6accd; - --foreground-subtle: #6272a4; - - --scrollbar-thumb: #44475a; - --scrollbar-thumb-hover: #6272a4; - --scrollbar-track: #282a36; - - --bg-overlay: rgba(0, 0, 0, 0.8); -} - -.gentlemansChoice { - --background: #1a1c1a; - --foreground: #d1c7a3; - --card: #2a2c2a; - --card-foreground: #d1c7a3; - --popover: #2a2c2a; - --popover-foreground: #d1c7a3; - --primary: #d1c7a3; - --primary-foreground: #1a1c1a; - --secondary: #3e4437; - --secondary-foreground: #d1c7a3; - --muted: #2a2c2a; - --muted-foreground: #8e8463; - --accent: #3e4437; - --accent-foreground: #d1c7a3; - --destructive: #9d3a3a; - --border: #3e4437; - --input: #2a2c2a; - --ring: #5a7a3a; - --chart-1: #5a7a3a; - --chart-2: #3a5a7a; - --chart-3: #b39a3a; - --chart-4: #7a3a5a; - --chart-5: #3a7a7a; - --sidebar: #151715; - --sidebar-foreground: #d1c7a3; - --sidebar-primary: #5a7a3a; - --sidebar-primary-foreground: #151715; - --sidebar-accent: #3e4437; - --sidebar-accent-foreground: #d1c7a3; - --sidebar-border: #3e4437; - --sidebar-ring: #5a7a3a; - - --bg-base: #1a1c1a; - --bg-elevated: #2a2c2a; - --bg-surface: #2a2c2a; - --bg-surface-hover: #3e4437; - --bg-input: #2a2c2a; - --bg-deepest: #151715; - --bg-header: #151715; - --bg-button: #2a2c2a; - --bg-active: #3e4437; - --bg-light: #2a2c2a; - --bg-subtle: #151715; - --bg-interact: #3e4437; - --border-base: #3e4437; - --border-panel: #151715; - --border-subtle: #3e4437; - --border-medium: #3e4437; - --bg-hover: #3e4437; - --bg-hover-alt: #3e4437; - --bg-pressed: #151715; - --border-hover: #5a7a3a; - --border-active: #d1c7a3; - - --foreground-secondary: #8e8463; - --foreground-subtle: #5e5841; - - --scrollbar-thumb: #3e4437; - --scrollbar-thumb-hover: #5e5841; - --scrollbar-track: #1a1c1a; - - --bg-overlay: rgba(0, 0, 0, 0.8); -} - -.midnightEspresso { - --background: #120f0d; - --foreground: #ceb195; - --card: #1f1a17; - --card-foreground: #ceb195; - --popover: #1f1a17; - --popover-foreground: #ceb195; - --primary: #ceb195; - --primary-foreground: #120f0d; - --secondary: #3d2b1f; - --secondary-foreground: #ceb195; - --muted: #1f1a17; - --muted-foreground: #9a8a7a; - --accent: #3d2b1f; - --accent-foreground: #ceb195; - --destructive: #a05a4a; - --border: #3d2b1f; - --input: #1f1a17; - --ring: #7a8a5a; - --chart-1: #7a8a5a; - --chart-2: #5a7a9a; - --chart-3: #b08a4a; - --chart-4: #8a5a7a; - --chart-5: #5a8a8a; - --sidebar: #0d0b0a; - --sidebar-foreground: #ceb195; - --sidebar-primary: #7a8a5a; - --sidebar-primary-foreground: #0d0b0a; - --sidebar-accent: #3d2b1f; - --sidebar-accent-foreground: #ceb195; - --sidebar-border: #3d2b1f; - --sidebar-ring: #7a8a5a; - - --bg-base: #120f0d; - --bg-elevated: #1f1a17; - --bg-surface: #1f1a17; - --bg-surface-hover: #3d2b1f; - --bg-input: #1f1a17; - --bg-deepest: #0d0b0a; - --bg-header: #0d0b0a; - --bg-button: #1f1a17; - --bg-active: #3d2b1f; - --bg-light: #1f1a17; - --bg-subtle: #0d0b0a; - --bg-interact: #3d2b1f; - --border-base: #3d2b1f; - --border-panel: #0d0b0a; - --border-subtle: #3d2b1f; - --border-medium: #3d2b1f; - --bg-hover: #3d2b1f; - --bg-hover-alt: #3d2b1f; - --bg-pressed: #0d0b0a; - --border-hover: #7a8a5a; - --border-active: #ceb195; - - --foreground-secondary: #9a8a7a; - --foreground-subtle: #6a5a4a; - - --scrollbar-thumb: #3d2b1f; - --scrollbar-thumb-hover: #6a5a4a; - --scrollbar-track: #120f0d; - - --bg-overlay: rgba(0, 0, 0, 0.8); -} - -.catppuccinMocha { - --background: #1e1e2e; - --foreground: #cdd6f4; - --card: #181825; - --card-foreground: #cdd6f4; - --popover: #181825; - --popover-foreground: #cdd6f4; - --primary: #cdd6f4; - --primary-foreground: #1e1e2e; - --secondary: #313244; - --secondary-foreground: #cdd6f4; - --muted: #181825; - --muted-foreground: #a6adc8; - --accent: #313244; - --accent-foreground: #cdd6f4; - --destructive: #f38ba8; - --border: #313244; - --input: #181825; - --ring: #89b4fa; - --chart-1: #89b4fa; - --chart-2: #a6e3a1; - --chart-3: #f9e2af; - --chart-4: #f5c2e7; - --chart-5: #94e2d5; - --sidebar: #11111b; - --sidebar-foreground: #cdd6f4; - --sidebar-primary: #89b4fa; - --sidebar-primary-foreground: #11111b; - --sidebar-accent: #313244; - --sidebar-accent-foreground: #cdd6f4; - --sidebar-border: #313244; - --sidebar-ring: #89b4fa; - - --bg-base: #1e1e2e; - --bg-elevated: #181825; - --bg-surface: #181825; - --bg-surface-hover: #313244; - --bg-input: #181825; - --bg-deepest: #11111b; - --bg-header: #11111b; - --bg-button: #181825; - --bg-active: #313244; - --bg-light: #181825; - --bg-subtle: #11111b; - --bg-interact: #313244; - --border-base: #313244; - --border-panel: #11111b; - --border-subtle: #313244; - --border-medium: #313244; - --bg-hover: #313244; - --bg-hover-alt: #313244; - --bg-pressed: #11111b; - --border-hover: #89b4fa; - --border-active: #cdd6f4; - - --foreground-secondary: #a6adc8; - --foreground-subtle: #7f849c; - - --scrollbar-thumb: #313244; - --scrollbar-thumb-hover: #45475a; - --scrollbar-track: #1e1e2e; - - --bg-overlay: rgba(0, 0, 0, 0.8); -} - -@layer base { - html, - body { - height: 100%; - } - - * { - @apply border-border outline-ring/50; - } - - body { - @apply bg-background text-foreground; - } -} - -.thin-scrollbar { - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); -} - -.thin-scrollbar::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -.thin-scrollbar::-webkit-scrollbar-track { - background: var(--scrollbar-track); -} - -.thin-scrollbar::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb); - border-radius: 3px; -} - -.thin-scrollbar::-webkit-scrollbar-thumb:hover { - background: var(--scrollbar-thumb-hover); -} - -.skinny-scrollbar { - scrollbar-width: thin; - scrollbar-color: var(--scrollbar-thumb) transparent; -} - -.skinny-scrollbar::-webkit-scrollbar { - width: 4px; - height: 4px; -} - -.skinny-scrollbar::-webkit-scrollbar-track { - background: transparent; -} - -.skinny-scrollbar::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb); - border-radius: 2px; -} - -.skinny-scrollbar::-webkit-scrollbar-thumb:hover { - background: var(--scrollbar-thumb-hover); -} - -.h-fade { - mask-image: linear-gradient(transparent, #000 6%, #000 94%, transparent); -} diff --git a/src/main.tsx b/src/main.tsx index 4a30cbcd..ff34a7ee 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,53 +1,88 @@ /* eslint-disable react-refresh/only-export-components */ import { prepareClientCacheVersion } from "@/lib/client-cache-version"; -import { StrictMode, Suspense, lazy, useEffect, useState, useRef } from "react"; +import { StrictMode, Suspense, lazy, useState, useRef, useEffect } from "react"; import { createRoot } from "react-dom/client"; -import "./index.css"; +import "./ui/index.css"; import { ThemeProvider } from "@/components/theme-provider"; -import "./i18n/i18n"; +import "./ui/i18n/i18n"; import { isElectron } from "@/lib/electron"; +import { Toaster } from "@/components/sonner"; +import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth"; +import { applyAccentColor, applyFontSize } from "@/lib/theme"; +import type { FontSizeId } from "@/types/ui-types"; +import { useServiceWorker } from "@/hooks/use-service-worker"; -const DesktopApp = lazy(() => import("@/ui/desktop/DesktopApp.tsx")); -const MobileApp = lazy(() => - import("@/ui/mobile/MobileApp.tsx").then((module) => ({ - default: module.MobileApp, - })), -); -const HostManagerApp = lazy( - () => import("./ui/desktop/apps/host-manager/HostManagerApp.tsx"), -); -const TerminalApp = lazy( - () => import("./ui/desktop/apps/features/terminal/TerminalApp.tsx"), +const AppShell = lazy(() => + import("@/AppShell").then((m) => ({ default: m.AppShell })), ); + +// Full-screen apps opened via query params (e.g. from external links or Electron) +const TerminalApp = lazy(() => import("@/features/terminal/TerminalApp")); const FileManagerApp = lazy( - () => import("./ui/desktop/apps/features/file-manager/FileManagerApp.tsx"), -); -const TunnelApp = lazy( - () => import("./ui/desktop/apps/features/tunnel/TunnelApp.tsx"), + () => import("@/features/file-manager/FileManagerApp"), ); +const TunnelApp = lazy(() => import("@/features/tunnel/TunnelApp")); const ServerStatsApp = lazy( - () => import("./ui/desktop/apps/features/server-stats/ServerStatsApp.tsx"), -); -const DockerApp = lazy( - () => import("./ui/desktop/apps/features/docker/DockerApp.tsx"), -); -const GuacamoleApp = lazy( - () => import("@/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx"), + () => import("@/features/server-stats/ServerStatsApp"), ); +const DockerApp = lazy(() => import("@/features/docker/DockerApp")); +const GuacamoleApp = lazy(() => import("@/features/guacamole/GuacamoleApp")); + const ElectronVersionCheck = lazy(() => - import("@/ui/desktop/user/ElectronVersionCheck.tsx").then((module) => ({ + import("@/user/ElectronVersionCheck").then((module) => ({ default: module.ElectronVersionCheck, })), ); -const FullscreenApp: React.FC = () => { +type Phase = "idle-auth" | "fading-in" | "idle-app" | "fading-out"; + +function useWindowWidth() { + const [width, setWidth] = useState(window.innerWidth); + const lastSwitchTime = useRef(0); + const isCurrentlyMobile = useRef(window.innerWidth < 768); + const hasSwitchedOnce = useRef(false); + + useEffect(() => { + let timeoutId: number; + const handleResize = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + const newWidth = window.innerWidth; + const newIsMobile = newWidth < 768; + const now = Date.now(); + if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) { + setWidth(newWidth); + return; + } + if ( + newIsMobile !== isCurrentlyMobile.current && + now - lastSwitchTime.current > 5000 + ) { + lastSwitchTime.current = now; + isCurrentlyMobile.current = newIsMobile; + hasSwitchedOnce.current = true; + setWidth(newWidth); + } else { + setWidth(newWidth); + } + }, 2000); + }; + window.addEventListener("resize", handleResize); + return () => { + clearTimeout(timeoutId); + window.removeEventListener("resize", handleResize); + }; + }, []); + + return width; +} + +function FullscreenApp() { const searchParams = new URLSearchParams(window.location.search); const view = searchParams.get("view"); const hostId = searchParams.get("hostId"); switch (view) { - case "host-manager": - return ; case "terminal": return ; case "file-manager": @@ -63,53 +98,83 @@ const FullscreenApp: React.FC = () => { case "telnet": return ; default: - return ; + return null; } -}; -import { useServiceWorker } from "@/hooks/use-service-worker"; +} -function useWindowWidth() { - const [width, setWidth] = useState(window.innerWidth); - const lastSwitchTime = useRef(0); - const isCurrentlyMobile = useRef(window.innerWidth < 768); - const hasSwitchedOnce = useRef(false); +function App() { + const stored = getStoredAuth(); + const [phase, setPhase] = useState( + stored?.loggedIn ? "idle-app" : "idle-auth", + ); + const [authUsername, setAuthUsername] = useState(stored?.username ?? ""); + const timerRef = useRef | null>(null); useEffect(() => { - let timeoutId: NodeJS.Timeout; - const handleResize = () => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - const newWidth = window.innerWidth; - const newIsMobile = newWidth < 768; - const now = Date.now(); - - if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) { - setWidth(newWidth); - return; - } - - if ( - newIsMobile !== isCurrentlyMobile.current && - now - lastSwitchTime.current > 5000 - ) { - lastSwitchTime.current = now; - isCurrentlyMobile.current = newIsMobile; - hasSwitchedOnce.current = true; - setWidth(newWidth); - } else { - setWidth(newWidth); - } - }, 2000); - }; - window.addEventListener("resize", handleResize); - + const savedAccent = localStorage.getItem("termix-accent"); + if (savedAccent) applyAccentColor(savedAccent); + const savedSize = localStorage.getItem( + "termix-font-size", + ) as FontSizeId | null; + applyFontSize(savedSize ?? "lg"); return () => { - clearTimeout(timeoutId); - window.removeEventListener("resize", handleResize); + if (timerRef.current) clearTimeout(timerRef.current); }; }, []); - return width; + function handleLogin(u: string) { + setAuthUsername(u); + setPhase("fading-in"); + timerRef.current = setTimeout(() => setPhase("idle-app"), 450); + } + + function handleLogout() { + clearStoredAuth(); + setPhase("fading-out"); + timerRef.current = setTimeout(() => { + setAuthUsername(""); + setPhase("idle-auth"); + }, 450); + } + + const showApp = + phase === "idle-app" || phase === "fading-in" || phase === "fading-out"; + const showAuth = + phase === "idle-auth" || phase === "fading-in" || phase === "fading-out"; + const appOpacity = phase === "idle-app" ? 1 : 0; + const authOpacity = phase === "idle-auth" ? 1 : 0; + + return ( + <> + {showApp && ( +
+ + + +
+ )} + + {showAuth && ( +
+ +
+ )} + + + + ); } function RootApp() { @@ -125,61 +190,26 @@ function RootApp() { (window as Window & { opera?: string }).opera || ""; const isTermixMobile = /Termix-Mobile/.test(userAgent); - const searchParams = new URLSearchParams(window.location.search); const isFullscreen = searchParams.has("view"); - const renderApp = () => { - if (isFullscreen) { - return ; - } + if (isFullscreen) { + return ( + + + + ); + } - if (isElectron()) { - return ; - } + if (isElectron() && showVersionCheck) { + return ( + + setShowVersionCheck(false)} /> + + ); + } - if (isTermixMobile) { - return ; - } - - return isMobile ? : ; - }; - - return ( - <> - {!isFullscreen && ( -
- )} -
- {isElectron() && showVersionCheck && !isFullscreen ? ( - - setShowVersionCheck(false)} - isAuthenticated={false} - /> - - ) : ( - {renderApp()} - )} -
- - ); + return ; } prepareClientCacheVersion().finally(() => { diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx new file mode 100644 index 00000000..6d620eef --- /dev/null +++ b/src/ui/AppShell.tsx @@ -0,0 +1,608 @@ +import { Separator } from "@/components/separator"; +import { Button } from "@/components/button"; +import { Sheet, SheetContent } from "@/components/sheet"; +import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react"; +import { useState, useRef, useCallback, useEffect } from "react"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { MobileBottomBar } from "@/shell/MobileBottomBar"; +import { CommandPalette } from "@/shell/CommandPalette"; +import { AppRail } from "@/sidebar/AppRail"; +import type { RailView } from "@/sidebar/AppRail"; +import { HostsPanel } from "@/sidebar/HostsPanel"; +import { QuickConnectPanel } from "@/sidebar/QuickConnectPanel"; +import { SshToolsPanel } from "@/sidebar/SshToolsPanel"; +import { SnippetsPanel } from "@/sidebar/SnippetsPanel"; +import { HistoryPanel } from "@/sidebar/HistoryPanel"; +import { SplitScreenPanel } from "@/sidebar/SplitScreenPanel"; +import { UserProfilePanel } from "@/sidebar/UserProfilePanel"; +import { AdminSettingsPanel } from "@/sidebar/AdminSettingsPanel"; +import { SplitView } from "@/shell/SplitView"; +import { TabBar } from "@/shell/TabBar"; +import type { + Tab, + TabType, + Host, + SplitMode, + HostFolder, +} from "@/types/ui-types"; +import { getSSHHosts } from "@/main-axios"; +import type { SSHHostWithStatus } from "@/main-axios"; + +function sshHostToHost(h: SSHHostWithStatus): Host { + return { + id: String(h.id), + name: h.name, + username: h.username, + ip: h.ip, + port: h.port, + folder: h.folder ?? "", + online: h.status === "online", + cpu: 0, + ram: 0, + lastAccess: "", + tags: h.tags ?? [], + authType: h.authType, + password: h.password, + key: typeof h.key === "string" ? h.key : undefined, + keyPassword: h.keyPassword, + keyType: h.keyType, + credentialId: h.credentialId != null ? String(h.credentialId) : undefined, + notes: h.notes, + pin: h.pin ?? false, + macAddress: h.macAddress, + enableTerminal: h.enableTerminal ?? true, + enableTunnel: h.enableTunnel ?? false, + enableFileManager: h.enableFileManager ?? false, + enableDocker: h.enableDocker ?? false, + enableSsh: h.connectionType === "ssh" || !h.connectionType, + enableRdp: h.connectionType === "rdp", + enableVnc: h.connectionType === "vnc", + enableTelnet: h.connectionType === "telnet", + sshPort: h.port, + rdpPort: 3389, + vncPort: 5900, + telnetPort: 23, + quickActions: (h.quickActions ?? []).map((a) => ({ + name: a.name, + snippetId: String(a.snippetId), + })), + serverTunnels: [], + defaultPath: h.defaultPath, + terminalConfig: h.terminalConfig as Host["terminalConfig"], + useSocks5: h.useSocks5, + socks5Host: h.socks5Host, + socks5Port: h.socks5Port, + socks5Username: h.socks5Username, + socks5Password: h.socks5Password, + }; +} + +function buildHostTree(hosts: SSHHostWithStatus[]): HostFolder { + const root: HostFolder = { name: "root", children: [] }; + const folderMap = new Map(); + const getOrCreateFolder = (path: string): HostFolder => { + if (folderMap.has(path)) return folderMap.get(path)!; + const parts = path.split(" / "); + let current = root; + let accumulated = ""; + for (const part of parts) { + accumulated = accumulated ? `${accumulated} / ${part}` : part; + if (!folderMap.has(accumulated)) { + const folder: HostFolder = { name: part, children: [] }; + folderMap.set(accumulated, folder); + current.children.push(folder); + } + current = folderMap.get(accumulated)!; + } + return current; + }; + for (const h of hosts) { + const host = sshHostToHost(h); + if (h.folder) { + getOrCreateFolder(h.folder).children.push(host); + } else { + root.children.push(host); + } + } + return root; +} +export { tabIcon, renderTabContent } from "@/shell/tabUtils"; +import { renderTabContent } from "@/shell/tabUtils"; + +const DASHBOARD_TAB: Tab = { + id: "dashboard", + type: "dashboard", + label: "Dashboard", +}; + +const SINGLETON_TAB_LABELS: Partial> = { + "host-manager": "Host Manager", + docker: "Docker", + tunnel: "Tunnels", +}; + +// ─── AppShell ──────────────────────────────────────────────────────────────── + +export function AppShell({ + username, + onLogout, +}: { + username: string; + onLogout: () => void; +}) { + const [tabs, setTabs] = useState([DASHBOARD_TAB]); + const [activeTabId, setActiveTabId] = useState("dashboard"); + const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); + const [splitMode, setSplitMode] = useState("none"); + const [paneTabIds, setPaneTabIds] = useState<(string | null)[]>( + Array(6).fill(null), + ); + const [realHostTree, setRealHostTree] = useState(null); + const [allHosts, setAllHosts] = useState([]); + + const [sidebarOpen, setSidebarOpen] = useState(true); + const [railView, setRailView] = useState("hosts"); + const [profileDropdownOpen, setProfileDropdownOpen] = useState(false); + const [hostManagerExpanded, setHostManagerExpanded] = useState(false); + const [sidebarWidth, setSidebarWidth] = useState(256); + const [sidebarDragging, setSidebarDragging] = useState(false); + + const isMobile = useIsMobile(); + + // Close the sidebar when switching to mobile (it becomes a sheet overlay) + useEffect(() => { + if (isMobile) setSidebarOpen(false); + }, [isMobile]); + + const pendingHostManagerEditId = useRef(null); + const pendingHostManagerAction = useRef<"add-host" | "add-credential" | null>( + null, + ); + const lastShiftTime = useRef(0); + + const sidebarTitle: Record = { + hosts: "Hosts", + "quick-connect": "Quick Connect", + "ssh-tools": "SSH Tools", + snippets: "Snippets", + history: "History", + "split-screen": "Split Screen", + "user-profile": "User Profile", + "admin-settings": "Admin Settings", + }; + + // Double-shift opens command palette + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "ShiftLeft") { + const now = Date.now(); + if (now - lastShiftTime.current < 300) + setCommandPaletteOpen((prev) => !prev); + lastShiftTime.current = now; + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + useEffect(() => { + const handle = () => onLogout(); + window.addEventListener("termix:logout", handle); + return () => window.removeEventListener("termix:logout", handle); + }, [onLogout]); + + // Load real hosts from API + const loadHosts = useCallback(async () => { + try { + const raw = await getSSHHosts(); + const converted = raw.map(sshHostToHost); + setAllHosts(converted); + setRealHostTree(buildHostTree(raw)); + } catch { + // Keep empty state on error + } + }, []); + + useEffect(() => { + loadHosts(); + }, [loadHosts]); + + // Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings) + useEffect(() => { + if (allHosts.length === 0) return; + setTabs((prev) => + prev.map((t) => + t.host + ? { ...t, host: allHosts.find((h) => h.id === t.host!.id) ?? t.host } + : t, + ), + ); + }, [allHosts]); + + // Let HostManager trigger tab opens via custom event + useEffect(() => { + const handle = (e: Event) => { + const { hostId, type } = ( + e as CustomEvent<{ hostId: string; type?: TabType }> + ).detail; + const host = allHosts.find((h) => h.id === hostId); + if (host) connectHost(host, type); + }; + window.addEventListener("termix:open-tab", handle); + return () => window.removeEventListener("termix:open-tab", handle); + }, [tabs, allHosts]); + + // ─── Tab management ────────────────────────────────────────────────────── + + function openTab(host: Host, type: TabType) { + setTabs((prev) => { + const same = prev.filter( + (t) => + t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name, + ); + if (same.length === 0) { + const tab = { + id: `${host.name}-${type}`, + type, + label: host.name, + host, + }; + setActiveTabId(tab.id); + return [...prev, tab]; + } + const next = prev.map((t) => + t.id === same[0].id && !/\(\d+\)$/.test(t.label) + ? { ...t, label: `${host.name} (1)`, host } + : t, + ); + const tab = { + id: `${host.name}-${type}-${Date.now()}`, + type, + label: `${host.name} (${same.length + 1})`, + host, + }; + setActiveTabId(tab.id); + return [...next, tab]; + }); + } + + function connectHost(host: Host, preferredType?: TabType) { + const type: TabType = + preferredType ?? + (host.enableSsh + ? "terminal" + : host.enableRdp + ? "rdp" + : host.enableVnc + ? "vnc" + : host.enableTelnet + ? "telnet" + : "terminal"); + openTab(host, type); + } + + function openSingletonTab(type: TabType, pendingEvent?: string) { + if (type === "host-manager") { + if (pendingEvent === "host-manager:add-host") + pendingHostManagerAction.current = "add-host"; + else if (pendingEvent === "host-manager:add-credential") + pendingHostManagerAction.current = "add-credential"; + + setHostManagerExpanded(true); + setSidebarOpen(true); + setRailView("hosts"); + + if (pendingEvent) { + // Use a small delay to ensure HostManager is mounted if it wasn't already, + // and to allow the current render cycle to complete. + setTimeout(() => { + window.dispatchEvent(new CustomEvent(pendingEvent)); + }, 0); + } + return; + } + if (type === "user-profile" || type === "admin-settings") { + handleRailClick(type as RailView); + return; + } + const id = type; + setTabs((prev) => { + if (prev.find((t) => t.id === id)) return prev; + return [...prev, { id, type, label: SINGLETON_TAB_LABELS[type] ?? type }]; + }); + setActiveTabId(id); + } + + function closeTab(id: string) { + setTabs((prev) => { + const next = prev.filter((t) => t.id !== id); + if (id === activeTabId) setActiveTabId(next[next.length - 1].id); + return next; + }); + } + + // ─── Rail / sidebar ────────────────────────────────────────────────────── + + function handleRailClick(view: RailView) { + if (railView === view && sidebarOpen) { + setSidebarOpen(false); + } else { + setRailView(view); + setSidebarOpen(true); + } + } + + function editHostInManager(host: Host) { + pendingHostManagerEditId.current = host.id; + setHostManagerExpanded(true); + setSidebarOpen(true); + setRailView("hosts"); + } + + const onSidebarMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setSidebarDragging(true); + const startX = e.clientX; + const startW = sidebarWidth; + function onMove(ev: MouseEvent) { + setSidebarWidth( + Math.max(160, Math.min(480, startW + ev.clientX - startX)), + ); + } + function onUp() { + setSidebarDragging(false); + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + } + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, + [sidebarWidth], + ); + + const activeTab = tabs.find((t) => t.id === activeTabId)!; + const isSplit = splitMode !== "none"; + const terminalTabs = tabs.filter((t) => t.type === "terminal"); + + // Sidebar panel content — shared between desktop inline sidebar and mobile sheet + const sidebarPanelContent = ( +
+ {railView === "hosts" && ( + setHostManagerExpanded(true)} + onCollapse={() => { + setHostManagerExpanded(false); + loadHosts(); + }} + pendingEditId={pendingHostManagerEditId} + pendingAction={pendingHostManagerAction} + onOpenTab={(host, type) => { + connectHost(host, type); + if (isMobile) setSidebarOpen(false); + }} + onEditHost={editHostInManager} + hostTree={realHostTree ?? undefined} + /> + )} + + {railView === "quick-connect" && } + + {railView === "ssh-tools" && ( +
+ +
+ )} + + {railView === "snippets" && ( +
+ +
+ )} + + {railView === "history" && ( +
+ +
+ )} + + {railView === "split-screen" && ( +
+ +
+ )} + + {railView === "user-profile" && ( +
+ +
+ )} + + {railView === "admin-settings" && ( +
+ +
+ )} +
+ ); + + // Sidebar header — shared + const sidebarHeader = ( +
+ + {sidebarTitle[railView]} + + {!hostManagerExpanded && !isMobile && ( + <> + + + + )} + + +
+ ); + + return ( + <> +
+ {/* Skinny icon rail — desktop only, hidden on mobile */} + + + {/* Desktop: inline resizable sidebar */} + {!isMobile && ( +
+ {sidebarHeader} + {sidebarPanelContent} + + {sidebarOpen && !(hostManagerExpanded && railView === "hosts") && ( +
+ )} +
+ )} + + {/* Mobile: sidebar as overlay sheet */} + {isMobile && ( + { + setSidebarOpen(open); + if (!open) setHostManagerExpanded(false); + }} + > + + {sidebarHeader} + {sidebarPanelContent} + + + )} + + {/* Main content area */} +
+ {!isMobile && !sidebarOpen && ( + + )} +
+ +
+ {isSplit && !isMobile ? ( + + ) : activeTab ? ( + renderTabContent(activeTab, openSingletonTab, openTab) + ) : null} +
+
+ + {/* Bottom nav bar — mobile only */} + +
+
+ + { + if ( + [ + "dashboard", + "host-manager", + "user-profile", + "admin-settings", + "docker", + "tunnel", + ].includes(type) + ) { + openSingletonTab(type, pendingEvent); + } else if (label) { + const host = allHosts.find((h) => h.name === label); + if (host) openTab(host, type); + } + }} + /> + + ); +} diff --git a/src/ui/desktop/apps/admin/AdminSettings.tsx b/src/ui/admin/AdminSettings.tsx similarity index 94% rename from src/ui/desktop/apps/admin/AdminSettings.tsx rename to src/ui/admin/AdminSettings.tsx index 6ac257e7..d1b8a511 100644 --- a/src/ui/desktop/apps/admin/AdminSettings.tsx +++ b/src/ui/admin/AdminSettings.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { useSidebar } from "@/components/ui/sidebar.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; +import { useSidebar } from "@/components/sidebar.tsx"; +import { Separator } from "@/components/separator.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, -} from "@/components/ui/tabs.tsx"; +} from "@/components/tabs.tsx"; import { Shield, Users, Database, Clock, Key } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -21,15 +21,15 @@ import { isElectron, getSessions, unlinkOIDCFromPasswordAccount, -} from "@/ui/main-axios.ts"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; -import { RolesTab } from "@/ui/desktop/apps/admin/tabs/RolesTab.tsx"; -import { GeneralSettingsTab } from "@/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx"; -import { OIDCSettingsTab } from "@/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx"; -import { UserManagementTab } from "@/ui/desktop/apps/admin/tabs/UserManagementTab.tsx"; -import { SessionManagementTab } from "@/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx"; -import { DatabaseSecurityTab } from "@/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx"; -import { ApiKeysTab } from "@/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx"; +} from "@/main-axios.ts"; +import { SimpleLoader } from "@/lib/SimpleLoader.tsx"; +import { RolesTab } from "@/admin/tabs/RolesTab.tsx"; +import { GeneralSettingsTab } from "@/admin/tabs/GeneralSettingsTab.tsx"; +import { OIDCSettingsTab } from "@/admin/tabs/OIDCSettingsTab.tsx"; +import { UserManagementTab } from "@/admin/tabs/UserManagementTab.tsx"; +import { SessionManagementTab } from "@/admin/tabs/SessionManagementTab.tsx"; +import { DatabaseSecurityTab } from "@/admin/tabs/DatabaseSecurityTab.tsx"; +import { ApiKeysTab } from "@/admin/tabs/ApiKeysTab.tsx"; import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx"; import { UserEditDialog } from "./dialogs/UserEditDialog.tsx"; import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx"; diff --git a/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx b/src/ui/admin/dialogs/CreateUserDialog.tsx similarity index 92% rename from src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx rename to src/ui/admin/dialogs/CreateUserDialog.tsx index 47e5cc22..d713db7c 100644 --- a/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx +++ b/src/ui/admin/dialogs/CreateUserDialog.tsx @@ -6,16 +6,16 @@ import { DialogHeader, DialogTitle, DialogFooter, -} from "@/components/ui/dialog.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { PasswordInput } from "@/components/ui/password-input.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +} from "@/components/dialog.tsx"; +import { Button } from "@/components/button.tsx"; +import { Label } from "@/components/label.tsx"; +import { Input } from "@/components/input.tsx"; +import { PasswordInput } from "@/components/password-input.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; import { useTranslation } from "react-i18next"; import { UserPlus, AlertCircle } from "lucide-react"; import { toast } from "sonner"; -import { registerUser } from "@/ui/main-axios.ts"; +import { registerUser } from "@/main-axios.ts"; interface CreateUserDialogProps { open: boolean; diff --git a/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx b/src/ui/admin/dialogs/LinkAccountDialog.tsx similarity index 93% rename from src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx rename to src/ui/admin/dialogs/LinkAccountDialog.tsx index b99b0950..4bbfd967 100644 --- a/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx +++ b/src/ui/admin/dialogs/LinkAccountDialog.tsx @@ -6,15 +6,15 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +} from "@/components/dialog.tsx"; +import { Button } from "@/components/button.tsx"; +import { Label } from "@/components/label.tsx"; +import { Input } from "@/components/input.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; import { useTranslation } from "react-i18next"; import { Link2 } from "lucide-react"; import { toast } from "sonner"; -import { linkOIDCToPasswordAccount } from "@/ui/main-axios.ts"; +import { linkOIDCToPasswordAccount } from "@/main-axios.ts"; interface LinkAccountDialogProps { open: boolean; diff --git a/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx b/src/ui/admin/dialogs/UserEditDialog.tsx similarity index 97% rename from src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx rename to src/ui/admin/dialogs/UserEditDialog.tsx index b8b41589..0f81cc9d 100644 --- a/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx +++ b/src/ui/admin/dialogs/UserEditDialog.tsx @@ -5,13 +5,13 @@ import { DialogDescription, DialogHeader, DialogTitle, -} from "@/components/ui/dialog.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; -import { Switch } from "@/components/ui/switch.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +} from "@/components/dialog.tsx"; +import { Button } from "@/components/button.tsx"; +import { Label } from "@/components/label.tsx"; +import { Badge } from "@/components/badge.tsx"; +import { Switch } from "@/components/switch.tsx"; +import { Separator } from "@/components/separator.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; import { useTranslation } from "react-i18next"; import { UserCog, @@ -34,7 +34,7 @@ import { deleteUser, type UserRole, type Role, -} from "@/ui/main-axios.ts"; +} from "@/main-axios.ts"; interface User { id: string; diff --git a/src/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx b/src/ui/admin/tabs/ApiKeysTab.tsx similarity index 97% rename from src/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx rename to src/ui/admin/tabs/ApiKeysTab.tsx index 8e5a4a87..9528bb61 100644 --- a/src/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx +++ b/src/ui/admin/tabs/ApiKeysTab.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button } from "@/components/ui/button.tsx"; +import { Button } from "@/components/button.tsx"; import { Table, TableBody, @@ -7,7 +7,7 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table.tsx"; +} from "@/components/table.tsx"; import { Dialog, DialogContent, @@ -15,12 +15,12 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog.tsx"; +} from "@/components/dialog.tsx"; import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/ui/popover.tsx"; +} from "@/components/popover.tsx"; import { Command, CommandEmpty, @@ -28,11 +28,11 @@ import { CommandInput, CommandItem, CommandList, -} from "@/components/ui/command.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +} from "@/components/command.tsx"; +import { Input } from "@/components/input.tsx"; +import { Label } from "@/components/label.tsx"; +import { Badge } from "@/components/badge.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; import { Key, Plus, @@ -54,7 +54,7 @@ import { getUserList, type ApiKey, type CreatedApiKey, -} from "@/ui/main-axios.ts"; +} from "@/main-axios.ts"; interface UserOption { id: string; diff --git a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx b/src/ui/admin/tabs/DatabaseSecurityTab.tsx similarity index 98% rename from src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx rename to src/ui/admin/tabs/DatabaseSecurityTab.tsx index 18bfe9f3..916f6eb7 100644 --- a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx +++ b/src/ui/admin/tabs/DatabaseSecurityTab.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { Button } from "@/components/ui/button.tsx"; +import { Button } from "@/components/button.tsx"; import { Download, Upload } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { isElectron } from "@/ui/main-axios.ts"; +import { isElectron } from "@/main-axios.ts"; import { getBasePath } from "@/lib/base-path"; export function DatabaseSecurityTab(): React.ReactElement { diff --git a/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx b/src/ui/admin/tabs/GeneralSettingsTab.tsx similarity index 98% rename from src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx rename to src/ui/admin/tabs/GeneralSettingsTab.tsx index cf4dcaac..a1c9ddb0 100644 --- a/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx +++ b/src/ui/admin/tabs/GeneralSettingsTab.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { Input } from "@/components/ui/input.tsx"; +import { Checkbox } from "@/components/checkbox.tsx"; +import { Input } from "@/components/input.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select.tsx"; +} from "@/components/select.tsx"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; @@ -23,8 +23,8 @@ import { updateLogLevel, getSessionTimeout, updateSessionTimeout, -} from "@/ui/main-axios.ts"; -import { Button } from "@/components/ui/button.tsx"; +} from "@/main-axios.ts"; +import { Button } from "@/components/button.tsx"; interface GeneralSettingsTabProps { allowRegistration: boolean; diff --git a/src/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx b/src/ui/admin/tabs/OIDCSettingsTab.tsx similarity index 96% rename from src/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx rename to src/ui/admin/tabs/OIDCSettingsTab.tsx index 5c27a2c4..71318212 100644 --- a/src/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx +++ b/src/ui/admin/tabs/OIDCSettingsTab.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { PasswordInput } from "@/components/ui/password-input.tsx"; -import { Label } from "@/components/ui/label.tsx"; +import { Button } from "@/components/button.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; +import { Input } from "@/components/input.tsx"; +import { PasswordInput } from "@/components/password-input.tsx"; +import { Label } from "@/components/label.tsx"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { Textarea } from "@/components/ui/textarea.tsx"; -import { updateOIDCConfig, disableOIDCConfig } from "@/ui/main-axios.ts"; +import { Textarea } from "@/components/textarea.tsx"; +import { updateOIDCConfig, disableOIDCConfig } from "@/main-axios.ts"; interface OIDCSettingsTabProps { allowPasswordLogin: boolean; diff --git a/src/ui/desktop/apps/admin/tabs/RolesTab.tsx b/src/ui/admin/tabs/RolesTab.tsx similarity index 96% rename from src/ui/desktop/apps/admin/tabs/RolesTab.tsx rename to src/ui/admin/tabs/RolesTab.tsx index 148f2100..520153f5 100644 --- a/src/ui/desktop/apps/admin/tabs/RolesTab.tsx +++ b/src/ui/admin/tabs/RolesTab.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button } from "@/components/ui/button.tsx"; +import { Button } from "@/components/button.tsx"; import { Dialog, DialogContent, @@ -7,7 +7,7 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog.tsx"; +} from "@/components/dialog.tsx"; import { Table, TableBody, @@ -15,11 +15,11 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Textarea } from "@/components/ui/textarea.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; +} from "@/components/table.tsx"; +import { Input } from "@/components/input.tsx"; +import { Label } from "@/components/label.tsx"; +import { Textarea } from "@/components/textarea.tsx"; +import { Badge } from "@/components/badge.tsx"; import { Shield, Plus, Edit, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -30,7 +30,7 @@ import { updateRole, deleteRole, type Role, -} from "@/ui/main-axios.ts"; +} from "@/main-axios.ts"; export function RolesTab(): React.ReactElement { const { t } = useTranslation(); diff --git a/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx b/src/ui/admin/tabs/SessionManagementTab.tsx similarity index 97% rename from src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx rename to src/ui/admin/tabs/SessionManagementTab.tsx index 344badfc..e2a93274 100644 --- a/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx +++ b/src/ui/admin/tabs/SessionManagementTab.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button } from "@/components/ui/button.tsx"; +import { Button } from "@/components/button.tsx"; import { Table, TableBody, @@ -7,12 +7,12 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table.tsx"; +} from "@/components/table.tsx"; import { Monitor, Smartphone, Globe, Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { revokeSession, revokeAllUserSessions } from "@/ui/main-axios.ts"; +import { revokeSession, revokeAllUserSessions } from "@/main-axios.ts"; interface Session { id: string; diff --git a/src/ui/desktop/apps/admin/tabs/UserManagementTab.tsx b/src/ui/admin/tabs/UserManagementTab.tsx similarity index 97% rename from src/ui/desktop/apps/admin/tabs/UserManagementTab.tsx rename to src/ui/admin/tabs/UserManagementTab.tsx index e229755d..a1a82616 100644 --- a/src/ui/desktop/apps/admin/tabs/UserManagementTab.tsx +++ b/src/ui/admin/tabs/UserManagementTab.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button } from "@/components/ui/button.tsx"; +import { Button } from "@/components/button.tsx"; import { Table, TableBody, @@ -7,12 +7,12 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table.tsx"; +} from "@/components/table.tsx"; import { UserPlus, Edit, Trash2, Link2, Unlink } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { deleteUser } from "@/ui/main-axios.ts"; +import { deleteUser } from "@/main-axios.ts"; interface User { id: string; diff --git a/src/ui/auth/Auth.tsx b/src/ui/auth/Auth.tsx new file mode 100644 index 00000000..9e5a717e --- /dev/null +++ b/src/ui/auth/Auth.tsx @@ -0,0 +1,1192 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { Button } from "@/components/button"; +import { Input } from "@/components/input"; +import { Separator } from "@/components/separator"; +import { toast } from "sonner"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/dropdown-menu"; +import { + Eye, + EyeOff, + User, + KeyRound, + ArrowLeft, + Shield, + CheckCircle2, + Globe, + ChevronDown, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n/i18n"; +import { + loginUser, + registerUser, + getUserInfo, + getRegistrationAllowed, + getPasswordLoginAllowed, + getOIDCConfig, + getSetupRequired, + initiatePasswordReset, + verifyPasswordResetCode, + completePasswordReset, + getOIDCAuthorizeUrl, + verifyTOTPLogin, + getServerConfig, + saveServerConfig, + isElectron, + getEmbeddedServerStatus, +} from "@/main-axios"; +import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig"; +import { ElectronLoginForm } from "@/auth/ElectronLoginForm"; +import { Checkbox } from "@/components/checkbox"; + +const STORAGE_KEY = "termix_auth"; + +export function getStoredAuth(): { + loggedIn: boolean; + username: string; +} | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw); + } catch { + return null; + } +} + +export function clearStoredAuth() { + localStorage.removeItem(STORAGE_KEY); +} + +function storeAuth(username: string) { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ loggedIn: true, username }), + ); +} + +type AuthView = "login" | "register" | "reset" | "totp" | "external"; +type ResetStep = "email" | "code" | "newpass"; + +interface AuthProps { + onLogin: (username: string, userId?: string, isAdmin?: boolean) => void; +} + +interface ExtendedWindow extends Window { + IS_ELECTRON_WEBVIEW?: boolean; +} + +const isInElectronWebView = () => { + if ((window as ExtendedWindow).IS_ELECTRON_WEBVIEW) return true; + try { + if (window.self !== window.top) return true; + } catch { + return true; + } + return false; +}; + +const AVAILABLE_LANGUAGES = [ + { code: "en", label: "English" }, + { code: "es", label: "Español" }, + { code: "fr", label: "Français" }, + { code: "de", label: "Deutsch" }, + { code: "zh", label: "中文" }, + { code: "ja", label: "日本語" }, + { code: "pt", label: "Português" }, + { code: "ru", label: "Русский" }, + { code: "ar", label: "العربية" }, + { code: "ko", label: "한국어" }, +]; + +function PasswordInput({ + value, + onChange, + placeholder, + disabled, + id, +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; + disabled?: boolean; + id?: string; +}) { + const [show, setShow] = useState(false); + return ( +
+ onChange(e.target.value)} + placeholder={placeholder ?? "••••••••"} + disabled={disabled} + className="pr-9 font-mono" + /> + +
+ ); +} + +function FieldLabel({ + htmlFor, + children, +}: { + htmlFor?: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +function Field({ + label, + htmlFor, + children, +}: { + label: string; + htmlFor?: string; + children: React.ReactNode; +}) { + return ( +
+ {label} + {children} +
+ ); +} + +export function Auth({ onLogin }: AuthProps) { + const { t } = useTranslation(); + const [view, setView] = useState("login"); + const [loading, setLoading] = useState(false); + const [oidcLoading, setOidcLoading] = useState(false); + const [currentLang, setCurrentLang] = useState( + AVAILABLE_LANGUAGES.find((l) => l.code === i18n.language) ?? + AVAILABLE_LANGUAGES[0], + ); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [rememberMe, setRememberMe] = useState(() => { + try { + return localStorage.getItem("rememberMe") === "true"; + } catch { + return false; + } + }); + + const [totpCode, setTotpCode] = useState(""); + const [totpTempToken, setTotpTempToken] = useState(""); + const totpInputRef = useRef(null); + + const [resetStep, setResetStep] = useState("email"); + const [resetCode, setResetCode] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmNewPassword, setConfirmNewPassword] = useState(""); + const [resetTempToken, setResetTempToken] = useState(""); + + const [registrationAllowed, setRegistrationAllowed] = useState(true); + const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true); + const [oidcConfigured, setOidcConfigured] = useState(false); + const [firstUser, setFirstUser] = useState(false); + const [dbConnectionFailed, setDbConnectionFailed] = useState(false); + const [dbHealthChecking, setDbHealthChecking] = useState(true); + + const [showServerConfig, setShowServerConfig] = useState( + null, + ); + const [currentServerUrl, setCurrentServerUrl] = useState(""); + const [webviewAuthSuccess, setWebviewAuthSuccess] = useState(false); + + useEffect(() => { + try { + localStorage.setItem("rememberMe", rememberMe.toString()); + } catch {} + }, [rememberMe]); + + useEffect(() => { + getRegistrationAllowed() + .then((res) => setRegistrationAllowed(res.allowed)) + .catch(() => {}); + getPasswordLoginAllowed() + .then((res) => setPasswordLoginAllowed(res.allowed)) + .catch(() => {}); + getOIDCConfig() + .then((res) => setOidcConfigured(!!res)) + .catch(() => setOidcConfigured(false)); + }, []); + + useEffect(() => { + if (showServerConfig) return; + setDbHealthChecking(true); + getSetupRequired() + .then((res) => { + if (res.setup_required) { + setFirstUser(true); + setView("register"); + } + setDbConnectionFailed(false); + }) + .catch(() => setDbConnectionFailed(true)) + .finally(() => setDbHealthChecking(false)); + }, [showServerConfig]); + + useEffect(() => { + const checkElectron = async () => { + if (isInElectronWebView()) { + setShowServerConfig(false); + return; + } + if (isElectron()) { + try { + const [config, status] = await Promise.all([ + getServerConfig(), + getEmbeddedServerStatus(), + ]); + if ( + status?.embedded && + status?.running && + config && + !config.serverUrl + ) { + setShowServerConfig(false); + setCurrentServerUrl(""); + return; + } + setCurrentServerUrl(config?.serverUrl || ""); + setShowServerConfig(!config || !config.serverUrl); + } catch { + setShowServerConfig(true); + } + } else { + setShowServerConfig(false); + } + }; + checkElectron(); + }, []); + + useEffect(() => { + if (view === "totp" && totpInputRef.current) totpInputRef.current.focus(); + }, [view]); + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const success = urlParams.get("success"); + const error = urlParams.get("error"); + if (error) { + if (error === "registration_disabled") + toast.error(t("messages.registrationDisabled")); + else if (error === "user_not_allowed") + toast.error(t("messages.userNotAllowed")); + else toast.error(`${t("errors.oidcAuthFailed")}: ${error}`); + window.history.replaceState({}, document.title, window.location.pathname); + return; + } + if (success) { + if (isInElectronWebView()) { + window.parent.postMessage( + { + type: "AUTH_SUCCESS", + source: "oidc_callback", + platform: "desktop", + timestamp: Date.now(), + }, + window.location.origin, + ); + setWebviewAuthSuccess(true); + window.history.replaceState( + {}, + document.title, + window.location.pathname, + ); + return; + } + getUserInfo() + .then((meRes) => { + storeAuth(meRes.username || ""); + onLogin( + meRes.username || "", + meRes.userId || undefined, + !!meRes.is_admin, + ); + window.history.replaceState( + {}, + document.title, + window.location.pathname, + ); + }) + .catch(() => { + toast.error(t("errors.failedUserInfo")); + }); + } + }, [onLogin, t]); + + const handleElectronAuthSuccess = useCallback( + async (previousJwt: string | null) => { + try { + const cookieReady = await window.electronAPI?.waitForSessionCookie?.( + "jwt", + currentServerUrl, + previousJwt, + 5000, + ); + if (cookieReady && !cookieReady.success) + throw new Error(cookieReady.error || "Auth cookie not ready"); + const meRes = await getUserInfo(); + if (!meRes) throw new Error("Failed to get user info"); + storeAuth(meRes.username || ""); + onLogin( + meRes.username || "", + meRes.userId || undefined, + !!meRes.is_admin, + ); + toast.success(t("messages.loginSuccess")); + } catch { + toast.error(t("errors.failedUserInfo")); + } + }, + [onLogin, currentServerUrl, t], + ); + + function resetAll() { + setUsername(""); + setPassword(""); + setConfirmPassword(""); + setResetStep("email"); + setResetCode(""); + setNewPassword(""); + setConfirmNewPassword(""); + setResetTempToken(""); + setTotpCode(""); + setTotpTempToken(""); + } + + function switchView(v: AuthView) { + resetAll(); + setView(v); + } + + async function handleLogin(e: React.FormEvent) { + e.preventDefault(); + if (!username.trim()) { + toast.error(t("errors.requiredField")); + return; + } + setLoading(true); + try { + const res = await loginUser(username.trim(), password, rememberMe); + if (res.requires_totp) { + setTotpTempToken(res.temp_token); + setView("totp"); + return; + } + if (!res?.success) throw new Error(t("errors.loginFailed")); + if (isInElectronWebView()) { + window.parent.postMessage( + { + type: "AUTH_SUCCESS", + source: "auth_component", + platform: "desktop", + timestamp: Date.now(), + }, + window.location.origin, + ); + setWebviewAuthSuccess(true); + return; + } + const meRes = await getUserInfo(); + storeAuth(meRes.username || username.trim()); + toast.success(t("messages.loginSuccess")); + onLogin( + meRes.username || username.trim(), + meRes.userId || undefined, + !!meRes.is_admin, + ); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string } }; + }; + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.unknownError"), + ); + } finally { + setLoading(false); + } + } + + async function handleRegister(e: React.FormEvent) { + e.preventDefault(); + if (!username.trim()) { + toast.error(t("errors.requiredField")); + return; + } + if (password.length < 6) { + toast.error(t("errors.minLength", { min: 6 })); + return; + } + if (password !== confirmPassword) { + toast.error(t("errors.passwordMismatch")); + return; + } + setLoading(true); + try { + await registerUser(username.trim(), password); + const res = await loginUser(username.trim(), password, rememberMe); + if (res.requires_totp) { + setTotpTempToken(res.temp_token); + setView("totp"); + return; + } + const meRes = await getUserInfo(); + storeAuth(meRes.username || username.trim()); + toast.success(t("messages.registrationSuccess")); + onLogin( + meRes.username || username.trim(), + meRes.userId || undefined, + !!meRes.is_admin, + ); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string } }; + }; + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.unknownError"), + ); + } finally { + setLoading(false); + } + } + + async function handleTOTP(e: React.FormEvent) { + e.preventDefault(); + if (totpCode.length !== 6) { + toast.error(t("auth.enterCode")); + return; + } + setLoading(true); + try { + const res = await verifyTOTPLogin(totpTempToken, totpCode, rememberMe); + if (!res?.success) throw new Error(t("errors.loginFailed")); + if (isInElectronWebView()) { + window.parent.postMessage( + { + type: "AUTH_SUCCESS", + source: "totp_auth_component", + platform: "desktop", + timestamp: Date.now(), + }, + window.location.origin, + ); + setWebviewAuthSuccess(true); + return; + } + storeAuth(res.username || username); + toast.success(t("messages.loginSuccess")); + onLogin( + res.username || username, + res.userId || undefined, + !!res.is_admin, + ); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string; code?: string } }; + }; + if (error?.response?.data?.code === "SESSION_EXPIRED") { + setView("login"); + toast.error(t("errors.sessionExpired")); + return; + } + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.invalidTotpCode"), + ); + } finally { + setLoading(false); + } + } + + async function handleResetInitiate(e: React.FormEvent) { + e.preventDefault(); + if (!username.trim()) { + toast.error(t("errors.requiredField")); + return; + } + setLoading(true); + try { + await initiatePasswordReset(username.trim()); + setResetStep("code"); + toast.success(t("messages.resetCodeSent")); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string } }; + }; + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.failedPasswordReset"), + ); + } finally { + setLoading(false); + } + } + + async function handleResetVerify(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + try { + const res = await verifyPasswordResetCode(username.trim(), resetCode); + setResetTempToken(res.tempToken as string); + setResetStep("newpass"); + toast.success(t("messages.codeVerified")); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string } }; + }; + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.failedVerifyCode"), + ); + } finally { + setLoading(false); + } + } + + async function handleResetComplete(e: React.FormEvent) { + e.preventDefault(); + if (newPassword.length < 6) { + toast.error(t("errors.minLength", { min: 6 })); + return; + } + if (newPassword !== confirmNewPassword) { + toast.error(t("errors.passwordMismatch")); + return; + } + setLoading(true); + try { + await completePasswordReset(username.trim(), resetTempToken, newPassword); + toast.success(t("messages.passwordResetSuccess")); + switchView("login"); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string } }; + }; + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.failedCompleteReset"), + ); + } finally { + setLoading(false); + } + } + + async function handleOIDCLogin() { + setOidcLoading(true); + try { + const authResponse = await getOIDCAuthorizeUrl(rememberMe); + const { auth_url: authUrl } = authResponse; + if (!authUrl || authUrl === "undefined") + throw new Error(t("errors.invalidAuthUrl")); + window.location.replace(authUrl); + } catch (err: unknown) { + const error = err as { + message?: string; + response?: { data?: { error?: string } }; + }; + toast.error( + error?.response?.data?.error || + error?.message || + t("errors.failedOidcLogin"), + ); + setOidcLoading(false); + } + } + + function changeLanguage(lang: (typeof AVAILABLE_LANGUAGES)[0]) { + setCurrentLang(lang); + i18n.changeLanguage(lang.code); + localStorage.setItem("termix-language", lang.code); + } + + // Electron server config / webview auth success screens + if (isElectron() && !isInElectronWebView()) { + if (showServerConfig === null) + return ( +
+
+
+ ); + if (showServerConfig) + return ( +
+
+ window.location.reload()} + onUseEmbedded={async () => { + await saveServerConfig({ + serverUrl: "", + lastUpdated: new Date().toISOString(), + }); + setShowServerConfig(false); + setCurrentServerUrl(""); + }} + onCancel={() => setShowServerConfig(false)} + isFirstTime={!currentServerUrl} + /> +
+
+ ); + if (currentServerUrl && !webviewAuthSuccess) + return ( +
+
+ setShowServerConfig(true)} + /> +
+
+ ); + } + + if (webviewAuthSuccess || (isInElectronWebView() && webviewAuthSuccess)) + return ( +
+
+ +

{t("auth.redirectingToApp")}

+
+
+ ); + + if (dbConnectionFailed) + return ( +
+
+

+ {t("errors.databaseConnection")} +

+

+ {t("messages.databaseConnectionFailed")} +

+ +
+
+ ); + + if (dbHealthChecking && showServerConfig === false) + return ( +
+
+
+ ); + + const TAB_ITEMS: { id: AuthView; label: string; show: boolean }[] = [ + { + id: "login", + label: t("common.login"), + show: passwordLoginAllowed && !firstUser, + }, + { + id: "register", + label: t("common.register"), + show: (passwordLoginAllowed || firstUser) && registrationAllowed, + }, + { id: "external", label: t("auth.external"), show: oidcConfigured }, + ]; + + return ( +
+ {/* Left decorative panel */} +
+
+
+ + TERMIX + +
+ + {t("auth.tagline")} + +
+
+ + {/* Right panel — auth form */} +
+ {/* Language selector */} +
+ + + + + + {AVAILABLE_LANGUAGES.map((lang) => ( + changeLanguage(lang)} + className={`text-xs font-mono ${currentLang.code === lang.code ? "text-accent-brand" : ""}`} + > + {lang.label} + + ))} + + +
+ +
+ {/* TOTP view */} + {view === "totp" && ( +
+
+

{t("auth.twoFactorAuth")}

+

+ {t("auth.enterCode")} +

+
+
+ + + setTotpCode(e.target.value.replace(/\D/g, "")) + } + disabled={loading} + className="text-center text-2xl tracking-widest font-mono" + autoComplete="one-time-code" + /> + + + +
+
+ )} + + {/* Reset password view */} + {view === "reset" && ( +
+ +
+

+ {t("auth.forgotPassword")} +

+

+ {t("auth.resetCodeDesc")} +

+
+
+ {(["email", "code", "newpass"] as ResetStep[]).map( + (step, i) => { + const stepIdx = ["email", "code", "newpass"].indexOf( + resetStep, + ); + const done = i < stepIdx; + const active = i === stepIdx; + return ( +
+
+ {done ? : i + 1} +
+ {i < 2 && ( +
+ )} +
+ ); + }, + )} +
+ {resetStep === "email" && ( +
+ + setUsername(e.target.value)} + placeholder="your_username" + disabled={loading} + /> + + +
+ )} + {resetStep === "code" && ( +
+ + + setResetCode(e.target.value.replace(/\D/g, "")) + } + placeholder="000000" + maxLength={6} + className="text-center font-mono text-lg tracking-widest" + disabled={loading} + /> + + + +
+ )} + {resetStep === "newpass" && ( +
+ + + + + + + +
+ )} +
+ )} + + {/* Login / Register / External */} + {(view === "login" || view === "register" || view === "external") && ( +
+
+ {TAB_ITEMS.filter((item) => item.show).map((item) => ( + + ))} +
+ +
+

+ {view === "login" + ? t("auth.loginTitle") + : view === "register" + ? t("auth.registerTitle") + : t("auth.loginWithExternal")} +

+

+ {view === "login" + ? t("auth.loginSubtitle", "") + : view === "register" + ? t("auth.registerSubtitle", "") + : t("auth.loginWithExternalDesc")} +

+
+ + {view === "external" && ( +
+ {isElectron() ? ( +

+ {t("auth.externalNotSupportedInElectron")} +

+ ) : ( + <> +
+ setRememberMe(v === true)} + /> + +
+ + + )} +
+ )} + + {view === "login" && ( +
+ +
+ + setUsername(e.target.value)} + placeholder="username" + className="pl-8" + disabled={loading} + autoFocus + /> +
+
+ + + +
+
+ setRememberMe(v === true)} + disabled={loading} + /> + +
+ +
+ +
+ )} + + {view === "register" && ( +
+ +
+ + setUsername(e.target.value)} + placeholder="choose_a_username" + className="pl-8" + disabled={loading} + autoFocus + /> +
+
+ + + + + + + +
+ )} + + +

+ {view === "login" && registrationAllowed ? ( + <> + {t("auth.noAccount", "Don't have an account?")}{" "} + + + ) : view === "register" && + passwordLoginAllowed && + !firstUser ? ( + <> + {t("auth.hasAccount", "Already have an account?")}{" "} + + + ) : null} +

+
+ )} +
+
+
+ ); +} diff --git a/src/ui/desktop/authentication/ElectronLoginForm.tsx b/src/ui/auth/ElectronLoginForm.tsx similarity index 99% rename from src/ui/desktop/authentication/ElectronLoginForm.tsx rename to src/ui/auth/ElectronLoginForm.tsx index 81222ee9..e6785340 100644 --- a/src/ui/desktop/authentication/ElectronLoginForm.tsx +++ b/src/ui/auth/ElectronLoginForm.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; import { useTranslation } from "react-i18next"; import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react"; diff --git a/src/ui/desktop/authentication/ElectronServerConfig.tsx b/src/ui/auth/ElectronServerConfig.tsx similarity index 97% rename from src/ui/desktop/authentication/ElectronServerConfig.tsx rename to src/ui/auth/ElectronServerConfig.tsx index 0c0033b8..06264c93 100644 --- a/src/ui/desktop/authentication/ElectronServerConfig.tsx +++ b/src/ui/auth/ElectronServerConfig.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx"; +import { Button } from "@/components/button.tsx"; +import { Input } from "@/components/input.tsx"; +import { Label } from "@/components/label.tsx"; +import { Alert, AlertTitle, AlertDescription } from "@/components/alert.tsx"; import { useTranslation } from "react-i18next"; import { getServerConfig, @@ -10,7 +10,7 @@ import { getEmbeddedServerStatus, setEmbeddedMode, type ServerConfig, -} from "@/ui/main-axios.ts"; +} from "@/main-axios.ts"; import { Server, Monitor, Loader2 } from "lucide-react"; import { useTheme } from "@/components/theme-provider"; diff --git a/src/ui/desktop/authentication/Auth.tsx b/src/ui/auth/LoginPage.tsx similarity index 98% rename from src/ui/desktop/authentication/Auth.tsx rename to src/ui/auth/LoginPage.tsx index aa7e5d08..4818e3c5 100644 --- a/src/ui/desktop/authentication/Auth.tsx +++ b/src/ui/auth/LoginPage.tsx @@ -1,13 +1,13 @@ import React, { useState, useEffect, useCallback } from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { PasswordInput } from "@/components/ui/password-input.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx"; +import { Button } from "@/components/button.tsx"; +import { Input } from "@/components/input.tsx"; +import { PasswordInput } from "@/components/password-input.tsx"; +import { Label } from "@/components/label.tsx"; +import { Checkbox } from "@/components/checkbox.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; +import { Tabs, TabsList, TabsTrigger } from "@/components/tabs.tsx"; import { useTranslation } from "react-i18next"; -import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx"; +import { LanguageSwitcher } from "@/user/LanguageSwitcher.tsx"; import { toast } from "sonner"; import { Sun, Moon, Monitor } from "lucide-react"; import { useTheme } from "@/components/theme-provider"; @@ -28,9 +28,9 @@ import { saveServerConfig, isElectron, getEmbeddedServerStatus, -} from "../../main-axios.ts"; -import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx"; -import { ElectronLoginForm } from "@/ui/desktop/authentication/ElectronLoginForm.tsx"; +} from "@/main-axios"; +import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx"; +import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx"; function isMissingServerConfigError(error: unknown): boolean { if (!(error instanceof Error)) { diff --git a/src/components/ui/accordion.tsx b/src/ui/components/accordion.tsx similarity index 100% rename from src/components/ui/accordion.tsx rename to src/ui/components/accordion.tsx diff --git a/src/components/ui/alert-dialog.tsx b/src/ui/components/alert-dialog.tsx similarity index 98% rename from src/components/ui/alert-dialog.tsx rename to src/ui/components/alert-dialog.tsx index 1c376739..dc04cf85 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/ui/components/alert-dialog.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import { cn } from "@/lib/utils"; -import { buttonVariants } from "@/components/ui/button"; +import { buttonVariants } from "@/components/button"; function AlertDialog({ ...props diff --git a/src/components/ui/alert.tsx b/src/ui/components/alert.tsx similarity index 100% rename from src/components/ui/alert.tsx rename to src/ui/components/alert.tsx diff --git a/src/components/ui/badge.tsx b/src/ui/components/badge.tsx similarity index 100% rename from src/components/ui/badge.tsx rename to src/ui/components/badge.tsx diff --git a/src/components/ui/button-group.tsx b/src/ui/components/button-group.tsx similarity index 90% rename from src/components/ui/button-group.tsx rename to src/ui/components/button-group.tsx index 46f8cda3..e742659d 100644 --- a/src/components/ui/button-group.tsx +++ b/src/ui/components/button-group.tsx @@ -1,6 +1,11 @@ -import { Children, ReactElement, cloneElement, isValidElement } from "react"; +import { + Children, + type ReactElement, + cloneElement, + isValidElement, +} from "react"; -import { type ButtonProps } from "@/components/ui/button"; +import { type ButtonProps } from "@/components/button"; import { cn } from "@/lib/utils"; interface ButtonGroupProps { diff --git a/src/ui/components/button.tsx b/src/ui/components/button.tsx new file mode 100644 index 00000000..dd09238a --- /dev/null +++ b/src/ui/components/button.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-7 rounded-none", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot.Root : "button"; + + return ( + + ); +} + +export type ButtonProps = React.ComponentProps<"button"> & + VariantProps & { asChild?: boolean }; + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/ui/components/card.tsx similarity index 54% rename from src/components/ui/card.tsx rename to src/ui/components/card.tsx index 5b8522b8..2d30a89a 100644 --- a/src/components/ui/card.tsx +++ b/src/ui/components/card.tsx @@ -2,12 +2,17 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -function Card({ className, ...props }: React.ComponentProps<"div">) { +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { return (
img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", className, )} {...props} @@ -20,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
) { return (
); @@ -42,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return (
); @@ -65,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { return (
); @@ -75,7 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return (
); diff --git a/src/components/ui/checkbox.tsx b/src/ui/components/checkbox.tsx similarity index 100% rename from src/components/ui/checkbox.tsx rename to src/ui/components/checkbox.tsx diff --git a/src/ui/components/command.tsx b/src/ui/components/command.tsx new file mode 100644 index 00000000..a302540c --- /dev/null +++ b/src/ui/components/command.tsx @@ -0,0 +1,130 @@ +import * as React from "react"; +import { Search } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Command = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Command.displayName = "Command"; + +const CommandInput = React.forwardRef< + HTMLInputElement, + React.InputHTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +
+)); +CommandInput.displayName = "CommandInput"; + +const CommandList = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CommandList.displayName = "CommandList"; + +const CommandGroup = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { heading?: string } +>(({ className, heading, children, ...props }, ref) => ( +
+ {heading &&
{heading}
} + {children} +
+)); +CommandGroup.displayName = "CommandGroup"; + +const CommandSeparator = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CommandSeparator.displayName = "CommandSeparator"; + +const CommandItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { onSelect?: () => void } +>(({ className, onSelect, ...props }, ref) => ( +
+)); +CommandItem.displayName = "CommandItem"; + +const CommandEmpty = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CommandEmpty.displayName = "CommandEmpty"; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +); +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, + CommandSeparator, + CommandEmpty, + CommandShortcut, +}; diff --git a/src/components/ui/dialog.tsx b/src/ui/components/dialog.tsx similarity index 60% rename from src/components/ui/dialog.tsx rename to src/ui/components/dialog.tsx index 64249170..4493c20f 100644 --- a/src/components/ui/dialog.tsx +++ b/src/ui/components/dialog.tsx @@ -1,8 +1,9 @@ import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { XIcon } from "lucide-react"; +import { Dialog as DialogPrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; +import { Button } from "@/components/button"; +import { XIcon } from "lucide-react"; function Dialog({ ...props @@ -36,7 +37,7 @@ function DialogOverlay({ + {children} {showCloseButton && ( - - - Close + + )} @@ -82,13 +86,20 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
); } -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean; +}) { return (
) { className, )} {...props} - /> + > + {children} + {showCloseButton && ( + + + + )} +
); } @@ -108,7 +126,7 @@ function DialogTitle({ return ( ); @@ -121,7 +139,10 @@ function DialogDescription({ return ( ); diff --git a/src/ui/components/dropdown-menu.tsx b/src/ui/components/dropdown-menu.tsx new file mode 100644 index 00000000..c03b7987 --- /dev/null +++ b/src/ui/components/dropdown-menu.tsx @@ -0,0 +1,271 @@ +import * as React from "react"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; +import { CheckIcon, ChevronRightIcon } from "lucide-react"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + align = "start", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/src/components/ui/folder.tsx b/src/ui/components/folder.tsx similarity index 100% rename from src/components/ui/folder.tsx rename to src/ui/components/folder.tsx diff --git a/src/components/ui/form.tsx b/src/ui/components/form.tsx similarity index 98% rename from src/components/ui/form.tsx rename to src/ui/components/form.tsx index 50ee37c3..ca28b235 100644 --- a/src/components/ui/form.tsx +++ b/src/ui/components/form.tsx @@ -13,7 +13,7 @@ import { } from "react-hook-form"; import { cn } from "@/lib/utils"; -import { Label } from "@/components/ui/label"; +import { Label } from "@/components/label"; const Form = FormProvider; diff --git a/src/ui/components/input.tsx b/src/ui/components/input.tsx new file mode 100644 index 00000000..bc0898a2 --- /dev/null +++ b/src/ui/components/input.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/src/ui/components/kbd.tsx b/src/ui/components/kbd.tsx new file mode 100644 index 00000000..bd172665 --- /dev/null +++ b/src/ui/components/kbd.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface KbdProps extends React.HTMLAttributes {} + +const Kbd = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ); + }, +); +Kbd.displayName = "Kbd"; + +const KbdKey = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +); +KbdKey.displayName = "KbdKey"; + +const KbdSeparator = ({ + className, + ...props +}: React.HTMLAttributes) => ( + + + + +); +KbdSeparator.displayName = "KbdSeparator"; + +export { Kbd, KbdKey, KbdSeparator }; diff --git a/src/components/ui/label.tsx b/src/ui/components/label.tsx similarity index 100% rename from src/components/ui/label.tsx rename to src/ui/components/label.tsx diff --git a/src/components/ui/password-input.tsx b/src/ui/components/password-input.tsx similarity index 96% rename from src/components/ui/password-input.tsx rename to src/ui/components/password-input.tsx index 5eac52b5..d8a57ff0 100644 --- a/src/components/ui/password-input.tsx +++ b/src/ui/components/password-input.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { Eye, EyeOff } from "lucide-react"; -import { Input } from "@/components/ui/input"; +import { Input } from "@/components/input"; import { cn } from "@/lib/utils"; type PasswordInputProps = React.InputHTMLAttributes; diff --git a/src/components/ui/popover.tsx b/src/ui/components/popover.tsx similarity index 100% rename from src/components/ui/popover.tsx rename to src/ui/components/popover.tsx diff --git a/src/components/ui/scroll-area.tsx b/src/ui/components/scroll-area.tsx similarity index 100% rename from src/components/ui/scroll-area.tsx rename to src/ui/components/scroll-area.tsx diff --git a/src/ui/components/section-card.tsx b/src/ui/components/section-card.tsx new file mode 100644 index 00000000..a1622b96 --- /dev/null +++ b/src/ui/components/section-card.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import type React from "react"; + +export function SectionCard({ + title, + icon, + action, + children, +}: { + title: string; + icon: React.ReactNode; + action?: React.ReactNode; + children: React.ReactNode; +}) { + return ( +
+
+ {icon} + + {title} + + {action &&
{action}
} +
+
{children}
+
+ ); +} + +export function SettingRow({ + label, + badge, + description, + children, +}: { + label: string; + badge?: string; + description?: string; + children: React.ReactNode; +}) { + return ( +
+
+
+ {label} + {badge && ( + + {badge} + + )} +
+ {description && ( + {description} + )} +
+
{children}
+
+ ); +} + +export function FakeSwitch({ + defaultChecked = false, + onChange, +}: { + defaultChecked?: boolean; + onChange?: (v: boolean) => void; +}) { + const [on, setOn] = useState(defaultChecked); + return ( + + ); +} diff --git a/src/components/ui/select.tsx b/src/ui/components/select.tsx similarity index 100% rename from src/components/ui/select.tsx rename to src/ui/components/select.tsx diff --git a/src/components/ui/separator.tsx b/src/ui/components/separator.tsx similarity index 64% rename from src/components/ui/separator.tsx rename to src/ui/components/separator.tsx index 72c18e33..d7fc9faa 100644 --- a/src/components/ui/separator.tsx +++ b/src/ui/components/separator.tsx @@ -1,7 +1,5 @@ -"use client"; - import * as React from "react"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; +import { Separator as SeparatorPrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; @@ -17,7 +15,7 @@ function Separator({ decorative={decorative} orientation={orientation} className={cn( - "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", + "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", className, )} {...props} diff --git a/src/components/ui/shadcn-io/status/index.tsx b/src/ui/components/shadcn-io/status/index.tsx similarity index 97% rename from src/components/ui/shadcn-io/status/index.tsx rename to src/ui/components/shadcn-io/status/index.tsx index 5131e84c..0ee2b367 100644 --- a/src/components/ui/shadcn-io/status/index.tsx +++ b/src/ui/components/shadcn-io/status/index.tsx @@ -1,5 +1,5 @@ import type { ComponentProps, HTMLAttributes } from "react"; -import { Badge } from "@/components/ui/badge"; +import { Badge } from "@/components/badge"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; diff --git a/src/components/ui/sheet.tsx b/src/ui/components/sheet.tsx similarity index 52% rename from src/components/ui/sheet.tsx rename to src/ui/components/sheet.tsx index f449ce91..c4168fe8 100644 --- a/src/components/ui/sheet.tsx +++ b/src/ui/components/sheet.tsx @@ -1,8 +1,9 @@ import * as React from "react"; -import * as SheetPrimitive from "@radix-ui/react-dialog"; -import { XIcon } from "lucide-react"; +import { Dialog as SheetPrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; +import { Button } from "@/components/button"; +import { XIcon } from "lucide-react"; function Sheet({ ...props }: React.ComponentProps) { return ; @@ -34,7 +35,7 @@ function SheetOverlay({ & { side?: "top" | "right" | "bottom" | "left"; + showCloseButton?: boolean; }) { return ( {children} - - - Close - + {showCloseButton && ( + + + + )} ); @@ -83,7 +87,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return (
); @@ -106,7 +110,10 @@ function SheetTitle({ return ( ); @@ -119,7 +126,7 @@ function SheetDescription({ return ( ); diff --git a/src/components/ui/sidebar.tsx b/src/ui/components/sidebar.tsx similarity index 63% rename from src/components/ui/sidebar.tsx rename to src/ui/components/sidebar.tsx index 8413b712..d2deb02f 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/ui/components/sidebar.tsx @@ -1,25 +1,29 @@ -/* eslint-disable react-refresh/only-export-components */ +"use client"; + import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; -import { PanelLeftIcon } from "lucide-react"; +import { Slot } from "radix-ui"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/button"; +import { Input } from "@/components/input"; +import { Separator } from "@/components/separator"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/sheet"; +import { Skeleton } from "@/components/skeleton"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip"; +import { PanelLeftIcon } from "lucide-react"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_KEYBOARD_SHORTCUT = "b"; @@ -119,25 +123,23 @@ function SidebarProvider({ return ( - -
- {children} -
-
+
+ {children} +
); } @@ -148,20 +150,21 @@ function Sidebar({ collapsible = "offcanvas", className, children, + dir, ...props }: React.ComponentProps<"div"> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; }) { - const { state } = useSidebar(); + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return (
- // - // - // Sidebar - // Displays the mobile sidebar. - // - //
{children}
- //
- // - // ) - // } + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ); + } return (
+ {/* This is what handles the sidebar gap on desktop */}
@@ -256,8 +258,8 @@ function SidebarTrigger({ data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" - size="icon" - className={cn("size-7", className)} + size="icon-sm" + className={cn(className)} onClick={(event) => { onClick?.(event); toggleSidebar(); @@ -282,10 +284,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { onClick={toggleSidebar} title="Toggle Sidebar" className={cn( - "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", + "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", - "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", + "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className, @@ -300,8 +302,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
); @@ -353,7 +354,7 @@ function SidebarSeparator({ ); @@ -365,7 +366,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="sidebar-content" data-sidebar="content" className={cn( - "flex min-h-0 flex-1 flex-col gap-2 overflow-auto thin-scrollbar group-data-[collapsible=icon]:overflow-hidden", + "no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden", className, )} {...props} @@ -389,15 +390,14 @@ function SidebarGroupLabel({ asChild = false, ...props }: React.ComponentProps<"div"> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "div"; + const Comp = asChild ? Slot.Root : "div"; return ( svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + "flex h-8 shrink-0 items-center rounded-none px-2 text-xs text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", className, )} {...props} @@ -410,17 +410,14 @@ function SidebarGroupAction({ asChild = false, ...props }: React.ComponentProps<"button"> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot.Root : "button"; return ( svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "group-data-[collapsible=icon]:hidden", + "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", className, )} {...props} @@ -436,7 +433,7 @@ function SidebarGroupContent({
); @@ -447,7 +444,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
    ); @@ -465,7 +462,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { } const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-none p-2 text-left text-xs ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", { variants: { variant: { @@ -474,9 +471,9 @@ const sidebarMenuButtonVariants = cva( "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", }, size: { - default: "h-8 text-sm", + default: "h-8 text-xs", sm: "h-7 text-xs", - lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", + lg: "h-12 text-xs group-data-[collapsible=icon]:p-0!", }, }, defaultVariants: { @@ -499,7 +496,7 @@ function SidebarMenuButton({ isActive?: boolean; tooltip?: string | React.ComponentProps; } & VariantProps) { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot.Root : "button"; const { isMobile, state } = useSidebar(); const button = ( @@ -545,22 +542,16 @@ function SidebarMenuAction({ asChild?: boolean; showOnHover?: boolean; }) { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot.Root : "button"; return ( svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", + "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", showOnHover && - "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0", className, )} {...props} @@ -577,12 +568,7 @@ function SidebarMenuBadge({ data-slot="sidebar-menu-badge" data-sidebar="menu-badge" className={cn( - "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", - "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", + "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-none px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground", className, )} {...props} @@ -606,12 +592,12 @@ function SidebarMenuSkeleton({
    {showIcon && ( )} @@ -634,8 +620,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { data-slot="sidebar-menu-sub" data-sidebar="menu-sub" className={cn( - "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", - "group-data-[collapsible=icon]:hidden", + "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden", className, )} {...props} @@ -668,7 +653,7 @@ function SidebarMenuSubButton({ size?: "sm" | "md"; isActive?: boolean; }) { - const Comp = asChild ? Slot : "a"; + const Comp = asChild ? Slot.Root : "a"; return ( svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", - size === "sm" && "text-xs", - size === "md" && "text-sm", - "group-data-[collapsible=icon]:hidden", + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-none px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-xs data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", className, )} {...props} diff --git a/src/components/ui/skeleton.tsx b/src/ui/components/skeleton.tsx similarity index 74% rename from src/components/ui/skeleton.tsx rename to src/ui/components/skeleton.tsx index 01689981..ac4620a7 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/ui/components/skeleton.tsx @@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return (
    ); diff --git a/src/components/ui/slider.tsx b/src/ui/components/slider.tsx similarity index 100% rename from src/components/ui/slider.tsx rename to src/ui/components/slider.tsx diff --git a/src/ui/components/sonner.tsx b/src/ui/components/sonner.tsx new file mode 100644 index 00000000..900e3bfe --- /dev/null +++ b/src/ui/components/sonner.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useTheme } from "@/components/theme-provider"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/components/ui/switch.tsx b/src/ui/components/switch.tsx similarity index 100% rename from src/components/ui/switch.tsx rename to src/ui/components/switch.tsx diff --git a/src/components/ui/table.tsx b/src/ui/components/table.tsx similarity index 100% rename from src/components/ui/table.tsx rename to src/ui/components/table.tsx diff --git a/src/components/ui/tabs.tsx b/src/ui/components/tabs.tsx similarity index 100% rename from src/components/ui/tabs.tsx rename to src/ui/components/tabs.tsx diff --git a/src/components/ui/textarea.tsx b/src/ui/components/textarea.tsx similarity index 95% rename from src/components/ui/textarea.tsx rename to src/ui/components/textarea.tsx index a5c84926..2f68150e 100644 --- a/src/components/ui/textarea.tsx +++ b/src/ui/components/textarea.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/lib/utils"; export type TextareaProps = React.TextareaHTMLAttributes; diff --git a/src/components/theme-provider.tsx b/src/ui/components/theme-provider.tsx similarity index 53% rename from src/components/theme-provider.tsx rename to src/ui/components/theme-provider.tsx index ba8b8e24..efb4bd78 100644 --- a/src/components/theme-provider.tsx +++ b/src/ui/components/theme-provider.tsx @@ -1,29 +1,32 @@ -/* eslint-disable react-refresh/only-export-components */ import { createContext, useContext, useEffect, useState } from "react"; - -type Theme = - | "dark" - | "light" - | "system" - | "dracula" - | "gentlemansChoice" - | "midnightEspresso" - | "catppuccinMocha"; +import type { ThemeId } from "@/types/ui-types"; type ThemeProviderProps = { children: React.ReactNode; - defaultTheme?: Theme; + defaultTheme?: ThemeId; storageKey?: string; }; type ThemeProviderState = { - theme: Theme; - setTheme: (theme: Theme) => void; - setThemePreview: (theme: Theme | null) => void; + theme: ThemeId; + setTheme: (theme: ThemeId) => void; + setThemePreview: (theme: ThemeId | null) => void; }; +const ALL_THEME_CLASSES = [ + "light", + "dark", + "dracula", + "catppuccin", + "nord", + "solarized", + "tokyo-night", + "one-dark", + "gruvbox", +]; + const initialState: ThemeProviderState = { - theme: "system", + theme: "dark", setTheme: () => null, setThemePreview: () => null, }; @@ -32,60 +35,41 @@ const ThemeProviderContext = createContext(initialState); export function ThemeProvider({ children, - defaultTheme = "system", + defaultTheme = "dark", storageKey = "vite-ui-theme", ...props }: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as ThemeId) || defaultTheme, ); - const [previewTheme, setPreviewTheme] = useState(null); + const [previewTheme, setPreviewTheme] = useState(null); + + const activeTheme = previewTheme ?? theme; useEffect(() => { const root = window.document.documentElement; - - root.classList.remove( - "light", - "dark", - "dracula", - "gentlemansChoice", - "midnightEspresso", - "catppuccinMocha", - ); - - const activeTheme = previewTheme || theme; + root.classList.remove(...ALL_THEME_CLASSES); if (activeTheme === "system") { const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") .matches ? "dark" : "light"; - root.classList.add(systemTheme); return; } root.classList.add(activeTheme); - - const darkCustomThemes: Theme[] = [ - "dracula", - "gentlemansChoice", - "midnightEspresso", - "catppuccinMocha", - ]; - if (darkCustomThemes.includes(activeTheme)) { - root.classList.add("dark"); - } - }, [theme, previewTheme]); + }, [activeTheme]); const value = { theme, - setTheme: (theme: Theme) => { + setTheme: (theme: ThemeId) => { localStorage.setItem(storageKey, theme); setTheme(theme); }, - setThemePreview: (theme: Theme | null) => { - setPreviewTheme(theme); + setThemePreview: (preview: ThemeId | null) => { + setPreviewTheme(preview); }, }; @@ -98,9 +82,7 @@ export function ThemeProvider({ export const useTheme = () => { const context = useContext(ThemeProviderContext); - if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider"); - return context; }; diff --git a/src/ui/components/tooltip.tsx b/src/ui/components/tooltip.tsx new file mode 100644 index 00000000..71ebfff5 --- /dev/null +++ b/src/ui/components/tooltip.tsx @@ -0,0 +1,57 @@ +"use client"; + +import * as React from "react"; +import { Tooltip as TooltipPrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ; +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ); +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/src/components/ui/version-alert.tsx b/src/ui/components/version-alert.tsx similarity index 98% rename from src/components/ui/version-alert.tsx rename to src/ui/components/version-alert.tsx index 4493e749..b4df3c32 100644 --- a/src/components/ui/version-alert.tsx +++ b/src/ui/components/version-alert.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx"; -import { Button } from "@/components/ui/button.tsx"; +import { Alert, AlertTitle, AlertDescription } from "@/components/alert.tsx"; +import { Button } from "@/components/button.tsx"; import { ExternalLink, Download, AlertTriangle, Info } from "lucide-react"; import { useTranslation } from "react-i18next"; diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/dashboard/Dashboard.tsx similarity index 75% rename from src/ui/desktop/apps/dashboard/Dashboard.tsx rename to src/ui/dashboard/Dashboard.tsx index 3727e5eb..ebfec4f9 100644 --- a/src/ui/desktop/apps/dashboard/Dashboard.tsx +++ b/src/ui/dashboard/Dashboard.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from "react"; -import { Auth } from "@/ui/desktop/authentication/Auth.tsx"; -import { AlertManager } from "@/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx"; -import { Button } from "@/components/ui/button.tsx"; +import React, { useEffect, useState, useRef, useCallback } from "react"; +import { Auth } from "@/auth/LoginPage.tsx"; +import { AlertManager } from "@/dashboard/panels/alerts/AlertManager.tsx"; +import { Button } from "@/components/button.tsx"; import { getUserInfo, getDatabaseHealth, @@ -19,21 +19,21 @@ import { getGuacamoleTokenFromHost, isCurrentAuthInvalidationError, type RecentActivityItem, -} from "@/ui/main-axios.ts"; -import { useSidebar } from "@/components/ui/sidebar.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; -import { Kbd } from "@/components/ui/kbd"; +} from "@/main-axios.ts"; +import { useSidebar } from "@/components/sidebar.tsx"; +import { Separator } from "@/components/separator.tsx"; +import { useTabs } from "@/shell/TabContext.tsx"; +import { Kbd } from "@/components/kbd"; import { useTranslation } from "react-i18next"; import { Settings as SettingsIcon } from "lucide-react"; -import { ServerOverviewCard } from "@/ui/desktop/apps/dashboard/cards/ServerOverviewCard"; -import { RecentActivityCard } from "@/ui/desktop/apps/dashboard/cards/RecentActivityCard"; -import { QuickActionsCard } from "@/ui/desktop/apps/dashboard/cards/QuickActionsCard"; -import { ServerStatsCard } from "@/ui/desktop/apps/dashboard/cards/ServerStatsCard"; -import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard"; -import { useDashboardPreferences } from "@/ui/desktop/apps/dashboard/hooks/useDashboardPreferences"; -import { DashboardSettingsDialog } from "@/ui/desktop/apps/dashboard/components/DashboardSettingsDialog"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader"; +import { ServerOverviewCard } from "@/dashboard/cards/ServerOverviewCard"; +import { RecentActivityCard } from "@/dashboard/cards/RecentActivityCard"; +import { QuickActionsCard } from "@/dashboard/cards/QuickActionsCard"; +import { ServerStatsCard } from "@/dashboard/cards/ServerStatsCard"; +import { NetworkGraphCard } from "@/dashboard/cards/NetworkGraphCard"; +import { useDashboardPreferences } from "@/dashboard/hooks/useDashboardPreferences"; +import { DashboardSettingsDialog } from "@/dashboard/components/DashboardSettingsDialog"; +import { SimpleLoader } from "@/lib/SimpleLoader"; interface DashboardProps { onSelectView: (view: string) => void; @@ -101,6 +101,34 @@ export function Dashboard({ resetLayout, } = useDashboardPreferences(loggedIn); + const containerRef = useRef(null); + const mainWidthPct = layout?.mainWidthPct ?? 68; + + const handleDividerMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + const startX = e.clientX; + const startPct = mainWidthPct; + const onMove = (ev: MouseEvent) => { + if (!containerRef.current) return; + const totalW = containerRef.current.getBoundingClientRect().width; + const delta = ev.clientX - startX; + const newPct = Math.min( + 85, + Math.max(30, startPct + (delta / totalW) * 100), + ); + if (layout) updateLayout({ ...layout, mainWidthPct: newPct }); + }; + const onUp = () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, + [mainWidthPct, layout, updateLayout], + ); + let sidebarState: "expanded" | "collapsed" = "expanded"; try { const sidebar = useSidebar(); @@ -683,79 +711,130 @@ export function Dashboard({ -
    - {!preferencesLoading && layout && ( -
    - {layout.cards +
    + {!preferencesLoading && + layout && + (() => { + const enabledCards = layout.cards .filter((card) => card.enabled) - .sort((a, b) => a.order - b.order) - .map((card) => { - if (card.id === "server_overview") { - return ( - a.order - b.order); + + const mainCards = enabledCards.filter( + (c) => (c.panel ?? "main") === "main", + ); + const sideCards = enabledCards.filter( + (c) => c.panel === "side", + ); + + const renderCard = (card: (typeof enabledCards)[0]) => { + if (card.id === "server_overview") { + return ( + + ); + } else if (card.id === "recent_activity") { + return ( + + ); + } else if (card.id === "network_graph") { + return ( + + ); + } else if (card.id === "quick_actions") { + return ( + + ); + } else if (card.id === "server_stats") { + return ( + + ); + } + return null; + }; + + return ( + <> +
    + {mainCards.map((card) => ( +
    - ); - } else if (card.id === "recent_activity") { - return ( - + {renderCard(card)} +
    + ))} +
    + +
    +
    +
    + +
    + {sideCards.map((card) => ( +
    - ); - } else if (card.id === "network_graph") { - return ( - - ); - } else if (card.id === "quick_actions") { - return ( - - ); - } else if (card.id === "server_stats") { - return ( - - ); - } - return null; - })} -
    - )} + style={ + card.height + ? { height: card.height, flexShrink: 0 } + : { flex: 1, minHeight: 280 } + } + > + {renderCard(card)} +
    + ))} +
    + + ); + })()}
    diff --git a/src/ui/dashboard/DashboardTab.tsx b/src/ui/dashboard/DashboardTab.tsx new file mode 100644 index 00000000..ea3c8f7b --- /dev/null +++ b/src/ui/dashboard/DashboardTab.tsx @@ -0,0 +1,1471 @@ +import { useState, useRef, useCallback, useEffect } from "react"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Button } from "@/components/button"; +import { Card } from "@/components/card"; +import { Separator } from "@/components/separator"; +import { + Activity, + Database, + GripHorizontal, + GripVertical, + KeyRound, + LayoutDashboard, + Network, + Plus, + RefreshCw, + Server, + Settings, + Terminal, + Trash2, + User, + Zap, +} from "lucide-react"; +import CytoscapeComponent from "react-cytoscapejs"; +import { Kbd } from "@/components/kbd"; +import { DASHBOARD_CARDS } from "@/lib/theme"; +import type { DashboardCardId, TabType, Host } from "@/types/ui-types"; +import { + getSSHHosts, + getUptime, + getRecentActivity, + getTunnelStatuses, + getCredentials, + resetRecentActivity, +} from "@/main-axios"; +import type { RecentActivityItem, SSHHostWithStatus } from "@/main-axios"; + +function sshHostToHost(h: SSHHostWithStatus): Host { + return { + id: String(h.id), + name: h.name, + user: h.username, + address: h.ip, + port: h.port, + folder: h.folder ?? "", + online: h.status === "online", + cpu: 0, + ram: 0, + lastAccess: "", + tags: h.tags ?? [], + authType: h.authType, + password: h.password, + key: typeof h.key === "string" ? h.key : undefined, + keyPassword: h.keyPassword, + keyType: h.keyType, + credentialId: h.credentialId != null ? String(h.credentialId) : undefined, + notes: h.notes, + pin: h.pin ?? false, + macAddress: h.macAddress, + enableTerminal: h.enableTerminal ?? true, + enableTunnel: h.enableTunnel ?? false, + enableFileManager: h.enableFileManager ?? false, + enableDocker: h.enableDocker ?? false, + enableSsh: h.connectionType === "ssh" || !h.connectionType, + enableRdp: h.connectionType === "rdp", + enableVnc: h.connectionType === "vnc", + enableTelnet: h.connectionType === "telnet", + sshPort: h.port, + rdpPort: 3389, + vncPort: 5900, + telnetPort: 23, + quickActions: (h.quickActions ?? []).map((a) => ({ + name: a.name, + snippetId: String(a.snippetId), + })), + serverTunnels: [], + defaultPath: h.defaultPath, + terminalConfig: h.terminalConfig as Host["terminalConfig"], + useSocks5: h.useSocks5, + socks5Host: h.socks5Host, + socks5Port: h.socks5Port, + socks5Username: h.socks5Username, + socks5Password: h.socks5Password, + }; +} + +// ─── Types ──────────────────────────────────────────────────────────────────── + +type PanelId = "main" | "side"; + +type CardSlot = { + id: DashboardCardId; + panel: PanelId; + order: number; + height: number | null; +}; + +type DragState = { + id: DashboardCardId; + sourcePanel: PanelId; + sourceOrder: number; +} | null; + +// ─── Default layout ─────────────────────────────────────────────────────────── + +const DEFAULT_SLOTS: CardSlot[] = [ + { id: "stats_bar", panel: "main", order: 0, height: 96 }, + { id: "counters_bar", panel: "main", order: 1, height: 48 }, + { id: "quick_actions", panel: "main", order: 2, height: 160 }, + { id: "host_status", panel: "main", order: 3, height: null }, + { id: "recent_activity", panel: "side", order: 0, height: null }, +]; + +const CARD_META: Record = { + stats_bar: { label: "Status Bar" }, + counters_bar: { label: "Counters" }, + quick_actions: { label: "Quick Actions" }, + host_status: { label: "Host Status" }, + recent_activity: { label: "Recent Activity" }, + network_graph: { label: "Network Graph" }, +}; + +// ─── useColumnResize ────────────────────────────────────────────────────────── + +function useColumnResize( + containerRef: React.RefObject, + mainWidthPct: number, + setMainWidthPct: (v: number) => void, +) { + const dragging = useRef(false); + const startX = useRef(0); + const startPct = useRef(0); + + const onMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + dragging.current = true; + startX.current = e.clientX; + startPct.current = mainWidthPct; + + const onMove = (ev: MouseEvent) => { + if (!dragging.current || !containerRef.current) return; + const totalW = containerRef.current.getBoundingClientRect().width; + const delta = ev.clientX - startX.current; + const newPct = Math.min( + 85, + Math.max(30, startPct.current + (delta / totalW) * 100), + ); + setMainWidthPct(newPct); + }; + const onUp = () => { + dragging.current = false; + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, + [containerRef, mainWidthPct, setMainWidthPct], + ); + + return onMouseDown; +} + +// ─── Card components ────────────────────────────────────────────────────────── + +function StatsBarCard({ + hosts, + uptimeFormatted, +}: { + hosts: Host[]; + uptimeFormatted: string; +}) { + const online = hosts.filter((h) => h.online).length; + return ( + +
    + + Version + + + v2.2.0 + + + STABLE + +
    +
    + + Uptime + + + {uptimeFormatted || "—"} + +
    +
    + + Database + + + Healthy + +
    +
    + + Hosts Online + +
    + {online} + + /{hosts.length} + +
    +
    +
    + ); +} + +function CountersBarCard({ + hosts, + credentialCount, + activeTunnelCount, +}: { + hosts: Host[]; + credentialCount: number; + activeTunnelCount: number; +}) { + return ( + +
    + + {hosts.length} + + Total Hosts + +
    +
    + + {credentialCount} + + Credentials + +
    +
    + + {activeTunnelCount} + + Active Tunnels + +
    +
    + ); +} + +function QuickActionsCard({ + onOpenSingletonTab, +}: { + onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; +}) { + return ( + +
    + + + Quick Actions + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + ); +} + +function HostStatusCard({ + hosts, + onOpenTab, +}: { + hosts: Host[]; + onOpenTab: (host: Host, type: TabType) => void; +}) { + const online = hosts.filter((h) => h.online).length; + return ( + +
    +
    + + + Host Status + +
    + + {online}/{hosts.length} online + +
    +
    + {hosts.length === 0 && ( +
    + No hosts configured +
    + )} + {hosts.map((host, i) => ( +
    onOpenTab(host, "stats")} + className="flex items-center justify-between px-4 py-2.5 border-b border-border last:border-0 hover:bg-muted/50 cursor-pointer" + > +
    + +
    + {host.name} + + {host.ip} + +
    +
    +
    + {host.online ? ( +
    +
    +
    + + CPU + + + {host.cpu ?? 0}% + +
    +
    +
    +
    +
    +
    +
    + + RAM + + + {host.ram ?? 0}% + +
    +
    +
    +
    +
    +
    + ) : ( +
    + + — + + + — + +
    + )} + + {host.online ? "ONLINE" : "OFFLINE"} + +
    +
    + ))} +
    + + ); +} + +function RecentActivityCard({ + activity, + hosts, + onOpenTab, + onClear, +}: { + activity: RecentActivityItem[]; + hosts: Host[]; + onOpenTab: (host: Host, type: TabType) => void; + onClear: () => void; +}) { + const typeIcon: Record = { + terminal: , + file_manager: , + server_stats: , + tunnel: , + docker: , + rdp: , + vnc: , + telnet: , + }; + const typeToTab: Record = { + terminal: "terminal", + file_manager: "files", + server_stats: "stats", + tunnel: "tunnel", + docker: "docker", + rdp: "rdp", + vnc: "vnc", + telnet: "telnet", + }; + function formatTime(ts: string) { + const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); + if (diff < 60) return `${diff}s ago`; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; + } + return ( + +
    +
    + + + Recent Activity + +
    + +
    +
    + {activity.length === 0 && ( +
    + No recent activity +
    + )} + {activity.map((item) => { + const host = hosts.find((h) => h.id === String(item.hostId)); + return ( +
    { + if (host) onOpenTab(host, typeToTab[item.type]); + }} + className="flex items-center justify-between px-4 py-2 border-b border-border last:border-0 hover:bg-muted/50 cursor-pointer" + > +
    + +
    + + {item.hostName} + +
    + {typeIcon[item.type]} + + {item.type.replace("_", " ")} + +
    +
    +
    + + {formatTime(item.timestamp)} + +
    + ); + })} +
    +
    + ); +} + +function NetworkGraphCard({ hosts }: { hosts: Host[] }) { + const cyRef = useRef(null); + const [contextMenu, setContextMenu] = useState<{ + visible: boolean; + x: number; + y: number; + node: any; + } | null>(null); + const contextMenuRef = useRef(null); + + const elements = hosts.map((h, i) => ({ + data: { + id: h.id, + label: h.name, + ip: `${h.ip}:${h.port ?? 22}`, + status: h.online ? "online" : "offline", + }, + position: { x: 120 + (i % 4) * 160, y: 80 + Math.floor(i / 4) * 100 }, + })); + + const buildNodeStyle = useCallback((ele: any) => { + const isOnline = ele.data("status") === "online"; + const name = ele.data("label") || ""; + const ip = ele.data("ip") || ""; + const statusColor = isOnline ? "rgb(251,146,60)" : "rgb(100,116,139)"; + const svg = ` + + + + + + ${name} + ${ip} + `; + return "data:image/svg+xml;utf8," + encodeURIComponent(svg); + }, []); + + const handleCyInit = useCallback( + (cy: any) => { + cyRef.current = cy; + cy.style() + .selector("node") + .style({ + label: "", + width: "160px", + height: "72px", + shape: "round-rectangle", + "border-width": "0px", + "background-opacity": 0, + "background-image": buildNodeStyle, + "background-fit": "contain", + }) + .selector("edge") + .style({ + width: "1.5px", + "line-color": "#2a2a2c", + "curve-style": "bezier", + "target-arrow-shape": "none", + }) + .selector("node:selected") + .style({ + "overlay-color": "#fb923c", + "overlay-opacity": 0.08, + "overlay-padding": "4px", + }) + .update(); + cy.nodes().ungrabify(); + cy.on("tap", (evt: any) => { + if (evt.target === cy) setContextMenu(null); + }); + cy.on("cxttap tap", "node", (evt: any) => { + evt.stopPropagation(); + const node = evt.target; + setContextMenu({ + visible: true, + x: evt.originalEvent.clientX, + y: evt.originalEvent.clientY, + node: node.data(), + }); + }); + cy.on("zoom pan", () => setContextMenu(null)); + }, + [buildNodeStyle], + ); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if ( + contextMenuRef.current && + !contextMenuRef.current.contains(e.target as Node) + ) + setContextMenu(null); + }; + document.addEventListener("mousedown", handler, true); + return () => document.removeEventListener("mousedown", handler, true); + }, []); + + return ( + +
    +
    + + + Network Graph + +
    +
    + + {hosts.length} nodes + + +
    +
    +
    + {contextMenu?.visible && ( +
    +
    + + {contextMenu.node.label} + + + {contextMenu.node.ip} + +
    + + +
    + )} + {hosts.length > 0 ? ( + + ) : ( +
    + No hosts to display +
    + )} +
    +
    + + + Online + +
    +
    + + + Offline + +
    +
    +
    +
    + ); +} + +// ─── CardItem ───────────────────────────────────────────────────────────────── + +function CardItem({ + slot, + editMode, + isDragging, + onDragStart, + onDrop, + onDragOver, + onRemove, + onHeightChange, + onOpenSingletonTab, + onOpenTab, + hosts, + uptimeFormatted, + credentialCount, + activeTunnelCount, + activity, + onClearActivity, +}: { + slot: CardSlot; + editMode: boolean; + isDragging: boolean; + onDragStart: () => void; + onDrop: () => void; + onDragOver: (e: React.DragEvent) => void; + onRemove: () => void; + onHeightChange: (id: DashboardCardId, h: number) => void; + onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; + onOpenTab: (host: Host, type: TabType) => void; + hosts: Host[]; + uptimeFormatted: string; + credentialCount: number; + activeTunnelCount: number; + activity: RecentActivityItem[]; + onClearActivity: () => void; +}) { + const cardRef = useRef(null); + + const onResizeMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const startY = e.clientY; + const startH = cardRef.current?.getBoundingClientRect().height ?? 100; + const onMove = (ev: MouseEvent) => { + onHeightChange(slot.id, Math.max(50, startH + (ev.clientY - startY))); + }; + const onUp = () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, + [slot.id, onHeightChange], + ); + + const isFlex = slot.height === null; + const cardProps = { + hosts, + uptimeFormatted, + credentialCount, + activeTunnelCount, + activity, + onOpenTab, + onOpenSingletonTab, + }; + + return ( +
    + {editMode && ( +
    + )} + {editMode && ( +
    +
    + +
    + +
    + )} +
    + {slot.id === "stats_bar" && ( + + )} + {slot.id === "counters_bar" && ( + + )} + {slot.id === "quick_actions" && ( + + )} + {slot.id === "host_status" && ( + + )} + {slot.id === "recent_activity" && ( + + )} + {slot.id === "network_graph" && ( + + )} +
    + {editMode && !isFlex && ( +
    +
    +
    + )} +
    + ); +} + +// ─── DropZone ───────────────────────────────────────────────────────────────── + +function DropZone({ + panel, + order, + onDrop, + onDragOver, + active, +}: { + panel: PanelId; + order: number; + onDrop: (panel: PanelId, order: number) => void; + onDragOver: (e: React.DragEvent) => void; + active: boolean; +}) { + const [over, setOver] = useState(false); + if (!active) return null; + return ( +
    { + onDragOver(e); + setOver(true); + }} + onDragLeave={() => setOver(false)} + onDrop={() => { + setOver(false); + onDrop(panel, order); + }} + /> + ); +} + +// ─── AddCardTray ────────────────────────────────────────────────────────────── + +function AddCardTray({ + activeIds, + onAdd, +}: { + activeIds: DashboardCardId[]; + onAdd: (id: DashboardCardId) => void; +}) { + const available = DASHBOARD_CARDS.filter((c) => !activeIds.includes(c.id)); + if (available.length === 0) return null; + return ( +
    + + Add: + + {available.map((card) => ( + + ))} +
    + ); +} + +// ─── PanelColumn ───────────────────────────────────────────────────────────── + +type PanelColumnProps = { + panel: PanelId; + slots: CardSlot[]; + editMode: boolean; + dragState: DragState; + onDragStart: (slot: CardSlot) => void; + onDrop: (targetPanel: PanelId, targetOrder: number) => void; + onDragOver: (e: React.DragEvent) => void; + onRemove: (id: DashboardCardId) => void; + onAdd: (id: DashboardCardId, panel: PanelId) => void; + onHeightChange: (id: DashboardCardId, h: number) => void; + onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; + onOpenTab: (host: Host, type: TabType) => void; + hosts: Host[]; + uptimeFormatted: string; + credentialCount: number; + activeTunnelCount: number; + activity: RecentActivityItem[]; + onClearActivity: () => void; +}; + +function PanelColumn({ + panel, + slots, + editMode, + dragState, + onDragStart, + onDrop, + onDragOver, + onRemove, + onAdd, + onHeightChange, + onOpenSingletonTab, + onOpenTab, + hosts, + uptimeFormatted, + credentialCount, + activeTunnelCount, + activity, + onClearActivity, +}: PanelColumnProps) { + const sorted = [...slots].sort((a, b) => a.order - b.order); + const allIds = slots.map((s) => s.id); + + return ( +
    + + {sorted.map((slot, idx) => ( +
    + {idx > 0 && ( +
    + +
    + )} + onDragStart(slot)} + onDrop={() => onDrop(slot.panel, slot.order)} + onDragOver={onDragOver} + onRemove={() => onRemove(slot.id)} + onHeightChange={onHeightChange} + onOpenSingletonTab={onOpenSingletonTab} + onOpenTab={onOpenTab} + hosts={hosts} + uptimeFormatted={uptimeFormatted} + credentialCount={credentialCount} + activeTunnelCount={activeTunnelCount} + activity={activity} + onClearActivity={onClearActivity} + /> +
    + ))} + + {editMode && ( + onAdd(id, panel)} /> + )} + {sorted.length === 0 && !editMode && ( +
    + Empty +
    + )} +
    + ); +} + +function ColumnDivider({ + onMouseDown, +}: { + onMouseDown: (e: React.MouseEvent) => void; +}) { + return ( +
    +
    +
    + +
    +
    + ); +} + +// ─── DashboardTab ───────────────────────────────────────────────────────────── + +export function DashboardTab({ + onOpenSingletonTab, + onOpenTab, +}: { + onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; + onOpenTab: (host: Host, type: TabType) => void; +}) { + const [slots, setSlots] = useState(DEFAULT_SLOTS); + const [editMode, setEditMode] = useState(false); + const [dragState, setDragState] = useState(null); + const [mainWidthPct, setMainWidthPct] = useState(68); + + const [hosts, setHosts] = useState([]); + const [uptimeFormatted, setUptimeFormatted] = useState(""); + const [credentialCount, setCredentialCount] = useState(0); + const [activeTunnelCount, setActiveTunnelCount] = useState(0); + const [activity, setActivity] = useState([]); + + useEffect(() => { + getSSHHosts() + .then((raw) => setHosts(raw.map(sshHostToHost))) + .catch(() => {}); + getUptime() + .then((u) => setUptimeFormatted(u.formatted)) + .catch(() => {}); + getRecentActivity(50) + .then(setActivity) + .catch(() => {}); + getCredentials() + .then((res: any) => + setCredentialCount( + Array.isArray(res?.credentials) ? res.credentials.length : 0, + ), + ) + .catch(() => {}); + getTunnelStatuses() + .then((statuses) => { + const active = Object.values(statuses ?? {}).filter( + (s: any) => s?.status === "CONNECTED", + ).length; + setActiveTunnelCount(active); + }) + .catch(() => {}); + }, []); + + const handleClearActivity = async () => { + try { + await resetRecentActivity(); + setActivity([]); + } catch { + /* ignore */ + } + }; + + const todayLabel = new Date().toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }); + const bodyRef = useRef(null); + + const mainSlots = slots + .filter((s) => s.panel === "main") + .sort((a, b) => a.order - b.order); + const sideSlots = slots + .filter((s) => s.panel === "side") + .sort((a, b) => a.order - b.order); + const hasSide = sideSlots.length > 0; + + const onColumnDividerMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + const startX = e.clientX; + const startPct = mainWidthPct; + const onMove = (ev: MouseEvent) => { + if (!bodyRef.current) return; + const totalW = bodyRef.current.getBoundingClientRect().width; + setMainWidthPct( + Math.min( + 85, + Math.max(25, startPct + ((ev.clientX - startX) / totalW) * 100), + ), + ); + }; + const onUp = () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, + [mainWidthPct], + ); + + const handleDragStart = (slot: CardSlot) => + setDragState({ + id: slot.id, + sourcePanel: slot.panel, + sourceOrder: slot.order, + }); + const handleDragOver = (e: React.DragEvent) => e.preventDefault(); + const handleDrop = (targetPanel: PanelId, targetOrder: number) => { + if (!dragState) return; + setSlots((prev) => { + const without = prev.filter((s) => s.id !== dragState.id); + const panelSlots = without + .filter((s) => s.panel === targetPanel) + .sort((a, b) => a.order - b.order); + const others = without.filter((s) => s.panel !== targetPanel); + const insertIdx = panelSlots.findIndex((s) => s.order > targetOrder); + const insertAt = insertIdx === -1 ? panelSlots.length : insertIdx; + const newPanelSlots = [ + ...panelSlots.slice(0, insertAt), + { + id: dragState.id, + panel: targetPanel, + order: 0, + height: prev.find((s) => s.id === dragState.id)?.height ?? null, + }, + ...panelSlots.slice(insertAt), + ].map((s, i) => ({ ...s, order: i })); + return [...others, ...newPanelSlots]; + }); + setDragState(null); + }; + const handleRemove = (id: DashboardCardId) => + setSlots((prev) => prev.filter((s) => s.id !== id)); + const handleAdd = (id: DashboardCardId, panel: PanelId) => { + setSlots((prev) => { + const panelSlots = prev.filter((s) => s.panel === panel); + const maxOrder = + panelSlots.length > 0 + ? Math.max(...panelSlots.map((s) => s.order)) + 1 + : 0; + const defaultHeight: number | null = + id === "host_status" || + id === "recent_activity" || + id === "network_graph" + ? null + : 150; + return [...prev, { id, panel, order: maxOrder, height: defaultHeight }]; + }); + }; + const handleHeightChange = (id: DashboardCardId, h: number) => + setSlots((prev) => + prev.map((s) => (s.id === id ? { ...s, height: h } : s)), + ); + const handleReset = () => { + setSlots(DEFAULT_SLOTS); + setMainWidthPct(72); + setEditMode(false); + }; + + const columnProps = { + hosts, + uptimeFormatted, + credentialCount, + activeTunnelCount, + activity, + onClearActivity: handleClearActivity, + onOpenSingletonTab, + onOpenTab, + }; + + const isMobile = useIsMobile(); + + if (isMobile) { + const allSlots = [...mainSlots, ...sideSlots]; + return ( +
    +
    + +
    +

    Dashboard

    +

    {todayLabel}

    +
    + +
    + {allSlots.map((slot) => ( +
    + {slot.id === "stats_bar" && ( + + )} + {slot.id === "counters_bar" && ( + + )} + {slot.id === "quick_actions" && ( + + )} + {slot.id === "host_status" && ( + + )} + {slot.id === "recent_activity" && ( + + )} + {slot.id === "network_graph" && ( + + )} +
    + ))} +
    +
    + ); + } + + return ( +
    + +
    +

    Dashboard

    +

    {todayLabel}

    +
    +
    +
    + + Command Palette + +
    + Shift + + + Shift +
    +
    + + + + + {editMode ? ( + <> + + + + ) : ( + + )} +
    +
    + + {editMode && ( +
    + + + Drag cards to reorder · Drag the column divider to resize columns · + Drag the bottom edge of a card to resize its height · Trash to + remove + +
    + )} + +
    +
    + +
    + + {(hasSide || editMode) && + (editMode ? ( + + ) : ( +
    + ))} + + {(hasSide || editMode) && ( +
    + +
    + )} +
    +
    + ); +} diff --git a/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx b/src/ui/dashboard/cards/NetworkGraphCard.tsx similarity index 99% rename from src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx rename to src/ui/dashboard/cards/NetworkGraphCard.tsx index 54434392..9c29c7f9 100644 --- a/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx +++ b/src/ui/dashboard/cards/NetworkGraphCard.tsx @@ -15,24 +15,24 @@ import { type SSHHostWithStatus, type NetworkTopologyEdge, type NetworkTopologyNode, -} from "@/ui/main-axios"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; +} from "@/main-axios"; +import { Button } from "@/components/button"; +import { Badge } from "@/components/badge"; import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogAction, -} from "@/components/ui/alert-dialog"; +} from "@/components/alert-dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +} from "@/components/dialog"; +import { Input } from "@/components/input"; +import { Label } from "@/components/label"; import { Plus, Trash2, @@ -58,21 +58,17 @@ import { ArrowDownUp, } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext"; +import { useTabs } from "@/shell/TabContext"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +} from "@/components/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/popover"; import { cn } from "@/lib/utils"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader"; +import { SimpleLoader } from "@/lib/SimpleLoader"; const AVAILABLE_COLORS = [ { value: "#ef4444", label: "Red" }, diff --git a/src/ui/desktop/apps/dashboard/cards/QuickActionsCard.tsx b/src/ui/dashboard/cards/QuickActionsCard.tsx similarity index 99% rename from src/ui/desktop/apps/dashboard/cards/QuickActionsCard.tsx rename to src/ui/dashboard/cards/QuickActionsCard.tsx index 1bc1bccf..1d2ce32b 100644 --- a/src/ui/desktop/apps/dashboard/cards/QuickActionsCard.tsx +++ b/src/ui/dashboard/cards/QuickActionsCard.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { FastForward, Server, Key, Settings, User } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/button"; interface QuickActionsCardProps { isAdmin: boolean; diff --git a/src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx b/src/ui/dashboard/cards/RecentActivityCard.tsx similarity index 97% rename from src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx rename to src/ui/dashboard/cards/RecentActivityCard.tsx index d030be15..80304b5f 100644 --- a/src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx +++ b/src/ui/dashboard/cards/RecentActivityCard.tsx @@ -12,8 +12,8 @@ import { Eye, MessagesSquare, } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { type RecentActivityItem } from "@/ui/main-axios"; +import { Button } from "@/components/button"; +import { type RecentActivityItem } from "@/main-axios"; interface RecentActivityCardProps { activities: RecentActivityItem[]; diff --git a/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx b/src/ui/dashboard/cards/ServerOverviewCard.tsx similarity index 97% rename from src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx rename to src/ui/dashboard/cards/ServerOverviewCard.tsx index 4821e305..775cd3b5 100644 --- a/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx +++ b/src/ui/dashboard/cards/ServerOverviewCard.tsx @@ -8,8 +8,8 @@ import { Key, ArrowDownUp, } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { UpdateLog } from "@/ui/desktop/apps/dashboard/apps/UpdateLog"; +import { Button } from "@/components/button"; +import { UpdateLog } from "@/dashboard/panels/UpdateLog"; interface ServerOverviewCardProps { loggedIn: boolean; diff --git a/src/ui/desktop/apps/dashboard/cards/ServerStatsCard.tsx b/src/ui/dashboard/cards/ServerStatsCard.tsx similarity index 98% rename from src/ui/desktop/apps/dashboard/cards/ServerStatsCard.tsx rename to src/ui/dashboard/cards/ServerStatsCard.tsx index 6e74dc0d..662da0a9 100644 --- a/src/ui/desktop/apps/dashboard/cards/ServerStatsCard.tsx +++ b/src/ui/dashboard/cards/ServerStatsCard.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { ChartLine, Loader2, Server } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/button"; interface ServerStat { id: number; diff --git a/src/ui/desktop/apps/dashboard/components/DashboardSettingsDialog.tsx b/src/ui/dashboard/components/DashboardSettingsDialog.tsx similarity index 72% rename from src/ui/desktop/apps/dashboard/components/DashboardSettingsDialog.tsx rename to src/ui/dashboard/components/DashboardSettingsDialog.tsx index 84da2239..8e89f81b 100644 --- a/src/ui/desktop/apps/dashboard/components/DashboardSettingsDialog.tsx +++ b/src/ui/dashboard/components/DashboardSettingsDialog.tsx @@ -6,12 +6,19 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Checkbox } from "@/components/ui/checkbox"; +} from "@/components/dialog"; +import { Button } from "@/components/button"; +import { Label } from "@/components/label"; +import { Checkbox } from "@/components/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/select"; import { useTranslation } from "react-i18next"; -import type { DashboardLayout } from "@/ui/main-axios"; +import type { DashboardLayout } from "@/main-axios"; interface DashboardSettingsDialogProps { open: boolean; @@ -44,6 +51,15 @@ export function DashboardSettingsDialog({ })); }; + const handleCardPanel = (cardId: string, panel: "main" | "side") => { + setLayout((prev) => ({ + ...prev, + cards: prev.cards.map((card) => + card.id === cardId ? { ...card, panel } : card, + ), + })); + }; + const handleSave = () => { onSave(layout); onOpenChange(false); @@ -96,6 +112,24 @@ export function DashboardSettingsDialog({ > {cardLabels[card.id] || card.id} +
    ))}
    diff --git a/src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts b/src/ui/dashboard/hooks/useDashboardPreferences.ts similarity index 61% rename from src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts rename to src/ui/dashboard/hooks/useDashboardPreferences.ts index f77578bf..3b262d78 100644 --- a/src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts +++ b/src/ui/dashboard/hooks/useDashboardPreferences.ts @@ -3,16 +3,17 @@ import { getDashboardPreferences, saveDashboardPreferences, type DashboardLayout, -} from "@/ui/main-axios"; +} from "@/main-axios"; const DEFAULT_LAYOUT: DashboardLayout = { cards: [ - { id: "server_overview", enabled: true, order: 1 }, - { id: "recent_activity", enabled: true, order: 2 }, - { id: "network_graph", enabled: false, order: 3 }, - { id: "quick_actions", enabled: true, order: 4 }, - { id: "server_stats", enabled: true, order: 5 }, + { id: "server_overview", enabled: true, order: 1, panel: "main" }, + { id: "quick_actions", enabled: true, order: 2, panel: "main" }, + { id: "server_stats", enabled: true, order: 3, panel: "main" }, + { id: "network_graph", enabled: false, order: 4, panel: "main" }, + { id: "recent_activity", enabled: true, order: 1, panel: "side" }, ], + mainWidthPct: 68, }; export function useDashboardPreferences(enabled: boolean = true) { @@ -31,7 +32,25 @@ export function useDashboardPreferences(enabled: boolean = true) { try { const preferences = await getDashboardPreferences(); if (preferences?.cards && Array.isArray(preferences.cards)) { - setLayout(preferences); + // Migrate old layouts that don't have panel assignments + const needsMigration = preferences.cards.some((c) => !c.panel); + if (needsMigration) { + const defaultCardMap = new Map( + DEFAULT_LAYOUT.cards.map((c) => [c.id, c]), + ); + const migrated: DashboardLayout = { + ...preferences, + mainWidthPct: + preferences.mainWidthPct ?? DEFAULT_LAYOUT.mainWidthPct, + cards: preferences.cards.map((c) => ({ + ...c, + panel: c.panel ?? defaultCardMap.get(c.id)?.panel ?? "main", + })), + }; + setLayout(migrated); + } else { + setLayout(preferences); + } } else { setLayout(DEFAULT_LAYOUT); } diff --git a/src/ui/desktop/apps/dashboard/apps/UpdateLog.tsx b/src/ui/dashboard/panels/UpdateLog.tsx similarity index 97% rename from src/ui/desktop/apps/dashboard/apps/UpdateLog.tsx rename to src/ui/dashboard/panels/UpdateLog.tsx index ff54d502..9531449f 100644 --- a/src/ui/desktop/apps/dashboard/apps/UpdateLog.tsx +++ b/src/ui/dashboard/panels/UpdateLog.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from "react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Sheet, SheetContent } from "@/components/ui/sheet.tsx"; -import { getReleasesRSS, getVersionInfo } from "@/ui/main-axios.ts"; +import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx"; +import { Button } from "@/components/button.tsx"; +import { Sheet, SheetContent } from "@/components/sheet.tsx"; +import { getReleasesRSS, getVersionInfo } from "@/main-axios.ts"; import { useTranslation } from "react-i18next"; import { X } from "lucide-react"; diff --git a/src/ui/desktop/apps/dashboard/apps/alerts/AlertCard.tsx b/src/ui/dashboard/panels/alerts/AlertCard.tsx similarity index 94% rename from src/ui/desktop/apps/dashboard/apps/alerts/AlertCard.tsx rename to src/ui/dashboard/panels/alerts/AlertCard.tsx index eaaa3053..31faf754 100644 --- a/src/ui/desktop/apps/dashboard/apps/alerts/AlertCard.tsx +++ b/src/ui/dashboard/panels/alerts/AlertCard.tsx @@ -5,9 +5,9 @@ import { CardFooter, CardHeader, CardTitle, -} from "@/components/ui/card.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; +} from "@/components/card.tsx"; +import { Button } from "@/components/button.tsx"; +import { Badge } from "@/components/badge.tsx"; import { X, ExternalLink, @@ -17,7 +17,7 @@ import { AlertCircle, } from "lucide-react"; import { useTranslation } from "react-i18next"; -import type { TermixAlert } from "../../../../../../types"; +import type { TermixAlert } from "@/types"; interface AlertCardProps { alert: TermixAlert; diff --git a/src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx b/src/ui/dashboard/panels/alerts/AlertManager.tsx similarity index 96% rename from src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx rename to src/ui/dashboard/panels/alerts/AlertManager.tsx index 52b06104..0d15c18c 100644 --- a/src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx +++ b/src/ui/dashboard/panels/alerts/AlertManager.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from "react"; import { AlertCard } from "./AlertCard.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { getUserAlerts, dismissAlert } from "@/ui/main-axios.ts"; +import { Button } from "@/components/button.tsx"; +import { getUserAlerts, dismissAlert } from "@/main-axios.ts"; import { useTranslation } from "react-i18next"; -import type { TermixAlert } from "../../../../../../types"; +import type { TermixAlert } from "@/types"; import { toast } from "sonner"; interface AlertManagerProps { diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx deleted file mode 100644 index 9ab6bca6..00000000 --- a/src/ui/desktop/DesktopApp.tsx +++ /dev/null @@ -1,810 +0,0 @@ -import React, { - useCallback, - Component, - Suspense, - lazy, - type ReactNode, - useEffect, - useRef, - useState, -} from "react"; -import { LeftSidebar } from "@/ui/desktop/navigation/LeftSidebar.tsx"; -import { AppView } from "@/ui/desktop/navigation/AppView.tsx"; -import { - TabProvider, - useTabs, -} from "@/ui/desktop/navigation/tabs/TabContext.tsx"; -import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx"; -import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; -import { ServerStatusProvider } from "@/ui/contexts/ServerStatusContext"; -import { Toaster } from "@/components/ui/sonner.tsx"; -import { toast } from "sonner"; -import { - getUserInfo, - logoutUser, - isCurrentAuthInvalidationError, -} from "@/ui/main-axios.ts"; -import { useTheme } from "@/components/theme-provider"; -import { dbHealthMonitor } from "@/lib/db-health-monitor.ts"; -import { useTranslation } from "react-i18next"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; - -const Dashboard = lazy(() => - import("@/ui/desktop/apps/dashboard/Dashboard.tsx").then((module) => ({ - default: module.Dashboard, - })), -); -const HostManager = lazy(() => - import("@/ui/desktop/apps/host-manager/hosts/HostManager.tsx").then( - (module) => ({ - default: module.HostManager, - }), - ), -); -const AdminSettings = lazy(() => - import("@/ui/desktop/apps/admin/AdminSettings.tsx").then((module) => ({ - default: module.AdminSettings, - })), -); -const UserProfile = lazy(() => - import("@/ui/desktop/user/UserProfile.tsx").then((module) => ({ - default: module.UserProfile, - })), -); -const CommandPalette = lazy(() => - import("@/ui/desktop/apps/command-palette/CommandPalette.tsx").then( - (module) => ({ - default: module.CommandPalette, - }), - ), -); - -function AppContent({ - onAuthStateChange, -}: { - onAuthStateChange?: (isAuthenticated: boolean) => void; -}) { - const { t } = useTranslation(); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [username, setUsername] = useState(null); - const [isAdmin, setIsAdmin] = useState(false); - const [authLoading, setAuthLoading] = useState(true); - const [isTopbarOpen, setIsTopbarOpen] = useState(() => { - const saved = localStorage.getItem("topNavbarOpen"); - return saved !== null ? JSON.parse(saved) : true; - }); - const [isTransitioning, setIsTransitioning] = useState(false); - const [transitionPhase, setTransitionPhase] = useState< - "idle" | "fadeOut" | "fadeIn" - >("idle"); - const { currentTab, tabs, updateTab, addTab } = useTabs(); - const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); - const { theme, setTheme } = useTheme(); - const [rightSidebarOpen, setRightSidebarOpen] = useState(false); - const [rightSidebarWidth, setRightSidebarWidth] = useState(400); - const isAuthenticatedRef = useRef(false); - - const isDarkMode = - theme === "dark" || - theme === "dracula" || - theme === "gentlemansChoice" || - theme === "midnightEspresso" || - theme === "catppuccinMocha" || - (theme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - const lineColor = isDarkMode ? "#151517" : "#f9f9f9"; - - const lastShiftPressTime = useRef(0); - - const lastAltPressTime = useRef(0); - - useEffect(() => { - const DEGRADED_TOAST_ID = "db-connection-degraded"; - - const handleDatabaseConnectionDegraded = () => { - // Non-blocking, non-dismissible status toast that stays visible until - // connectivity is recovered. A Reload action lets users force-refresh - // the page if they want to, but the app itself remains fully usable. - toast.loading( - t("common.connectionDegraded", "Server connection lost, recovering…"), - { - id: DEGRADED_TOAST_ID, - duration: Infinity, - dismissible: false, - closeButton: false, - action: { - label: t("common.reload", "Reload"), - onClick: () => window.location.reload(), - }, - }, - ); - }; - - const handleDatabaseConnectionDegradedCleared = () => { - toast.dismiss(DEGRADED_TOAST_ID); - toast.success(t("common.backendReconnected")); - }; - - const handleSessionExpired = () => { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - }; - - dbHealthMonitor.on( - "database-connection-degraded", - handleDatabaseConnectionDegraded, - ); - dbHealthMonitor.on( - "database-connection-degraded-cleared", - handleDatabaseConnectionDegradedCleared, - ); - dbHealthMonitor.on("session-expired", handleSessionExpired); - - return () => { - dbHealthMonitor.off( - "database-connection-degraded", - handleDatabaseConnectionDegraded, - ); - dbHealthMonitor.off( - "database-connection-degraded-cleared", - handleDatabaseConnectionDegradedCleared, - ); - dbHealthMonitor.off("session-expired", handleSessionExpired); - toast.dismiss(DEGRADED_TOAST_ID); - }; - }, [t]); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.code === "ShiftLeft") { - if (event.repeat) { - return; - } - const shortcutEnabled = - localStorage.getItem("commandPaletteShortcutEnabled") !== "false"; - if (!shortcutEnabled) { - return; - } - const now = Date.now(); - if (now - lastShiftPressTime.current < 300) { - setIsCommandPaletteOpen((isOpen) => !isOpen); - lastShiftPressTime.current = 0; - } else { - lastShiftPressTime.current = now; - } - } - - if (event.code === "AltLeft" && !event.repeat) { - const now = Date.now(); - if (now - lastAltPressTime.current < 300) { - const currentIsDark = - theme === "dark" || - (theme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - const newTheme = currentIsDark ? "light" : "dark"; - setTheme(newTheme); - lastAltPressTime.current = 0; - } else { - lastAltPressTime.current = now; - } - } - - if (event.key === "Escape") { - setIsCommandPaletteOpen(false); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [theme, setTheme]); - - useEffect(() => { - const path = window.location.pathname; - const terminalMatch = path.match(/^\/terminal\/([a-zA-Z0-9_-]+)$/); - const legacyMatch = path.match(/^\/hosts\/([a-zA-Z0-9_-]+)\/terminal$/); - const hostIdentifier = terminalMatch?.[1] || legacyMatch?.[1]; - - if (hostIdentifier) { - const openTerminal = async () => { - try { - const { getSSHHostById, getSSHHosts } = - await import("@/ui/main-axios.ts"); - let host = null; - - if (/^\d+$/.test(hostIdentifier)) { - host = await getSSHHostById(parseInt(hostIdentifier, 10)); - } else { - const hosts = await getSSHHosts(); - host = - hosts.find((h: { name?: string }) => h.name === hostIdentifier) || - null; - } - - if (host) { - addTab({ - type: "terminal", - title: host.name || host.ip, - data: { host, initialCommand: "" }, - }); - window.history.replaceState({}, "", "/"); - } else { - toast.error(`Host "${hostIdentifier}" not found`); - } - } catch (error) { - console.error("Failed to open terminal:", error); - toast.error("Failed to open terminal for host"); - } - }; - openTerminal(); - } - }, [addTab]); - - const isCheckingAuth = useRef(false); - const clientTunnelAutoStartStarted = useRef(false); - - const startClientTunnelAutoStart = useCallback(() => { - if ( - clientTunnelAutoStartStarted.current || - !window.electronAPI?.isElectron - ) { - return; - } - - clientTunnelAutoStartStarted.current = true; - window.electronAPI.startC2SAutoStartTunnels?.().catch((error) => { - clientTunnelAutoStartStarted.current = false; - console.error("Failed to start client tunnel auto-start entries:", error); - }); - }, []); - - useEffect(() => { - const checkAuth = () => { - if (isCheckingAuth.current) return; - isCheckingAuth.current = true; - setAuthLoading(true); - getUserInfo() - .then((meRes) => { - if (typeof meRes === "string" || !meRes.username) { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - } else { - setIsAuthenticated(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - startClientTunnelAutoStart(); - } - }) - .catch((err) => { - if (isCurrentAuthInvalidationError(err)) { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - console.warn("Session expired - please log in again"); - return; - } - - if (!isAuthenticatedRef.current) { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - } - }) - .finally(() => { - setAuthLoading(false); - isCheckingAuth.current = false; - }); - }; - - checkAuth(); - - const handleStorageChange = () => checkAuth(); - window.addEventListener("storage", handleStorageChange); - - return () => window.removeEventListener("storage", handleStorageChange); - }, [startClientTunnelAutoStart]); - - useEffect(() => { - localStorage.setItem("topNavbarOpen", JSON.stringify(isTopbarOpen)); - }, [isTopbarOpen]); - - useEffect(() => { - onAuthStateChange?.(isAuthenticated); - isAuthenticatedRef.current = isAuthenticated; - }, [isAuthenticated, onAuthStateChange]); - - const handleAuthSuccess = useCallback( - (authData: { - isAdmin: boolean; - username: string | null; - userId: string | null; - }) => { - setIsTransitioning(true); - setTransitionPhase("fadeOut"); - - setTimeout(() => { - setIsAuthenticated(true); - setIsAdmin(authData.isAdmin); - setUsername(authData.username); - startClientTunnelAutoStart(); - setTransitionPhase("fadeIn"); - - setTimeout(() => { - setIsTransitioning(false); - setTransitionPhase("idle"); - }, 800); - }, 1200); - }, - [startClientTunnelAutoStart], - ); - - const handleLogout = useCallback(async () => { - setIsTransitioning(true); - setTransitionPhase("fadeOut"); - - setTimeout(async () => { - try { - await logoutUser(); - } catch (error) { - console.error("Logout failed:", error); - } - - window.location.reload(); - }, 1200); - }, []); - - const currentTabData = tabs.find((tab) => tab.id === currentTab); - const showTerminalView = - currentTabData?.type === "terminal" || - currentTabData?.type === "server_stats" || - currentTabData?.type === "file_manager" || - currentTabData?.type === "rdp" || - currentTabData?.type === "vnc" || - currentTabData?.type === "telnet" || - currentTabData?.type === "tunnel" || - currentTabData?.type === "docker" || - currentTabData?.type === "network_graph"; - const showHome = currentTabData?.type === "home"; - const showSshManager = currentTabData?.type === "ssh_manager"; - const showAdmin = currentTabData?.type === "admin"; - const showProfile = currentTabData?.type === "user_profile"; - - if (authLoading) { - return ( -
    -
    -
    -
    -
    -

    - {t("common.checkingAuthentication")} -

    -
    -
    -
    -
    - ); - } - - return ( -
    - - - - {!isAuthenticated && ( -
    - - - -
    - )} - - {isAuthenticated && ( - -
    - -
    - - {showHome && ( -
    - - -
    - } - > - - -
    - )} - - {showSshManager && ( -
    - - -
    - } - > - - -
    - )} - - {showAdmin && ( -
    - - -
    - } - > - - -
    - )} - - {showProfile && ( -
    - - -
    - } - > - - -
    - )} - - { - setRightSidebarOpen(isOpen); - setRightSidebarWidth(width); - }} - /> - - )} - - {isTransitioning && ( -
    - {transitionPhase === "fadeOut" && ( - <> -
    -
    -
    -
    -
    -
    -
    - TERMIX -
    -
    - SSH SERVER MANAGER -
    -
    -
    - - - )} -
    - )} - - -
    - ); -} - -class TabErrorBoundary extends Component< - { children: ReactNode }, - { hasError: boolean; errorCount: number } -> { - constructor(props: { children: ReactNode }) { - super(props); - this.state = { hasError: false, errorCount: 0 }; - } - - static getDerivedStateFromError(error: Error) { - if (error.message?.includes("useTabs must be used within a TabProvider")) { - return { hasError: true }; - } - throw error; - } - - componentDidCatch(error: Error, _errorInfo: ErrorInfo) { - if (error.message?.includes("useTabs must be used within a TabProvider")) { - console.warn( - "TabProvider mounting race condition detected, recovering...", - ); - this.setState((prev) => ({ errorCount: prev.errorCount + 1 })); - setTimeout(() => { - this.setState({ hasError: false }); - }, 0); - } - } - - render() { - if (this.state.hasError) { - return null; - } - return this.props.children; - } -} - -function DesktopApp() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - return ( - - - - - - - - - - ); -} - -export default DesktopApp; diff --git a/src/ui/desktop/apps/command-palette/CommandPalette.tsx b/src/ui/desktop/apps/command-palette/CommandPalette.tsx deleted file mode 100644 index fc11253a..00000000 --- a/src/ui/desktop/apps/command-palette/CommandPalette.tsx +++ /dev/null @@ -1,700 +0,0 @@ -import { - Command, - CommandInput, - CommandItem, - CommandList, - CommandGroup, - CommandSeparator, -} from "@/components/ui/command.tsx"; -import React, { useEffect, useRef, useState } from "react"; -import { cn } from "@/lib/utils"; -import { Kbd, KbdKey, KbdSeparator } from "@/components/ui/kbd"; -import { - Key, - Server, - Settings, - User, - Terminal, - Monitor, - Eye, - MessagesSquare, - FolderOpen, - Pencil, - EllipsisVertical, - ArrowDownUp, - Container, -} from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { BiMoney, BiSupport } from "react-icons/bi"; -import { BsDiscord } from "react-icons/bs"; -import { FaGithub } from "react-icons/fa"; -import { GrUpdate } from "react-icons/gr"; -import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; -import { - getRecentActivity, - getSSHHosts, - getGuacamoleDpi, - getGuacamoleTokenFromHost, - logActivity, -} from "@/ui/main-axios.ts"; -import type { RecentActivityItem } from "@/ui/main-axios.ts"; -import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button.tsx"; -import { ButtonGroup } from "@/components/ui/button-group.tsx"; - -interface SSHHost { - id: number; - name: string; - ip: string; - port: number; - username: string; - folder: string; - tags: string[]; - pin: boolean; - authType: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - enableTerminal: boolean; - enableTunnel: boolean; - enableFileManager: boolean; - enableDocker: boolean; - defaultPath: string; - tunnelConnections: unknown[]; - statsConfig?: string; - createdAt: string; - updatedAt: string; - connectionType?: "ssh" | "rdp" | "vnc" | "telnet"; - domain?: string; - security?: string; - ignoreCert?: boolean; - guacamoleConfig?: unknown; - showTerminalInSidebar?: boolean; - showFileManagerInSidebar?: boolean; - showTunnelInSidebar?: boolean; - showDockerInSidebar?: boolean; - showServerStatsInSidebar?: boolean; -} - -function shouldShowMetrics(host: SSHHost): boolean { - try { - const statsConfig = host.statsConfig - ? JSON.parse(host.statsConfig) - : DEFAULT_STATS_CONFIG; - return statsConfig.metricsEnabled !== false; - } catch { - return true; - } -} - -function hasTunnelConnections(host: SSHHost): boolean { - try { - const tunnelConnections = Array.isArray(host.tunnelConnections) - ? host.tunnelConnections - : JSON.parse(host.tunnelConnections as string); - return Array.isArray(tunnelConnections) && tunnelConnections.length > 0; - } catch { - return false; - } -} - -export function CommandPalette({ - isOpen, - setIsOpen, -}: { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; -}) { - const { t } = useTranslation(); - const inputRef = useRef(null); - const { addTab, setCurrentTab, tabs: tabList, updateTab } = useTabs(); - const [recentActivity, setRecentActivity] = useState( - [], - ); - const [hosts, setHosts] = useState([]); - - useEffect(() => { - if (isOpen) { - inputRef.current?.focus(); - getRecentActivity(50).then((activity) => { - setRecentActivity(activity.slice(0, 5)); - }); - getSSHHosts().then((allHosts) => { - setHosts(allHosts); - }); - } - }, [isOpen]); - - const handleAddHost = () => { - const sshManagerTab = tabList.find((t) => t.type === "ssh_manager"); - if (sshManagerTab) { - updateTab(sshManagerTab.id, { - initialTab: "add_host", - hostConfig: undefined, - }); - setCurrentTab(sshManagerTab.id); - } else { - const id = addTab({ - type: "ssh_manager", - title: t("commandPalette.hostManager"), - initialTab: "add_host", - }); - setCurrentTab(id); - } - setIsOpen(false); - }; - - const handleAddCredential = () => { - const sshManagerTab = tabList.find((t) => t.type === "ssh_manager"); - if (sshManagerTab) { - updateTab(sshManagerTab.id, { - initialTab: "add_credential", - hostConfig: undefined, - }); - setCurrentTab(sshManagerTab.id); - } else { - const id = addTab({ - type: "ssh_manager", - title: t("commandPalette.hostManager"), - initialTab: "add_credential", - }); - setCurrentTab(id); - } - setIsOpen(false); - }; - - const handleOpenAdminSettings = () => { - const adminTab = tabList.find((t) => t.type === "admin"); - if (adminTab) { - setCurrentTab(adminTab.id); - } else { - const id = addTab({ - type: "admin", - title: t("commandPalette.adminSettings"), - }); - setCurrentTab(id); - } - setIsOpen(false); - }; - - const handleOpenUserProfile = () => { - const userProfileTab = tabList.find((t) => t.type === "user_profile"); - if (userProfileTab) { - setCurrentTab(userProfileTab.id); - } else { - const id = addTab({ - type: "user_profile", - title: t("commandPalette.userProfile"), - }); - setCurrentTab(id); - } - setIsOpen(false); - }; - - const handleOpenUpdateLog = () => { - window.open("https://github.com/Termix-SSH/Termix/releases", "_blank"); - setIsOpen(false); - }; - - const handleGitHub = () => { - window.open("https://github.com/Termix-SSH/Termix", "_blank"); - setIsOpen(false); - }; - - const handleSupport = () => { - window.open("https://github.com/Termix-SSH/Support/issues/new", "_blank"); - setIsOpen(false); - }; - - const handleDiscord = () => { - window.open("https://discord.com/invite/jVQGdvHDrf", "_blank"); - setIsOpen(false); - }; - - const handleDonate = () => { - window.open("https://github.com/sponsors/LukeGus", "_blank"); - setIsOpen(false); - }; - - const handleActivityClick = (item: RecentActivityItem) => { - getSSHHosts().then((hosts) => { - const host = hosts.find((h: { id: number }) => h.id === item.hostId); - if (!host) return; - - if (item.type === "terminal") { - addTab({ - type: "terminal", - title: item.hostName, - hostConfig: host, - }); - } else if (item.type === "file_manager") { - addTab({ - type: "file_manager", - title: item.hostName, - hostConfig: host, - }); - } - }); - setIsOpen(false); - }; - - const handleHostTerminalClick = async (host: SSHHost) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; - - if ( - host.connectionType === "rdp" || - host.connectionType === "vnc" || - host.connectionType === "telnet" - ) { - try { - const protocol = host.connectionType as "rdp" | "vnc" | "telnet"; - const result = await getGuacamoleTokenFromHost(host.id); - - addTab({ - type: protocol, - title, - hostConfig: host, - connectionConfig: { - token: result.token, - protocol, - type: protocol, - hostname: host.ip, - port: host.port, - username: host.username, - password: host.password, - domain: host.domain, - security: host.security, - "ignore-cert": host.ignoreCert, - dpi: getGuacamoleDpi(host), - }, - }); - - try { - await logActivity(protocol, host.id, title); - } catch (err) { - console.warn(`Failed to log ${protocol} activity:`, err); - } - } catch (err) { - console.error("Failed to get Guacamole token:", err); - } - setIsOpen(false); - return; - } - - addTab({ type: "terminal", title, hostConfig: host }); - setIsOpen(false); - }; - - const handleHostFileManagerClick = (host: SSHHost) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; - addTab({ type: "file_manager", title, hostConfig: host }); - setIsOpen(false); - }; - - const handleHostServerDetailsClick = (host: SSHHost) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; - addTab({ type: "server_stats", title, hostConfig: host }); - setIsOpen(false); - }; - - const handleHostTunnelClick = (host: SSHHost) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; - addTab({ type: "tunnel", title, hostConfig: host }); - setIsOpen(false); - }; - - const handleHostDockerClick = (host: SSHHost) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; - addTab({ type: "docker", title, hostConfig: host }); - setIsOpen(false); - }; - - const handleHostEditClick = (host: SSHHost) => { - addTab({ - type: "ssh_manager", - title: t("commandPalette.hostManager"), - hostConfig: host, - initialTab: "add_host", - }); - setIsOpen(false); - }; - - return ( -
    setIsOpen(false)} - > - e.stopPropagation()} - > - - - {recentActivity.length > 0 && ( - <> - - {recentActivity.map((item, index) => ( - handleActivityClick(item)} - > - {item.type === "terminal" ? : } - {item.hostName} - - ))} - - - - )} - - - - {t("commandPalette.addHost")} - - - - {t("commandPalette.addCredential")} - - - - {t("commandPalette.adminSettings")} - - - - {t("commandPalette.userProfile")} - - - - {t("commandPalette.updateLog")} - - - - {hosts.length > 0 && ( - <> - - {hosts.map((host, index) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; - - const isSSH = - !host.connectionType || host.connectionType === "ssh"; - const showMetrics = shouldShowMetrics(host); - const hasTunnels = hasTunnelConnections(host); - - const visibleButtons = [ - host.enableTerminal && (host.showTerminalInSidebar ?? true), - isSSH && - host.enableFileManager && - (host.showFileManagerInSidebar ?? false), - isSSH && - host.enableTunnel && - hasTunnels && - (host.showTunnelInSidebar ?? false), - isSSH && - host.enableDocker && - (host.showDockerInSidebar ?? false), - isSSH && - showMetrics && - (host.showServerStatsInSidebar ?? false), - ].filter(Boolean).length; - - return ( - { - if (host.enableTerminal) { - handleHostTerminalClick(host); - } - }} - className="flex items-center justify-between" - > -
    - - {title} -
    - e.stopPropagation()} - > - {host.enableTerminal && - (host.showTerminalInSidebar ?? true) && ( - - )} - - {isSSH && - host.enableFileManager && - (host.showFileManagerInSidebar ?? false) && ( - - )} - - {isSSH && - host.enableTunnel && - hasTunnels && - (host.showTunnelInSidebar ?? false) && ( - - )} - - {isSSH && - host.enableDocker && - (host.showDockerInSidebar ?? false) && ( - - )} - - {isSSH && - showMetrics && - (host.showServerStatsInSidebar ?? false) && ( - - )} - - - - - - - {host.enableTerminal && - !(host.showTerminalInSidebar ?? true) && ( - { - e.stopPropagation(); - handleHostTerminalClick(host); - }} - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" - > - {host.connectionType === "rdp" ? ( - - ) : host.connectionType === "vnc" ? ( - - ) : host.connectionType === "telnet" ? ( - - ) : ( - - )} - - {t("hosts.openTerminal")} - - - )} - {isSSH && - showMetrics && - !(host.showServerStatsInSidebar ?? false) && ( - { - e.stopPropagation(); - handleHostServerDetailsClick(host); - }} - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" - > - - - {t("hosts.openServerStats")} - - - )} - {isSSH && - host.enableFileManager && - !(host.showFileManagerInSidebar ?? false) && ( - { - e.stopPropagation(); - handleHostFileManagerClick(host); - }} - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" - > - - - {t("hosts.openFileManager")} - - - )} - {isSSH && - host.enableTunnel && - hasTunnels && - !(host.showTunnelInSidebar ?? false) && ( - { - e.stopPropagation(); - handleHostTunnelClick(host); - }} - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" - > - - - {t("hosts.openTunnels")} - - - )} - {isSSH && - host.enableDocker && - !(host.showDockerInSidebar ?? false) && ( - { - e.stopPropagation(); - handleHostDockerClick(host); - }} - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" - > - - - {t("hosts.openDocker")} - - - )} - { - e.stopPropagation(); - handleHostEditClick(host); - }} - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" - > - - {t("common.edit")} - - - - -
    - ); - })} -
    - - - )} - - - - {t("commandPalette.github")} - - - - {t("commandPalette.support")} - - - - {t("commandPalette.discord")} - - - - {t("commandPalette.donate")} - - -
    -
    -
    - {t("commandPalette.press")} - - Shift - - Shift - - {t("commandPalette.toToggle")} -
    -
    - {t("commandPalette.close")} - Esc -
    -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/docker/components/ContainerCard.tsx b/src/ui/desktop/apps/features/docker/components/ContainerCard.tsx deleted file mode 100644 index 49e3cb5d..00000000 --- a/src/ui/desktop/apps/features/docker/components/ContainerCard.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import React from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; -import { - Play, - Square, - RotateCw, - Pause, - Trash2, - PlayCircle, -} from "lucide-react"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import type { DockerContainer } from "@/types"; -import { - startDockerContainer, - stopDockerContainer, - restartDockerContainer, - pauseDockerContainer, - unpauseDockerContainer, - removeDockerContainer, -} from "@/ui/main-axios.ts"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip.tsx"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; - -interface ContainerCardProps { - container: DockerContainer; - sessionId: string; - onSelect?: () => void; - isSelected?: boolean; - onRefresh?: () => void; -} - -export function ContainerCard({ - container, - sessionId, - onSelect, - isSelected = false, - onRefresh, -}: ContainerCardProps): React.ReactElement { - const { t } = useTranslation(); - const { confirmWithToast } = useConfirmation(); - const [isStarting, setIsStarting] = React.useState(false); - const [isStopping, setIsStopping] = React.useState(false); - const [isRestarting, setIsRestarting] = React.useState(false); - const [isPausing, setIsPausing] = React.useState(false); - const [isRemoving, setIsRemoving] = React.useState(false); - - const statusColors = { - running: { - bg: "bg-green-500/10", - border: "border-green-500/20", - text: "text-green-400", - badge: "bg-green-500/20 text-green-300 border-green-500/30", - }, - exited: { - bg: "bg-red-500/10", - border: "border-red-500/20", - text: "text-red-400", - badge: "bg-red-500/20 text-red-300 border-red-500/30", - }, - paused: { - bg: "bg-yellow-500/10", - border: "border-yellow-500/20", - text: "text-yellow-400", - badge: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", - }, - created: { - bg: "bg-blue-500/10", - border: "border-blue-500/20", - text: "text-blue-400", - badge: "bg-blue-500/20 text-blue-300 border-blue-500/30", - }, - restarting: { - bg: "bg-orange-500/10", - border: "border-orange-500/20", - text: "text-orange-400", - badge: "bg-orange-500/20 text-orange-300 border-orange-500/30", - }, - removing: { - bg: "bg-purple-500/10", - border: "border-purple-500/20", - text: "text-purple-400", - badge: "bg-purple-500/20 text-purple-300 border-purple-500/30", - }, - dead: { - bg: "bg-muted/10", - border: "border-muted/20", - text: "text-muted-foreground", - badge: "bg-muted/20 text-muted-foreground border-muted/30", - }, - }; - - const colors = statusColors[container.state] || statusColors.created; - - const handleStart = async (e: React.MouseEvent) => { - e.stopPropagation(); - setIsStarting(true); - try { - await startDockerContainer(sessionId, container.id); - toast.success(t("docker.containerStarted", { name: container.name })); - onRefresh?.(); - } catch (error) { - toast.error( - t("docker.failedToStartContainer", { - error: error instanceof Error ? error.message : "Unknown error", - }), - ); - } finally { - setIsStarting(false); - } - }; - - const handleStop = async (e: React.MouseEvent) => { - e.stopPropagation(); - setIsStopping(true); - try { - await stopDockerContainer(sessionId, container.id); - toast.success(t("docker.containerStopped", { name: container.name })); - onRefresh?.(); - } catch (error) { - toast.error( - t("docker.failedToStopContainer", { - error: error instanceof Error ? error.message : "Unknown error", - }), - ); - } finally { - setIsStopping(false); - } - }; - - const handleRestart = async (e: React.MouseEvent) => { - e.stopPropagation(); - setIsRestarting(true); - try { - await restartDockerContainer(sessionId, container.id); - toast.success(t("docker.containerRestarted", { name: container.name })); - onRefresh?.(); - } catch (error) { - toast.error( - t("docker.failedToRestartContainer", { - error: error instanceof Error ? error.message : "Unknown error", - }), - ); - } finally { - setIsRestarting(false); - } - }; - - const handlePause = async (e: React.MouseEvent) => { - e.stopPropagation(); - setIsPausing(true); - try { - if (container.state === "paused") { - await unpauseDockerContainer(sessionId, container.id); - toast.success(t("docker.containerUnpaused", { name: container.name })); - } else { - await pauseDockerContainer(sessionId, container.id); - toast.success(t("docker.containerPaused", { name: container.name })); - } - onRefresh?.(); - } catch (error) { - toast.error( - t("docker.failedToTogglePauseContainer", { - action: container.state === "paused" ? "unpause" : "pause", - error: error instanceof Error ? error.message : "Unknown error", - }), - ); - } finally { - setIsPausing(false); - } - }; - - const handleRemove = async (e: React.MouseEvent) => { - e.stopPropagation(); - const containerName = container.name.startsWith("/") - ? container.name.slice(1) - : container.name; - - let confirmMessage = t("docker.confirmRemoveContainer", { - name: containerName, - }); - - if (container.state === "running") { - confirmMessage += " " + t("docker.runningContainerWarning"); - } - - confirmWithToast( - confirmMessage, - async () => { - setIsRemoving(true); - try { - const force = container.state === "running"; - await removeDockerContainer(sessionId, container.id, force); - toast.success(t("docker.containerRemoved", { name: containerName })); - onRefresh?.(); - } catch (error) { - toast.error( - t("docker.failedToRemoveContainer", { - error: error instanceof Error ? error.message : "Unknown error", - }), - ); - } finally { - setIsRemoving(false); - } - }, - t("common.remove"), - t("common.cancel"), - ); - }; - - const isLoading = - isStarting || isStopping || isRestarting || isPausing || isRemoving; - - const formatCreatedDate = (dateStr: string): string => { - try { - const cleanDate = dateStr.replace(/\s*\+\d{4}\s*UTC\s*$/, "").trim(); - return cleanDate; - } catch { - return dateStr; - } - }; - - const parsePorts = (portsStr: string | undefined): string[] => { - if (!portsStr || portsStr.trim() === "") return []; - - return portsStr - .split(",") - .map((p) => p.trim()) - .filter((p) => p.length > 0); - }; - - const portsList = parsePorts(container.ports); - - return ( - <> - - -
    - - {container.name.startsWith("/") - ? container.name.slice(1) - : container.name} - - - {container.state} - -
    -
    - -
    -
    - - {t("docker.image")} - - - {container.image} - -
    -
    - - {t("docker.idLabel")} - - - {container.id.substring(0, 12)} - -
    -
    - - {t("docker.ports")} - -
    - {portsList.length > 0 ? ( - portsList.map((port, idx) => ( - - {port} - - )) - ) : ( - - {t("docker.noPorts")} - - )} -
    -
    -
    - - {t("docker.created")} - - - {formatCreatedDate(container.created)} - -
    -
    - -
    - - {container.state !== "running" && ( - - - - - {t("docker.start")} - - )} - - {container.state === "running" && ( - - - - - {t("docker.stop")} - - )} - - {(container.state === "running" || - container.state === "paused") && ( - - - - - - {container.state === "paused" - ? t("docker.unpause") - : t("docker.pause")} - - - )} - - - - - - {t("docker.restart")}{" "} - - - - - - - {t("docker.remove")} - - -
    -
    -
    - - ); -} diff --git a/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx b/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx deleted file mode 100644 index 101375fe..00000000 --- a/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; -import { ArrowLeft } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { DockerContainer, SSHHost } from "@/types"; -import { LogViewer } from "./LogViewer.tsx"; -import { ContainerStats } from "./ContainerStats.tsx"; -import { ConsoleTerminal } from "./ConsoleTerminal.tsx"; - -interface ContainerDetailProps { - sessionId: string; - containerId: string; - containers: DockerContainer[]; - hostConfig: SSHHost; - onBack: () => void; -} - -export function ContainerDetail({ - sessionId, - containerId, - containers, - hostConfig, - onBack, -}: ContainerDetailProps): React.ReactElement { - const { t } = useTranslation(); - const [activeTab, setActiveTab] = React.useState("logs"); - - const container = containers.find((c) => c.id === containerId); - - if (!container) { - return ( -
    -
    -

    - {t("docker.containerNotFound")} -

    - -
    -
    - ); - } - - return ( -
    -
    - -
    -

    {container.name}

    -

    - {container.image} -

    -
    -
    - - -
    - -
    - - {t("docker.logs")} - {t("docker.stats")} - - {t("docker.consoleTab")} - - -
    - - - - - - - - - - - - -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/docker/components/ContainerList.tsx b/src/ui/desktop/apps/features/docker/components/ContainerList.tsx deleted file mode 100644 index 6c85e6e1..00000000 --- a/src/ui/desktop/apps/features/docker/components/ContainerList.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from "react"; -import { Input } from "@/components/ui/input.tsx"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select.tsx"; -import { Search, Filter } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { DockerContainer } from "@/types"; -import { ContainerCard } from "./ContainerCard.tsx"; - -interface ContainerListProps { - containers: DockerContainer[]; - sessionId: string; - onSelectContainer: (containerId: string) => void; - selectedContainerId?: string | null; - onRefresh?: () => void; -} - -export function ContainerList({ - containers, - sessionId, - onSelectContainer, - selectedContainerId = null, - onRefresh, -}: ContainerListProps): React.ReactElement { - const { t } = useTranslation(); - const [searchQuery, setSearchQuery] = React.useState(""); - const [statusFilter, setStatusFilter] = React.useState("all"); - - const filteredContainers = React.useMemo(() => { - return containers.filter((container) => { - const matchesSearch = - container.name.toLowerCase().includes(searchQuery.toLowerCase()) || - container.image.toLowerCase().includes(searchQuery.toLowerCase()) || - container.id.toLowerCase().includes(searchQuery.toLowerCase()); - - const matchesStatus = - statusFilter === "all" || container.state === statusFilter; - - return matchesSearch && matchesStatus; - }); - }, [containers, searchQuery, statusFilter]); - - const statusCounts = React.useMemo(() => { - const counts: Record = {}; - containers.forEach((c) => { - counts[c.state] = (counts[c.state] || 0) + 1; - }); - return counts; - }, [containers]); - - if (containers.length === 0) { - return ( -
    -
    -

    - {t("docker.noContainersFound")} -

    -

    - {t("docker.noContainersFoundHint")} -

    -
    -
    - ); - } - - return ( -
    -
    -
    - - setSearchQuery(e.target.value)} - className="pl-10" - /> -
    -
    - - -
    -
    - - {filteredContainers.length === 0 ? ( -
    -
    -

    - {t("docker.noContainersMatchFilters")} -

    -

    - {t("docker.noContainersMatchFiltersHint")} -

    -
    -
    - ) : ( -
    -
    - {filteredContainers.map((container) => ( - onSelectContainer(container.id)} - isSelected={selectedContainerId === container.id} - onRefresh={onRefresh} - /> - ))} -
    -
    - )} -
    - ); -} diff --git a/src/ui/desktop/apps/features/docker/components/ContainerStats.tsx b/src/ui/desktop/apps/features/docker/components/ContainerStats.tsx deleted file mode 100644 index a57566ed..00000000 --- a/src/ui/desktop/apps/features/docker/components/ContainerStats.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import React from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card.tsx"; -import { Progress } from "@/components/ui/progress.tsx"; -import { Cpu, MemoryStick, Network, HardDrive, Activity } from "lucide-react"; -import type { DockerStats } from "@/types"; -import { getContainerStats } from "@/ui/main-axios.ts"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; -import { useTranslation } from "react-i18next"; - -interface ContainerStatsProps { - sessionId: string; - containerId: string; - containerName: string; - containerState: string; -} - -export function ContainerStats({ - sessionId, - containerId, - containerName, - containerState, -}: ContainerStatsProps): React.ReactElement { - const { t } = useTranslation(); - const [stats, setStats] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - const fetchStats = React.useCallback(async () => { - if (containerState !== "running") { - setError(t("docker.containerMustBeRunningToViewStats")); - return; - } - - setIsLoading(true); - setError(null); - try { - const data = await getContainerStats(sessionId, containerId); - setStats(data); - } catch (err) { - setError( - err instanceof Error ? err.message : t("docker.failedToFetchStats"), - ); - } finally { - setIsLoading(false); - } - }, [sessionId, containerId, containerState]); - - React.useEffect(() => { - fetchStats(); - - const interval = setInterval(fetchStats, 2000); - - return () => clearInterval(interval); - }, [fetchStats]); - - if (containerState !== "running") { - return ( -
    -
    - -

    - {t("docker.containerNotRunning")} -

    -

    - {t("docker.startContainerToViewStats")} -

    -
    -
    - ); - } - - if (isLoading && !stats) { - return ( -
    -
    - -

    - {t("docker.loadingStats")} -

    -
    -
    - ); - } - - if (error) { - return ( -
    -
    -

    - {t("docker.errorLoadingStats")} -

    -

    {error}

    -
    -
    - ); - } - - if (!stats) { - return ( -
    -

    {t("docker.noStatsAvailable")}

    -
    - ); - } - - const cpuPercent = parseFloat(stats.cpu) || 0; - const memPercent = parseFloat(stats.memoryPercent) || 0; - - return ( -
    - - - - - {t("docker.cpuUsage")} - - - -
    -
    - - {t("docker.current")} - {" "} - - {stats.cpu} - -
    - -
    -
    -
    - - - - - - {t("docker.memoryUsage")} - - - -
    -
    - - {t("docker.usedLimit")} - - - {stats.memoryUsed} / {stats.memoryLimit} - -
    -
    - - {t("docker.percentage")} - - - {stats.memoryPercent} - -
    - -
    -
    -
    - - - - - - {t("docker.networkIo")} - - - -
    -
    - {t("docker.input")} - {stats.netInput} -
    -
    - - {t("docker.output")} - - - {stats.netOutput} - -
    -
    -
    -
    - - - - - - {t("docker.blockIo")} - - - -
    -
    - {t("docker.read")} - - {stats.blockRead} - -
    -
    - {t("docker.write")} - - {stats.blockWrite} - -
    - {stats.pids && ( -
    - - {t("docker.pids")} - - {stats.pids} -
    - )} -
    -
    -
    - - - - - - {t("docker.containerInformation")} - - - -
    -
    - {t("docker.name")} - {containerName} -
    -
    - {t("docker.id")} - - {containerId.substring(0, 12)} - -
    -
    - {t("docker.state")} - - {containerState} - -
    -
    -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/docker/components/LogViewer.tsx b/src/ui/desktop/apps/features/docker/components/LogViewer.tsx deleted file mode 100644 index 8d016c83..00000000 --- a/src/ui/desktop/apps/features/docker/components/LogViewer.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select.tsx"; -import { Card, CardContent } from "@/components/ui/card.tsx"; -import { Switch } from "@/components/ui/switch.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Download, RefreshCw, Filter } from "lucide-react"; -import { toast } from "sonner"; -import type { DockerLogOptions } from "@/types"; -import { getContainerLogs, downloadContainerLogs } from "@/ui/main-axios.ts"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; - -interface LogViewerProps { - sessionId: string; - containerId: string; - containerName: string; -} - -export function LogViewer({ - sessionId, - containerId, - containerName, -}: LogViewerProps): React.ReactElement { - const [logs, setLogs] = React.useState(""); - const [isLoading, setIsLoading] = React.useState(false); - const [isDownloading, setIsDownloading] = React.useState(false); - const [tailLines, setTailLines] = React.useState("100"); - const [showTimestamps, setShowTimestamps] = React.useState(false); - const [autoRefresh, setAutoRefresh] = React.useState(false); - const [searchFilter, setSearchFilter] = React.useState(""); - const logsEndRef = React.useRef(null); - - const fetchLogs = React.useCallback(async () => { - setIsLoading(true); - try { - const options: DockerLogOptions = { - tail: tailLines === "all" ? undefined : parseInt(tailLines, 10), - timestamps: showTimestamps, - }; - - const data = await getContainerLogs(sessionId, containerId, options); - setLogs(data.logs); - } catch (error) { - toast.error( - `Failed to fetch logs: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } finally { - setIsLoading(false); - } - }, [sessionId, containerId, tailLines, showTimestamps]); - - React.useEffect(() => { - fetchLogs(); - }, [fetchLogs]); - - React.useEffect(() => { - if (!autoRefresh) return; - - const interval = setInterval(() => { - fetchLogs(); - }, 3000); - - return () => clearInterval(interval); - }, [autoRefresh, fetchLogs]); - - React.useEffect(() => { - if (autoRefresh && logsEndRef.current) { - logsEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [logs, autoRefresh]); - - const handleDownload = async () => { - setIsDownloading(true); - try { - const options: DockerLogOptions = { - timestamps: showTimestamps, - }; - - const blob = await downloadContainerLogs(sessionId, containerId, options); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${containerName.replace(/[^a-z0-9]/gi, "_")}_logs.txt`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - - toast.success("Logs downloaded successfully"); - } catch (error) { - toast.error( - `Failed to download logs: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } finally { - setIsDownloading(false); - } - }; - - const filteredLogs = React.useMemo(() => { - if (!searchFilter.trim()) return logs; - - return logs - .split("\n") - .filter((line) => line.toLowerCase().includes(searchFilter.toLowerCase())) - .join("\n"); - }, [logs, searchFilter]); - - return ( -
    - - -
    -
    - - -
    - -
    - -
    - - - {showTimestamps ? "Enabled" : "Disabled"} - -
    -
    - -
    - -
    - - - {autoRefresh ? "On" : "Off"} - -
    -
    - -
    - -
    - - -
    -
    -
    - -
    -
    - - setSearchFilter(e.target.value)} - className="w-full pl-10 pr-4 py-2 border border-input rounded-md text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary" - /> -
    -
    -
    -
    - - - - {isLoading && !logs ? ( -
    - -
    - ) : ( -
    -
    -                {filteredLogs || (
    -                  
    -                    No logs available
    -                  
    -                )}
    -                
    -
    -
    - )} -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/file-manager/DragIndicator.tsx b/src/ui/desktop/apps/features/file-manager/DragIndicator.tsx deleted file mode 100644 index 19c33e43..00000000 --- a/src/ui/desktop/apps/features/file-manager/DragIndicator.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from "react"; -import { cn } from "@/lib/utils.ts"; -import { useTranslation } from "react-i18next"; -import { - Download, - FileDown, - FolderDown, - Loader2, - CheckCircle, - AlertCircle, -} from "lucide-react"; - -interface DragIndicatorProps { - isVisible: boolean; - isDragging: boolean; - isDownloading: boolean; - progress: number; - fileName?: string; - fileCount?: number; - error?: string | null; - className?: string; -} - -export function DragIndicator({ - isVisible, - isDragging, - isDownloading, - progress, - fileName, - fileCount = 1, - error, - className, -}: DragIndicatorProps) { - const { t } = useTranslation(); - - if (!isVisible) return null; - - const getIcon = () => { - if (error) { - return ; - } - - if (isDragging) { - return ; - } - - if (isDownloading) { - return ; - } - - if (fileCount > 1) { - return ; - } - - return ; - }; - - const getStatusText = () => { - if (error) { - return t("dragIndicator.error", { error }); - } - - if (isDragging) { - return t("dragIndicator.dragging", { fileName: fileName || "" }); - } - - if (isDownloading) { - return t("dragIndicator.preparing", { fileName: fileName || "" }); - } - - if (fileCount > 1) { - return t("dragIndicator.readyMultiple", { count: fileCount }); - } - - return t("dragIndicator.readySingle", { fileName: fileName || "" }); - }; - - return ( -
    -
    -
    {getIcon()}
    - -
    -
    - {fileCount > 1 - ? t("dragIndicator.batchDrag") - : t("dragIndicator.dragToDesktop")} -
    - -
    - {getStatusText()} -
    - - {(isDownloading || isDragging) && !error && ( -
    -
    -
    - )} - - {(isDownloading || isDragging) && !error && ( -
    - {progress.toFixed(0)}% -
    - )} - - {isDragging && !error && ( -
    - - {t("dragIndicator.canDragAnywhere")} -
    - )} -
    -
    - - {isDragging && !error && ( -
    - )} -
    - ); -} diff --git a/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx b/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx deleted file mode 100644 index b5faa7ab..00000000 --- a/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { useTranslation } from "react-i18next"; -import { Shield } from "lucide-react"; - -interface FileItem { - name: string; - type: "file" | "directory" | "link"; - path: string; - permissions?: string; - owner?: string; - group?: string; -} - -interface PermissionsDialogProps { - file: FileItem | null; - open: boolean; - onOpenChange: (open: boolean) => void; - onSave: (file: FileItem, permissions: string) => Promise; -} - -const parsePermissions = ( - perms: string, -): { owner: number; group: number; other: number } => { - if (!perms) { - return { owner: 0, group: 0, other: 0 }; - } - - if (/^\d{3,4}$/.test(perms)) { - const numStr = perms.slice(-3); - return { - owner: parseInt(numStr[0] || "0", 10), - group: parseInt(numStr[1] || "0", 10), - other: parseInt(numStr[2] || "0", 10), - }; - } - const cleanPerms = perms.replace(/^-/, "").substring(0, 9); - - const calcBits = (str: string): number => { - let value = 0; - if (str[0] === "r") value += 4; - if (str[1] === "w") value += 2; - if (str[2] === "x") value += 1; - return value; - }; - - return { - owner: calcBits(cleanPerms.substring(0, 3)), - group: calcBits(cleanPerms.substring(3, 6)), - other: calcBits(cleanPerms.substring(6, 9)), - }; -}; - -const toNumeric = (owner: number, group: number, other: number): string => { - return `${owner}${group}${other}`; -}; - -export function PermissionsDialog({ - file, - open, - onOpenChange, - onSave, -}: PermissionsDialogProps) { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - - const initialPerms = parsePermissions(file?.permissions || "644"); - const [ownerRead, setOwnerRead] = useState((initialPerms.owner & 4) !== 0); - const [ownerWrite, setOwnerWrite] = useState((initialPerms.owner & 2) !== 0); - const [ownerExecute, setOwnerExecute] = useState( - (initialPerms.owner & 1) !== 0, - ); - - const [groupRead, setGroupRead] = useState((initialPerms.group & 4) !== 0); - const [groupWrite, setGroupWrite] = useState((initialPerms.group & 2) !== 0); - const [groupExecute, setGroupExecute] = useState( - (initialPerms.group & 1) !== 0, - ); - - const [otherRead, setOtherRead] = useState((initialPerms.other & 4) !== 0); - const [otherWrite, setOtherWrite] = useState((initialPerms.other & 2) !== 0); - const [otherExecute, setOtherExecute] = useState( - (initialPerms.other & 1) !== 0, - ); - - useEffect(() => { - if (file) { - const perms = parsePermissions(file.permissions || "644"); - setOwnerRead((perms.owner & 4) !== 0); - setOwnerWrite((perms.owner & 2) !== 0); - setOwnerExecute((perms.owner & 1) !== 0); - setGroupRead((perms.group & 4) !== 0); - setGroupWrite((perms.group & 2) !== 0); - setGroupExecute((perms.group & 1) !== 0); - setOtherRead((perms.other & 4) !== 0); - setOtherWrite((perms.other & 2) !== 0); - setOtherExecute((perms.other & 1) !== 0); - } - }, [file]); - - const calculateOctal = (): string => { - const owner = - (ownerRead ? 4 : 0) + (ownerWrite ? 2 : 0) + (ownerExecute ? 1 : 0); - const group = - (groupRead ? 4 : 0) + (groupWrite ? 2 : 0) + (groupExecute ? 1 : 0); - const other = - (otherRead ? 4 : 0) + (otherWrite ? 2 : 0) + (otherExecute ? 1 : 0); - return toNumeric(owner, group, other); - }; - - const handleSave = async () => { - if (!file) return; - - setLoading(true); - try { - const permissions = calculateOctal(); - await onSave(file, permissions); - onOpenChange(false); - } catch (error) { - console.error("Failed to update permissions:", error); - } finally { - setLoading(false); - } - }; - - if (!file) return null; - - const octal = calculateOctal(); - - return ( - - - - - - {t("fileManager.changePermissions")} - - - {t("fileManager.changePermissionsDesc")}:{" "} - {file.name} - - - -
    -
    -
    - -

    - {file.permissions || "644"} -

    -
    -
    - -

    {octal}

    -
    -
    - -
    - -
    -
    - setOwnerRead(checked === true)} - /> - -
    -
    - setOwnerWrite(checked === true)} - /> - -
    -
    - - setOwnerExecute(checked === true) - } - /> - -
    -
    -
    - -
    - -
    -
    - setGroupRead(checked === true)} - /> - -
    -
    - setGroupWrite(checked === true)} - /> - -
    -
    - - setGroupExecute(checked === true) - } - /> - -
    -
    -
    - -
    - -
    -
    - setOtherRead(checked === true)} - /> - -
    -
    - setOtherWrite(checked === true)} - /> - -
    -
    - - setOtherExecute(checked === true) - } - /> - -
    -
    -
    -
    - - - - - -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/CpuWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/CpuWidget.tsx deleted file mode 100644 index 151e6a53..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/CpuWidget.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from "react"; -import { Cpu } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; -import { RechartsPrimitive } from "@/components/ui/chart.tsx"; - -const { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} = RechartsPrimitive; - -interface CpuWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) { - const { t } = useTranslation(); - - const chartData = React.useMemo(() => { - return metricsHistory.map((m, index) => ({ - index, - cpu: m.cpu?.percent || 0, - })); - }, [metricsHistory]); - - return ( -
    -
    - -

    - {t("serverStats.cpuUsage")} -

    -
    - -
    -
    -
    - {typeof metrics?.cpu?.percent === "number" - ? `${metrics.cpu.percent}%` - : "N/A"} -
    -
    - {typeof metrics?.cpu?.cores === "number" - ? t("serverStats.cpuCores", { count: metrics.cpu.cores }) - : t("serverStats.naCpus")} -
    -
    -
    - {metrics?.cpu?.load - ? t("serverStats.loadAverage", { - avg1: metrics.cpu.load[0].toFixed(2), - avg5: metrics.cpu.load[1].toFixed(2), - avg15: metrics.cpu.load[2].toFixed(2), - }) - : t("serverStats.loadAverageNA")} -
    -
    - - - - - - [`${value.toFixed(1)}%`, "CPU"]} - /> - - - -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/DiskWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/DiskWidget.tsx deleted file mode 100644 index 78d1fd22..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/DiskWidget.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from "react"; -import { HardDrive } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; -import { RechartsPrimitive } from "@/components/ui/chart.tsx"; - -const { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} = RechartsPrimitive; - -interface DiskWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function DiskWidget({ metrics, metricsHistory }: DiskWidgetProps) { - const { t } = useTranslation(); - - const chartData = React.useMemo(() => { - return metricsHistory.map((m, index) => ({ - index, - disk: m.disk?.percent || 0, - })); - }, [metricsHistory]); - - return ( -
    -
    - -

    - {t("serverStats.diskUsage")} -

    -
    - -
    -
    -
    - {typeof metrics?.disk?.percent === "number" - ? `${metrics.disk.percent}%` - : "N/A"} -
    -
    - {(() => { - const used = metrics?.disk?.usedHuman; - const total = metrics?.disk?.totalHuman; - if (used && total) { - return `${used} / ${total}`; - } - return "N/A"; - })()} -
    -
    -
    - {(() => { - const available = metrics?.disk?.availableHuman; - return available - ? `${t("serverStats.available")}: ${available}` - : `${t("serverStats.available")}: N/A`; - })()} -
    -
    - - - - - - - - - - - - [`${value.toFixed(1)}%`, "Disk"]} - cursor={{ - stroke: "#fb923c", - strokeWidth: 1, - strokeDasharray: "3 3", - }} - /> - - - -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/FirewallWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/FirewallWidget.tsx deleted file mode 100644 index 17571547..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/FirewallWidget.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React from "react"; -import { Shield, ShieldOff, ShieldCheck, ChevronDown } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; -import type { - FirewallMetrics, - FirewallChain, - FirewallRule, -} from "@/types/stats-widgets"; - -interface FirewallWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -function RuleRow({ rule }: { rule: FirewallRule }) { - const { t } = useTranslation(); - - const getTargetStyle = (target: string) => { - switch (target.toUpperCase()) { - case "ACCEPT": - return "text-green-400"; - case "DROP": - return "text-red-400"; - case "REJECT": - return "text-orange-400"; - default: - return "text-muted-foreground"; - } - }; - - const getTargetLabel = (target: string) => { - switch (target.toUpperCase()) { - case "ACCEPT": - return t("serverStats.firewall.accept"); - case "DROP": - return t("serverStats.firewall.drop"); - case "REJECT": - return t("serverStats.firewall.reject"); - default: - return target; - } - }; - - const formatSource = () => { - if (rule.interface) { - return rule.interface; - } - if (rule.state) { - return rule.state; - } - if (rule.source === "0.0.0.0/0") { - return t("serverStats.firewall.anywhere"); - } - return rule.source; - }; - - return ( -
    -
    - {getTargetLabel(rule.target)} -
    -
    - {rule.protocol.toUpperCase()} -
    -
    - {rule.dport || "-"} -
    -
    - {formatSource()} -
    -
    - ); -} - -function ChainSection({ chain }: { chain: FirewallChain }) { - const { t } = useTranslation(); - const [isOpen, setIsOpen] = React.useState(true); - - const getPolicyStyle = (policy: string) => { - switch (policy.toUpperCase()) { - case "ACCEPT": - return "text-green-400"; - case "DROP": - return "text-red-400"; - case "REJECT": - return "text-orange-400"; - default: - return "text-muted-foreground"; - } - }; - - return ( -
    - - {isOpen && ( - <> - {chain.rules.length > 0 ? ( -
    -
    -
    {t("serverStats.firewall.action")}
    -
    {t("serverStats.firewall.protocol")}
    -
    {t("serverStats.firewall.port")}
    -
    {t("serverStats.firewall.source")}
    -
    -
    - {chain.rules.map((rule, idx) => ( - - ))} -
    -
    - ) : ( -
    - {t("serverStats.firewall.noRules")} -
    - )} - - )} -
    - ); -} - -export function FirewallWidget({ metrics }: FirewallWidgetProps) { - const { t } = useTranslation(); - - const firewall = (metrics as ServerMetrics & { firewall?: FirewallMetrics }) - ?.firewall; - - const getStatusIcon = () => { - if (!firewall || firewall.type === "none") { - return ; - } - if (firewall.status === "active") { - return ; - } - return ; - }; - - const getStatusText = () => { - if (!firewall || firewall.type === "none") { - return t("serverStats.firewall.notDetected"); - } - if (firewall.status === "active") { - return t("serverStats.firewall.active"); - } - return t("serverStats.firewall.inactive"); - }; - - return ( -
    -
    - {getStatusIcon()} -

    - {t("serverStats.firewall.title")} -

    - {firewall && firewall.type !== "none" && ( - - {firewall.type} - - )} -
    - -
    - - {getStatusText()} - -
    - - {firewall && firewall.chains.length > 0 ? ( -
    - {firewall.chains.map((chain) => ( - - ))} -
    - ) : ( -
    -

    - {t("serverStats.firewall.noData")} -

    -
    - )} -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/LoginStatsWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/LoginStatsWidget.tsx deleted file mode 100644 index fe2ea698..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/LoginStatsWidget.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React from "react"; -import { UserCheck, UserX, MapPin, Activity } from "lucide-react"; -import { useTranslation } from "react-i18next"; - -interface LoginRecord { - user: string; - ip: string; - time: string; - status: "success" | "failed"; -} - -interface LoginStatsMetrics { - recentLogins: LoginRecord[]; - failedLogins: LoginRecord[]; - totalLogins: number; - uniqueIPs: number; -} - -interface ServerMetrics { - login_stats?: LoginStatsMetrics; -} - -interface LoginStatsWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) { - const { t } = useTranslation(); - - const loginStats = metrics?.login_stats; - const recentLogins = loginStats?.recentLogins || []; - const failedLogins = loginStats?.failedLogins || []; - const totalLogins = loginStats?.totalLogins || 0; - const uniqueIPs = loginStats?.uniqueIPs || 0; - - return ( -
    -
    - -

    - {t("serverStats.loginStats")} -

    -
    - -
    -
    -
    -
    - - {t("serverStats.totalLogins")} -
    -
    - {totalLogins} -
    -
    -
    -
    - - {t("serverStats.uniqueIPs")} -
    -
    {uniqueIPs}
    -
    -
    - -
    -
    -
    - - - {t("serverStats.recentSuccessfulLogins")} - -
    - {recentLogins.length === 0 ? ( -
    - {t("serverStats.noRecentLoginData")} -
    - ) : ( -
    - {recentLogins.slice(0, 5).map((login, idx) => ( -
    -
    - - {login.user} - - - {t("serverStats.from")} - - - {login.ip} - -
    - - {new Date(login.time).toLocaleString()} - -
    - ))} -
    - )} -
    - - {failedLogins.length > 0 && ( -
    -
    - - - {t("serverStats.recentFailedAttempts")} - -
    -
    - {failedLogins.slice(0, 3).map((login) => ( -
    -
    - - {login.user} - - - {t("serverStats.from")} - - - {login.ip} - -
    - - {new Date(login.time).toLocaleString()} - -
    - ))} -
    -
    - )} -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/MemoryWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/MemoryWidget.tsx deleted file mode 100644 index 55d95ea2..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/MemoryWidget.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React from "react"; -import { MemoryStick } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; -import { RechartsPrimitive } from "@/components/ui/chart.tsx"; - -const { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} = RechartsPrimitive; - -interface MemoryWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) { - const { t } = useTranslation(); - - const chartData = React.useMemo(() => { - return metricsHistory.map((m, index) => ({ - index, - memory: m.memory?.percent || 0, - })); - }, [metricsHistory]); - - return ( -
    -
    - -

    - {t("serverStats.memoryUsage")} -

    -
    - -
    -
    -
    - {typeof metrics?.memory?.percent === "number" - ? `${metrics.memory.percent}%` - : "N/A"} -
    -
    - {(() => { - const used = metrics?.memory?.usedGiB; - const total = metrics?.memory?.totalGiB; - if (typeof used === "number" && typeof total === "number") { - return `${used.toFixed(1)} / ${total.toFixed(1)} GiB`; - } - return "N/A"; - })()} -
    -
    -
    - {(() => { - const used = metrics?.memory?.usedGiB; - const total = metrics?.memory?.totalGiB; - const free = - typeof used === "number" && typeof total === "number" - ? (total - used).toFixed(1) - : "N/A"; - return `${t("serverStats.free")}: ${free} GiB`; - })()} -
    -
    - - - - - - - - - - - - [ - `${value.toFixed(1)}%`, - "Memory", - ]} - cursor={{ - stroke: "#34d399", - strokeWidth: 1, - strokeDasharray: "3 3", - }} - /> - - - -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/NetworkWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/NetworkWidget.tsx deleted file mode 100644 index 08eaf348..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/NetworkWidget.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from "react"; -import { Network, Wifi, WifiOff } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; - -interface NetworkWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function NetworkWidget({ metrics }: NetworkWidgetProps) { - const { t } = useTranslation(); - - const metricsWithNetwork = metrics as ServerMetrics & { - network?: { - interfaces?: Array<{ - name: string; - state: string; - ip: string; - }>; - }; - }; - const network = metricsWithNetwork?.network; - const interfaces = network?.interfaces || []; - - return ( -
    -
    - -

    - {t("serverStats.networkInterfaces")} -

    -
    - -
    - {interfaces.length === 0 ? ( -
    - -

    {t("serverStats.noInterfacesFound")}

    -
    - ) : ( - interfaces.map((iface, index: number) => ( -
    -
    -
    - - - {iface.name} - -
    - - {iface.state} - -
    -
    - {iface.ip} -
    -
    - )) - )} -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/PortsWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/PortsWidget.tsx deleted file mode 100644 index 6e346310..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/PortsWidget.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from "react"; -import { Network } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; -import type { PortsMetrics, ListeningPort } from "@/types/stats-widgets"; - -interface PortsWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -function PortRow({ port }: { port: ListeningPort }) { - const formatAddress = (addr: string) => { - if (addr === "0.0.0.0" || addr === "*" || addr === "::") { - return "*"; - } - return addr; - }; - - return ( -
    -
    - {port.protocol.toUpperCase()} -
    -
    {port.localPort}
    -
    - {formatAddress(port.localAddress)} -
    -
    {port.state || "-"}
    -
    - {port.process || (port.pid ? `PID:${port.pid}` : "-")} -
    -
    - ); -} - -export function PortsWidget({ metrics }: PortsWidgetProps) { - const { t } = useTranslation(); - - const portsData = (metrics as ServerMetrics & { ports?: PortsMetrics }) - ?.ports; - - const tcpPorts = portsData?.ports.filter((p) => p.protocol === "tcp") || []; - const udpPorts = portsData?.ports.filter((p) => p.protocol === "udp") || []; - - return ( -
    -
    - -

    - {t("serverStats.ports.title")} -

    - {portsData && portsData.source !== "none" && ( - - {portsData.source === "ss" - ? "Socket Stats" - : portsData.source === "netstat" - ? "Netstat" - : portsData.source} - - )} -
    - -
    - - TCP:{" "} - {tcpPorts.length} - - - UDP:{" "} - {udpPorts.length} - -
    - - {portsData && portsData.ports.length > 0 ? ( -
    -
    -
    {t("serverStats.ports.protocol")}
    -
    {t("serverStats.ports.port")}
    -
    {t("serverStats.ports.address")}
    -
    {t("serverStats.ports.state")}
    -
    {t("serverStats.ports.process")}
    -
    -
    - {portsData.ports.map((port, idx) => ( - - ))} -
    -
    - ) : ( -
    -

    - {t("serverStats.ports.noData")} -

    -
    - )} -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/ProcessesWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/ProcessesWidget.tsx deleted file mode 100644 index 7b00d2a6..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/ProcessesWidget.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from "react"; -import { List, Activity } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; - -interface ProcessesWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function ProcessesWidget({ metrics }: ProcessesWidgetProps) { - const { t } = useTranslation(); - - const metricsWithProcesses = metrics as ServerMetrics & { - processes?: { - total?: number; - running?: number; - top?: Array<{ - pid: number; - cpu: number; - mem: number; - command: string; - user: string; - }>; - }; - }; - const processes = metricsWithProcesses?.processes; - const topProcesses = processes?.top || []; - - return ( -
    -
    - -

    - {t("serverStats.processes")} -

    -
    - -
    -
    - {t("serverStats.totalProcesses")}:{" "} - - {processes?.total ?? "N/A"} - -
    -
    - {t("serverStats.running")}:{" "} - - {processes?.running ?? "N/A"} - -
    -
    - -
    - {topProcesses.length === 0 ? ( -
    - -

    {t("serverStats.noProcessesFound")}

    -
    - ) : ( -
    - {topProcesses.map((proc, index) => ( -
    -
    - - PID: {proc.pid} - -
    - CPU: {proc.cpu}% - MEM: {proc.mem}% -
    -
    -
    - {proc.command} -
    -
    - User: {proc.user} -
    -
    - ))} -
    - )} -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/SystemWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/SystemWidget.tsx deleted file mode 100644 index 1f598484..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/SystemWidget.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import { Server, Info } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; - -interface SystemWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function SystemWidget({ metrics }: SystemWidgetProps) { - const { t } = useTranslation(); - - const metricsWithSystem = metrics as ServerMetrics & { - system?: { - hostname?: string; - os?: string; - kernel?: string; - }; - }; - const system = metricsWithSystem?.system; - - return ( -
    -
    - -

    - {t("serverStats.systemInfo")} -

    -
    - -
    -
    - -
    -

    - {t("serverStats.hostname")} -

    -

    - {system?.hostname || "N/A"} -

    -
    -
    - -
    - -
    -

    - {t("serverStats.operatingSystem")} -

    -

    - {system?.os || "N/A"} -

    -
    -
    - -
    - -
    -

    - {t("serverStats.kernel")} -

    -

    - {system?.kernel || "N/A"} -

    -
    -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/server-stats/widgets/UptimeWidget.tsx b/src/ui/desktop/apps/features/server-stats/widgets/UptimeWidget.tsx deleted file mode 100644 index bb1bfdbf..00000000 --- a/src/ui/desktop/apps/features/server-stats/widgets/UptimeWidget.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import { Clock, Activity } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import type { ServerMetrics } from "@/ui/main-axios.ts"; - -interface UptimeWidgetProps { - metrics: ServerMetrics | null; - metricsHistory: ServerMetrics[]; -} - -export function UptimeWidget({ metrics }: UptimeWidgetProps) { - const { t } = useTranslation(); - - const metricsWithUptime = metrics as ServerMetrics & { - uptime?: { - formatted?: string; - seconds?: number; - }; - }; - const uptime = metricsWithUptime?.uptime; - - return ( -
    -
    - -

    - {t("serverStats.uptime")} -

    -
    - -
    -
    -
    - -
    -
    - -
    -
    - {uptime?.formatted || "N/A"} -
    -
    - {t("serverStats.totalUptime")} -
    - {uptime?.seconds && ( -
    - {Math.floor(uptime.seconds).toLocaleString()}{" "} - {t("serverStats.seconds")} -
    - )} -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/features/terminal/SudoPasswordPopup.tsx b/src/ui/desktop/apps/features/terminal/SudoPasswordPopup.tsx deleted file mode 100644 index f2dab957..00000000 --- a/src/ui/desktop/apps/features/terminal/SudoPasswordPopup.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { KeyRound } from "lucide-react"; -import { Button } from "@/components/ui/button.tsx"; - -interface SudoPasswordPopupProps { - isOpen: boolean; - hostPassword: string; - backgroundColor: string; - onConfirm: (password: string) => void; - onDismiss: () => void; -} - -export function SudoPasswordPopup({ - isOpen, - hostPassword, - backgroundColor, - onConfirm, - onDismiss, -}: SudoPasswordPopupProps) { - const { t } = useTranslation(); - - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - onConfirm(hostPassword); - } else if (e.key === "Escape") { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - onDismiss(); - } - }; - - window.addEventListener("keydown", handleKeyDown, true); - return () => window.removeEventListener("keydown", handleKeyDown, true); - }, [isOpen, onConfirm, onDismiss, hostPassword]); - - if (!isOpen) return null; - - return ( -
    -
    -
    - -
    -
    -

    - {t("terminal.sudoPasswordPopupTitle", "Insert password?")} -

    -

    - {t( - "terminal.sudoPasswordPopupHint", - "Press Enter to insert, Esc to dismiss", - )} -

    -
    -
    -
    - - -
    -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/HostManagerApp.tsx b/src/ui/desktop/apps/host-manager/HostManagerApp.tsx deleted file mode 100644 index 5d5e746f..00000000 --- a/src/ui/desktop/apps/host-manager/HostManagerApp.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager.tsx"; -import React from "react"; - -const HostManagerApp: React.FC = () => { - return ( -
    - {}} /> -
    - ); -}; - -export default HostManagerApp; diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx deleted file mode 100644 index a9034908..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx +++ /dev/null @@ -1,625 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { Button } from "@/components/ui/button.tsx"; -import { Form } from "@/components/ui/form.tsx"; -import { ScrollArea } from "@/components/ui/scroll-area.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; -import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; -import React, { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import { ArrowLeft } from "lucide-react"; -import { - createCredential, - updateCredential, - getCredentials, - getCredentialDetails, - detectKeyType, - detectPublicKeyType, -} from "@/ui/main-axios.ts"; -import { useTranslation } from "react-i18next"; -import { oneDark } from "@codemirror/theme-one-dark"; -import { githubLight } from "@uiw/codemirror-theme-github"; -import { useTheme } from "@/components/theme-provider.tsx"; -import type { - Credential, - CredentialEditorProps, - CredentialData, -} from "../../../../../types"; -import { CredentialGeneralTab } from "./tabs/CredentialGeneralTab"; -import { CredentialAuthenticationTab } from "./tabs/CredentialAuthenticationTab"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; - -export function CredentialEditor({ - editingCredential, - onFormSubmit, - onBack, -}: CredentialEditorProps) { - const { t } = useTranslation(); - const { theme: appTheme } = useTheme(); - - const isDarkMode = - appTheme === "dark" || - (appTheme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - const editorTheme = isDarkMode ? oneDark : githubLight; - const [, setCredentials] = useState([]); - const [folders, setFolders] = useState([]); - const [, setLoading] = useState(true); - const [fullCredentialDetails, setFullCredentialDetails] = - useState(null); - - const [authTab, setAuthTab] = useState<"password" | "key">("password"); - const [detectedKeyType, setDetectedKeyType] = useState(null); - const [keyDetectionLoading, setKeyDetectionLoading] = useState(false); - const keyDetectionTimeoutRef = useRef(null); - const [activeTab, setActiveTab] = useState("general"); - const [formError, setFormError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - - const [detectedPublicKeyType, setDetectedPublicKeyType] = useState< - string | null - >(null); - const [publicKeyDetectionLoading, setPublicKeyDetectionLoading] = - useState(false); - const publicKeyDetectionTimeoutRef = useRef(null); - - useEffect(() => { - setFormError(null); - }, [activeTab]); - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const credentialsData = await getCredentials(); - setCredentials(credentialsData); - - const uniqueFolders = [ - ...new Set( - credentialsData - .filter( - (credential) => - credential.folder && credential.folder.trim() !== "", - ) - .map((credential) => credential.folder!), - ), - ].sort() as string[]; - - setFolders(uniqueFolders); - } catch { - // Keep the editor usable even if credentials cannot be loaded. - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - useEffect(() => { - const fetchCredentialDetails = async () => { - if (editingCredential) { - try { - const fullDetails = await getCredentialDetails(editingCredential.id); - setFullCredentialDetails(fullDetails); - } catch { - toast.error(t("credentials.failedToFetchCredentialDetails")); - } - } else { - setFullCredentialDetails(null); - } - }; - - fetchCredentialDetails(); - }, [editingCredential, t]); - - const formSchema = z - .object({ - name: z.string().min(1), - description: z.string().optional(), - folder: z.string().optional(), - tags: z.array(z.string().min(1)).default([]), - authType: z.enum(["password", "key"]), - username: z.string().optional(), - password: z.string().optional(), - key: z.any().optional().nullable(), - publicKey: z.string().optional(), - keyPassword: z.string().optional(), - keyType: z - .enum([ - "auto", - "ssh-rsa", - "ssh-ed25519", - "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", - "ecdsa-sha2-nistp521", - "ssh-dss", - "ssh-rsa-sha2-256", - "ssh-rsa-sha2-512", - ]) - .optional(), - }) - .superRefine((data, ctx) => { - if (data.authType === "password") { - if (!data.password || data.password.trim() === "") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("credentials.passwordRequired"), - path: ["password"], - }); - } - } else if (data.authType === "key") { - if (!data.key && !editingCredential) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("credentials.sshKeyRequired"), - path: ["key"], - }); - } - } - }); - - type FormData = z.infer; - - const form = useForm({ - resolver: zodResolver(formSchema) as unknown as Parameters< - typeof useForm - >[0]["resolver"], - mode: "all", - defaultValues: { - name: "", - description: "", - folder: "", - tags: [], - authType: "password", - username: "", - password: "", - key: null, - publicKey: "", - keyPassword: "", - keyType: "auto", - }, - }); - - const watchedFields = form.watch(); - - const isFormValid = React.useMemo(() => { - const values = form.getValues(); - - if (!values.name) return false; - - if (authTab === "password") { - return !!(values.password && values.password.trim() !== ""); - } else if (authTab === "key") { - if (editingCredential) { - return true; - } - return !!values.key; - } - - return false; - }, [watchedFields, authTab, editingCredential]); - - useEffect(() => { - const updateAuthFields = async () => { - form.setValue("authType", authTab, { shouldValidate: true }); - - if (authTab === "password") { - form.setValue("key", null, { shouldValidate: true }); - form.setValue("publicKey", "", { shouldValidate: true }); - form.setValue("keyPassword", "", { shouldValidate: true }); - form.setValue("keyType", "auto", { shouldValidate: true }); - } else if (authTab === "key") { - form.setValue("password", "", { shouldValidate: true }); - } - - await form.trigger(); - }; - - updateAuthFields(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [authTab]); - - useEffect(() => { - if (editingCredential && fullCredentialDetails) { - const defaultAuthType = fullCredentialDetails.authType; - setAuthTab(defaultAuthType); - - setTimeout(() => { - const formData = { - name: fullCredentialDetails.name || "", - description: fullCredentialDetails.description || "", - folder: fullCredentialDetails.folder || "", - tags: fullCredentialDetails.tags || [], - authType: defaultAuthType as "password" | "key", - username: fullCredentialDetails.username || "", - password: "", - key: null, - publicKey: "", - keyPassword: "", - keyType: "auto" as const, - }; - - if (defaultAuthType === "password") { - formData.password = fullCredentialDetails.password || ""; - } else if (defaultAuthType === "key") { - formData.key = fullCredentialDetails.key || ""; - formData.publicKey = fullCredentialDetails.publicKey || ""; - formData.keyPassword = fullCredentialDetails.keyPassword || ""; - formData.keyType = - (fullCredentialDetails.keyType as string) || ("auto" as const); - } - - form.reset(formData); - setTagInput(""); - }, 100); - } else if (!editingCredential) { - setAuthTab("password"); - form.reset({ - name: "", - description: "", - folder: "", - tags: [], - authType: "password", - username: "", - password: "", - key: null, - publicKey: "", - keyPassword: "", - keyType: "auto", - }); - setTagInput(""); - } - }, [editingCredential?.id, fullCredentialDetails, form]); - - useEffect(() => { - return () => { - if (keyDetectionTimeoutRef.current) { - clearTimeout(keyDetectionTimeoutRef.current); - } - if (publicKeyDetectionTimeoutRef.current) { - clearTimeout(publicKeyDetectionTimeoutRef.current); - } - }; - }, []); - - const handleKeyTypeDetection = async ( - keyValue: string, - keyPassword?: string, - ) => { - if (!keyValue || keyValue.trim() === "") { - setDetectedKeyType(null); - return; - } - - setKeyDetectionLoading(true); - try { - const result = await detectKeyType(keyValue, keyPassword); - if (result.success) { - setDetectedKeyType(result.keyType); - } else { - setDetectedKeyType("invalid"); - } - } catch (error) { - setDetectedKeyType("error"); - console.error("Key type detection error:", error); - } finally { - setKeyDetectionLoading(false); - } - }; - - const debouncedKeyDetection = (keyValue: string, keyPassword?: string) => { - if (keyDetectionTimeoutRef.current) { - clearTimeout(keyDetectionTimeoutRef.current); - } - keyDetectionTimeoutRef.current = setTimeout(() => { - handleKeyTypeDetection(keyValue, keyPassword); - }, 1000); - }; - - const handlePublicKeyTypeDetection = async (publicKeyValue: string) => { - if (!publicKeyValue || publicKeyValue.trim() === "") { - setDetectedPublicKeyType(null); - return; - } - - setPublicKeyDetectionLoading(true); - try { - const result = await detectPublicKeyType(publicKeyValue); - if (result.success) { - setDetectedPublicKeyType(result.keyType); - } else { - setDetectedPublicKeyType("invalid"); - console.warn("Public key detection failed:", result.error); - } - } catch (error) { - setDetectedPublicKeyType("error"); - console.error("Public key type detection error:", error); - } finally { - setPublicKeyDetectionLoading(false); - } - }; - - const debouncedPublicKeyDetection = (publicKeyValue: string) => { - if (publicKeyDetectionTimeoutRef.current) { - clearTimeout(publicKeyDetectionTimeoutRef.current); - } - publicKeyDetectionTimeoutRef.current = setTimeout(() => { - handlePublicKeyTypeDetection(publicKeyValue); - }, 1000); - }; - - const getFriendlyKeyTypeName = (keyType: string): string => { - const keyTypeMap: Record = { - "ssh-rsa": t("credentials.keyTypeRSA"), - "ssh-ed25519": t("credentials.keyTypeEd25519"), - "ecdsa-sha2-nistp256": t("credentials.keyTypeEcdsaP256"), - "ecdsa-sha2-nistp384": t("credentials.keyTypeEcdsaP384"), - "ecdsa-sha2-nistp521": t("credentials.keyTypeEcdsaP521"), - "ssh-dss": t("credentials.keyTypeDsa"), - "rsa-sha2-256": t("credentials.keyTypeRsaSha256"), - "rsa-sha2-512": t("credentials.keyTypeRsaSha512"), - invalid: t("credentials.invalidKey"), - error: t("credentials.detectionError"), - unknown: t("credentials.unknown"), - }; - return keyTypeMap[keyType] || keyType; - }; - - const onSubmit = async (data: FormData) => { - try { - setIsSubmitting(true); - setFormError(null); - - if (!data.name || data.name.trim() === "") { - data.name = data.username || "Unnamed Credential"; - } - - const submitData: CredentialData = { - name: data.name, - description: data.description, - folder: data.folder, - tags: data.tags, - authType: data.authType, - username: data.username || undefined, - keyType: data.keyType, - }; - - submitData.password = null; - submitData.key = null; - submitData.publicKey = null; - submitData.keyPassword = null; - submitData.keyType = null; - - if (data.authType === "password") { - submitData.password = data.password; - } else if (data.authType === "key") { - submitData.key = data.key; - submitData.publicKey = data.publicKey; - submitData.keyPassword = data.keyPassword; - submitData.keyType = data.keyType; - } - - if (editingCredential) { - await updateCredential(editingCredential.id, submitData); - toast.success( - t("credentials.credentialUpdatedSuccessfully", { name: data.name }), - ); - } else { - await createCredential(submitData); - toast.success( - t("credentials.credentialAddedSuccessfully", { name: data.name }), - ); - } - - if (onFormSubmit) { - onFormSubmit(); - } - - window.dispatchEvent(new CustomEvent("credentials:changed")); - - form.reset(); - } catch (error) { - console.error("Credential save error:", error); - if (error instanceof Error) { - toast.error(error.message); - } else { - toast.error(t("credentials.failedToSaveCredential")); - } - } finally { - setIsSubmitting(false); - } - }; - - const handleFormError = () => { - const errors = form.formState.errors; - - if ( - errors.name || - errors.username || - errors.description || - errors.folder || - errors.tags - ) { - setActiveTab("general"); - } else if ( - errors.password || - errors.key || - errors.publicKey || - errors.keyPassword || - errors.keyType - ) { - setActiveTab("authentication"); - } - }; - - const [tagInput, setTagInput] = useState(""); - - const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); - const folderInputRef = useRef(null); - const folderDropdownRef = useRef(null); - - const folderValue = form.watch("folder"); - const filteredFolders = React.useMemo(() => { - if (!folderValue) return folders; - return folders.filter((f) => - f.toLowerCase().includes(folderValue.toLowerCase()), - ); - }, [folderValue, folders]); - - const handleFolderClick = (folder: string) => { - form.setValue("folder", folder); - setFolderDropdownOpen(false); - }; - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - folderDropdownRef.current && - !folderDropdownRef.current.contains(event.target as Node) && - folderInputRef.current && - !folderInputRef.current.contains(event.target as Node) - ) { - setFolderDropdownOpen(false); - } - } - - if (folderDropdownOpen) { - document.addEventListener("mousedown", handleClickOutside); - } else { - document.removeEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [folderDropdownOpen]); - - return ( -
    - -
    - - - {formError && ( - - {formError} - - )} -
    - {onBack && ( - - )} -

    - {editingCredential - ? t("credentials.editCredential") - : t("credentials.addCredential")} -

    -
    - - - - {t("credentials.general")} - - - {t("credentials.authentication")} - - - - - - - - - -
    -
    - - {!isSubmitting && ( - - )} -
    -
    - -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx deleted file mode 100644 index fab27109..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { FormControl, FormItem, FormLabel } from "@/components/ui/form.tsx"; -import { getCredentials } from "@/ui/main-axios.ts"; -import { useTranslation } from "react-i18next"; -import type { Credential } from "../../../../../types"; -import { toast } from "sonner"; - -interface CredentialSelectorProps { - value?: number | null; - onValueChange: (credentialId: number | null) => void; - onCredentialSelect?: (credential: Credential | null) => void; -} - -export function CredentialSelector({ - value, - onValueChange, - onCredentialSelect, -}: CredentialSelectorProps) { - const { t } = useTranslation(); - const [credentials, setCredentials] = useState([]); - const [loading, setLoading] = useState(true); - const [dropdownOpen, setDropdownOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - - const buttonRef = useRef(null); - const dropdownRef = useRef(null); - - useEffect(() => { - const fetchCredentials = async () => { - try { - setLoading(true); - const data = await getCredentials(); - const credentialsArray = Array.isArray(data) - ? data - : data.credentials || data.data || []; - setCredentials(credentialsArray); - } catch { - toast.error(t("credentials.failedToFetchCredentials")); - setCredentials([]); - } finally { - setLoading(false); - } - }; - - fetchCredentials(); - }, []); - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) && - buttonRef.current && - !buttonRef.current.contains(event.target as Node) - ) { - setDropdownOpen(false); - } - } - - if (dropdownOpen) { - document.addEventListener("mousedown", handleClickOutside); - } else { - document.removeEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [dropdownOpen]); - - const selectedCredential = credentials.find((c) => c.id === value); - - const filteredCredentials = credentials.filter((credential) => { - if (!searchQuery) return true; - const searchLower = searchQuery.toLowerCase(); - return ( - credential.name.toLowerCase().includes(searchLower) || - credential.username.toLowerCase().includes(searchLower) || - (credential.folder && - credential.folder.toLowerCase().includes(searchLower)) - ); - }); - - const handleCredentialSelect = (credential: Credential) => { - onValueChange(credential.id); - if (onCredentialSelect) { - onCredentialSelect(credential); - } - setDropdownOpen(false); - setSearchQuery(""); - }; - - return ( - - {t("hosts.selectCredential")} - -
    - - - {dropdownOpen && ( -
    -
    - setSearchQuery(e.target.value)} - className="h-8" - /> -
    - -
    - {loading ? ( -
    - {t("common.loading")} -
    - ) : filteredCredentials.length === 0 ? ( -
    - {searchQuery - ? t("credentials.noCredentialsMatchFilters") - : t("credentials.noCredentialsYet")} -
    - ) : ( -
    - {filteredCredentials.map((credential) => ( - - ))} -
    - )} -
    -
    - )} -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx deleted file mode 100644 index 56032cd0..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx +++ /dev/null @@ -1,528 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { ScrollArea } from "@/components/ui/scroll-area.tsx"; -import { - Sheet, - SheetContent, - SheetFooter, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet.tsx"; -import { - Key, - User, - Calendar, - Hash, - Folder, - Edit3, - Copy, - Shield, - Clock, - Server, - Eye, - EyeOff, - AlertTriangle, - CheckCircle, - FileText, -} from "lucide-react"; -import { getCredentialDetails, getCredentialHosts } from "@/ui/main-axios.ts"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import type { - Credential, - HostInfo, - CredentialViewerProps, -} from "../../../types/index.js"; - -const CredentialViewer: React.FC = ({ - credential, - onClose, - onEdit, -}) => { - const { t } = useTranslation(); - const [credentialDetails, setCredentialDetails] = useState( - null, - ); - const [hostsUsing, setHostsUsing] = useState([]); - const [loading, setLoading] = useState(true); - const [showSensitive, setShowSensitive] = useState>( - {}, - ); - const [activeTab, setActiveTab] = useState<"overview" | "security" | "usage">( - "overview", - ); - - useEffect(() => { - fetchCredentialDetails(); - fetchHostsUsing(); - }, [credential.id]); - - const fetchCredentialDetails = async () => { - try { - const response = await getCredentialDetails(credential.id); - setCredentialDetails(response); - } catch { - toast.error(t("credentials.failedToFetchCredentialDetails")); - } - }; - - const fetchHostsUsing = async () => { - try { - const response = await getCredentialHosts(credential.id); - setHostsUsing(response); - } catch { - toast.error(t("credentials.failedToFetchHostsUsing")); - } finally { - setLoading(false); - } - }; - - const toggleSensitiveVisibility = (field: string) => { - setShowSensitive((prev) => ({ - ...prev, - [field]: !prev[field], - })); - }; - - const copyToClipboard = async (text: string, fieldName: string) => { - try { - await navigator.clipboard.writeText(text); - toast.success(t("copiedToClipboard", { field: fieldName })); - } catch { - toast.error(t("credentials.failedToCopy")); - } - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString(); - }; - - const getAuthIcon = (authType: string) => { - return authType === "password" ? ( - - ) : ( - - ); - }; - - const renderSensitiveField = ( - value: string | undefined, - fieldName: string, - label: string, - isMultiline = false, - ) => { - if (!value) return null; - - const isVisible = showSensitive[fieldName]; - - return ( -
    -
    - -
    - - -
    -
    -
    - {isVisible ? ( -
    -              {value}
    -            
    - ) : ( -
    - {"•".repeat(isMultiline ? 50 : 20)} -
    - )} -
    -
    - ); - }; - - if (loading || !credentialDetails) { - return ( - - -
    -
    -
    -
    -
    - ); - } - - return ( - - - - -
    - {getAuthIcon(credentialDetails.authType)} -
    -
    -
    - {credentialDetails.name} -
    -
    - {credentialDetails.description} -
    -
    -
    - - {credentialDetails.authType} - - {credentialDetails.keyType && ( - - {credentialDetails.keyType} - - )} -
    -
    -
    - -
    -
    - - - -
    - - {activeTab === "overview" && ( -
    - - - - {t("credentials.basicInformation")} - - - -
    -
    - -
    -
    -
    - {t("common.username")} -
    -
    - {credentialDetails.username} -
    -
    -
    - - {credentialDetails.folder && ( -
    - -
    -
    - {t("common.folder")} -
    -
    - {credentialDetails.folder} -
    -
    -
    - )} - - {credentialDetails.tags.length > 0 && ( -
    - -
    -
    - {t("hosts.tags")} -
    -
    - {credentialDetails.tags.map((tag, index) => ( - - {tag} - - ))} -
    -
    -
    - )} - - - -
    - -
    -
    - {t("credentials.created")} -
    -
    - {formatDate(credentialDetails.createdAt)} -
    -
    -
    - -
    - -
    -
    - {t("credentials.lastModified")} -
    -
    - {formatDate(credentialDetails.updatedAt)} -
    -
    -
    -
    -
    - - - - - {t("credentials.usageStatistics")} - - - -
    -
    - {credentialDetails.usageCount} -
    -
    - {t("credentials.timesUsed")} -
    -
    - - {credentialDetails.lastUsed && ( -
    - -
    -
    - {t("credentials.lastUsed")} -
    -
    - {formatDate(credentialDetails.lastUsed)} -
    -
    -
    - )} - -
    - -
    -
    - {t("credentials.connectedHosts")} -
    -
    {hostsUsing.length}
    -
    -
    -
    -
    -
    - )} - - {activeTab === "security" && ( - - - - - {t("credentials.securityDetails")} - - - {t("credentials.securityDetailsDescription")} - - - -
    - -
    -
    - {t("credentials.credentialSecured")} -
    -
    - {t("credentials.credentialSecuredDescription")} -
    -
    -
    - - {credentialDetails.authType === "password" && ( -
    -

    - {t("credentials.passwordAuthentication")} -

    - {renderSensitiveField( - credentialDetails.password, - "password", - t("common.password"), - )} -
    - )} - - {credentialDetails.authType === "key" && ( -
    -

    - {t("credentials.keyAuthentication")} -

    - -
    -
    -
    - {t("credentials.keyType")} -
    - - {credentialDetails.keyType?.toUpperCase() || - t("unknown").toUpperCase()} - -
    -
    - - {renderSensitiveField( - credentialDetails.key, - "key", - t("credentials.privateKey"), - true, - )} - - {credentialDetails.keyPassword && - renderSensitiveField( - credentialDetails.keyPassword, - "keyPassword", - t("credentials.keyPassphrase"), - )} -
    - )} - -
    - -
    -
    - {t("credentials.securityReminder")} -
    -
    - {t("credentials.securityReminderText")} -
    -
    -
    -
    -
    - )} - - {activeTab === "usage" && ( - - - - - {t("credentials.hostsUsingCredential")} - {hostsUsing.length} - - - - {hostsUsing.length === 0 ? ( -
    - -

    {t("credentials.noHostsUsingCredential")}

    -
    - ) : ( - -
    - {hostsUsing.map((host) => ( -
    -
    -
    - -
    -
    -
    - {host.name || `${host.ip}:${host.port}`} -
    -
    - {host.ip}:{host.port} -
    -
    -
    -
    - {formatDate(host.createdAt)} -
    -
    - ))} -
    -
    - )} -
    -
    - )} -
    - - - - - -
    -
    - ); -}; - -export default CredentialViewer; diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx deleted file mode 100644 index f240e445..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx +++ /dev/null @@ -1,1217 +0,0 @@ -import React, { useState, useEffect, useMemo, useRef } from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { ScrollArea } from "@/components/ui/scroll-area.tsx"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion.tsx"; -import { Sheet, SheetContent } from "@/components/ui/sheet.tsx"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip.tsx"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover.tsx"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from "@/components/ui/command.tsx"; -import { - Search, - Key, - Folder, - Edit, - Trash2, - Shield, - Tag, - Info, - FolderMinus, - Pencil, - X, - Check, - Upload, - Server, - User, - ChevronsUpDown, - Terminal, - Copy, - Plus, - RefreshCw, -} from "lucide-react"; -import { cn } from "@/lib/utils.ts"; -import { - getCredentials, - deleteCredential, - updateCredential, - renameCredentialFolder, - deployCredentialToHost, - getSSHHosts, -} from "@/ui/main-axios.ts"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import CredentialViewer from "./CredentialViewer.tsx"; -import type { Credential, CredentialsManagerProps } from "../../../../../types"; - -export function CredentialsManager({ - onEditCredential, - onAddCredential, -}: CredentialsManagerProps) { - const { t } = useTranslation(); - const { confirmWithToast } = useConfirmation(); - const [credentials, setCredentials] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); - const [showViewer, setShowViewer] = useState(false); - const [viewingCredential] = useState(null); - const [draggedCredential, setDraggedCredential] = useState( - null, - ); - const [dragOverFolder, setDragOverFolder] = useState(null); - const [editingFolder, setEditingFolder] = useState(null); - const [editingFolderName, setEditingFolderName] = useState(""); - const [operationLoading, setOperationLoading] = useState(false); - const [showDeployDialog, setShowDeployDialog] = useState(false); - const [deployingCredential, setDeployingCredential] = - useState(null); - const [availableHosts, setAvailableHosts] = useState< - Array<{ - id: number; - name: string; - ip: string; - port: number; - username: string; - }> - >([]); - const [selectedHostId, setSelectedHostId] = useState(""); - const [deployLoading, setDeployLoading] = useState(false); - const [refreshing, setRefreshing] = useState(false); - const [hostComboboxOpen, setHostComboboxOpen] = useState(false); - const [showCopyCommandDialog, setShowCopyCommandDialog] = useState(false); - const [copyCommandCredential, setCopyCommandCredential] = - useState(null); - const dragCounter = useRef(0); - - useEffect(() => { - fetchCredentials(); - fetchHosts(); - }, []); - - useEffect(() => { - if (showDeployDialog) { - setHostComboboxOpen(false); - setSelectedHostId(""); - } - }, [showDeployDialog]); - - const fetchHosts = async () => { - try { - const hosts = await getSSHHosts(); - setAvailableHosts(hosts); - } catch (err) { - console.error("Failed to fetch hosts:", err); - } - }; - - const fetchCredentials = async (options: { showLoading?: boolean } = {}) => { - const { showLoading = true } = options; - try { - if (showLoading) setLoading(true); - const data = await getCredentials(); - setCredentials(data); - setError(null); - } catch { - setError(t("credentials.failedToFetchCredentials")); - } finally { - if (showLoading) setLoading(false); - } - }; - - const handleRefreshCredentials = async () => { - setRefreshing(true); - try { - await Promise.all([ - fetchCredentials({ showLoading: false }), - fetchHosts(), - ]); - } finally { - setRefreshing(false); - } - }; - - const handleEdit = (credential: Credential) => { - if (onEditCredential) { - onEditCredential(credential); - } - }; - - const handleDeploy = (credential: Credential) => { - if (credential.authType !== "key") { - toast.error(t("credentials.keyBasedOnlyForDeployment")); - return; - } - if (!credential.publicKey) { - toast.error(t("credentials.publicKeyRequiredForDeployment")); - return; - } - setDeployingCredential(credential); - setSelectedHostId(""); - setHostComboboxOpen(false); - setShowDeployDialog(true); - }; - - const handleCopyCommand = (credential: Credential) => { - if (credential.authType !== "key") { - toast.error(t("credentials.keyBasedOnlyForDeployment")); - return; - } - if (!credential.publicKey) { - toast.error(t("credentials.publicKeyRequiredForDeployment")); - return; - } - setCopyCommandCredential(credential); - setShowCopyCommandDialog(true); - }; - - const getDeployCommand = (credential: Credential): string => { - const publicKey = credential.publicKey?.trim() || ""; - const comment = `${credential.name || credential.username}@Termix`; - const keyWithComment = publicKey.includes(" ") - ? `${publicKey.split(" ").slice(0, 2).join(" ")} ${comment}` - : `${publicKey} ${comment}`; - return `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "${keyWithComment}" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`; - }; - - const copyDeployCommand = async () => { - if (!copyCommandCredential) return; - try { - await navigator.clipboard.writeText( - getDeployCommand(copyCommandCredential), - ); - toast.success(t("credentials.deployCommandCopied")); - } catch { - toast.error(t("credentials.failedToCopy")); - } - }; - - const performDeploy = async () => { - if (!deployingCredential || !selectedHostId) { - toast.error(t("credentials.selectTargetHost")); - return; - } - - setDeployLoading(true); - try { - const result = await deployCredentialToHost( - deployingCredential.id, - parseInt(selectedHostId), - ); - - if (result.success) { - toast.success(result.message || "SSH key deployed successfully"); - setShowDeployDialog(false); - setDeployingCredential(null); - setSelectedHostId(""); - } else { - toast.error(result.error || t("credentials.deploymentFailed")); - } - } catch (error) { - console.error("Deployment error:", error); - toast.error(t("credentials.failedToDeployKey")); - } finally { - setDeployLoading(false); - } - }; - - const handleDelete = async (credentialId: number, credentialName: string) => { - confirmWithToast( - t("credentials.confirmDeleteCredential", { name: credentialName }), - async () => { - try { - await deleteCredential(credentialId); - toast.success( - t("credentials.credentialDeletedSuccessfully", { - name: credentialName, - }), - ); - await fetchCredentials(); - window.dispatchEvent(new CustomEvent("credentials:changed")); - } catch (err: unknown) { - const error = err as { - response?: { data?: { error?: string; details?: string } }; - }; - if (error.response?.data?.details) { - toast.error( - `${error.response.data.error}\n${error.response.data.details}`, - ); - } else { - toast.error(t("credentials.failedToDeleteCredential")); - } - } - }, - "destructive", - ); - }; - - const handleRemoveFromFolder = async (credential: Credential) => { - confirmWithToast( - t("credentials.confirmRemoveFromFolder", { - name: credential.name || credential.username, - folder: credential.folder, - }), - async () => { - try { - setOperationLoading(true); - const updatedCredential = { ...credential, folder: "" }; - await updateCredential(credential.id, updatedCredential); - toast.success( - t("credentials.removedFromFolder", { - name: credential.name || credential.username, - }), - ); - await fetchCredentials(); - window.dispatchEvent(new CustomEvent("credentials:changed")); - } catch { - toast.error(t("credentials.failedToRemoveFromFolder")); - } finally { - setOperationLoading(false); - } - }, - ); - }; - - const handleFolderRename = async (oldName: string) => { - if (!editingFolderName.trim() || editingFolderName === oldName) { - setEditingFolder(null); - setEditingFolderName(""); - return; - } - - try { - setOperationLoading(true); - await renameCredentialFolder(oldName, editingFolderName.trim()); - toast.success( - t("credentials.folderRenamed", { - oldName, - newName: editingFolderName.trim(), - }), - ); - await fetchCredentials(); - window.dispatchEvent(new CustomEvent("credentials:changed")); - setEditingFolder(null); - setEditingFolderName(""); - } catch { - toast.error(t("credentials.failedToRenameFolder")); - } finally { - setOperationLoading(false); - } - }; - - const startFolderEdit = (folderName: string) => { - setEditingFolder(folderName); - setEditingFolderName(folderName); - }; - - const cancelFolderEdit = () => { - setEditingFolder(null); - setEditingFolderName(""); - }; - - const handleDragStart = (e: React.DragEvent, credential: Credential) => { - setDraggedCredential(credential); - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/plain", ""); - }; - - const handleDragEnd = () => { - setDraggedCredential(null); - setDragOverFolder(null); - dragCounter.current = 0; - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }; - - const handleDragEnter = (e: React.DragEvent, folderName: string) => { - e.preventDefault(); - dragCounter.current++; - setDragOverFolder(folderName); - }; - - const handleDragLeave = () => { - dragCounter.current--; - if (dragCounter.current === 0) { - setDragOverFolder(null); - } - }; - - const handleDrop = async (e: React.DragEvent, targetFolder: string) => { - e.preventDefault(); - dragCounter.current = 0; - setDragOverFolder(null); - - if (!draggedCredential) return; - - const newFolder = - targetFolder === t("credentials.uncategorized") ? "" : targetFolder; - - if (draggedCredential.folder === newFolder) { - setDraggedCredential(null); - return; - } - - try { - setOperationLoading(true); - const updatedCredential = { ...draggedCredential, folder: newFolder }; - await updateCredential(draggedCredential.id, updatedCredential); - toast.success( - t("credentials.movedToFolder", { - name: draggedCredential.name || draggedCredential.username, - folder: targetFolder, - }), - ); - await fetchCredentials(); - window.dispatchEvent(new CustomEvent("credentials:changed")); - } catch { - toast.error(t("credentials.failedToMoveToFolder")); - } finally { - setOperationLoading(false); - setDraggedCredential(null); - } - }; - - const filteredAndSortedCredentials = useMemo(() => { - let filtered = credentials; - - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - filtered = credentials.filter((credential) => { - const searchableText = [ - credential.name || "", - credential.username, - credential.description || "", - ...(credential.tags || []), - credential.authType, - credential.keyType || "", - ] - .join(" ") - .toLowerCase(); - return searchableText.includes(query); - }); - } - - return filtered.sort((a, b) => { - const aName = a.name || a.username; - const bName = b.name || b.username; - return aName.localeCompare(bName); - }); - }, [credentials, searchQuery]); - - const credentialsByFolder = useMemo(() => { - const grouped: { [key: string]: Credential[] } = {}; - - filteredAndSortedCredentials.forEach((credential) => { - const folder = credential.folder || t("credentials.uncategorized"); - if (!grouped[folder]) { - grouped[folder] = []; - } - grouped[folder].push(credential); - }); - - const sortedFolders = Object.keys(grouped).sort((a, b) => { - if (a === t("credentials.uncategorized")) return -1; - if (b === t("credentials.uncategorized")) return 1; - return a.localeCompare(b); - }); - - const sortedGrouped: { [key: string]: Credential[] } = {}; - sortedFolders.forEach((folder) => { - sortedGrouped[folder] = grouped[folder]; - }); - - return sortedGrouped; - }, [filteredAndSortedCredentials, t]); - - if (loading) { - return ( -
    -
    -
    -

    - {t("credentials.loadingCredentials")} -

    -
    -
    - ); - } - - if (error) { - return ( -
    -
    -

    {error}

    - -
    -
    - ); - } - - if (credentials.length === 0) { - return ( -
    -
    -
    -

    - {t("credentials.sshCredentials")} -

    -

    - {t("credentials.credentialsCount", { count: 0 })} -

    -
    -
    - -
    -
    - -
    -
    - - setSearchQuery(e.target.value)} - className="pl-10 h-9" - disabled - /> -
    - -
    - -
    -
    - -

    - {t("credentials.noCredentials")} -

    -

    - {t("credentials.noCredentialsMessage")} -

    -
    -
    -
    - ); - } - - return ( -
    -
    -
    -

    - {t("credentials.sshCredentials")} -

    -

    - {t("credentials.credentialsCount", { - count: filteredAndSortedCredentials.length, - })} -

    -
    -
    - -
    -
    - -
    -
    - - setSearchQuery(e.target.value)} - className="pl-10 h-9" - /> -
    - -
    - - -
    - {Object.entries(credentialsByFolder).map( - ([folder, folderCredentials]) => ( -
    handleDragEnter(e, folder)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, folder)} - > - - - -
    - - {editingFolder === folder ? ( -
    e.stopPropagation()} - > - - setEditingFolderName(e.target.value) - } - onKeyDown={(e) => { - if (e.key === "Enter") - handleFolderRename(folder); - if (e.key === "Escape") cancelFolderEdit(); - }} - className="h-6 text-sm px-2 flex-1" - autoFocus - disabled={operationLoading} - /> - - -
    - ) : ( - <> - { - e.stopPropagation(); - if (folder !== t("credentials.uncategorized")) { - startFolderEdit(folder); - } - }} - title={ - folder !== t("credentials.uncategorized") - ? t("credentials.clickToRenameFolder") - : "" - } - > - {folder} - - {folder !== t("credentials.uncategorized") && ( - - )} - - )} - - {folderCredentials.length} - -
    -
    - -
    - {folderCredentials.map((credential) => ( - - - -
    - handleDragStart(e, credential) - } - onDragEnd={handleDragEnd} - className={`bg-field border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-hover-alt transition-all duration-200 p-3 group relative ${ - draggedCredential?.id === credential.id - ? "opacity-50 scale-95" - : "" - }`} - onClick={() => handleEdit(credential)} - > -
    -
    -
    -

    - {credential.name || - `${credential.username}`} -

    -
    -

    - {credential.username} -

    -

    - {t("credentials.idLabel")}{" "} - {credential.id} -

    -

    - {credential.authType === "password" - ? t("credentials.password") - : t("credentials.sshKey")} -

    -
    -
    - {credential.folder && - credential.folder !== "" && ( - - - - - -

    - Remove from folder " - {credential.folder}" -

    -
    -
    - )} - - - - - -

    Edit credential

    -
    -
    - {credential.authType === "key" && ( - <> - - - - - -

    Deploy SSH key to host

    -
    -
    - - - - - -

    - {t( - "credentials.copyDeployCommand", - )} -

    -
    -
    - - )} - - - - - -

    Delete credential

    -
    -
    -
    -
    - -
    - {credential.tags && - credential.tags.length > 0 && ( -
    - {credential.tags - .slice(0, 6) - .map((tag, index) => ( - - - {tag} - - ))} - {credential.tags.length > 6 && ( - - +{credential.tags.length - 6} - - )} -
    - )} - -
    - - {credential.authType === "password" ? ( - - ) : ( - - )} - {credential.authType} - -
    -
    -
    -
    - -
    -

    - Click to edit credential -

    -

    - Drag to move between folders -

    -
    -
    -
    -
    - ))} -
    -
    -
    -
    -
    - ), - )} -
    -
    - - {showViewer && viewingCredential && ( - setShowViewer(false)} - onEdit={() => { - setShowViewer(false); - handleEdit(viewingCredential); - }} - /> - )} - - - -
    -
    -
    -
    -
    - {t("credentials.deploySSHKey")} -
    -
    - {t("credentials.deploySSHKeyDescription")} -
    -
    -
    -
    - -
    - {deployingCredential && ( -
    -

    - - {t("credentials.sourceCredential")} -

    -
    -
    -
    - -
    -
    -
    - {t("common.name")} -
    -
    - {deployingCredential.name || - deployingCredential.username} -
    -
    -
    -
    -
    - -
    -
    -
    - {t("common.username")} -
    -
    - {deployingCredential.username} -
    -
    -
    -
    -
    - -
    -
    -
    - {t("credentials.keyType")} -
    -
    - {deployingCredential.keyType || - t("credentials.sshKey")} -
    -
    -
    -
    -
    - )} - -
    - - - - - - - - - - {availableHosts.length === 0 - ? t("credentials.noHostsAvailable") - : t("credentials.noHostsMatchSearch")} - - - {availableHosts.map((host) => ( - { - setSelectedHostId(host.id.toString()); - setHostComboboxOpen(false); - }} - > - -
    -
    - -
    -
    -
    - {host.name || host.ip} -
    -
    - {host.username}@{host.ip}:{host.port} -
    -
    -
    -
    - ))} -
    -
    -
    -
    -
    - -
    -
    - -
    -

    - {t("credentials.deploymentProcess")} -

    -

    - {t("credentials.deploymentProcessDescription")} -

    -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    -
    -
    - - - -
    -
    -
    -
    -
    - {t("credentials.copyDeployCommand")} -
    -
    - {t("credentials.copyDeployCommandDescription")} -
    -
    -
    -
    - -
    - {copyCommandCredential && ( - <> -
    -

    - - {t("credentials.sourceCredential")} -

    -
    -
    -
    - -
    -
    -
    - {t("common.name")} -
    -
    - {copyCommandCredential.name || - copyCommandCredential.username} -
    -
    -
    -
    -
    - -
    -
    -
    - {t("credentials.keyType")} -
    -
    - {copyCommandCredential.keyType || - t("credentials.sshKey")} -
    -
    -
    -
    -
    - -
    - -
    -                      {getDeployCommand(copyCommandCredential)}
    -                    
    -
    - -
    -
    - -

    - {t("credentials.manualDeployInfo")} -

    -
    -
    - -
    -
    - - -
    -
    - - )} -
    -
    -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx deleted file mode 100644 index 7bb98fbd..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx +++ /dev/null @@ -1,514 +0,0 @@ -import { - FormField, - FormItem, - FormLabel, - FormControl, -} from "@/components/ui/form.tsx"; -import { PasswordInput } from "@/components/ui/password-input.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; -import { Controller } from "react-hook-form"; -import CodeMirror from "@uiw/react-codemirror"; -import { EditorView } from "@codemirror/view"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; -import { - generateKeyPair, - generatePublicKeyFromPrivate, -} from "@/ui/main-axios.ts"; -import type { CredentialAuthenticationTabProps } from "./shared/tab-types"; - -export function CredentialAuthenticationTab({ - form, - authTab, - setAuthTab, - detectedKeyType, - detectedPublicKeyType, - keyDetectionLoading, - publicKeyDetectionLoading, - editorTheme, - debouncedKeyDetection, - debouncedPublicKeyDetection, - getFriendlyKeyTypeName, -}: CredentialAuthenticationTabProps) { - const { t } = useTranslation(); - - return ( - <> - - {t("credentials.authentication")} - - { - const newAuthType = value as "password" | "key"; - setAuthTab(newAuthType); - form.setValue("authType", newAuthType); - - form.setValue("password", ""); - form.setValue("key", null); - form.setValue("keyPassword", ""); - form.setValue("keyType", "auto"); - }} - className="flex-1 flex flex-col h-full min-h-0" - > - - - {t("credentials.password")} - - - {t("credentials.key")} - - - - ( - - {t("credentials.password")} - - - - - )} - /> - - -
    -
    - - {t("credentials.generateKeyPair")} - - -
    -
    - {t("credentials.generateKeyPairDescription")} -
    -
    - -
    - - - -
    -
    -
    - ( - - - {t("credentials.sshPrivateKey")} - -
    -
    - { - const file = e.target.files?.[0]; - if (file) { - try { - const fileContent = await file.text(); - field.onChange(fileContent); - debouncedKeyDetection( - fileContent, - form.watch("keyPassword"), - ); - } catch (error) { - console.error( - "Failed to read uploaded file:", - error, - ); - } - } - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
    -
    - - { - field.onChange(value); - debouncedKeyDetection( - value, - form.watch("keyPassword"), - ); - }} - placeholder={t("placeholders.pastePrivateKey")} - theme={editorTheme} - className="border border-input rounded-md overflow-hidden" - minHeight="120px" - basicSetup={{ - lineNumbers: true, - foldGutter: false, - dropCursor: false, - allowMultipleSelections: false, - highlightSelectionMatches: false, - searchKeymap: false, - scrollPastEnd: false, - }} - extensions={[ - EditorView.theme({ - ".cm-scroller": { - overflow: "auto", - scrollbarWidth: "thin", - scrollbarColor: - "var(--scrollbar-thumb) var(--scrollbar-track)", - }, - }), - ]} - /> - - {detectedKeyType && ( -
    - - {t("credentials.detectedKeyType")}:{" "} - - - {getFriendlyKeyTypeName(detectedKeyType)} - - {keyDetectionLoading && ( - - ({t("credentials.detectingKeyType")}) - - )} -
    - )} -
    - )} - /> - ( - - - {t("credentials.sshPublicKey")} - -
    -
    - { - const file = e.target.files?.[0]; - if (file) { - try { - const fileContent = await file.text(); - field.onChange(fileContent); - debouncedPublicKeyDetection(fileContent); - } catch (error) { - console.error( - "Failed to read uploaded public key file:", - error, - ); - } - } - }} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - -
    - -
    - - { - field.onChange(value); - debouncedPublicKeyDetection(value); - }} - placeholder={t("placeholders.pastePublicKey")} - theme={editorTheme} - className="border border-input rounded-md overflow-hidden" - minHeight="120px" - basicSetup={{ - lineNumbers: true, - foldGutter: false, - dropCursor: false, - allowMultipleSelections: false, - highlightSelectionMatches: false, - searchKeymap: false, - scrollPastEnd: false, - }} - extensions={[ - EditorView.theme({ - ".cm-scroller": { - overflow: "auto", - scrollbarWidth: "thin", - scrollbarColor: - "var(--scrollbar-thumb) var(--scrollbar-track)", - }, - }), - ]} - /> - - {detectedPublicKeyType && field.value && ( -
    - - {t("credentials.detectedKeyType")}:{" "} - - - {getFriendlyKeyTypeName(detectedPublicKeyType)} - - {publicKeyDetectionLoading && ( - - ({t("credentials.detectingKeyType")}) - - )} -
    - )} -
    - )} - /> -
    -
    - ( - - {t("credentials.keyPassword")} - - - - - )} - /> -
    -
    -
    -
    - - ); -} diff --git a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx deleted file mode 100644 index 60833b3d..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { - FormField, - FormItem, - FormLabel, - FormControl, -} from "@/components/ui/form.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import type { CredentialGeneralTabProps } from "./shared/tab-types"; - -export function CredentialGeneralTab({ - form, - tagInput, - setTagInput, - folderDropdownOpen, - setFolderDropdownOpen, - folderInputRef, - folderDropdownRef, - filteredFolders, - handleFolderClick, -}: CredentialGeneralTabProps) { - const { t } = useTranslation(); - - return ( - <> - - {t("credentials.basicInformation")} - -
    - ( - - {t("credentials.credentialName")} - - - - - )} - /> - - ( - - {t("credentials.username")} - - - - - )} - /> -
    - - {t("credentials.organization")} - -
    - ( - - {t("credentials.description")} - - - - - )} - /> - - ( - - {t("credentials.folder")} - - setFolderDropdownOpen(true)} - onChange={(e) => { - field.onChange(e); - setFolderDropdownOpen(true); - }} - /> - - {folderDropdownOpen && filteredFolders.length > 0 && ( -
    -
    - {filteredFolders.map((folder) => ( - - ))} -
    -
    - )} -
    - )} - /> - - ( - - {t("credentials.tags")} - -
    - {(field.value || []).map((tag: string, idx: number) => ( - - {tag} - - - ))} - setTagInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === " " && tagInput.trim() !== "") { - e.preventDefault(); - const currentTags = field.value || []; - if (!currentTags.includes(tagInput.trim())) { - field.onChange([...currentTags, tagInput.trim()]); - } - setTagInput(""); - } else if (e.key === "Enter" && tagInput.trim() !== "") { - e.preventDefault(); - const currentTags = field.value || []; - if (!currentTags.includes(tagInput.trim())) { - field.onChange([...currentTags, tagInput.trim()]); - } - setTagInput(""); - } else if ( - e.key === "Backspace" && - tagInput === "" && - (field.value || []).length > 0 - ) { - const currentTags = field.value || []; - field.onChange(currentTags.slice(0, -1)); - } - }} - placeholder={t("credentials.addTagsSpaceToAdd")} - /> -
    -
    -
    - )} - /> -
    - - ); -} diff --git a/src/ui/desktop/apps/host-manager/credentials/tabs/shared/tab-types.ts b/src/ui/desktop/apps/host-manager/credentials/tabs/shared/tab-types.ts deleted file mode 100644 index 2fb22ef6..00000000 --- a/src/ui/desktop/apps/host-manager/credentials/tabs/shared/tab-types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { UseFormReturn } from "react-hook-form"; -import type React from "react"; - -export interface CredentialGeneralTabProps { - form: UseFormReturn; - folders: string[]; - tagInput: string; - setTagInput: (value: string) => void; - folderDropdownOpen: boolean; - setFolderDropdownOpen: (value: boolean) => void; - folderInputRef: React.RefObject; - folderDropdownRef: React.RefObject; - filteredFolders: string[]; - handleFolderClick: (folder: string) => void; -} - -export interface CredentialAuthenticationTabProps { - form: UseFormReturn; - authTab: "password" | "key"; - setAuthTab: (value: "password" | "key") => void; - detectedKeyType: string | null; - setDetectedKeyType: (value: string | null) => void; - keyDetectionLoading: boolean; - setKeyDetectionLoading: (value: boolean) => void; - detectedPublicKeyType: string | null; - setDetectedPublicKeyType: (value: string | null) => void; - publicKeyDetectionLoading: boolean; - setPublicKeyDetectionLoading: (value: boolean) => void; - keyDetectionTimeoutRef: React.MutableRefObject; - publicKeyDetectionTimeoutRef: React.MutableRefObject; - editorTheme: unknown; - debouncedKeyDetection: (keyValue: string, keyPassword?: string) => void; - debouncedPublicKeyDetection: (publicKeyValue: string) => void; - getFriendlyKeyTypeName: (keyType: string) => string; -} diff --git a/src/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx b/src/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx deleted file mode 100644 index 6c559c73..00000000 --- a/src/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog.tsx"; -import { Button } from "@/components/ui/button.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { useTranslation } from "react-i18next"; -import { - Folder, - Server, - Cloud, - Database, - Box, - Package, - Layers, - Archive, - HardDrive, - Globe, -} from "lucide-react"; - -interface FolderEditDialogProps { - folderName: string; - currentColor?: string; - currentIcon?: string; - open: boolean; - onOpenChange: (open: boolean) => void; - onSave: (color: string, icon: string) => Promise; -} - -const AVAILABLE_COLORS = [ - { value: "#ef4444", label: "Red" }, - { value: "#f97316", label: "Orange" }, - { value: "#eab308", label: "Yellow" }, - { value: "#22c55e", label: "Green" }, - { value: "#3b82f6", label: "Blue" }, - { value: "#a855f7", label: "Purple" }, - { value: "#ec4899", label: "Pink" }, - { value: "#6b7280", label: "Gray" }, -]; - -const AVAILABLE_ICONS = [ - { value: "Folder", label: "Folder", Icon: Folder }, - { value: "Server", label: "Server", Icon: Server }, - { value: "Cloud", label: "Cloud", Icon: Cloud }, - { value: "Database", label: "Database", Icon: Database }, - { value: "Box", label: "Box", Icon: Box }, - { value: "Package", label: "Package", Icon: Package }, - { value: "Layers", label: "Layers", Icon: Layers }, - { value: "Archive", label: "Archive", Icon: Archive }, - { value: "HardDrive", label: "HardDrive", Icon: HardDrive }, - { value: "Globe", label: "Globe", Icon: Globe }, -]; - -export function FolderEditDialog({ - folderName, - currentColor, - currentIcon, - open, - onOpenChange, - onSave, -}: FolderEditDialogProps) { - const { t } = useTranslation(); - const [selectedColor, setSelectedColor] = useState( - currentColor || AVAILABLE_COLORS[0].value, - ); - const [selectedIcon, setSelectedIcon] = useState( - currentIcon || AVAILABLE_ICONS[0].value, - ); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (open) { - setSelectedColor(currentColor || AVAILABLE_COLORS[0].value); - setSelectedIcon(currentIcon || AVAILABLE_ICONS[0].value); - } - }, [open, currentColor, currentIcon]); - - const handleSave = async () => { - setLoading(true); - try { - await onSave(selectedColor, selectedIcon); - onOpenChange(false); - } catch (error) { - console.error("Failed to save folder metadata:", error); - } finally { - setLoading(false); - } - }; - - return ( - - - - - - {t("hosts.editFolderAppearance")} - - - {t("hosts.editFolderAppearanceDesc")}:{" "} - {folderName} - - - -
    -
    - -
    - {AVAILABLE_COLORS.map((color) => ( -
    -
    - -
    - -
    - {AVAILABLE_ICONS.map(({ value, label, Icon }) => ( - - ))} -
    -
    - -
    - -
    - {(() => { - const IconComponent = - AVAILABLE_ICONS.find((i) => i.value === selectedIcon)?.Icon || - Folder; - return ( - - ); - })()} - {folderName} -
    -
    -
    - - - - - -
    -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx deleted file mode 100644 index 0545f8b6..00000000 --- a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import { HostManagerViewer } from "@/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { HostManagerEditor } from "@/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx"; -import { CredentialsManager } from "@/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx"; -import { CredentialEditor } from "@/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx"; -import { useSidebar } from "@/components/ui/sidebar.tsx"; -import { useTranslation } from "react-i18next"; -import { exportSSHHostWithCredentials } from "@/ui/main-axios.ts"; -import type { SSHHost, HostManagerProps } from "../../../types/index"; - -export function HostManager({ - isTopbarOpen, - initialTab = "hosts", - hostConfig, - _updateTimestamp, - rightSidebarOpen = false, - rightSidebarWidth = 400, - currentTabId, - updateTab, -}: HostManagerProps): React.ReactElement { - const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState( - initialTab === "host_viewer" || initialTab === "add_host" - ? "hosts" - : initialTab === "credentials" || initialTab === "add_credential" - ? "credentials" - : initialTab, - ); - const [editingHost, setEditingHost] = useState( - hostConfig || null, - ); - const [isAddingHost, setIsAddingHost] = useState(false); - const [isAddingCredential, setIsAddingCredential] = useState(false); - - useEffect(() => {}, [editingHost]); - - const [editingCredential, setEditingCredential] = useState<{ - id: number; - name?: string; - username: string; - } | null>(null); - const { state: sidebarState } = useSidebar(); - const ignoreNextHostConfigChangeRef = useRef(false); - const lastProcessedHostIdRef = useRef(undefined); - - useEffect(() => { - const handleAddHostEvent = () => { - setActiveTab("hosts"); - setEditingHost(null); - setIsAddingHost(true); - setIsAddingCredential(false); - }; - - const handleAddCredentialEvent = () => { - setActiveTab("credentials"); - setEditingCredential(null); - setIsAddingCredential(true); - setIsAddingHost(false); - }; - - window.addEventListener("host-manager:add-host", handleAddHostEvent); - window.addEventListener( - "host-manager:add-credential", - handleAddCredentialEvent, - ); - - return () => { - window.removeEventListener("host-manager:add-host", handleAddHostEvent); - window.removeEventListener( - "host-manager:add-credential", - handleAddCredentialEvent, - ); - }; - }, []); - - useEffect(() => { - if (_updateTimestamp !== undefined) { - const normalizedTab = - initialTab === "host_viewer" || initialTab === "add_host" - ? "hosts" - : initialTab === "credentials" || initialTab === "add_credential" - ? "credentials" - : initialTab; - - if (initialTab && normalizedTab !== activeTab) { - setActiveTab(normalizedTab); - } - - if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) { - lastProcessedHostIdRef.current = hostConfig.id; - setIsAddingHost(false); - exportSSHHostWithCredentials(hostConfig.id) - .then((fullHost) => - setEditingHost({ ...hostConfig, ...fullHost } as SSHHost), - ) - .catch(() => setEditingHost(hostConfig)); - } else if (!hostConfig && editingHost) { - setEditingHost(null); - setIsAddingHost(false); - } - } else { - if (initialTab) { - const normalizedTab = - initialTab === "host_viewer" || initialTab === "add_host" - ? "hosts" - : initialTab === "credentials" || initialTab === "add_credential" - ? "credentials" - : initialTab; - setActiveTab(normalizedTab); - } - if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) { - lastProcessedHostIdRef.current = hostConfig.id; - setIsAddingHost(false); - exportSSHHostWithCredentials(hostConfig.id) - .then((fullHost) => - setEditingHost({ ...hostConfig, ...fullHost } as SSHHost), - ) - .catch(() => setEditingHost(hostConfig)); - } - } - }, [_updateTimestamp, initialTab, hostConfig]); - - const handleEditHost = async (host: SSHHost) => { - setIsAddingHost(false); - lastProcessedHostIdRef.current = host.id; - try { - const fullHost = await exportSSHHostWithCredentials(host.id); - setEditingHost({ ...host, ...fullHost } as SSHHost); - } catch { - setEditingHost(host); - } - }; - - const handleAddHost = () => { - setEditingHost(null); - setIsAddingHost(true); - lastProcessedHostIdRef.current = undefined; - }; - - const handleFormSubmit = (savedHost?: SSHHost) => { - ignoreNextHostConfigChangeRef.current = true; - const isUpdatingHost = Boolean(editingHost?.id && savedHost?.id); - - if (isUpdatingHost) { - setEditingHost((current) => ({ ...current, ...savedHost }) as SSHHost); - setIsAddingHost(false); - lastProcessedHostIdRef.current = savedHost.id; - if (updateTab && currentTabId !== undefined) { - updateTab(currentTabId, { hostConfig: savedHost }); - } - return; - } - - const savedHostId = savedHost?.id || editingHost?.id; - setEditingHost(null); - setIsAddingHost(false); - setTimeout(() => { - lastProcessedHostIdRef.current = savedHostId; - }, 500); - }; - - const handleEditCredential = (credential: { - id: number; - name?: string; - username: string; - }) => { - setEditingCredential(credential); - setIsAddingCredential(false); - }; - - const handleAddCredential = () => { - setEditingCredential(null); - setIsAddingCredential(true); - }; - - const handleCredentialFormSubmit = () => { - setEditingCredential(null); - setIsAddingCredential(false); - }; - - const handleTabChange = (value: string) => { - if (activeTab !== value) { - setEditingHost(null); - setEditingCredential(null); - setIsAddingHost(false); - setIsAddingCredential(false); - lastProcessedHostIdRef.current = undefined; - - if (updateTab && currentTabId !== undefined) { - updateTab(currentTabId, { hostConfig: null }); - } - } - - setActiveTab(value); - - if (updateTab && currentTabId !== undefined) { - updateTab(currentTabId, { initialTab: value }); - } - }; - - const topMarginPx = isTopbarOpen ? 74 : 26; - const leftMarginPx = sidebarState === "collapsed" ? 26 : 8; - const bottomMarginPx = 8; - - return ( -
    -
    -
    - - - - {t("hosts.hosts")} - - - {t("credentials.credentials")} - - - - - {editingHost !== null || isAddingHost ? ( -
    - { - setEditingHost(null); - setIsAddingHost(false); - lastProcessedHostIdRef.current = undefined; - }} - /> -
    - ) : ( - - )} -
    - - - {editingCredential !== null || isAddingCredential ? ( -
    - { - setEditingCredential(null); - setIsAddingCredential(false); - }} - /> -
    - ) : ( -
    - -
    - )} -
    -
    -
    -
    -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx deleted file mode 100644 index caa159eb..00000000 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ /dev/null @@ -1,1458 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm, type Resolver } from "react-hook-form"; -import { z } from "zod"; - -import { Button } from "@/components/ui/button.tsx"; -import { - Form, - FormControl, - FormField, - FormItem, -} from "@/components/ui/form.tsx"; - -import { ScrollArea } from "@/components/ui/scroll-area.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; -import React, { useEffect, useRef, useState } from "react"; -import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; -import { toast } from "sonner"; -import { - createSSHHost, - getCredentials, - getSSHHosts, - updateSSHHost, - enableAutoStart, - disableAutoStart, - getSnippets, - getGuacamoleSettings, -} from "@/ui/main-axios.ts"; -import { useTranslation } from "react-i18next"; -import { oneDark } from "@codemirror/theme-one-dark"; -import { githubLight } from "@uiw/codemirror-theme-github"; -import { useTheme } from "@/components/theme-provider.tsx"; -import type { StatsConfig } from "@/types/stats-widgets.ts"; -import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; - -import { DEFAULT_TERMINAL_CONFIG } from "@/constants/terminal-themes.ts"; -import type { SSHHost, SSHHostData, Credential } from "@/types"; -import { ArrowLeft } from "lucide-react"; -import { HostGeneralTab } from "./tabs/HostGeneralTab"; -import { HostTerminalTab } from "./tabs/HostTerminalTab"; -import { HostDockerTab } from "./tabs/HostDockerTab"; -import { HostTunnelTab } from "./tabs/HostTunnelTab"; -import { HostFileManagerTab } from "./tabs/HostFileManagerTab"; -import { HostStatisticsTab } from "./tabs/HostStatisticsTab"; -import { HostStatusTab } from "./tabs/HostStatusTab"; -import { HostSharingTab } from "./tabs/HostSharingTab"; -import { HostRemoteDesktopTab } from "./tabs/HostRemoteDesktopTab"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; - -interface SSHManagerHostEditorProps { - editingHost?: SSHHost | null; - onFormSubmit?: (updatedHost?: SSHHost) => void; - onBack?: () => void; -} - -function isValidIPv4(value: string) { - const parts = value.split("."); - return ( - parts.length === 4 && - parts.every((part) => { - if (!/^\d+$/.test(part)) return false; - const parsed = Number(part); - return parsed >= 0 && parsed <= 255; - }) - ); -} - -export function HostManagerEditor({ - editingHost, - onFormSubmit, - onBack, -}: SSHManagerHostEditorProps) { - const { t } = useTranslation(); - const { theme: appTheme } = useTheme(); - - const isDarkMode = - appTheme === "dark" || - (appTheme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - const editorTheme = isDarkMode ? oneDark : githubLight; - const [folders, setFolders] = useState([]); - const [hosts, setHosts] = useState([]); - const [credentials, setCredentials] = useState([]); - const [snippets, setSnippets] = useState< - Array<{ id: number; name: string; content: string }> - >([]); - const [proxyMode, setProxyMode] = useState<"single" | "chain">("single"); - - const [authTab, setAuthTab] = useState< - "password" | "key" | "credential" | "none" | "opkssh" - >("password"); - const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( - "upload", - ); - const [isSubmitting, setIsSubmitting] = useState(false); - const [activeTab, setActiveTab] = useState("general"); - const [formError, setFormError] = useState(null); - const [guacEnabled, setGuacEnabled] = useState(true); - - useEffect(() => { - setFormError(null); - }, [activeTab]); - - useEffect(() => { - getGuacamoleSettings() - .then((data) => setGuacEnabled(data.enabled)) - .catch(() => {}); - }, []); - - const [statusIntervalUnit, setStatusIntervalUnit] = useState< - "seconds" | "minutes" - >("seconds"); - const [metricsIntervalUnit, setMetricsIntervalUnit] = useState< - "seconds" | "minutes" - >("seconds"); - - const ipInputRef = useRef(null); - - useEffect(() => { - const fetchData = async () => { - try { - const [hostsData, credentialsData, snippetsData] = await Promise.all([ - getSSHHosts(), - getCredentials(), - getSnippets(), - ]); - setHosts(hostsData); - setCredentials(credentialsData as Credential[]); - setSnippets(Array.isArray(snippetsData) ? snippetsData : []); - - const uniqueFolders = [ - ...new Set( - hostsData - .filter((host) => host.folder && host.folder.trim() !== "") - .map((host) => host.folder), - ), - ].sort(); - - setFolders(uniqueFolders); - } catch (error) { - console.error("Host manager operation failed:", error); - } - }; - - fetchData(); - }, []); - - useEffect(() => { - const handleCredentialChange = async () => { - try { - const hostsData = await getSSHHosts(); - - const uniqueFolders = [ - ...new Set( - hostsData - .filter((host) => host.folder && host.folder.trim() !== "") - .map((host) => host.folder), - ), - ].sort(); - - const uniqueConfigurations = [ - ...new Set( - hostsData - .filter((host) => host.name && host.name.trim() !== "") - .map((host) => host.name), - ), - ].sort(); - - setFolders(uniqueFolders); - setSshConfigurations(uniqueConfigurations); - } catch (error) { - console.error("Host manager operation failed:", error); - } - }; - - window.addEventListener("credentials:changed", handleCredentialChange); - - return () => { - window.removeEventListener("credentials:changed", handleCredentialChange); - }; - }, []); - - const tunnelConnectionSchema = z.object({ - scope: z.enum(["s2s", "c2s"]).default("s2s").optional(), - mode: z.enum(["local", "remote", "dynamic"]).default("remote").optional(), - tunnelType: z.enum(["local", "remote"]).default("remote").optional(), - bindHost: z.string().optional(), - sourcePort: z.coerce.number().min(1).max(65535), - endpointPort: z.coerce.number().min(1).max(65535), - endpointHost: z.string().optional(), - targetHost: z.string().optional(), - endpointPassword: z.string().optional(), - endpointKey: z.string().optional(), - endpointKeyPassword: z.string().optional(), - endpointAuthType: z.string().optional(), - endpointKeyType: z.string().optional(), - maxRetries: z.coerce.number().min(0).max(100).default(3), - retryInterval: z.coerce.number().min(1).max(3600).default(10), - autoStart: z.boolean().default(false), - }); - - const formSchema = z - .object({ - connectionType: z.enum(["ssh", "rdp", "vnc", "telnet"]).default("ssh"), - name: z.string().optional(), - ip: z.string().min(1, t("hosts.ipRequired", "IP address is required")), - port: z.coerce.number().min(1).max(65535), - username: z.string().optional(), - folder: z.string().optional(), - tags: z.array(z.string().min(1)).default([]), - pin: z.boolean().default(false), - authType: z.enum(["password", "key", "credential", "none", "opkssh"]), - credentialId: z.number().optional().nullable(), - overrideCredentialUsername: z.boolean().optional(), - password: z.string().optional(), - key: z.any().optional().nullable(), - keyPassword: z.string().optional(), - keyType: z - .enum([ - "auto", - "ssh-rsa", - "ssh-ed25519", - "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", - "ecdsa-sha2-nistp521", - "ssh-dss", - "ssh-rsa-sha2-256", - "ssh-rsa-sha2-512", - ]) - .optional(), - enableTerminal: z.boolean().default(true), - enableTunnel: z.boolean().default(true), - tunnelConnections: z.array(tunnelConnectionSchema).default([]), - serverTunnels: z.array(tunnelConnectionSchema).default([]), - enableFileManager: z.boolean().default(true), - defaultPath: z.string().optional(), - statsConfig: z - .object({ - enabledWidgets: z - .array( - z.enum([ - "cpu", - "memory", - "disk", - "network", - "uptime", - "processes", - "system", - "login_stats", - "ports", - "firewall", - ]), - ) - .default([ - "cpu", - "memory", - "disk", - "network", - "uptime", - "system", - "login_stats", - "ports", - "firewall", - ]), - statusCheckEnabled: z.boolean().default(true), - statusCheckInterval: z.number().min(5).max(3600).default(30), - useGlobalStatusInterval: z.boolean().default(true), - metricsEnabled: z.boolean().default(true), - metricsInterval: z.number().min(5).max(3600).default(30), - useGlobalMetricsInterval: z.boolean().default(true), - disableTcpPing: z.boolean().default(false), - }) - .default({ - enabledWidgets: [ - "cpu", - "memory", - "disk", - "network", - "uptime", - "system", - "login_stats", - "ports", - "firewall", - ], - statusCheckEnabled: true, - statusCheckInterval: 30, - useGlobalStatusInterval: true, - metricsEnabled: true, - metricsInterval: 30, - useGlobalMetricsInterval: true, - disableTcpPing: false, - }), - terminalConfig: z - .object({ - cursorBlink: z.boolean(), - cursorStyle: z.enum(["block", "underline", "bar"]), - fontSize: z.number().min(8).max(24), - fontFamily: z.string(), - letterSpacing: z.number().min(-2).max(10), - lineHeight: z.number().min(1.0).max(2.0), - theme: z.string(), - scrollback: z.number().min(1000).max(50000), - bellStyle: z.enum(["none", "sound", "visual", "both"]), - rightClickSelectsWord: z.boolean(), - fastScrollModifier: z.enum(["alt", "ctrl", "shift"]), - fastScrollSensitivity: z.number().min(1).max(10), - minimumContrastRatio: z.number().min(1).max(21), - backspaceMode: z.enum(["normal", "control-h"]), - agentForwarding: z.boolean(), - environmentVariables: z.array( - z.object({ - key: z.string(), - value: z.string(), - }), - ), - startupSnippetId: z.number().nullable(), - autoMosh: z.boolean(), - moshCommand: z.string(), - sudoPasswordAutoFill: z.boolean(), - sudoPassword: z.string().optional(), - keepaliveInterval: z.number().min(0).max(300000).optional(), - keepaliveCountMax: z.number().min(0).max(100).optional(), - autoTmux: z.boolean(), - }) - .optional(), - forceKeyboardInteractive: z.boolean().optional(), - jumpHosts: z - .array( - z.object({ - hostId: z.number().min(1), - }), - ) - .default([]), - quickActions: z - .array( - z.object({ - name: z.string().min(1), - snippetId: z.number().min(1), - }), - ) - .default([]), - notes: z.string().optional(), - useSocks5: z.boolean().optional(), - socks5Host: z.string().optional(), - socks5Port: z.coerce.number().min(1).max(65535).optional(), - socks5Username: z.string().optional(), - socks5Password: z.string().optional(), - socks5ProxyChain: z - .array( - z.object({ - host: z.string().min(1), - port: z.number().min(1).max(65535), - type: z.union([z.literal(4), z.literal(5)]), - username: z.string().optional(), - password: z.string().optional(), - }), - ) - .optional(), - macAddress: z.string().optional(), - portKnockSequence: z - .array( - z.object({ - port: z.coerce.number().min(1).max(65535), - protocol: z.enum(["tcp", "udp"]).default("tcp"), - delay: z.coerce.number().min(0).max(60000).default(100), - }), - ) - .optional(), - enableDocker: z.boolean().default(false), - domain: z.string().optional(), - security: z.string().optional(), - ignoreCert: z.boolean().default(true), - guacamoleConfig: z.record(z.string(), z.unknown()).optional(), - showTerminalInSidebar: z.boolean().default(true), - showFileManagerInSidebar: z.boolean().default(false), - showTunnelInSidebar: z.boolean().default(false), - showDockerInSidebar: z.boolean().default(false), - showServerStatsInSidebar: z.boolean().default(false), - }) - .superRefine((data, ctx) => { - if (data.connectionType !== "ssh") { - return; - } - - if (!data.username || data.username.trim() === "") { - if (data.authType !== "credential") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("hosts.usernameRequired", "Username is required"), - path: ["username"], - }); - } - } - - const serverTunnels = data.serverTunnels; - - serverTunnels.forEach((tunnel, index) => { - if ( - (tunnel.scope || "s2s") === "s2s" && - (!tunnel.endpointHost || tunnel.endpointHost.trim() === "") - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t( - "tunnels.endpointSshConfigRequired", - "Endpoint SSH configuration is required", - ), - path: ["serverTunnels", index, "endpointHost"], - }); - } - - const currentHostIp = - tunnel.mode === "remote" ? tunnel.targetHost : tunnel.bindHost; - if (currentHostIp && !isValidIPv4(currentHostIp)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("tunnels.invalidCurrentHostIp"), - path: [ - "serverTunnels", - index, - tunnel.mode === "remote" ? "targetHost" : "bindHost", - ], - }); - } - }); - - if (data.authType === "none") { - return; - } - - if (data.authType === "opkssh") { - return; - } - - if (data.authType === "password") { - if ( - !data.password || - (typeof data.password === "string" && - data.password.trim() === "" && - data.password !== "existing_password") - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("hosts.passwordRequired", "Password is required"), - path: ["password"], - }); - } - } else if (data.authType === "key") { - if ( - !data.key || - (typeof data.key === "string" && - data.key.trim() === "" && - data.key !== "existing_key") - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("hosts.sshKeyRequired", "SSH key is required"), - path: ["key"], - }); - } - if (!data.keyType) { - data.keyType = "auto"; - } - } else if (data.authType === "credential") { - if (!data.credentialId) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("hosts.credentialRequired", "Credential is required"), - path: ["credentialId"], - }); - } - } - }); - - type FormData = z.infer; - - const form = useForm({ - resolver: zodResolver(formSchema) as unknown as Resolver, - mode: "all", - defaultValues: { - connectionType: "ssh" as const, - name: "", - ip: "", - port: 22, - username: "", - folder: "", - tags: [], - pin: false, - authType: "password" as const, - credentialId: null, - overrideCredentialUsername: false, - password: "", - key: null, - keyPassword: "", - keyType: "auto" as const, - enableTerminal: true, - enableTunnel: true, - enableFileManager: true, - showTerminalInSidebar: true, - showFileManagerInSidebar: false, - showTunnelInSidebar: false, - showDockerInSidebar: false, - showServerStatsInSidebar: false, - defaultPath: "/", - tunnelConnections: [], - serverTunnels: [], - jumpHosts: [], - quickActions: [], - statsConfig: DEFAULT_STATS_CONFIG, - terminalConfig: DEFAULT_TERMINAL_CONFIG, - forceKeyboardInteractive: false, - notes: "", - useSocks5: false, - socks5Host: "", - socks5Port: 1080, - socks5Username: "", - socks5Password: "", - socks5ProxyChain: [], - macAddress: "", - portKnockSequence: [], - enableDocker: false, - domain: "", - security: "any", - ignoreCert: true, - guacamoleConfig: {}, - }, - }); - - const watchedFields = form.watch(); - const formState = form.formState; - const watchedConnectionType = form.watch("connectionType") || "ssh"; - - const prevConnectionTypeRef = useRef("ssh"); - useEffect(() => { - const prev = prevConnectionTypeRef.current; - const current = watchedConnectionType; - if (prev === current) return; - prevConnectionTypeRef.current = current; - - const portDefaults: Record = { - ssh: 22, - rdp: 3389, - vnc: 5900, - telnet: 23, - }; - const currentPort = form.getValues("port"); - const oldDefault = portDefaults[prev] || 22; - if (currentPort === oldDefault) { - form.setValue("port", portDefaults[current] || 22); - } - - if (current !== "ssh") { - const currentStatsConfig = form.getValues("statsConfig"); - form.setValue("statsConfig", { - ...currentStatsConfig, - metricsEnabled: false, - disableTcpPing: false, - }); - } - - if (activeTab !== "general" && current !== "ssh") { - setActiveTab("general"); - } - }, [watchedConnectionType]); - - const isFormValid = React.useMemo(() => { - const errors = formState.errors; - - if (!watchedFields.ip) return false; - - if (watchedFields.connectionType !== "ssh") { - const port = Number(watchedFields.port); - return !errors.ip && port >= 1 && port <= 65535; - } - - if (!watchedFields.username || watchedFields.username.trim() === "") - return false; - - if (authTab === "password") { - if ( - !watchedFields.password || - (watchedFields.password !== "existing_password" && - watchedFields.password.trim() === "") - ) - return false; - } else if (authTab === "key") { - if (!watchedFields.key || !watchedFields.keyType) return false; - } else if (authTab === "credential") { - if (!watchedFields.credentialId) return false; - } else if (authTab === "none") { - // No auth required - } else if (authTab === "opkssh") { - // No auth required - } else { - return false; - } - - return Object.keys(errors).length === 0; - }, [watchedFields, authTab, formState.errors]); - - useEffect(() => { - const updateAuthFields = async () => { - form.setValue("authType", authTab, { shouldValidate: true }); - - if (authTab === "password") { - form.setValue("key", null, { shouldValidate: true }); - form.setValue("keyPassword", "", { shouldValidate: true }); - form.setValue("keyType", "auto", { shouldValidate: true }); - form.setValue("credentialId", null, { shouldValidate: true }); - } else if (authTab === "key") { - form.setValue("password", "", { shouldValidate: true }); - form.setValue("credentialId", null, { shouldValidate: true }); - } else if (authTab === "credential") { - form.setValue("password", "", { shouldValidate: true }); - form.setValue("key", null, { shouldValidate: true }); - form.setValue("keyPassword", "", { shouldValidate: true }); - form.setValue("keyType", "auto", { shouldValidate: true }); - - const currentCredentialId = form.getValues("credentialId"); - const overrideUsername = form.getValues("overrideCredentialUsername"); - if (currentCredentialId && !overrideUsername) { - const selectedCredential = credentials.find( - (c) => c.id === currentCredentialId, - ); - if (selectedCredential?.username) { - form.setValue("username", selectedCredential.username, { - shouldValidate: true, - }); - } - } - } else if (authTab === "none") { - const connectionType = form.getValues("connectionType"); - if (connectionType === "ssh") { - form.setValue("password", "", { shouldValidate: true }); - } - form.setValue("key", null, { shouldValidate: true }); - form.setValue("keyPassword", "", { shouldValidate: true }); - form.setValue("keyType", "auto", { shouldValidate: true }); - form.setValue("credentialId", null, { shouldValidate: true }); - } else if (authTab === "opkssh") { - form.setValue("password", "", { shouldValidate: true }); - form.setValue("key", null, { shouldValidate: true }); - form.setValue("keyPassword", "", { shouldValidate: true }); - form.setValue("keyType", "auto", { shouldValidate: true }); - form.setValue("credentialId", null, { shouldValidate: true }); - } - - await form.trigger(); - }; - - updateAuthFields(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [authTab, credentials]); - - useEffect(() => { - if (editingHost) { - const cleanedHost = { ...editingHost }; - if (cleanedHost.connectionType === "ssh") { - if (cleanedHost.credentialId && cleanedHost.key) { - cleanedHost.key = undefined; - cleanedHost.keyPassword = undefined; - cleanedHost.keyType = undefined; - } else if (cleanedHost.credentialId && cleanedHost.password) { - cleanedHost.password = undefined; - } else if (cleanedHost.key && cleanedHost.password) { - cleanedHost.password = undefined; - } - } - - const defaultAuthType = (cleanedHost.authType || - (cleanedHost.credentialId - ? "credential" - : cleanedHost.key - ? "key" - : cleanedHost.password - ? "password" - : "none")) as - | "password" - | "key" - | "credential" - | "none" - | "opkssh"; - - setAuthTab(defaultAuthType); - - let parsedStatsConfig: StatsConfig = DEFAULT_STATS_CONFIG; - try { - if (cleanedHost.statsConfig) { - parsedStatsConfig = - typeof cleanedHost.statsConfig === "string" - ? JSON.parse(cleanedHost.statsConfig) - : (cleanedHost.statsConfig as StatsConfig); - } - } catch (error) { - console.error("Failed to parse statsConfig:", error); - } - - parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig }; - - const formData: Partial = { - connectionType: cleanedHost.connectionType || "ssh", - name: cleanedHost.name || "", - ip: cleanedHost.ip || "", - port: cleanedHost.port || 22, - username: cleanedHost.username || "", - folder: cleanedHost.folder || "", - tags: Array.isArray(cleanedHost.tags) ? cleanedHost.tags : [], - pin: Boolean(cleanedHost.pin), - authType: defaultAuthType as - | "password" - | "key" - | "credential" - | "none" - | "opkssh", - credentialId: cleanedHost.credentialId, - overrideCredentialUsername: Boolean( - cleanedHost.overrideCredentialUsername, - ), - password: "", - key: null, - keyPassword: "", - keyType: "auto" as const, - enableTerminal: Boolean(cleanedHost.enableTerminal), - enableTunnel: Boolean(cleanedHost.enableTunnel), - enableFileManager: Boolean(cleanedHost.enableFileManager), - defaultPath: cleanedHost.defaultPath || "/", - tunnelConnections: Array.isArray(cleanedHost.tunnelConnections) - ? cleanedHost.tunnelConnections.map((conn) => ({ - ...conn, - scope: conn.scope || "s2s", - mode: conn.mode || conn.tunnelType || "remote", - tunnelType: conn.tunnelType || "remote", - })) - : [], - jumpHosts: Array.isArray(cleanedHost.jumpHosts) - ? cleanedHost.jumpHosts - : [], - quickActions: Array.isArray(cleanedHost.quickActions) - ? cleanedHost.quickActions - : [], - statsConfig: parsedStatsConfig, - terminalConfig: { - ...DEFAULT_TERMINAL_CONFIG, - ...(cleanedHost.terminalConfig || {}), - environmentVariables: Array.isArray( - cleanedHost.terminalConfig?.environmentVariables, - ) - ? cleanedHost.terminalConfig.environmentVariables - : [], - sudoPassword: - cleanedHost.sudoPassword || - cleanedHost.terminalConfig?.sudoPassword || - "", - sudoPasswordAutoFill: - cleanedHost.terminalConfig?.sudoPasswordAutoFill ?? - Boolean(cleanedHost.sudoPassword), - }, - forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive), - notes: cleanedHost.notes || "", - useSocks5: Boolean(cleanedHost.useSocks5), - socks5Host: cleanedHost.socks5Host || "", - socks5Port: cleanedHost.socks5Port || 1080, - socks5Username: cleanedHost.socks5Username || "", - socks5Password: cleanedHost.socks5Password || "", - socks5ProxyChain: Array.isArray(cleanedHost.socks5ProxyChain) - ? cleanedHost.socks5ProxyChain - : [], - macAddress: cleanedHost.macAddress || "", - portKnockSequence: Array.isArray(cleanedHost.portKnockSequence) - ? cleanedHost.portKnockSequence - : [], - enableDocker: Boolean(cleanedHost.enableDocker), - domain: cleanedHost.domain || "", - security: cleanedHost.security || "any", - ignoreCert: cleanedHost.ignoreCert ?? true, - guacamoleConfig: (() => { - const cfg = cleanedHost.guacamoleConfig; - if (!cfg) return {}; - if (typeof cfg === "string") { - try { - return JSON.parse(cfg); - } catch { - return {}; - } - } - return cfg; - })(), - showTerminalInSidebar: cleanedHost.showTerminalInSidebar ?? true, - showFileManagerInSidebar: cleanedHost.showFileManagerInSidebar ?? false, - showTunnelInSidebar: cleanedHost.showTunnelInSidebar ?? false, - showDockerInSidebar: cleanedHost.showDockerInSidebar ?? false, - showServerStatsInSidebar: cleanedHost.showServerStatsInSidebar ?? false, - }; - - if ( - Array.isArray(cleanedHost.socks5ProxyChain) && - cleanedHost.socks5ProxyChain.length > 0 - ) { - setProxyMode("chain"); - } else { - setProxyMode("single"); - } - - if (cleanedHost.connectionType !== "ssh") { - if (cleanedHost.password) { - formData.password = cleanedHost.password; - } - } else if (defaultAuthType === "password") { - formData.password = - cleanedHost.password || (editingHost.id ? "existing_password" : ""); - } else if (defaultAuthType === "key") { - formData.key = editingHost.id ? "existing_key" : editingHost.key; - formData.keyPassword = cleanedHost.keyPassword || ""; - formData.keyType = - (cleanedHost.keyType as - | "auto" - | "ssh-rsa" - | "ssh-ed25519" - | "ecdsa-sha2-nistp256" - | "ecdsa-sha2-nistp384" - | "ecdsa-sha2-nistp521" - | "ssh-dss" - | "ssh-rsa-sha2-256" - | "ssh-rsa-sha2-512") || "auto"; - } else if (defaultAuthType === "credential") { - formData.credentialId = cleanedHost.credentialId; - } - - const serverTunnels = Array.isArray(formData.tunnelConnections) - ? formData.tunnelConnections.filter((tunnel) => tunnel.scope !== "c2s") - : []; - formData.serverTunnels = serverTunnels; - form.reset(formData as FormData); - } else { - setAuthTab("password"); - const defaultFormData: Partial = { - connectionType: "ssh" as const, - name: "", - ip: "", - port: 22, - username: "", - folder: "", - tags: [], - pin: false, - authType: "password" as const, - credentialId: null, - overrideCredentialUsername: false, - password: "", - key: null, - keyPassword: "", - keyType: "auto" as const, - enableTerminal: true, - enableTunnel: true, - enableFileManager: true, - defaultPath: "/", - tunnelConnections: [], - serverTunnels: [], - jumpHosts: [], - quickActions: [], - statsConfig: DEFAULT_STATS_CONFIG, - terminalConfig: DEFAULT_TERMINAL_CONFIG, - forceKeyboardInteractive: false, - enableDocker: false, - domain: "", - security: "any", - ignoreCert: true, - guacamoleConfig: {}, - showTerminalInSidebar: true, - showFileManagerInSidebar: false, - showTunnelInSidebar: false, - showDockerInSidebar: false, - showServerStatsInSidebar: false, - }; - - form.reset(defaultFormData as FormData); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editingHost]); - - useEffect(() => { - const focusTimer = setTimeout(() => { - if (ipInputRef.current) { - ipInputRef.current.focus(); - } - }, 300); - - return () => clearTimeout(focusTimer); - }, [editingHost]); - - const onSubmit = async (data: FormData) => { - try { - setIsSubmitting(true); - setFormError(null); - - if (!data.name || data.name.trim() === "") { - data.name = `${data.username}@${data.ip}`; - } - - type SubmitHostData = SSHHostData & { serverTunnels?: unknown }; - const submitData: SubmitHostData = { - ...data, - }; - delete submitData.serverTunnels; - - const serverTunnels = Array.isArray(data.serverTunnels) - ? data.serverTunnels - : []; - const serverTunnelConnections = serverTunnels.map((tunnel) => ({ - ...tunnel, - scope: "s2s" as const, - })); - - submitData.tunnelConnections = serverTunnelConnections; - - submitData.connectionType = data.connectionType; - submitData.domain = data.domain; - submitData.security = data.security; - submitData.ignoreCert = data.ignoreCert; - submitData.guacamoleConfig = data.guacamoleConfig; - - if (data.connectionType !== "ssh") { - submitData.authType = "none"; - submitData.key = undefined; - submitData.keyPassword = undefined; - submitData.keyType = undefined; - submitData.credentialId = undefined; - submitData.tunnelConnections = []; - submitData.jumpHosts = []; - submitData.useSocks5 = false; - submitData.socks5ProxyChain = []; - submitData.forceKeyboardInteractive = false; - submitData.enableTunnel = false; - submitData.enableFileManager = false; - submitData.enableDocker = false; - submitData.enableTerminal = true; - } else { - if ( - data.terminalConfig?.sudoPasswordAutoFill && - data.terminalConfig?.sudoPassword - ) { - submitData.sudoPassword = data.terminalConfig.sudoPassword; - } - - if (data.authType !== "credential") { - submitData.credentialId = undefined; - } - if (data.authType !== "password") { - submitData.password = undefined; - } - if (data.authType !== "key") { - submitData.key = undefined; - submitData.keyPassword = undefined; - submitData.keyType = undefined; - } - } - - if (data.authType === "key") { - if (data.key instanceof File) { - submitData.key = await data.key.text(); - } else if (data.key === "existing_key") { - delete submitData.key; - } - } - - if ( - data.authType === "password" && - data.password === "existing_password" - ) { - delete submitData.password; - } - - let savedHost; - if (editingHost && editingHost.id) { - savedHost = await updateSSHHost(editingHost.id, submitData); - toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name })); - } else { - savedHost = await createSSHHost(submitData); - toast.success(t("hosts.hostAddedSuccessfully", { name: data.name })); - } - - if (savedHost && savedHost.id && serverTunnelConnections) { - const hasAutoStartTunnels = serverTunnelConnections.some( - (tunnel) => tunnel.autoStart, - ); - - if (hasAutoStartTunnels) { - try { - await enableAutoStart(savedHost.id); - } catch (error) { - console.warn( - `Failed to enable AutoStart plaintext cache for SSH host ${savedHost.id}:`, - error, - ); - toast.warning( - t("tunnels.autoStartEnableFailed", { name: data.name }), - ); - } - } else { - try { - await disableAutoStart(savedHost.id); - } catch (error) { - console.warn( - `Failed to disable AutoStart plaintext cache for SSH host ${savedHost.id}:`, - error, - ); - } - } - } - - if (onFormSubmit) { - onFormSubmit(savedHost); - } - - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - toast.error(t("hosts.failedToSaveHost") + ": " + errorMessage); - console.error("Failed to save host:", error); - } finally { - setIsSubmitting(false); - } - }; - - const TAB_PRIORITY = [ - "general", - "terminal", - "tunnel", - "file_manager", - "docker", - "statistics", - ] as const; - - const FIELD_TO_TAB_MAP: Record = { - ip: "general", - port: "general", - username: "general", - name: "general", - folder: "general", - tags: "general", - pin: "general", - password: "general", - key: "general", - keyPassword: "general", - keyType: "general", - credentialId: "general", - overrideCredentialUsername: "general", - forceKeyboardInteractive: "general", - jumpHosts: "general", - authType: "general", - notes: "general", - useSocks5: "general", - socks5Host: "general", - socks5Port: "general", - socks5Username: "general", - socks5Password: "general", - socks5ProxyChain: "general", - portKnockSequence: "general", - quickActions: "general", - enableTerminal: "terminal", - terminalConfig: "terminal", - enableDocker: "docker", - domain: "general", - security: "general", - ignoreCert: "general", - guacamoleConfig: "remote_desktop", - connectionType: "general", - enableTunnel: "tunnel", - tunnelConnections: "tunnel", - serverTunnels: "tunnel", - enableFileManager: "file_manager", - defaultPath: "file_manager", - statsConfig: "statistics", - }; - - const handleFormError = async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - - const errors = form.formState.errors; - const errorFields = Object.keys(errors); - - if (errorFields.length === 0) return; - - const firstErrorField = errorFields[0]; - const firstError = errors[firstErrorField as keyof typeof errors]; - const errorMessage = - firstError && typeof firstError === "object" && "message" in firstError - ? (firstError.message as string) - : t("hosts.failedToSaveHost"); - toast.error(errorMessage); - - for (const tab of TAB_PRIORITY) { - const hasErrorInTab = errorFields.some((field) => { - const baseField = field.split(".")[0].split("[")[0]; - return FIELD_TO_TAB_MAP[baseField] === tab; - }); - - if (hasErrorInTab) { - setActiveTab(tab); - return; - } - } - }; - - const [tagInput, setTagInput] = useState(""); - - const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); - const folderInputRef = useRef(null); - const folderDropdownRef = useRef(null); - - const folderValue = form.watch("folder"); - const filteredFolders = React.useMemo(() => { - if (!folderValue) return folders; - return folders.filter((f) => - f.toLowerCase().includes(folderValue.toLowerCase()), - ); - }, [folderValue, folders]); - - const handleFolderClick = (folder: string) => { - form.setValue("folder", folder); - setFolderDropdownOpen(false); - }; - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - folderDropdownRef.current && - !folderDropdownRef.current.contains(event.target as Node) && - folderInputRef.current && - !folderInputRef.current.contains(event.target as Node) - ) { - setFolderDropdownOpen(false); - } - } - - if (folderDropdownOpen) { - document.addEventListener("mousedown", handleClickOutside); - } else { - document.removeEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [folderDropdownOpen]); - - const keyTypeOptions = [ - { value: "auto", label: t("hosts.autoDetect") }, - { value: "ssh-rsa", label: t("hosts.rsa") }, - { value: "ssh-ed25519", label: t("hosts.ed25519") }, - { value: "ecdsa-sha2-nistp256", label: t("hosts.ecdsaNistP256") }, - { value: "ecdsa-sha2-nistp384", label: t("hosts.ecdsaNistP384") }, - { value: "ecdsa-sha2-nistp521", label: t("hosts.ecdsaNistP521") }, - { value: "ssh-dss", label: t("hosts.dsa") }, - { value: "ssh-rsa-sha2-256", label: t("hosts.rsaSha2256") }, - { value: "ssh-rsa-sha2-512", label: t("hosts.rsaSha2512") }, - ]; - - const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); - const keyTypeButtonRef = useRef(null); - const keyTypeDropdownRef = useRef(null); - - useEffect(() => { - function onClickOutside(event: MouseEvent) { - if ( - keyTypeDropdownOpen && - keyTypeDropdownRef.current && - !keyTypeDropdownRef.current.contains(event.target as Node) && - keyTypeButtonRef.current && - !keyTypeButtonRef.current.contains(event.target as Node) - ) { - setKeyTypeDropdownOpen(false); - } - } - - document.addEventListener("mousedown", onClickOutside); - return () => document.removeEventListener("mousedown", onClickOutside); - }, [keyTypeDropdownOpen]); - - return ( -
    - -
    - - -
    - {formError && ( - - {formError} - - )} -
    - {onBack && ( - - )} -

    - {editingHost - ? editingHost.id - ? t("hosts.editHost") - : t("hosts.cloneHost") - : t("hosts.addHost")} -

    -
    - {guacEnabled && ( - ( - - - - - - {t("hosts.ssh")} - - - {t("hosts.rdp")} - - - {t("hosts.vnc")} - - - {t("hosts.telnet")} - - - - - - )} - /> - )} - - - - {t("hosts.general")} - - {watchedConnectionType === "ssh" && ( - <> - - {t("hosts.terminal")} - - - Docker - - - {t("hosts.tunnel")} - - - {t("hosts.fileManager")} - - - )} - - {watchedConnectionType === "ssh" - ? t("hosts.statistics") - : t("hosts.status")} - - {watchedConnectionType !== "ssh" && ( - - {t("hosts.remoteDesktop")} - - )} - {watchedConnectionType === "ssh" && - !editingHost?.isShared && ( - - {t("rbac.sharing")} - - )} - - - - - {watchedConnectionType === "ssh" && ( - <> - - - - - - - - - - - - - - )} - - {watchedConnectionType === "ssh" ? ( - - ) : ( - - )} - - {watchedConnectionType !== "ssh" && ( - - - - )} - {watchedConnectionType === "ssh" && ( - - - - )} - -
    -
    -
    - - {!editingHost?.isShared && !isSubmitting && ( -
    - - {form.formState.isDirty && ( - - {t("common.unsavedChanges")} - - )} -
    - )} -
    -
    - -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx deleted file mode 100644 index 8bff0c75..00000000 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx +++ /dev/null @@ -1,2591 +0,0 @@ -import React, { - useState, - useEffect, - useMemo, - useRef, - useCallback, -} from "react"; -import { Button } from "@/components/ui/button.tsx"; -import { Badge } from "@/components/ui/badge.tsx"; -import { ScrollArea } from "@/components/ui/scroll-area.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion.tsx"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip.tsx"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu.tsx"; -import { - getSSHHosts, - deleteSSHHost, - bulkImportSSHHosts, - bulkUpdateSSHHosts, - updateSSHHost, - renameFolder, - exportSSHHostWithCredentials, - exportAllSSHHosts, - getSSHFolders, - updateFolderMetadata, - deleteAllHostsInFolder, - refreshServerPolling, - isElectron, - getConfiguredServerUrl, - getGuacamoleDpi, - getGuacamoleTokenFromHost, - logActivity, -} from "@/ui/main-axios.ts"; -import { useServerStatus } from "@/ui/contexts/ServerStatusContext"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status"; -import { - Edit, - Trash2, - Server, - Folder, - Tag, - Pin, - Terminal, - Network, - FileEdit, - Search, - Upload, - X, - Check, - Pencil, - FolderMinus, - Copy, - Palette, - Trash, - Cloud, - Database, - Box, - Package, - Layers, - Archive, - HardDrive, - Globe, - FolderOpen, - Share2, - ArrowDownUp, - Container, - Link, - Plus, - ListChecks, - ChevronDown, - Monitor, - MessagesSquare, - Eye, - ChevronsDownUp, - ChevronsUpDown, - RefreshCw, -} from "lucide-react"; -import type { - SSHHost, - SSHFolder, - SSHManagerHostViewerProps, -} from "../../../../../types"; -import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; -import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { FolderEditDialog } from "@/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx"; -import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; -import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; - -const INITIAL_HOSTS_PER_FOLDER = 12; - -export function HostManagerViewer({ - onEditHost, - onAddHost, -}: SSHManagerHostViewerProps) { - const { t } = useTranslation(); - const { confirmWithToast } = useConfirmation(); - const { addTab } = useTabs(); - const [hosts, setHosts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); - const [importing, setImporting] = useState(false); - const overwriteRef = useRef(false); - const [draggedHost, setDraggedHost] = useState(null); - const [dragOverFolder, setDragOverFolder] = useState(null); - const [editingFolder, setEditingFolder] = useState(null); - const [editingFolderName, setEditingFolderName] = useState(""); - const [operationLoading, setOperationLoading] = useState(false); - const [refreshing, setRefreshing] = useState(false); - const [folderMetadata, setFolderMetadata] = useState>( - new Map(), - ); - const [editingFolderAppearance, setEditingFolderAppearance] = useState< - string | null - >(null); - const [expandedFolders, setExpandedFolders] = useState>( - new Set(), - ); - const [openAccordions, setOpenAccordions] = useState([]); - const [selectionMode, setSelectionMode] = useState(false); - const [selectedHostIds, setSelectedHostIds] = useState>( - new Set(), - ); - const [bulkUpdating, setBulkUpdating] = useState(false); - const { getStatus, refreshStatuses } = useServerStatus(); - const dragCounter = useRef(0); - - useEffect(() => { - fetchHosts(); - fetchFolderMetadata(); - - const handleHostsRefresh = () => { - fetchHosts(); - fetchFolderMetadata(); - }; - - const handleFoldersRefresh = () => { - fetchFolderMetadata(); - }; - - window.addEventListener("hosts:refresh", handleHostsRefresh); - window.addEventListener("ssh-hosts:changed", handleHostsRefresh); - window.addEventListener("folders:changed", handleFoldersRefresh); - - return () => { - window.removeEventListener("hosts:refresh", handleHostsRefresh); - window.removeEventListener("ssh-hosts:changed", handleHostsRefresh); - window.removeEventListener("folders:changed", handleFoldersRefresh); - }; - }, []); - - const fetchHosts = async (options: { showLoading?: boolean } = {}) => { - const { showLoading = true } = options; - try { - if (showLoading) setLoading(true); - const data = await getSSHHosts(); - - const cleanedHosts = data.map((host) => { - const cleanedHost = { ...host }; - if (cleanedHost.credentialId && cleanedHost.key) { - cleanedHost.key = undefined; - cleanedHost.keyPassword = undefined; - cleanedHost.keyType = undefined; - cleanedHost.authType = "credential"; - } else if (cleanedHost.credentialId && cleanedHost.password) { - cleanedHost.password = undefined; - cleanedHost.authType = "credential"; - } else if (cleanedHost.key && cleanedHost.password) { - cleanedHost.password = undefined; - cleanedHost.authType = "key"; - } - return cleanedHost; - }); - - setHosts(cleanedHosts); - setError(null); - } catch { - setError(t("hosts.failedToLoadHosts")); - } finally { - if (showLoading) setLoading(false); - } - }; - - const fetchFolderMetadata = async () => { - try { - const folders = await getSSHFolders(); - const metadataMap = new Map(); - folders.forEach((folder) => { - metadataMap.set(folder.name, folder); - }); - setFolderMetadata(metadataMap); - } catch (error) { - console.error("Failed to fetch folder metadata:", error); - } - }; - - const handleRefreshHosts = async () => { - setRefreshing(true); - try { - await Promise.all([ - fetchHosts({ showLoading: false }), - fetchFolderMetadata(), - refreshServerPolling(), - refreshStatuses(), - ]); - } finally { - setRefreshing(false); - } - }; - - const handleSaveFolderAppearance = async ( - folderName: string, - color: string, - icon: string, - ) => { - try { - await updateFolderMetadata(folderName, color, icon); - toast.success(t("hosts.folderAppearanceUpdated")); - await fetchFolderMetadata(); - window.dispatchEvent(new CustomEvent("folders:changed")); - } catch (error) { - console.error("Failed to update folder appearance:", error); - toast.error(t("hosts.failedToUpdateFolderAppearance")); - } - }; - - const handleDeleteAllHostsInFolder = async (folderName: string) => { - const hostsInFolder = hostsByFolder[folderName] || []; - confirmWithToast( - t("hosts.confirmDeleteAllHostsInFolder", { - folder: folderName, - count: hostsInFolder.length, - }), - async () => { - try { - const result = await deleteAllHostsInFolder(folderName); - toast.success( - t("hosts.allHostsInFolderDeleted", { - folder: folderName, - count: result.deletedCount, - }), - ); - await fetchHosts(); - await fetchFolderMetadata(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - } catch (error) { - console.error("Failed to delete hosts in folder:", error); - toast.error(t("hosts.failedToDeleteHostsInFolder")); - } - }, - "destructive", - ); - }; - - const getFolderIcon = (folderName: string) => { - const metadata = folderMetadata.get(folderName); - if (!metadata?.icon) return Folder; - - const iconMap: Record = { - Folder, - Server, - Cloud, - Database, - Box, - Package, - Layers, - Archive, - HardDrive, - Globe, - }; - - return iconMap[metadata.icon] || Folder; - }; - - const getFolderColor = (folderName: string) => { - const metadata = folderMetadata.get(folderName); - return metadata?.color; - }; - - const handleDelete = async (hostId: number, hostName: string) => { - confirmWithToast( - t("hosts.confirmDelete", { name: hostName }), - async () => { - try { - await deleteSSHHost(hostId); - toast.success(t("hosts.hostDeletedSuccessfully", { name: hostName })); - await fetchHosts(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - - refreshServerPolling(); - } catch { - toast.error(t("hosts.failedToDeleteHost")); - } - }, - "destructive", - ); - }; - - const handleExport = (host: SSHHost) => { - const actualAuthType = host.credentialId - ? "credential" - : host.key - ? "key" - : "password"; - - if (actualAuthType === "credential") { - const confirmMessage = t("hosts.exportCredentialWarning", { - name: host.name || `${host.username}@${host.ip}`, - }); - - confirmWithToast(confirmMessage, () => { - performExport(host); - }); - return; - } else if (actualAuthType === "password" || actualAuthType === "key") { - const confirmMessage = t("hosts.exportSensitiveDataWarning", { - name: host.name || `${host.username}@${host.ip}`, - }); - - confirmWithToast(confirmMessage, () => { - performExport(host); - }); - return; - } - - performExport(host); - }; - - const performExport = async (host: SSHHost) => { - try { - const decryptedHost = await exportSSHHostWithCredentials(host.id); - - const cleanExportData = Object.fromEntries( - Object.entries(decryptedHost).filter( - ([, value]) => value !== undefined, - ), - ); - - const exportFormat = { - hosts: [cleanExportData], - }; - - const blob = new Blob([JSON.stringify(exportFormat, null, 2)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${host.name || host.username + "@" + host.ip}-host-config.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - toast.success( - t("hosts.exportedHostConfig", { - name: host.name || `${host.username}@${host.ip}`, - }), - ); - } catch { - toast.error(t("hosts.failedToExportHost")); - } - }; - - const handleEdit = (host: SSHHost) => { - if (selectionMode) return; - if (onEditHost) { - onEditHost(host); - } - }; - - const toggleHostSelection = (hostId: number) => { - setSelectedHostIds((prev) => { - const next = new Set(prev); - if (next.has(hostId)) next.delete(hostId); - else next.add(hostId); - return next; - }); - }; - - const exitSelectionMode = () => { - setSelectionMode(false); - setSelectedHostIds(new Set()); - }; - - const handleBulkUpdate = async (updates: Record) => { - if (selectedHostIds.size === 0) return; - try { - setBulkUpdating(true); - const result = await bulkUpdateSSHHosts( - Array.from(selectedHostIds), - updates, - ); - if (result.updated > 0) { - toast.success(t("hosts.bulkUpdateSuccess", { count: result.updated })); - } - if (result.errors.length > 0) { - toast.error(result.errors.join(", ")); - } - await fetchHosts(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - } catch { - toast.error(t("hosts.bulkUpdateFailed")); - } finally { - setBulkUpdating(false); - } - }; - - const selectAllHosts = () => { - const selectableIds = hosts.filter((h) => !h.isShared).map((h) => h.id); - setSelectedHostIds(new Set(selectableIds)); - }; - - const deselectAllHosts = () => { - setSelectedHostIds(new Set()); - }; - - const handleClone = (host: SSHHost) => { - if (onEditHost) { - const clonedHost = { ...host }; - delete clonedHost.id; - onEditHost(clonedHost); - } - }; - - const copyFullScreenUrl = (host: SSHHost, appType: string) => { - const baseUrl = isElectron() - ? getConfiguredServerUrl() || window.location.origin - : window.location.origin; - const url = `${baseUrl}?view=${appType}&hostId=${host.id}`; - - if (navigator.clipboard && window.isSecureContext) { - navigator.clipboard.writeText(url).then( - () => { - toast.success(t("hosts.fullScreenUrlCopied")); - }, - () => { - fallbackCopyTextToClipboard(url); - }, - ); - } else { - fallbackCopyTextToClipboard(url); - } - }; - - const fallbackCopyTextToClipboard = (text: string) => { - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.position = "fixed"; - textArea.style.left = "-999999px"; - textArea.style.top = "-999999px"; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - try { - document.execCommand("copy"); - toast.success(t("hosts.fullScreenUrlCopied")); - } catch { - toast.error(t("hosts.failedToCopyUrl")); - } - document.body.removeChild(textArea); - }; - - const handleRemoveFromFolder = async (host: SSHHost) => { - confirmWithToast( - t("hosts.confirmRemoveFromFolder", { - name: host.name || `${host.username}@${host.ip}`, - folder: host.folder, - }), - async () => { - try { - setOperationLoading(true); - const updatedHost = { ...host, folder: "" }; - await updateSSHHost(host.id, updatedHost); - toast.success( - t("hosts.removedFromFolder", { - name: host.name || `${host.username}@${host.ip}`, - }), - ); - await fetchHosts(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - } catch { - toast.error(t("hosts.failedToRemoveFromFolder")); - } finally { - setOperationLoading(false); - } - }, - ); - }; - - const handleFolderRename = async (oldName: string) => { - if (!editingFolderName.trim() || editingFolderName === oldName) { - setEditingFolder(null); - setEditingFolderName(""); - return; - } - - try { - setOperationLoading(true); - await renameFolder(oldName, editingFolderName.trim()); - toast.success( - t("hosts.folderRenamed", { - oldName, - newName: editingFolderName.trim(), - }), - ); - await fetchHosts(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - setEditingFolder(null); - setEditingFolderName(""); - } catch { - toast.error(t("hosts.failedToRenameFolder")); - } finally { - setOperationLoading(false); - } - }; - - const startFolderEdit = (folderName: string) => { - setEditingFolder(folderName); - setEditingFolderName(folderName); - }; - - const cancelFolderEdit = () => { - setEditingFolder(null); - setEditingFolderName(""); - }; - - const handleDragStart = (e: React.DragEvent, host: SSHHost) => { - setDraggedHost(host); - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/plain", ""); - }; - - const handleDragEnd = () => { - setDraggedHost(null); - setDragOverFolder(null); - dragCounter.current = 0; - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }; - - const handleDragEnter = (e: React.DragEvent, folderName: string) => { - e.preventDefault(); - dragCounter.current++; - setDragOverFolder(folderName); - }; - - const handleDragLeave = () => { - dragCounter.current--; - if (dragCounter.current === 0) { - setDragOverFolder(null); - } - }; - - const handleDrop = async (e: React.DragEvent, targetFolder: string) => { - e.preventDefault(); - dragCounter.current = 0; - setDragOverFolder(null); - - if (!draggedHost) return; - - const newFolder = - targetFolder === t("hosts.uncategorized") ? "" : targetFolder; - - if (draggedHost.folder === newFolder) { - setDraggedHost(null); - return; - } - - try { - setOperationLoading(true); - const updatedHost = { ...draggedHost, folder: newFolder }; - await updateSSHHost(draggedHost.id, updatedHost); - toast.success( - t("hosts.movedToFolder", { - name: draggedHost.name || `${draggedHost.username}@${draggedHost.ip}`, - folder: targetFolder, - }), - ); - await fetchHosts(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - } catch { - toast.error(t("hosts.failedToMoveToFolder")); - } finally { - setOperationLoading(false); - setDraggedHost(null); - } - }; - - const getSampleData = () => ({ - hosts: [ - { - connectionType: "ssh", - name: t("interface.webServerProduction"), - ip: "192.168.1.100", - port: 22, - username: "admin", - authType: "password", - password: "your_secure_password_here", - folder: t("interface.productionFolder"), - tags: ["web", "production", "nginx"], - pin: true, - notes: "Main production web server running Nginx", - enableTerminal: true, - enableTunnel: false, - enableFileManager: true, - enableDocker: false, - showTerminalInSidebar: true, - showFileManagerInSidebar: true, - showServerStatsInSidebar: true, - defaultPath: "/var/www", - }, - { - connectionType: "ssh", - name: t("interface.databaseServer"), - ip: "192.168.1.101", - port: 22, - username: "dbadmin", - authType: "key", - key: "-----BEGIN OPENSSH PRIVATE KEY-----\\nYour SSH private key content here\\n-----END OPENSSH PRIVATE KEY-----", - keyPassword: "optional_key_passphrase", - keyType: "ssh-ed25519", - folder: t("interface.productionFolder"), - tags: ["database", "production", "postgresql"], - pin: false, - notes: "PostgreSQL production database", - enableTerminal: true, - enableTunnel: true, - enableFileManager: false, - enableDocker: false, - showTerminalInSidebar: true, - showTunnelInSidebar: true, - showServerStatsInSidebar: true, - tunnelConnections: [ - { - sourcePort: 5432, - endpointPort: 5432, - endpointHost: t("interface.webServerProduction"), - maxRetries: 3, - retryInterval: 10, - autoStart: true, - }, - ], - statsConfig: { - enabledWidgets: ["cpu", "memory", "disk", "network", "uptime"], - statusCheckEnabled: true, - statusCheckInterval: 30, - metricsEnabled: true, - metricsInterval: 30, - }, - }, - { - connectionType: "ssh", - name: t("interface.developmentServer"), - ip: "192.168.1.102", - port: 2222, - username: "developer", - authType: "credential", - credentialId: 1, - overrideCredentialUsername: false, - folder: t("interface.developmentFolder"), - tags: ["dev", "testing"], - pin: false, - notes: "Development environment for testing", - enableTerminal: true, - enableTunnel: false, - enableFileManager: true, - enableDocker: true, - showTerminalInSidebar: true, - showFileManagerInSidebar: true, - showDockerInSidebar: true, - defaultPath: "/home/developer", - sudoPassword: "dev_sudo_password", - }, - { - connectionType: "rdp", - name: "Windows Server 2022", - ip: "192.168.1.200", - port: 3389, - username: "Administrator", - password: "windows_password", - domain: "COMPANY", - security: "nla", - ignoreCert: false, - folder: "Remote Desktop", - tags: ["rdp", "windows", "production"], - pin: false, - notes: "Production Windows Server with RDP access", - guacamoleConfig: { - "enable-drive": true, - "drive-path": "/shared", - "create-drive-path": true, - "server-layout": "en-us-qwerty", - "resize-method": "display-update", - }, - }, - { - connectionType: "vnc", - name: "Ubuntu Desktop", - ip: "192.168.1.201", - port: 5900, - username: "vncuser", - password: "vnc_password", - folder: "Remote Desktop", - tags: ["vnc", "linux", "desktop"], - pin: false, - notes: "Ubuntu desktop with VNC server", - guacamoleConfig: { - "color-depth": 24, - cursor: "remote", - "read-only": false, - "clipboard-encoding": "UTF-8", - }, - }, - { - connectionType: "telnet", - name: "Network Switch", - ip: "192.168.1.254", - port: 23, - username: "admin", - password: "switch_password", - folder: "Infrastructure", - tags: ["telnet", "network", "switch"], - pin: false, - notes: "Legacy network switch with Telnet access", - guacamoleConfig: { - "color-scheme": "green-black", - "font-name": "monospace", - "font-size": 12, - scrollback: 1024, - backspace: 127, - }, - }, - { - connectionType: "ssh", - name: "Jump Host Server", - ip: "10.0.0.50", - port: 22, - username: "sysadmin", - authType: "password", - password: "secure_password", - folder: "Infrastructure", - tags: ["bastion", "jump-host"], - notes: "Jump host for accessing internal network", - enableTerminal: true, - enableTunnel: true, - enableFileManager: true, - enableDocker: false, - jumpHosts: [ - { - hostId: 1, - }, - ], - quickActions: [ - { - name: "System Update", - snippetId: 5, - }, - ], - }, - { - connectionType: "ssh", - name: "Server with SOCKS5 Proxy", - ip: "10.10.10.100", - port: 22, - username: "proxyuser", - authType: "password", - password: "secure_password", - folder: "Proxied Hosts", - tags: ["proxy", "socks5"], - notes: "Accessible through SOCKS5 proxy", - enableTerminal: true, - enableTunnel: false, - enableFileManager: true, - enableDocker: false, - useSocks5: true, - socks5Host: "proxy.example.com", - socks5Port: 1080, - socks5Username: "proxyauth", - socks5Password: "proxypass", - }, - { - connectionType: "ssh", - name: "Customized Terminal Server", - ip: "192.168.1.150", - port: 22, - username: "devops", - authType: "password", - password: "terminal_password", - folder: t("interface.developmentFolder"), - tags: ["custom", "terminal"], - notes: "Server with custom terminal configuration", - enableTerminal: true, - enableTunnel: false, - enableFileManager: true, - enableDocker: false, - defaultPath: "/opt/apps", - terminalConfig: { - cursorBlink: true, - cursorStyle: "bar", - fontSize: 16, - fontFamily: "jetbrainsMono", - letterSpacing: 0.5, - lineHeight: 1.2, - theme: "monokai", - scrollback: 50000, - bellStyle: "visual", - rightClickSelectsWord: true, - fastScrollModifier: "ctrl", - fastScrollSensitivity: 7, - minimumContrastRatio: 4, - backspaceMode: "normal", - agentForwarding: true, - environmentVariables: [ - { - key: "NODE_ENV", - value: "development", - }, - ], - autoMosh: false, - sudoPasswordAutoFill: true, - sudoPassword: "sudo_password_here", - }, - }, - ], - }); - - const handleDownloadSample = () => { - const sampleData = getSampleData(); - const blob = new Blob([JSON.stringify(sampleData, null, 2)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "sample-ssh-hosts.json"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const [exporting, setExporting] = useState(false); - - const handleExportAll = () => { - confirmWithToast( - t("hosts.exportAllSensitiveWarning"), - async () => { - setExporting(true); - try { - const data = await exportAllSSHHosts(); - const blob = new Blob([JSON.stringify(data, null, 2)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `termix-hosts-export-${new Date().toISOString().slice(0, 10)}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - toast.success( - t("hosts.exportedAllHosts", { count: data.hosts.length }), - ); - } catch { - toast.error(t("hosts.failedToExportAllHosts")); - } finally { - setExporting(false); - } - }, - "destructive", - ); - }; - - const handleJsonImport = async ( - event: React.ChangeEvent, - ) => { - const file = event.target.files?.[0]; - if (!file) return; - - try { - setImporting(true); - const text = await file.text(); - const data = JSON.parse(text); - - if (!Array.isArray(data.hosts) && !Array.isArray(data)) { - throw new Error(t("hosts.jsonMustContainHosts")); - } - - const hostsArray = Array.isArray(data.hosts) ? data.hosts : data; - - if (hostsArray.length === 0) { - throw new Error(t("hosts.noHostsInJson")); - } - - if (hostsArray.length > 100) { - throw new Error(t("hosts.maxHostsAllowed")); - } - - const result = await bulkImportSSHHosts(hostsArray, overwriteRef.current); - - if (result.success > 0 || result.updated > 0) { - const parts: string[] = []; - if (result.success > 0) - parts.push(`${result.success} ${t("hosts.importCreated")}`); - if (result.updated > 0) - parts.push(`${result.updated} ${t("hosts.importUpdated")}`); - if (result.failed > 0) - parts.push(`${result.failed} ${t("hosts.importFailedCount")}`); - toast.success(parts.join(", ")); - if (result.errors.length > 0) { - toast.error(`Import errors: ${result.errors.join(", ")}`); - } - await fetchHosts(); - window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - } else { - toast.error(t("hosts.importFailed") + `: ${result.errors.join(", ")}`); - } - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : t("hosts.failedToImportJson"); - toast.error(t("hosts.importError") + `: ${errorMessage}`); - } finally { - setImporting(false); - event.target.value = ""; - } - }; - - const filteredAndSortedHosts = useMemo(() => { - let filtered = hosts; - - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - filtered = hosts.filter((host) => { - const searchableText = [ - host.name || "", - host.username, - host.ip, - host.folder || "", - ...(host.tags || []), - host.authType, - host.defaultPath || "", - ] - .join(" ") - .toLowerCase(); - return searchableText.includes(query); - }); - } - - return filtered.sort((a, b) => { - if (a.pin && !b.pin) return -1; - if (!a.pin && b.pin) return 1; - - const aName = a.name || a.username; - const bName = b.name || b.username; - return aName.localeCompare(bName); - }); - }, [hosts, searchQuery]); - - const hostsByFolder = useMemo(() => { - const grouped: { [key: string]: SSHHost[] } = {}; - - filteredAndSortedHosts.forEach((host) => { - const folder = host.folder || t("hosts.uncategorized"); - if (!grouped[folder]) { - grouped[folder] = []; - } - grouped[folder].push(host); - }); - - const sortedFolders = Object.keys(grouped).sort((a, b) => { - if (a === t("hosts.uncategorized")) return -1; - if (b === t("hosts.uncategorized")) return 1; - return a.localeCompare(b); - }); - - const sortedGrouped: { [key: string]: SSHHost[] } = {}; - sortedFolders.forEach((folder) => { - sortedGrouped[folder] = grouped[folder]; - }); - - return sortedGrouped; - }, [filteredAndSortedHosts]); - - const folderKeys = useMemo(() => Object.keys(hostsByFolder), [hostsByFolder]); - const folderKeysString = useMemo(() => folderKeys.join(","), [folderKeys]); - - useEffect(() => { - setOpenAccordions((prev) => { - if (prev.length === 0 && folderKeys.length > 0) { - return folderKeys; - } - const existingFolders = prev.filter((folder) => - folderKeys.includes(folder), - ); - const newFolders = folderKeys.filter((folder) => !prev.includes(folder)); - return [...existingFolders, ...newFolders]; - }); - }, [folderKeysString]); - - const toggleFolderExpansion = useCallback((folderName: string) => { - setExpandedFolders((prev) => { - const next = new Set(prev); - if (next.has(folderName)) { - next.delete(folderName); - } else { - next.add(folderName); - } - return next; - }); - }, []); - - const getVisibleHosts = useCallback( - (folderName: string, allHosts: SSHHost[]) => { - if ( - expandedFolders.has(folderName) || - allHosts.length <= INITIAL_HOSTS_PER_FOLDER - ) { - return allHosts; - } - return allHosts.slice(0, INITIAL_HOSTS_PER_FOLDER); - }, - [expandedFolders], - ); - - if (loading) { - return ; - } - - if (error) { - return ( -
    -
    -

    {error}

    - -
    -
    - ); - } - - if (hosts.length === 0) { - return ( - -
    -
    -
    -

    {t("hosts.sshHosts")}

    -

    - {t("hosts.hostsCount", { count: 0 })} -

    -
    -
    - - - - - - { - overwriteRef.current = false; - document.getElementById("json-import-input")?.click(); - }} - > - {t("hosts.importSkipExisting")} - - { - overwriteRef.current = true; - document.getElementById("json-import-input")?.click(); - }} - > - {t("hosts.importOverwriteExisting")} - - - - - - - - -
    - - -
    -
    - - - -
    -
    - - setSearchQuery(e.target.value)} - className="pl-10 h-9" - disabled - /> -
    - -
    - -
    -
    - -

    - {t("hosts.noHosts")} -

    -

    - {t("hosts.noHostsMessage")} -

    -
    -
    -
    - - ); - } - - return ( - -
    -
    -
    -

    {t("hosts.sshHosts")}

    -

    - {t("hosts.hostsCount", { count: filteredAndSortedHosts.length })} -

    -
    -
    - - - - - - { - overwriteRef.current = false; - document.getElementById("json-import-input")?.click(); - }} - > - {t("hosts.importSkipExisting")} - - { - overwriteRef.current = true; - document.getElementById("json-import-input")?.click(); - }} - > - {t("hosts.importOverwriteExisting")} - - - - - - - - - - -
    - - -
    -
    - - - -
    -
    - - setSearchQuery(e.target.value)} - className="pl-10 h-9" - /> -
    - - - -
    - - -
    - {Object.entries(hostsByFolder).map(([folder, folderHosts]) => ( -
    handleDragEnter(e, folder)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, folder)} - > - - - -
    - {(() => { - const FolderIcon = getFolderIcon(folder); - const folderColor = getFolderColor(folder); - return ( - - ); - })()} - {editingFolder === folder ? ( -
    e.stopPropagation()} - > - - setEditingFolderName(e.target.value) - } - onKeyDown={(e) => { - if (e.key === "Enter") - handleFolderRename(folder); - if (e.key === "Escape") cancelFolderEdit(); - }} - className="h-6 text-sm px-2 flex-1" - autoFocus - disabled={operationLoading} - /> - - -
    - ) : ( - <> - { - e.stopPropagation(); - if (folder !== t("hosts.uncategorized")) { - startFolderEdit(folder); - } - }} - title={ - folder !== t("hosts.uncategorized") - ? t("hosts.clickToRenameFolder") - : "" - } - > - {folder} - - {folder !== t("hosts.uncategorized") && ( - - )} - - )} - - {folderHosts.length} - - {selectionMode && - (() => { - const selectableIds = folderHosts - .filter((h) => !h.isShared) - .map((h) => h.id); - const allSelected = - selectableIds.length > 0 && - selectableIds.every((id) => - selectedHostIds.has(id), - ); - return ( - { - setSelectedHostIds((prev) => { - const next = new Set(prev); - if (checked) { - selectableIds.forEach((id) => - next.add(id), - ); - } else { - selectableIds.forEach((id) => - next.delete(id), - ); - } - return next; - }); - }} - onClick={(e) => e.stopPropagation()} - className="ml-1" - /> - ); - })()} - {folder !== t("hosts.uncategorized") && ( -
    - - - - - - {t("hosts.editFolderAppearance")} - - - - - - - - {t("hosts.deleteAllHostsInFolder")} - - -
    - )} -
    -
    - -
    - {getVisibleHosts(folder, folderHosts).map((host) => ( - - -
    handleDragStart(e, host)} - onDragEnd={handleDragEnd} - className={`bg-field border rounded-lg cursor-pointer hover:shadow-lg hover:bg-hover-alt transition-all duration-200 p-3 group relative ${ - draggedHost?.id === host.id - ? "opacity-50 scale-95" - : "" - } ${ - selectionMode && selectedHostIds.has(host.id) - ? "ring-2 ring-blue-500 border-blue-500" - : "border-input hover:border-blue-400/50" - } ${ - selectionMode && host.isShared - ? "opacity-50 pointer-events-none" - : "" - }`} - onClick={() => { - if (selectionMode && !host.isShared) { - toggleHostSelection(host.id); - } else { - handleEdit(host); - } - }} - > -
    -
    -
    - {selectionMode && !host.isShared && ( - - toggleHostSelection(host.id) - } - onClick={(e) => e.stopPropagation()} - className="bg-background border-2 mr-1 flex-shrink-0" - /> - )} - {(() => { - const statsConfig = (() => { - if (!host.statsConfig) { - return DEFAULT_STATS_CONFIG; - } - if ( - typeof host.statsConfig === "object" - ) { - return host.statsConfig; - } - try { - return JSON.parse(host.statsConfig); - } catch { - return DEFAULT_STATS_CONFIG; - } - })(); - const shouldShowStatus = ![ - false, - "false", - ].includes( - statsConfig.statusCheckEnabled, - ); - - if (!shouldShowStatus) return null; - - const serverStatus = getStatus(host.id); - return ( - - - - ); - })()} - {host.pin && ( - - )} -

    - {host.name || - (host.username - ? `${host.username}@${host.ip}` - : host.ip)} -

    - {host.isShared && ( - - {t("rbac.shared")} - - )} -
    -

    - {host.ip}:{host.port} -

    - {host.username && ( -

    - {host.username} -

    - )} -

    - ID: {host.id} -

    -
    -
    - {!host.isShared && - host.folder && - host.folder !== "" && ( - - - - - -

    - {t("hosts.removeFromFolder", { - folder: host.folder, - })} -

    -
    -
    - )} - - - - - -

    {t("hosts.editHostTooltip")}

    -
    -
    - {!host.isShared && ( - <> - - - - - -

    - {t("hosts.deleteHostTooltip")} -

    -
    -
    - - - - - -

    - {t("hosts.exportHostTooltip")} -

    -
    -
    - - - - - -

    {t("hosts.cloneHostTooltip")}

    -
    -
    - - - - - - - - -

    - {t("hosts.copyFullScreenUrl")} -

    -
    -
    - - {(() => { - const connType = - host.connectionType; - const isRemoteDesktop = - connType === "rdp" || - connType === "vnc" || - connType === "telnet"; - - if (isRemoteDesktop) { - return ( - { - e.stopPropagation(); - copyFullScreenUrl( - host, - connType, - ); - }} - > - {connType === "rdp" ? ( - - ) : connType === "vnc" ? ( - - ) : ( - - )} - {t( - "hosts.copyRemoteDesktopUrl", - )} - - ); - } - - return ( - <> - {host.enableTerminal && ( - { - e.stopPropagation(); - copyFullScreenUrl( - host, - "terminal", - ); - }} - > - - {t( - "hosts.copyTerminalUrl", - )} - - )} - {host.enableFileManager && ( - { - e.stopPropagation(); - copyFullScreenUrl( - host, - "file-manager", - ); - }} - > - - {t( - "hosts.copyFileManagerUrl", - )} - - )} - {host.enableTunnel && ( - { - e.stopPropagation(); - copyFullScreenUrl( - host, - "tunnel", - ); - }} - > - - {t("hosts.copyTunnelUrl")} - - )} - { - e.stopPropagation(); - copyFullScreenUrl( - host, - "server-stats", - ); - }} - > - - {t( - "hosts.copyServerStatsUrl", - )} - - {host.enableDocker && ( - { - e.stopPropagation(); - copyFullScreenUrl( - host, - "docker", - ); - }} - > - - {t("hosts.copyDockerUrl")} - - )} - - ); - })()} - -
    - - )} -
    -
    - -
    - {host.tags && host.tags.length > 0 && ( -
    - {host.tags - .slice(0, 6) - .map((tag, index) => ( - - - {tag} - - ))} - {host.tags.length > 6 && ( - - +{host.tags.length - 6} - - )} -
    - )} - -
    - {(() => { - const connType = host.connectionType; - if (connType === "rdp") { - return ( - - - {t("hosts.rdp")} - - ); - } - if (connType === "vnc") { - return ( - - - {t("hosts.vnc")} - - ); - } - if (connType === "telnet") { - return ( - - - {t("hosts.telnet")} - - ); - } - return ( - <> - {host.enableTerminal && ( - - - {t("hosts.terminalBadge")} - - )} - {host.enableTunnel && ( - - - {t("hosts.tunnelBadge")} - {host.tunnelConnections && - host.tunnelConnections.length > - 0 && ( - - ( - { - host.tunnelConnections - .length - } - ) - - )} - - )} - {host.enableFileManager && ( - - - {t("hosts.fileManagerBadge")} - - )} - {host.enableDocker && ( - - - {t("hosts.docker")} - - )} - - ); - })()} -
    -
    - -
    - {(() => { - const connType = host.connectionType; - const isRemoteDesktop = - connType === "rdp" || - connType === "vnc" || - connType === "telnet"; - - if (isRemoteDesktop) { - return ( - - - - - -

    {t("hosts.remoteDesktop")}

    -
    -
    - ); - } - - return ( - <> - {host.enableTerminal && ( - - - - - -

    {t("hosts.openTerminal")}

    -
    -
    - )} - {host.enableFileManager && ( - - - - - -

    - {t("hosts.openFileManager")} -

    -
    -
    - )} - {host.enableTunnel && ( - - - - - -

    {t("hosts.openTunnels")}

    -
    -
    - )} - {host.enableDocker && ( - - - - - -

    {t("hosts.openDocker")}

    -
    -
    - )} - - - - - -

    {t("hosts.openServerStats")}

    -
    -
    - - ); - })()} -
    -
    -
    - -
    -

    - {t("hosts.clickToEditHost")} -

    -

    - {t("hosts.dragToMoveBetweenFolders")} -

    -
    -
    -
    - ))} -
    - {folderHosts.length > INITIAL_HOSTS_PER_FOLDER && ( -
    - -
    - )} -
    -
    -
    -
    - ))} -
    -
    - - {editingFolderAppearance && ( - { - if (!open) setEditingFolderAppearance(null); - }} - onSave={async (color, icon) => { - await handleSaveFolderAppearance( - editingFolderAppearance, - color, - icon, - ); - setEditingFolderAppearance(null); - }} - /> - )} - - {selectionMode && ( -
    - - {t("hosts.selectedCount", { count: selectedHostIds.size })} - -
    - - - -
    - - - - - - - { - e.preventDefault(); - handleBulkUpdate({ - statsConfig: { statusCheckEnabled: true }, - }); - }} - > - - {t("hosts.enableStatusCheck")} - - { - e.preventDefault(); - handleBulkUpdate({ - statsConfig: { statusCheckEnabled: false }, - }); - }} - > - - {t("hosts.disableStatusCheck")} - - { - e.preventDefault(); - handleBulkUpdate({ - statsConfig: { useGlobalStatusInterval: true }, - }); - }} - > - - {t("hosts.useGlobalStatusDefault")} - -
    - { - e.preventDefault(); - handleBulkUpdate({ statsConfig: { metricsEnabled: true } }); - }} - > - - {t("hosts.enableMetrics")} - - { - e.preventDefault(); - handleBulkUpdate({ - statsConfig: { metricsEnabled: false }, - }); - }} - > - - {t("hosts.disableMetrics")} - - { - e.preventDefault(); - handleBulkUpdate({ - statsConfig: { useGlobalMetricsInterval: true }, - }); - }} - > - - {t("hosts.useGlobalMetricsDefault")} - - - - - - - - - - { - e.preventDefault(); - handleBulkUpdate({ - enableTerminal: true, - enableFileManager: true, - enableTunnel: true, - enableDocker: true, - }); - }} - > - - {t("hosts.enableAllFeatures")} - - { - e.preventDefault(); - handleBulkUpdate({ - enableTerminal: false, - enableFileManager: false, - enableTunnel: false, - enableDocker: false, - }); - }} - > - - {t("hosts.disableAllFeatures")} - -
    - { - e.preventDefault(); - handleBulkUpdate({ enableTerminal: true }); - }} - > - - {t("hosts.bulkEnableTerminal")} - - { - e.preventDefault(); - handleBulkUpdate({ enableTerminal: false }); - }} - > - - {t("hosts.bulkDisableTerminal")} - - { - e.preventDefault(); - handleBulkUpdate({ enableFileManager: true }); - }} - > - - {t("hosts.bulkEnableFileManager")} - - { - e.preventDefault(); - handleBulkUpdate({ enableFileManager: false }); - }} - > - - {t("hosts.bulkDisableFileManager")} - - { - e.preventDefault(); - handleBulkUpdate({ enableTunnel: true }); - }} - > - - {t("hosts.bulkEnableTunnel")} - - { - e.preventDefault(); - handleBulkUpdate({ enableTunnel: false }); - }} - > - - {t("hosts.bulkDisableTunnel")} - - { - e.preventDefault(); - handleBulkUpdate({ enableDocker: true }); - }} - > - - {t("hosts.bulkEnableDocker")} - - { - e.preventDefault(); - handleBulkUpdate({ enableDocker: false }); - }} - > - - {t("hosts.bulkDisableDocker")} - - - - - - - - - - { - e.preventDefault(); - handleBulkUpdate({ folder: "" }); - }} - > - - {t("hosts.uncategorized")} - - {Object.keys(hostsByFolder) - .filter((f) => f !== t("hosts.uncategorized")) - .map((f) => { - const FolderIcon = getFolderIcon(f); - const folderColor = getFolderColor(f); - return ( - { - e.preventDefault(); - handleBulkUpdate({ folder: f }); - }} - > - - {f} - - ); - })} - - - - - - - - - { - e.preventDefault(); - handleBulkUpdate({ pin: true }); - }} - > - - {t("hosts.bulkPin")} - - { - e.preventDefault(); - handleBulkUpdate({ pin: false }); - }} - > - - {t("hosts.bulkUnpin")} - - - - -
    - -
    - )} -
    - - ); -} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx deleted file mode 100644 index 610bc180..00000000 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, -} from "@/components/ui/form.tsx"; -import { Switch } from "@/components/ui/switch.tsx"; -import type { HostDockerTabProps } from "./shared/tab-types"; -import { Button } from "@/components/ui/button.tsx"; - -export function HostDockerTab({ form, t }: HostDockerTabProps) { - return ( -
    - - ( - - {t("hosts.enableDocker")} - - - - {t("hosts.enableDockerDesc")} - - )} - /> -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx deleted file mode 100644 index dbc67941..00000000 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, -} from "@/components/ui/form.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Switch } from "@/components/ui/switch.tsx"; -import type { HostFileManagerTabProps } from "./shared/tab-types"; - -export function HostFileManagerTab({ form, t }: HostFileManagerTabProps) { - return ( -
    - ( - - {t("hosts.enableFileManager")} - - - - - {t("hosts.enableFileManagerDesc")} - - - )} - /> - - {form.watch("enableFileManager") && ( -
    - ( - - {t("hosts.defaultPath")} - - { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - {t("hosts.defaultPathDesc")} - - )} - /> -
    - )} -
    - ); -} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx deleted file mode 100644 index d8a19982..00000000 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx +++ /dev/null @@ -1,1602 +0,0 @@ -import React, { useState } from "react"; -import { Controller } from "react-hook-form"; -import { cn } from "@/lib/utils.ts"; -import { Button } from "@/components/ui/button.tsx"; -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, -} from "@/components/ui/form.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { PasswordInput } from "@/components/ui/password-input.tsx"; -import { Textarea } from "@/components/ui/textarea.tsx"; -import { Switch } from "@/components/ui/switch.tsx"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion.tsx"; -import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; -import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; -import CodeMirror from "@uiw/react-codemirror"; -import { EditorView } from "@codemirror/view"; -import { Plus, X, ArrowRight, Loader2 } from "lucide-react"; -import type { HostGeneralTabProps } from "./shared/tab-types"; -import { JumpHostItem } from "./shared/JumpHostItem"; -import { testProxyConnection } from "@/ui/main-axios"; -import { toast } from "sonner"; -import type { ProxyNode } from "@/types"; - -type JumpHostSelection = { hostId: number }; - -export function HostGeneralTab({ - form, - connectionType, - authTab, - setAuthTab, - keyInputMethod, - setKeyInputMethod, - proxyMode, - setProxyMode, - tagInput, - setTagInput, - folderDropdownOpen, - setFolderDropdownOpen, - folderInputRef, - folderDropdownRef, - filteredFolders, - handleFolderClick, - keyTypeDropdownOpen, - setKeyTypeDropdownOpen, - keyTypeButtonRef, - keyTypeDropdownRef, - keyTypeOptions, - ipInputRef, - editorTheme, - hosts, - editingHost, - credentials, - t, -}: HostGeneralTabProps) { - const [proxyTesting, setProxyTesting] = useState(false); - - const handleTestProxy = async () => { - setProxyTesting(true); - try { - const chain = form.watch("socks5ProxyChain") || []; - const host = form.watch("socks5Host"); - const port = form.watch("socks5Port"); - - let result; - if (proxyMode === "chain" && chain.length > 0) { - result = await testProxyConnection({ proxyChain: chain }); - } else if (host) { - result = await testProxyConnection({ - singleProxy: { - host, - port: port || 1080, - type: 5, - username: form.watch("socks5Username") || undefined, - password: form.watch("socks5Password") || undefined, - }, - }); - } else { - toast.error( - t("hosts.proxyTestFailed", { error: "No proxy configured" }), - ); - setProxyTesting(false); - return; - } - - if (result.success) { - toast.success( - t("hosts.proxyTestSuccess", { latency: result.latencyMs ?? 0 }), - ); - } else { - toast.error( - t("hosts.proxyTestFailed", { - error: result.error || "Unknown error", - }), - ); - } - } catch (err) { - toast.error( - t("hosts.proxyTestFailed", { - error: err instanceof Error ? err.message : "Unknown error", - }), - ); - } finally { - setProxyTesting(false); - } - }; - - // Build connection path for visualization - const buildConnectionPath = () => { - const parts: string[] = [ - t("hosts.connectionPath") === "Connection Path" ? "You" : "You", - ]; - const useSocks5 = form.watch("useSocks5"); - const jumpHosts = (form.watch("jumpHosts") || []) as JumpHostSelection[]; - - if (useSocks5) { - if (proxyMode === "chain") { - const chain = (form.watch("socks5ProxyChain") || []) as ProxyNode[]; - chain.forEach((node) => { - if (node.host) { - const typeLabel = - node.type === "http" ? "HTTP" : `SOCKS${node.type}`; - parts.push(`${typeLabel} ${node.host}:${node.port}`); - } - }); - } else { - const host = form.watch("socks5Host"); - const port = form.watch("socks5Port") || 1080; - if (host) { - parts.push(`SOCKS5 ${host}:${port}`); - } - } - } - - if (jumpHosts.length > 0 && hosts) { - jumpHosts.forEach((jh) => { - const found = hosts.find((h) => h.id === jh.hostId); - if (found) { - parts.push(`Jump: ${found.name || found.ip}`); - } - }); - } - - const ip = form.watch("ip"); - const port = form.watch("port"); - if (ip) { - parts.push(`${ip}:${port || 22}`); - } - - return parts; - }; - - const socks5ProxyChain = (form.watch("socks5ProxyChain") || - []) as ProxyNode[]; - - return ( -
    - - {t("hosts.connectionDetails")} - -
    - ( - - {t("hosts.ipAddress")} - - { - field.ref(e); - ipInputRef.current = e; - }} - onBlur={(e) => { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - - )} - /> - - ( - - {t("hosts.port")} - - - - - )} - /> - - { - const isCredentialAuth = authTab === "credential"; - const credentialId = form.watch("credentialId"); - const overrideEnabled = form.watch("overrideCredentialUsername"); - const selectedCredential = credentials.find( - (c) => c.id === credentialId, - ); - const shouldDisable = - isCredentialAuth && - selectedCredential?.username && - !overrideEnabled; - - return ( - - {t("hosts.username")} - - { - field.onChange(e.target.value); - if ( - isCredentialAuth && - selectedCredential && - !selectedCredential.username && - e.target.value.trim() !== "" - ) { - form.setValue("overrideCredentialUsername", true); - } - }} - onBlur={(e) => { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - - ); - }} - /> -
    -
    - ( - - {t("hosts.macAddress")} - - { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - {t("hosts.macAddressDesc")} - - )} - /> -
    - - {t("hosts.organization")} - -
    - ( - - {t("hosts.name")} - - { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - - )} - /> - - ( - - {t("hosts.folder")} - - setFolderDropdownOpen(true)} - onChange={(e) => { - field.onChange(e); - setFolderDropdownOpen(true); - }} - onBlur={(e) => { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - {folderDropdownOpen && filteredFolders.length > 0 && ( -
    -
    - {filteredFolders.map((folder) => ( - - ))} -
    -
    - )} -
    - )} - /> - - ( - - {t("hosts.tags")} - -
    - {field.value.map((tag: string, idx: number) => ( - - {tag} - - - ))} - setTagInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === " " && tagInput.trim() !== "") { - e.preventDefault(); - if (!field.value.includes(tagInput.trim())) { - field.onChange([...field.value, tagInput.trim()]); - } - setTagInput(""); - } else if ( - e.key === "Backspace" && - tagInput === "" && - field.value.length > 0 - ) { - field.onChange(field.value.slice(0, -1)); - } - }} - placeholder={t("hosts.addTagsSpaceToAdd")} - /> -
    -
    -
    - )} - /> - - ( - - {t("hosts.pin")} - - - - - )} - /> - - ( - - {t("hosts.notes")} - -