Compare commits
1 Commits
fix/remove
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0e7e94a23 |
4
.github/release-drafter.yml
vendored
4
.github/release-drafter.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# Template for release drafts
|
||||
name-template: 'v$NEXT_PATCH_VERSION' # You can switch to $NEXT_MINOR_VERSION or $NEXT_MAJOR_VERSION
|
||||
tag-template: 'v$NEXT_PATCH_VERSION'
|
||||
name-template: 'v$NEXT_MINOR_VERSION' # You can switch to $NEXT_MINOR_VERSION or $NEXT_MAJOR_VERSION
|
||||
tag-template: 'v$NEXT_MINOR_VERSION'
|
||||
|
||||
# Exclude PRs with this label from release notes
|
||||
exclude-labels:
|
||||
|
||||
242
package-lock.json
generated
242
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"@tanstack/react-query": "^5.90.3",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.545.0",
|
||||
"next": "^15.5.5",
|
||||
"next": "^15.5.3",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -47,9 +47,9 @@
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
@@ -61,7 +61,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
},
|
||||
@@ -1797,9 +1797,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz",
|
||||
"integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz",
|
||||
"integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1813,9 +1813,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz",
|
||||
"integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz",
|
||||
"integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1829,9 +1829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz",
|
||||
"integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz",
|
||||
"integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1845,9 +1845,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz",
|
||||
"integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz",
|
||||
"integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1861,9 +1861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz",
|
||||
"integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz",
|
||||
"integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1877,9 +1877,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz",
|
||||
"integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz",
|
||||
"integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1893,9 +1893,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz",
|
||||
"integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz",
|
||||
"integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1909,9 +1909,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz",
|
||||
"integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz",
|
||||
"integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1925,9 +1925,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz",
|
||||
"integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz",
|
||||
"integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2769,9 +2769,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.3.tgz",
|
||||
"integrity": "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==",
|
||||
"version": "5.90.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz",
|
||||
"integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2779,12 +2779,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.90.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.3.tgz",
|
||||
"integrity": "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==",
|
||||
"version": "5.90.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz",
|
||||
"integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.3"
|
||||
"@tanstack/query-core": "5.90.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -3077,9 +3077,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
|
||||
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
|
||||
"version": "24.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
|
||||
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
@@ -3101,9 +3101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -3135,17 +3135,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
|
||||
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
|
||||
"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/type-utils": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/type-utils": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -3159,7 +3159,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -3175,16 +3175,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz",
|
||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3200,14 +3200,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz",
|
||||
"integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
|
||||
"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.1",
|
||||
"@typescript-eslint/types": "^8.46.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.0",
|
||||
"@typescript-eslint/types": "^8.46.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3222,14 +3222,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz",
|
||||
"integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
|
||||
"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1"
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3240,9 +3240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz",
|
||||
"integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3257,15 +3257,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz",
|
||||
"integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -3282,9 +3282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz",
|
||||
"integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
|
||||
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3296,16 +3296,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz",
|
||||
"integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
|
||||
"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.46.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"@typescript-eslint/project-service": "8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -3381,9 +3381,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -3394,16 +3394,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz",
|
||||
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
|
||||
"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1"
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3418,13 +3418,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz",
|
||||
"integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
|
||||
"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -7941,12 +7941,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz",
|
||||
"integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==",
|
||||
"version": "15.5.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz",
|
||||
"integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.5",
|
||||
"@next/env": "15.5.4",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -7959,14 +7959,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.5",
|
||||
"@next/swc-darwin-x64": "15.5.5",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.5",
|
||||
"@next/swc-linux-arm64-musl": "15.5.5",
|
||||
"@next/swc-linux-x64-gnu": "15.5.5",
|
||||
"@next/swc-linux-x64-musl": "15.5.5",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.5",
|
||||
"@next/swc-win32-x64-msvc": "15.5.5",
|
||||
"@next/swc-darwin-arm64": "15.5.4",
|
||||
"@next/swc-darwin-x64": "15.5.4",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.4",
|
||||
"@next/swc-linux-arm64-musl": "15.5.4",
|
||||
"@next/swc-linux-x64-gnu": "15.5.4",
|
||||
"@next/swc-linux-x64-musl": "15.5.4",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.4",
|
||||
"@next/swc-win32-x64-msvc": "15.5.4",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -10430,16 +10430,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz",
|
||||
"integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==",
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz",
|
||||
"integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.46.1",
|
||||
"@typescript-eslint/parser": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.46.0",
|
||||
"@typescript-eslint/parser": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
10
package.json
10
package.json
@@ -24,7 +24,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"@tanstack/react-query": "^5.90.3",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.545.0",
|
||||
"next": "^15.5.5",
|
||||
"next": "^15.5.3",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -61,9 +61,9 @@
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
@@ -75,7 +75,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"typescript-eslint": "^8.27.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
|
||||
41
scripts/json/openwrt.json
Normal file
41
scripts/json/openwrt.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "OpenWrt",
|
||||
"slug": "openwrt",
|
||||
"categories": [
|
||||
4,
|
||||
2
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "vm",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://openwrt.org/docs/start",
|
||||
"website": "https://openwrt.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/openwrt.webp",
|
||||
"config_path": "",
|
||||
"description": "OpenWrt is a powerful open-source firmware that can transform a wide range of networking devices into highly customizable and feature-rich routers, providing users with greater control and flexibility over their network infrastructure.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "vm/openwrt.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 0.5,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "If you use VLANs (default LAN is set to VLAN 999), make sure the Proxmox Linux Bridge is configured as VLAN-aware, otherwise the VM may fail to start.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
scripts/json/petio.json
Normal file
35
scripts/json/petio.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Petio",
|
||||
"slug": "petio",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2024-06-12",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 7777,
|
||||
"documentation": "https://docs.petio.tv/",
|
||||
"website": "https://petio.tv/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/petio.webp",
|
||||
"config_path": "",
|
||||
"description": "Petio is a third party companion app available to Plex server owners to allow their users to request, review and discover content.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/petio.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "ubuntu",
|
||||
"version": "20.04"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
144
server.js
144
server.js
@@ -51,7 +51,6 @@ const handle = app.getRequestHandler();
|
||||
* @property {string} [mode]
|
||||
* @property {ServerInfo} [server]
|
||||
* @property {boolean} [isUpdate]
|
||||
* @property {boolean} [isShell]
|
||||
* @property {string} [containerId]
|
||||
*/
|
||||
|
||||
@@ -208,15 +207,13 @@ class ScriptExecutionHandler {
|
||||
* @param {WebSocketMessage} message
|
||||
*/
|
||||
async handleMessage(ws, message) {
|
||||
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message;
|
||||
const { action, scriptPath, executionId, input, mode, server, isUpdate, containerId } = message;
|
||||
|
||||
switch (action) {
|
||||
case 'start':
|
||||
if (scriptPath && executionId) {
|
||||
if (isUpdate && containerId) {
|
||||
await this.startUpdateExecution(ws, containerId, executionId, mode, server);
|
||||
} else if (isShell && containerId) {
|
||||
await this.startShellExecution(ws, containerId, executionId, mode, server);
|
||||
} else {
|
||||
await this.startScriptExecution(ws, scriptPath, executionId, mode, server);
|
||||
}
|
||||
@@ -712,145 +709,6 @@ class ScriptExecutionHandler {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start shell execution
|
||||
* @param {ExtendedWebSocket} ws
|
||||
* @param {string} containerId
|
||||
* @param {string} executionId
|
||||
* @param {string} mode
|
||||
* @param {ServerInfo|null} server
|
||||
*/
|
||||
async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) {
|
||||
try {
|
||||
|
||||
// Send start message
|
||||
this.sendMessage(ws, {
|
||||
type: 'start',
|
||||
data: `Starting shell session for container ${containerId}...`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
if (mode === 'ssh' && server) {
|
||||
await this.startSSHShellExecution(ws, containerId, executionId, server);
|
||||
} else {
|
||||
await this.startLocalShellExecution(ws, containerId, executionId);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: `Failed to start shell: ${error instanceof Error ? error.message : String(error)}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start local shell execution
|
||||
* @param {ExtendedWebSocket} ws
|
||||
* @param {string} containerId
|
||||
* @param {string} executionId
|
||||
*/
|
||||
async startLocalShellExecution(ws, containerId, executionId) {
|
||||
const { spawn } = await import('node-pty');
|
||||
|
||||
// Create a shell process that will run pct enter
|
||||
const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cwd: process.cwd(),
|
||||
env: process.env
|
||||
});
|
||||
|
||||
// Store the execution
|
||||
this.activeExecutions.set(executionId, {
|
||||
process: childProcess,
|
||||
ws
|
||||
});
|
||||
|
||||
// Handle pty data
|
||||
childProcess.onData((data) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'output',
|
||||
data: data.toString(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
// Note: No automatic command is sent - user can type commands interactively
|
||||
|
||||
// Handle process exit
|
||||
childProcess.onExit((e) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'end',
|
||||
data: `Shell session ended with exit code: ${e.exitCode}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
this.activeExecutions.delete(executionId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start SSH shell execution
|
||||
* @param {ExtendedWebSocket} ws
|
||||
* @param {string} containerId
|
||||
* @param {string} executionId
|
||||
* @param {ServerInfo} server
|
||||
*/
|
||||
async startSSHShellExecution(ws, containerId, executionId, server) {
|
||||
const sshService = getSSHExecutionService();
|
||||
|
||||
try {
|
||||
const execution = await sshService.executeCommand(
|
||||
server,
|
||||
`pct enter ${containerId}`,
|
||||
/** @param {string} data */
|
||||
(data) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'output',
|
||||
data: data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
},
|
||||
/** @param {string} error */
|
||||
(error) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: error,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
},
|
||||
/** @param {number} code */
|
||||
(code) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'end',
|
||||
data: `Shell session ended with exit code: ${code}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
this.activeExecutions.delete(executionId);
|
||||
}
|
||||
);
|
||||
|
||||
// Store the execution
|
||||
this.activeExecutions.set(executionId, {
|
||||
process: /** @type {any} */ (execution).process,
|
||||
ws
|
||||
});
|
||||
|
||||
// Note: No automatic command is sent - user can type commands interactively
|
||||
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: `SSH shell execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TerminalHandler removed - not used by current application
|
||||
|
||||
@@ -385,7 +385,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
|
||||
);
|
||||
}
|
||||
|
||||
if (!downloadedScripts?.length) {
|
||||
if (!downloadedScripts || downloadedScripts.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-muted-foreground">
|
||||
|
||||
@@ -12,10 +12,10 @@ export function Footer({ onOpenReleaseNotes }: FooterProps) {
|
||||
const { data: versionData } = api.version.getCurrentVersion.useQuery();
|
||||
|
||||
return (
|
||||
<footer className="sticky bottom-0 mt-auto border-t border-border bg-muted/30 py-3 backdrop-blur-sm">
|
||||
<footer className="sticky bottom-0 mt-auto border-t border-border bg-muted/30 py-6 backdrop-blur-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>© 2024 PVE Scripts Local</span>
|
||||
{versionData?.success && versionData.version && (
|
||||
<Button
|
||||
@@ -29,7 +29,7 @@ export function Footer({ onOpenReleaseNotes }: FooterProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -20,10 +20,6 @@ interface InstalledScript {
|
||||
server_ip: string | null;
|
||||
server_user: string | null;
|
||||
server_password: string | null;
|
||||
server_auth_type: string | null;
|
||||
server_ssh_key: string | null;
|
||||
server_ssh_key_passphrase: string | null;
|
||||
server_ssh_port: number | null;
|
||||
server_color: string | null;
|
||||
installation_date: string;
|
||||
status: 'in_progress' | 'success' | 'failed';
|
||||
@@ -39,7 +35,6 @@ export function InstalledScriptsTab() {
|
||||
const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null);
|
||||
const [openingShell, setOpeningShell] = useState<{ id: number; containerId: string; server?: any } | null>(null);
|
||||
const [editingScriptId, setEditingScriptId] = useState<number | null>(null);
|
||||
const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' });
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
@@ -64,7 +59,6 @@ export function InstalledScriptsTab() {
|
||||
} | null>(null);
|
||||
const [controllingScriptId, setControllingScriptId] = useState<number | null>(null);
|
||||
const scriptsRef = useRef<InstalledScript[]>([]);
|
||||
const statusCheckTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Error modal state
|
||||
const [errorModal, setErrorModal] = useState<{
|
||||
@@ -318,34 +312,17 @@ export function InstalledScriptsTab() {
|
||||
|
||||
// Function to fetch container statuses - simplified to just check all servers
|
||||
const fetchContainerStatuses = useCallback(() => {
|
||||
console.log('fetchContainerStatuses called, isPending:', containerStatusMutation.isPending);
|
||||
const currentScripts = scriptsRef.current;
|
||||
|
||||
// Prevent multiple simultaneous status checks
|
||||
if (containerStatusMutation.isPending) {
|
||||
console.log('Status check already pending, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing timeout
|
||||
if (statusCheckTimeoutRef.current) {
|
||||
clearTimeout(statusCheckTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Debounce status checks by 500ms
|
||||
statusCheckTimeoutRef.current = setTimeout(() => {
|
||||
const currentScripts = scriptsRef.current;
|
||||
|
||||
// Get unique server IDs from scripts
|
||||
const serverIds = [...new Set(currentScripts
|
||||
.filter(script => script.server_id)
|
||||
.map(script => script.server_id!))];
|
||||
// Get unique server IDs from scripts
|
||||
const serverIds = [...new Set(currentScripts
|
||||
.filter(script => script.server_id)
|
||||
.map(script => script.server_id!))];
|
||||
|
||||
console.log('Executing status check for server IDs:', serverIds);
|
||||
if (serverIds.length > 0) {
|
||||
containerStatusMutation.mutate({ serverIds });
|
||||
}
|
||||
}, 500);
|
||||
}, [containerStatusMutation]);
|
||||
if (serverIds.length > 0) {
|
||||
containerStatusMutation.mutate({ serverIds });
|
||||
}
|
||||
}, []); // Empty dependency array to prevent infinite loops
|
||||
|
||||
// Run cleanup when component mounts and scripts are loaded (only once)
|
||||
useEffect(() => {
|
||||
@@ -356,22 +333,17 @@ export function InstalledScriptsTab() {
|
||||
}, [scripts.length, serversData?.servers, cleanupMutation]);
|
||||
|
||||
|
||||
|
||||
// Note: Individual status fetching removed - using bulk fetchContainerStatuses instead
|
||||
|
||||
// Trigger status check when tab becomes active (component mounts)
|
||||
useEffect(() => {
|
||||
if (scripts.length > 0) {
|
||||
console.log('Status check triggered - scripts length:', scripts.length);
|
||||
fetchContainerStatuses();
|
||||
}
|
||||
}, [scripts.length, fetchContainerStatuses]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (statusCheckTimeoutRef.current) {
|
||||
clearTimeout(statusCheckTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [scripts.length]); // Only depend on scripts.length to prevent infinite loops
|
||||
|
||||
// Update scripts with container statuses
|
||||
const scriptsWithStatus = scripts.map(script => ({
|
||||
...script,
|
||||
container_status: script.container_id ? containerStatuses.get(script.id) ?? 'unknown' : undefined
|
||||
@@ -531,17 +503,13 @@ export function InstalledScriptsTab() {
|
||||
onConfirm: () => {
|
||||
// Get server info if it's SSH mode
|
||||
let server = null;
|
||||
if (script.server_id && script.server_user) {
|
||||
if (script.server_id && script.server_user && script.server_password) {
|
||||
server = {
|
||||
id: script.server_id,
|
||||
name: script.server_name,
|
||||
ip: script.server_ip,
|
||||
user: script.server_user,
|
||||
password: script.server_password,
|
||||
auth_type: script.server_auth_type ?? 'password',
|
||||
ssh_key: script.server_ssh_key,
|
||||
ssh_key_passphrase: script.server_ssh_key_passphrase,
|
||||
ssh_port: script.server_ssh_port ?? 22
|
||||
password: script.server_password
|
||||
};
|
||||
}
|
||||
|
||||
@@ -559,91 +527,6 @@ export function InstalledScriptsTab() {
|
||||
setUpdatingScript(null);
|
||||
};
|
||||
|
||||
const handleOpenShell = (script: InstalledScript) => {
|
||||
if (!script.container_id) {
|
||||
setErrorModal({
|
||||
isOpen: true,
|
||||
title: 'Shell Access Failed',
|
||||
message: 'No Container ID available for this script',
|
||||
details: 'This script does not have a valid container ID and cannot be accessed via shell.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get server info if it's SSH mode
|
||||
let server = null;
|
||||
if (script.server_id && script.server_user) {
|
||||
server = {
|
||||
id: script.server_id,
|
||||
name: script.server_name,
|
||||
ip: script.server_ip,
|
||||
user: script.server_user,
|
||||
password: script.server_password,
|
||||
auth_type: script.server_auth_type ?? 'password',
|
||||
ssh_key: script.server_ssh_key,
|
||||
ssh_key_passphrase: script.server_ssh_key_passphrase,
|
||||
ssh_port: script.server_ssh_port ?? 22
|
||||
};
|
||||
}
|
||||
|
||||
setOpeningShell({
|
||||
id: script.id,
|
||||
containerId: script.container_id,
|
||||
server: server
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseShellTerminal = () => {
|
||||
setOpeningShell(null);
|
||||
};
|
||||
|
||||
// Auto-scroll to terminals when they open
|
||||
useEffect(() => {
|
||||
if (openingShell) {
|
||||
// Small delay to ensure the terminal is rendered
|
||||
setTimeout(() => {
|
||||
const terminalElement = document.querySelector('[data-terminal="shell"]');
|
||||
if (terminalElement) {
|
||||
// Scroll to the terminal with smooth animation
|
||||
terminalElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
inline: 'nearest'
|
||||
});
|
||||
|
||||
// Add a subtle highlight effect
|
||||
terminalElement.classList.add('animate-pulse');
|
||||
setTimeout(() => {
|
||||
terminalElement.classList.remove('animate-pulse');
|
||||
}, 2000);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}, [openingShell]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updatingScript) {
|
||||
// Small delay to ensure the terminal is rendered
|
||||
setTimeout(() => {
|
||||
const terminalElement = document.querySelector('[data-terminal="update"]');
|
||||
if (terminalElement) {
|
||||
// Scroll to the terminal with smooth animation
|
||||
terminalElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
inline: 'nearest'
|
||||
});
|
||||
|
||||
// Add a subtle highlight effect
|
||||
terminalElement.classList.add('animate-pulse');
|
||||
setTimeout(() => {
|
||||
terminalElement.classList.remove('animate-pulse');
|
||||
}, 2000);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}, [updatingScript]);
|
||||
|
||||
const handleEditScript = (script: InstalledScript) => {
|
||||
setEditingScriptId(script.id);
|
||||
setEditFormData({
|
||||
@@ -756,7 +639,7 @@ export function InstalledScriptsTab() {
|
||||
<div className="space-y-6">
|
||||
{/* Update Terminal */}
|
||||
{updatingScript && (
|
||||
<div className="mb-8" data-terminal="update">
|
||||
<div className="mb-8">
|
||||
<Terminal
|
||||
scriptPath={`update-${updatingScript.containerId}`}
|
||||
onClose={handleCloseUpdateTerminal}
|
||||
@@ -768,20 +651,6 @@ export function InstalledScriptsTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shell Terminal */}
|
||||
{openingShell && (
|
||||
<div className="mb-8" data-terminal="shell">
|
||||
<Terminal
|
||||
scriptPath={`shell-${openingShell.containerId}`}
|
||||
onClose={handleCloseShellTerminal}
|
||||
mode={openingShell.server ? 'ssh' : 'local'}
|
||||
server={openingShell.server}
|
||||
isShell={true}
|
||||
containerId={openingShell.containerId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header with Stats */}
|
||||
<div className="bg-card rounded-lg shadow p-6">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
|
||||
@@ -1103,7 +972,6 @@ export function InstalledScriptsTab() {
|
||||
onSave={handleSaveEdit}
|
||||
onCancel={handleCancelEdit}
|
||||
onUpdate={() => handleUpdateScript(script)}
|
||||
onShell={() => handleOpenShell(script)}
|
||||
onDelete={() => handleDeleteScript(Number(script.id))}
|
||||
isUpdating={updateScriptMutation.isPending}
|
||||
isDeleting={deleteScriptMutation.isPending}
|
||||
@@ -1199,14 +1067,15 @@ export function InstalledScriptsTab() {
|
||||
>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{editingScriptId === script.id ? (
|
||||
<div className="flex items-center min-h-[2.5rem]">
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editFormData.script_name}
|
||||
onChange={(e) => handleInputChange('script_name', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm font-medium border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Script name"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">{script.script_path}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
@@ -1217,15 +1086,13 @@ export function InstalledScriptsTab() {
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{editingScriptId === script.id ? (
|
||||
<div className="flex items-center min-h-[2.5rem]">
|
||||
<input
|
||||
type="text"
|
||||
value={editFormData.container_id}
|
||||
onChange={(e) => handleInputChange('container_id', e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
placeholder="Container ID"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={editFormData.container_id}
|
||||
onChange={(e) => handleInputChange('container_id', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm font-mono border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Container ID"
|
||||
/>
|
||||
) : (
|
||||
script.container_id ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -1312,17 +1179,6 @@ export function InstalledScriptsTab() {
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
{/* Shell button - only show for SSH scripts with container_id */}
|
||||
{script.container_id && script.execution_mode === 'ssh' && (
|
||||
<Button
|
||||
onClick={() => handleOpenShell(script)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={containerStatuses.get(script.id) === 'stopped'}
|
||||
>
|
||||
Shell
|
||||
</Button>
|
||||
)}
|
||||
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
||||
{script.container_id && script.execution_mode === 'ssh' && (
|
||||
<>
|
||||
@@ -1331,10 +1187,6 @@ export function InstalledScriptsTab() {
|
||||
disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'}
|
||||
variant={(containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'destructive' : 'default'}
|
||||
size="sm"
|
||||
className={(containerStatuses.get(script.id) ?? 'unknown') === 'running'
|
||||
? "bg-red-600 hover:bg-red-700 text-white border border-red-500 hover:border-red-400 hover:scale-105 hover:shadow-lg hover:shadow-red-500/25 transition-all duration-200 disabled:hover:scale-100"
|
||||
: "bg-green-600 hover:bg-green-700 text-white border border-green-500 hover:border-green-400 hover:scale-105 hover:shadow-lg hover:shadow-green-500/25 transition-all duration-200 disabled:hover:scale-100"
|
||||
}
|
||||
>
|
||||
{controllingScriptId === script.id ? 'Working...' : (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'Stop' : 'Start'}
|
||||
</Button>
|
||||
@@ -1343,7 +1195,6 @@ export function InstalledScriptsTab() {
|
||||
disabled={controllingScriptId === script.id}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="bg-red-800 hover:bg-red-900 text-white border border-red-600 hover:border-red-500 hover:scale-105 hover:shadow-lg hover:shadow-red-600/30 transition-all duration-200 disabled:hover:scale-100"
|
||||
>
|
||||
{controllingScriptId === script.id ? 'Working...' : 'Destroy'}
|
||||
</Button>
|
||||
|
||||
@@ -93,22 +93,9 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
|
||||
);
|
||||
|
||||
if (keyLine) {
|
||||
let keyType = 'Unknown';
|
||||
|
||||
// Check for traditional PEM format keys
|
||||
if (keyLine.includes('RSA')) {
|
||||
keyType = 'RSA';
|
||||
} else if (keyLine.includes('ED25519')) {
|
||||
keyType = 'ED25519';
|
||||
} else if (keyLine.includes('ECDSA')) {
|
||||
keyType = 'ECDSA';
|
||||
} else if (keyLine.includes('OPENSSH PRIVATE KEY')) {
|
||||
// For OpenSSH format keys, try to detect type from the key content
|
||||
// This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns
|
||||
// We'll default to "OpenSSH" for now since we can't reliably detect the type
|
||||
keyType = 'OpenSSH';
|
||||
}
|
||||
|
||||
const keyType = keyLine.includes('RSA') ? 'RSA' :
|
||||
keyLine.includes('ED25519') ? 'ED25519' :
|
||||
keyLine.includes('ECDSA') ? 'ECDSA' : 'Unknown';
|
||||
return `${keyType} key (${keyContent.length} characters)`;
|
||||
}
|
||||
|
||||
@@ -155,7 +142,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa,ed25519,id_rsa,id_ed25519,id_ecdsa,*"
|
||||
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
@@ -166,7 +153,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
|
||||
Drag and drop your SSH private key here, or click to browse
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, ed25519, etc.)
|
||||
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,6 @@ interface InstalledScript {
|
||||
server_ip: string | null;
|
||||
server_user: string | null;
|
||||
server_password: string | null;
|
||||
server_auth_type: string | null;
|
||||
server_ssh_key: string | null;
|
||||
server_ssh_key_passphrase: string | null;
|
||||
server_ssh_port: number | null;
|
||||
server_color: string | null;
|
||||
installation_date: string;
|
||||
status: 'in_progress' | 'success' | 'failed';
|
||||
@@ -35,7 +31,6 @@ interface ScriptInstallationCardProps {
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
onUpdate: () => void;
|
||||
onShell: () => void;
|
||||
onDelete: () => void;
|
||||
isUpdating: boolean;
|
||||
isDeleting: boolean;
|
||||
@@ -55,7 +50,6 @@ export function ScriptInstallationCard({
|
||||
onSave,
|
||||
onCancel,
|
||||
onUpdate,
|
||||
onShell,
|
||||
onDelete,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
@@ -209,18 +203,6 @@ export function ScriptInstallationCard({
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
{/* Shell button - only show for SSH scripts with container_id */}
|
||||
{script.container_id && script.execution_mode === 'ssh' && (
|
||||
<Button
|
||||
onClick={onShell}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-1 min-w-0"
|
||||
disabled={containerStatus === 'stopped'}
|
||||
>
|
||||
Shell
|
||||
</Button>
|
||||
)}
|
||||
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
||||
{script.container_id && script.execution_mode === 'ssh' && (
|
||||
<>
|
||||
|
||||
@@ -571,7 +571,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!scriptsWithStatus?.length) {
|
||||
if (!scriptsWithStatus || scriptsWithStatus.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-muted-foreground">
|
||||
@@ -626,13 +626,11 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
<Button
|
||||
onClick={handleBatchDownload}
|
||||
disabled={loadSingleScriptMutation.isPending}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30 text-blue-300 hover:text-blue-200 hover:border-blue-400/50"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{loadSingleScriptMutation.isPending ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current mr-2"></div>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Downloading...
|
||||
</>
|
||||
) : (
|
||||
@@ -644,7 +642,6 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
onClick={handleDownloadAllFiltered}
|
||||
disabled={filteredScripts.length === 0 || loadSingleScriptMutation.isPending}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{loadSingleScriptMutation.isPending ? (
|
||||
<>
|
||||
@@ -660,8 +657,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
{selectedSlugs.size > 0 && (
|
||||
<Button
|
||||
onClick={clearSelection}
|
||||
variant="outline"
|
||||
size="default"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
Clear Selection
|
||||
</Button>
|
||||
@@ -670,8 +667,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
{filteredScripts.length > 0 && (
|
||||
<Button
|
||||
onClick={selectAllVisible}
|
||||
variant="outline"
|
||||
size="default"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
Select All Visible
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from 'react';
|
||||
import type { Server, CreateServerData } from '../../types/server';
|
||||
import { ServerForm } from './ServerForm';
|
||||
import { Button } from './ui/button';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
|
||||
interface ServerListProps {
|
||||
servers: Server[];
|
||||
@@ -16,14 +15,6 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [testingConnections, setTestingConnections] = useState<Set<number>>(new Set());
|
||||
const [connectionResults, setConnectionResults] = useState<Map<number, { success: boolean; message: string }>>(new Map());
|
||||
const [confirmationModal, setConfirmationModal] = useState<{
|
||||
isOpen: boolean;
|
||||
variant: 'danger';
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText: string;
|
||||
onConfirm: () => void;
|
||||
} | null>(null);
|
||||
|
||||
const handleEdit = (server: Server) => {
|
||||
setEditingId(server.id);
|
||||
@@ -41,20 +32,9 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
const server = servers.find(s => s.id === id);
|
||||
if (!server) return;
|
||||
|
||||
setConfirmationModal({
|
||||
isOpen: true,
|
||||
variant: 'danger',
|
||||
title: 'Delete Server',
|
||||
message: `This will permanently delete the server configuration "${server.name}" (${server.ip}) and all associated installed scripts. This action cannot be undone!`,
|
||||
confirmText: server.name,
|
||||
onConfirm: () => {
|
||||
onDelete(id);
|
||||
setConfirmationModal(null);
|
||||
}
|
||||
});
|
||||
if (window.confirm('Are you sure you want to delete this server configuration?')) {
|
||||
onDelete(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async (server: Server) => {
|
||||
@@ -248,21 +228,6 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{confirmationModal && (
|
||||
<ConfirmationModal
|
||||
isOpen={confirmationModal.isOpen}
|
||||
onClose={() => setConfirmationModal(null)}
|
||||
onConfirm={confirmationModal.onConfirm}
|
||||
title={confirmationModal.title}
|
||||
message={confirmationModal.message}
|
||||
variant={confirmationModal.variant}
|
||||
confirmText={confirmationModal.confirmText}
|
||||
confirmButtonText="Delete Server"
|
||||
cancelButtonText="Cancel"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ interface TerminalProps {
|
||||
mode?: 'local' | 'ssh';
|
||||
server?: any;
|
||||
isUpdate?: boolean;
|
||||
isShell?: boolean;
|
||||
containerId?: string;
|
||||
}
|
||||
|
||||
@@ -21,7 +20,7 @@ interface TerminalMessage {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, containerId }: TerminalProps) {
|
||||
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, containerId }: TerminalProps) {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
@@ -333,7 +332,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
mode,
|
||||
server,
|
||||
isUpdate,
|
||||
isShell,
|
||||
containerId
|
||||
};
|
||||
ws.send(JSON.stringify(message));
|
||||
@@ -374,7 +372,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [scriptPath, mode, server, isUpdate, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const startScript = () => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {
|
||||
@@ -390,7 +388,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
mode,
|
||||
server,
|
||||
isUpdate,
|
||||
isShell,
|
||||
containerId
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -173,9 +173,6 @@ export async function DELETE(
|
||||
);
|
||||
}
|
||||
|
||||
// Delete all installed scripts associated with this server
|
||||
db.deleteInstalledScriptsByServer(id);
|
||||
|
||||
const result = db.deleteServer(id);
|
||||
|
||||
return NextResponse.json(
|
||||
|
||||
154
src/app/page.tsx
154
src/app/page.tsx
@@ -20,13 +20,7 @@ import { api } from '~/trpc/react';
|
||||
|
||||
export default function Home() {
|
||||
const [runningScript, setRunningScript] = useState<{ path: string; name: string; mode?: 'local' | 'ssh'; server?: any } | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'scripts' | 'downloaded' | 'installed'>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedTab = localStorage.getItem('activeTab') as 'scripts' | 'downloaded' | 'installed';
|
||||
return savedTab || 'scripts';
|
||||
}
|
||||
return 'scripts';
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<'scripts' | 'downloaded' | 'installed'>('scripts');
|
||||
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
|
||||
const [highlightVersion, setHighlightVersion] = useState<string | undefined>(undefined);
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
@@ -37,13 +31,6 @@ export default function Home() {
|
||||
const { data: installedScriptsData } = api.installedScripts.getAllInstalledScripts.useQuery();
|
||||
const { data: versionData } = api.version.getCurrentVersion.useQuery();
|
||||
|
||||
// Save active tab to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('activeTab', activeTab);
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
// Auto-show release notes modal after update
|
||||
useEffect(() => {
|
||||
if (versionData?.success && versionData.version) {
|
||||
@@ -70,42 +57,15 @@ export default function Home() {
|
||||
|
||||
// Calculate script counts
|
||||
const scriptCounts = {
|
||||
available: (() => {
|
||||
if (!scriptCardsData?.success) return 0;
|
||||
|
||||
// Deduplicate scripts using Map by slug (same logic as ScriptsGrid.tsx)
|
||||
const scriptMap = new Map<string, any>();
|
||||
|
||||
scriptCardsData.cards?.forEach(script => {
|
||||
if (script?.name && script?.slug) {
|
||||
// Use slug as unique identifier, only keep first occurrence
|
||||
if (!scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, script);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return scriptMap.size;
|
||||
})(),
|
||||
available: scriptCardsData?.success ? scriptCardsData.cards?.length ?? 0 : 0,
|
||||
downloaded: (() => {
|
||||
if (!scriptCardsData?.success || !localScriptsData?.scripts) return 0;
|
||||
|
||||
// First deduplicate GitHub scripts using Map by slug
|
||||
const scriptMap = new Map<string, any>();
|
||||
|
||||
scriptCardsData.cards?.forEach(script => {
|
||||
if (script?.name && script?.slug) {
|
||||
if (!scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, script);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const deduplicatedGithubScripts = Array.from(scriptMap.values());
|
||||
// Count scripts that are both in GitHub data and have local versions
|
||||
const githubScripts = scriptCardsData.cards ?? [];
|
||||
const localScripts = localScriptsData.scripts ?? [];
|
||||
|
||||
// Count scripts that are both in deduplicated GitHub data and have local versions
|
||||
return deduplicatedGithubScripts.filter(script => {
|
||||
return githubScripts.filter(script => {
|
||||
if (!script?.name) return false;
|
||||
return localScripts.some(local => {
|
||||
if (!local?.name) return false;
|
||||
@@ -171,58 +131,64 @@ export default function Home() {
|
||||
{/* Tab Navigation */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<div className="border-b border-border">
|
||||
<nav className="-mb-px flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('scripts')}
|
||||
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
|
||||
activeTab === 'scripts'
|
||||
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
|
||||
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
|
||||
}`}>
|
||||
<Package className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Available Scripts</span>
|
||||
<span className="sm:hidden">Available</span>
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
|
||||
{scriptCounts.available}
|
||||
</span>
|
||||
<nav className="-mb-px flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 lg:space-x-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('scripts')}
|
||||
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
|
||||
activeTab === 'scripts'
|
||||
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
|
||||
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
|
||||
}`}>
|
||||
<Package className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Available Scripts</span>
|
||||
<span className="sm:hidden">Available</span>
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
|
||||
{scriptCounts.available}
|
||||
</span>
|
||||
</Button>
|
||||
<ContextualHelpIcon section="available-scripts" tooltip="Help with Available Scripts" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('downloaded')}
|
||||
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
|
||||
activeTab === 'downloaded'
|
||||
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
|
||||
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
|
||||
}`}>
|
||||
<HardDrive className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Downloaded Scripts</span>
|
||||
<span className="sm:hidden">Downloaded</span>
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
|
||||
{scriptCounts.downloaded}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('downloaded')}
|
||||
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
|
||||
activeTab === 'downloaded'
|
||||
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
|
||||
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
|
||||
}`}>
|
||||
<HardDrive className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Downloaded Scripts</span>
|
||||
<span className="sm:hidden">Downloaded</span>
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
|
||||
{scriptCounts.downloaded}
|
||||
</span>
|
||||
</Button>
|
||||
<ContextualHelpIcon section="downloaded-scripts" tooltip="Help with Downloaded Scripts" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('installed')}
|
||||
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
|
||||
activeTab === 'installed'
|
||||
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
|
||||
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
|
||||
}`}>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Installed Scripts</span>
|
||||
<span className="sm:hidden">Installed</span>
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
|
||||
{scriptCounts.installed}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('installed')}
|
||||
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
|
||||
activeTab === 'installed'
|
||||
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
|
||||
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
|
||||
}`}>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Installed Scripts</span>
|
||||
<span className="sm:hidden">Installed</span>
|
||||
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
|
||||
{scriptCounts.installed}
|
||||
</span>
|
||||
</Button>
|
||||
<ContextualHelpIcon section="installed-scripts" tooltip="Help with Installed Scripts" />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* to ensure optimal readability based on luminance
|
||||
*/
|
||||
export function getContrastColor(hexColor: string): 'black' | 'white' {
|
||||
if (!hexColor?.length || hexColor.length !== 7 || !hexColor.startsWith('#')) {
|
||||
if (!hexColor || hexColor.length !== 7 || !hexColor.startsWith('#')) {
|
||||
return 'black'; // Default to black for invalid colors
|
||||
}
|
||||
|
||||
|
||||
@@ -551,31 +551,23 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const listCommand = 'pct list';
|
||||
let listOutput = '';
|
||||
|
||||
// Add timeout to prevent hanging connections
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('SSH command timeout after 30 seconds')), 30000);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
listCommand,
|
||||
(data: string) => {
|
||||
listOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`pct list error on server ${(server as any).name}:`, error);
|
||||
reject(new Error(error));
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
server as any,
|
||||
listCommand,
|
||||
(data: string) => {
|
||||
listOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`pct list error on server ${(server as any).name}:`, error);
|
||||
reject(new Error(error));
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
}),
|
||||
timeoutPromise
|
||||
]);
|
||||
|
||||
// Parse pct list output
|
||||
const lines = listOutput.split('\n').filter(line => line.trim());
|
||||
@@ -914,8 +906,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
|
||||
} catch (_error) {
|
||||
// If status check fails, continue with destroy attempt
|
||||
// The destroy command will handle the error appropriately
|
||||
}
|
||||
|
||||
// Execute destroy command
|
||||
|
||||
@@ -90,7 +90,7 @@ class DatabaseService {
|
||||
installation_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT NOT NULL CHECK(status IN ('in_progress', 'success', 'failed')),
|
||||
output_log TEXT,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -180,10 +180,6 @@ class DatabaseService {
|
||||
s.ip as server_ip,
|
||||
s.user as server_user,
|
||||
s.password as server_password,
|
||||
s.auth_type as server_auth_type,
|
||||
s.ssh_key as server_ssh_key,
|
||||
s.ssh_key_passphrase as server_ssh_key_passphrase,
|
||||
s.ssh_port as server_ssh_port,
|
||||
s.color as server_color
|
||||
FROM installed_scripts inst
|
||||
LEFT JOIN servers s ON inst.server_id = s.id
|
||||
@@ -276,14 +272,6 @@ class DatabaseService {
|
||||
return stmt.run(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} server_id
|
||||
*/
|
||||
deleteInstalledScriptsByServer(server_id) {
|
||||
const stmt = this.db.prepare('DELETE FROM installed_scripts WHERE server_id = ?');
|
||||
return stmt.run(server_id);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ class SSHExecutionService {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'ssh-key-'));
|
||||
const tempKeyPath = join(tempDir, 'private_key');
|
||||
|
||||
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
|
||||
const normalizedKey = ssh_key.trimEnd() + '\n';
|
||||
writeFileSync(tempKeyPath, normalizedKey);
|
||||
writeFileSync(tempKeyPath, ssh_key);
|
||||
chmodSync(tempKeyPath, 0o600); // Set proper permissions
|
||||
|
||||
return tempKeyPath;
|
||||
@@ -297,7 +295,7 @@ class SSHExecutionService {
|
||||
try {
|
||||
unlinkSync(tempKeyPath);
|
||||
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
|
||||
rmdirSync(tempDir);
|
||||
unlinkSync(tempDir);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
|
||||
}
|
||||
@@ -316,7 +314,7 @@ class SSHExecutionService {
|
||||
try {
|
||||
unlinkSync(tempKeyPath);
|
||||
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
|
||||
rmdirSync(tempDir);
|
||||
unlinkSync(tempDir);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
|
||||
}
|
||||
@@ -330,7 +328,7 @@ class SSHExecutionService {
|
||||
try {
|
||||
unlinkSync(tempKeyPath);
|
||||
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
|
||||
rmdirSync(tempDir);
|
||||
unlinkSync(tempDir);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
|
||||
}
|
||||
@@ -385,7 +383,7 @@ class SSHExecutionService {
|
||||
try {
|
||||
unlinkSync(tempKeyPath);
|
||||
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
|
||||
rmdirSync(tempDir);
|
||||
unlinkSync(tempDir);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
|
||||
}
|
||||
@@ -416,7 +414,7 @@ class SSHExecutionService {
|
||||
try {
|
||||
unlinkSync(tempKeyPath);
|
||||
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
|
||||
rmdirSync(tempDir);
|
||||
unlinkSync(tempDir);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
|
||||
}
|
||||
|
||||
@@ -557,9 +557,7 @@ expect {
|
||||
tempKeyPath = join(tempDir, 'private_key');
|
||||
|
||||
// Write the private key to temporary file
|
||||
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
|
||||
const normalizedKey = ssh_key.trimEnd() + '\n';
|
||||
writeFileSync(tempKeyPath, normalizedKey);
|
||||
writeFileSync(tempKeyPath, ssh_key);
|
||||
chmodSync(tempKeyPath, 0o600); // Set proper permissions
|
||||
|
||||
// Build SSH command
|
||||
|
||||
Reference in New Issue
Block a user