Compare commits
16 Commits
fix/source
...
update-ver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb5402c74 | ||
|
|
dcf923551b | ||
|
|
69a5ac3a56 | ||
|
|
7b8c1ebdf1 | ||
|
|
580b623939 | ||
|
|
ac21fbb181 | ||
|
|
588ae65dfd | ||
|
|
30acba39a5 | ||
|
|
3a5bb3dc45 | ||
|
|
f42c0d956e | ||
|
|
0ed13fcf0f | ||
|
|
afc87910e6 | ||
|
|
b97eca9620 | ||
|
|
8f0ae3a341 | ||
|
|
b5450bd221 | ||
|
|
88dbe4ea85 |
311
package-lock.json
generated
311
package-lock.json
generated
@@ -9,8 +9,8 @@
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/adapter-better-sqlite3": "^7.0.1",
|
||||
"@prisma/client": "^7.0.1",
|
||||
"@prisma/adapter-better-sqlite3": "^7.1.0",
|
||||
"@prisma/client": "^7.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
@@ -33,11 +33,11 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next": "^16.0.6",
|
||||
"next": "^16.0.7",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"refractor": "^5.0.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"prisma": "^7.0.1",
|
||||
"prisma": "^7.1.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -195,7 +195,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -591,7 +590,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -635,7 +633,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -645,8 +642,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz",
|
||||
"integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@electric-sql/pglite-socket": {
|
||||
"version": "0.0.6",
|
||||
@@ -1329,9 +1325,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz",
|
||||
"integrity": "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==",
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz",
|
||||
"integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1937,9 +1933,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.6.tgz",
|
||||
"integrity": "sha512-PFTK/G/vM3UJwK5XDYMFOqt8QW42mmhSgdKDapOlCqBUAOfJN2dyOnASR/xUR/JRrro0pLohh/zOJ77xUQWQAg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
|
||||
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1953,9 +1949,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.6.tgz",
|
||||
"integrity": "sha512-AGzKiPlDiui+9JcPRHLI4V9WFTTcKukhJTfK9qu3e0tz+Y/88B7vo5yZoO7UaikplJEHORzG3QaBFQfkjhnL0Q==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
|
||||
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1969,9 +1965,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.6.tgz",
|
||||
"integrity": "sha512-LlLLNrK9WCIUkq2GciWDcquXYIf7vLxX8XE49gz7EncssZGL1vlHwgmURiJsUZAvk0HM1a8qb1ABDezsjAE/jw==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
|
||||
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1985,9 +1981,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.6.tgz",
|
||||
"integrity": "sha512-r04NzmLSGGfG8EPXKVK72N5zDNnq9pa9el78LhdtqIC3zqKh74QfKHnk24DoK4PEs6eY7sIK/CnNpt30oc59kg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
|
||||
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2001,9 +1997,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.6.tgz",
|
||||
"integrity": "sha512-hfB/QV0hA7lbD1OJxp52wVDlpffUMfyxUB5ysZbb/pBC5iuhyLcEKSVQo56PFUUmUQzbMsAtUu6k2Gh9bBtWXA==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
|
||||
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2017,9 +2013,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.6.tgz",
|
||||
"integrity": "sha512-PZJushBgfvKhJBy01yXMdgL+l5XKr7uSn5jhOQXQXiH3iPT2M9iG64yHpPNGIKitKrHJInwmhPVGogZBAJOCPw==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
|
||||
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2033,9 +2029,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.6.tgz",
|
||||
"integrity": "sha512-LqY76IojrH9yS5fyATjLzlOIOgwyzBuNRqXwVxcGfZ58DWNQSyfnLGlfF6shAEqjwlDNLh4Z+P0rnOI87Y9jEw==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
|
||||
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2049,9 +2045,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.6.tgz",
|
||||
"integrity": "sha512-eIfSNNqAkj0tqKRf0u7BVjqylJCuabSrxnpSENY3YKApqwDMeAqYPmnOwmVe6DDl3Lvkbe7cJAyP6i9hQ5PmmQ==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
|
||||
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2065,9 +2061,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.6.tgz",
|
||||
"integrity": "sha512-QGs18P4OKdK9y2F3Th42+KGnwsc2iaThOe6jxQgP62kslUU4W+g6AzI6bdIn/pslhSfxjAMU5SjakfT5Fyo/xA==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
|
||||
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2136,22 +2132,22 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/adapter-better-sqlite3": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/adapter-better-sqlite3/-/adapter-better-sqlite3-7.0.1.tgz",
|
||||
"integrity": "sha512-fgnn+lkUV/7pUvPnQSI/ZHONwGU+eisWqU6tvBFGK2ovgBUAJN8zG0fbt8v8Brphyo4h4AA3+jyG0TFFb+qVxQ==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/adapter-better-sqlite3/-/adapter-better-sqlite3-7.1.0.tgz",
|
||||
"integrity": "sha512-Ex4CimAONWMoUrhU27lpGXb4MdX/59qj+4PBTIuPVJLXZfTxSWuU8KowlRtq1w5iE91WiwMgU1KgeBOKJ81nEA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/driver-adapter-utils": "7.0.1",
|
||||
"@prisma/driver-adapter-utils": "7.1.0",
|
||||
"better-sqlite3": "^12.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.0.1.tgz",
|
||||
"integrity": "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.1.0.tgz",
|
||||
"integrity": "sha512-qf7GPYHmS/xybNiSOpzv9wBo+UwqfL2PeyX+08v+KVHDI0AlSCQIh5bBySkH3alu06NX9wy98JEnckhMHoMFfA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/client-runtime-utils": "7.0.1"
|
||||
"@prisma/client-runtime-utils": "7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19 || ^22.12 || >=24.0"
|
||||
@@ -2170,15 +2166,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client-runtime-utils": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.0.1.tgz",
|
||||
"integrity": "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.1.0.tgz",
|
||||
"integrity": "sha512-39xmeBrNTN40FzF34aJMjfX1PowVCqoT3UKUWBBSP3aXV05NRqGBC3x2wCDs96ti6ZgdiVzqnRDHtbzU8X+lPQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.0.1.tgz",
|
||||
"integrity": "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.1.0.tgz",
|
||||
"integrity": "sha512-Uz+I43Wn1RYNHtuYtOhOnUcNMWp2Pd3GUDDKs37xlHptCGpzEG3MRR9L+8Y2ISMsMI24z/Ni+ww6OB/OO8M0sQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -2230,35 +2226,49 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config/node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz",
|
||||
"integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.1.0.tgz",
|
||||
"integrity": "sha512-pPAckG6etgAsEBusmZiFwM9bldLSNkn++YuC4jCTJACdK5hLOVnOzX7eSL2FgaU6Gomd6wIw21snUX2dYroMZQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/dev": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.13.0.tgz",
|
||||
"integrity": "sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA==",
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.15.0.tgz",
|
||||
"integrity": "sha512-KhWaipnFlS/fWEs6I6Oqjcy2S08vKGmxJ5LexqUl/3Ve0EgLUsZwdKF0MvqPM5F5ttw8GtfZarjM5y7VLwv9Ow==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@electric-sql/pglite": "0.3.2",
|
||||
"@electric-sql/pglite-socket": "0.0.6",
|
||||
"@electric-sql/pglite-tools": "0.2.7",
|
||||
"@hono/node-server": "1.14.2",
|
||||
"@hono/node-server": "1.19.6",
|
||||
"@mrleebo/prisma-ast": "0.12.1",
|
||||
"@prisma/get-platform": "6.8.2",
|
||||
"@prisma/query-plan-executor": "6.18.0",
|
||||
"foreground-child": "3.3.1",
|
||||
"get-port-please": "3.1.2",
|
||||
"hono": "4.7.10",
|
||||
"hono": "4.10.6",
|
||||
"http-status-codes": "2.3.0",
|
||||
"pathe": "2.0.3",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"remeda": "2.21.3",
|
||||
"std-env": "3.9.0",
|
||||
"valibot": "1.1.0",
|
||||
"valibot": "1.2.0",
|
||||
"zeptomatch": "2.0.2"
|
||||
}
|
||||
},
|
||||
@@ -2270,65 +2280,65 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/driver-adapter-utils": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.0.1.tgz",
|
||||
"integrity": "sha512-sBbxm/yysHLLF2iMAB+qcX/nn3WFgsiC4DQNz0uM6BwGSIs8lIvgo0u8nR9nxe5gvFgKiIH8f4z2fgOEMeXc8w==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.1.0.tgz",
|
||||
"integrity": "sha512-AlVLzeXkw81+47MvQ9M8DvTiHkRfJ8xzklTbYjpskb0cTTDVHboTI/OVwT6Wcep/bNvfLKJYO0nylBiM5rxgww==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "7.0.1"
|
||||
"@prisma/debug": "7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.0.1.tgz",
|
||||
"integrity": "sha512-f+D/vdKeImqUHysd5Bgv8LQ1whl4sbLepHyYMQQMK61cp4WjwJVryophleLUrfEJRpBLGTBI/7fnLVENxxMFPQ==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.1.0.tgz",
|
||||
"integrity": "sha512-KQlraOybdHAzVv45KWKJzpR9mJLkib7/TyApQpqrsL7FUHfgjIcy8jrVGt3iNfG6/GDDl+LNlJ84JSQwIfdzxA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "7.0.1",
|
||||
"@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6",
|
||||
"@prisma/fetch-engine": "7.0.1",
|
||||
"@prisma/get-platform": "7.0.1"
|
||||
"@prisma/debug": "7.1.0",
|
||||
"@prisma/engines-version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||
"@prisma/fetch-engine": "7.1.0",
|
||||
"@prisma/get-platform": "7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6.tgz",
|
||||
"integrity": "sha512-RA7pShKvijHib4USRB3YuLTQamHKJPkTRDc45AwxfahUQngiGVMlIj4ix4emUxkrum4o/jwn82WIwlG57EtgiQ==",
|
||||
"version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba.tgz",
|
||||
"integrity": "sha512-qZUevUh+yPhGT28rDQnV8V2kLnFjirzhVD67elRPIJHRsUV/mkII10HSrJrhK/U2GYgAxXR2VEREtq7AsfS8qw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz",
|
||||
"integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.1.0.tgz",
|
||||
"integrity": "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "7.0.1"
|
||||
"@prisma/debug": "7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.0.1.tgz",
|
||||
"integrity": "sha512-5DnSairYIYU7dcv/9pb1KCwIRHZfhVOd34855d01lUI5QdF9rdCkMywPQbBM67YP7iCgQoEZO0/COtOMpR4i9A==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.1.0.tgz",
|
||||
"integrity": "sha512-GZYF5Q8kweXWGfn87hTu17kw7x1DgnehgKoE4Zg1BmHYF3y1Uu0QRY/qtSE4veH3g+LW8f9HKqA0tARG66bxxQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "7.0.1",
|
||||
"@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6",
|
||||
"@prisma/get-platform": "7.0.1"
|
||||
"@prisma/debug": "7.1.0",
|
||||
"@prisma/engines-version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||
"@prisma/get-platform": "7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz",
|
||||
"integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.1.0.tgz",
|
||||
"integrity": "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "7.0.1"
|
||||
"@prisma/debug": "7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
@@ -3705,7 +3715,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz",
|
||||
"integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.11"
|
||||
},
|
||||
@@ -3815,7 +3824,6 @@
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@trpc/server": "11.7.2",
|
||||
"typescript": ">=5.7.2"
|
||||
@@ -3846,7 +3854,6 @@
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.7.2"
|
||||
}
|
||||
@@ -3867,7 +3874,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -4053,7 +4061,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -4064,7 +4071,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -4139,7 +4145,6 @@
|
||||
"integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.48.1",
|
||||
"@typescript-eslint/types": "8.48.1",
|
||||
@@ -4795,7 +4800,6 @@
|
||||
"integrity": "sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.15",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -4848,8 +4852,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
@@ -4857,7 +4860,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4908,6 +4910,7 @@
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -5370,7 +5373,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -6085,7 +6087,8 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
@@ -6444,7 +6447,6 @@
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -6630,7 +6632,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -7439,9 +7440,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/grammex": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.11.tgz",
|
||||
"integrity": "sha512-HNwLkgRg9SqTAd1N3Uh/MnKwTBTzwBxTOPbXQ8pb0tpwydjk90k4zRE8JUn9fMUiRwKtXFZ1TWFmms3dZHN+Fg==",
|
||||
"version": "3.1.12",
|
||||
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
|
||||
"integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -7646,12 +7647,11 @@
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.7.10",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.7.10.tgz",
|
||||
"integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==",
|
||||
"version": "4.10.6",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz",
|
||||
"integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -8453,7 +8453,6 @@
|
||||
"integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.23",
|
||||
"@asamuzakjp/dom-selector": "^6.7.4",
|
||||
@@ -8597,12 +8596,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
@@ -9078,6 +9077,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -10230,12 +10230,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.0.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.6.tgz",
|
||||
"integrity": "sha512-2zOZ/4FdaAp5hfCU/RnzARlZzBsjaTZ/XjNQmuyYLluAPM7kcrbIkdeO2SL0Ysd1vnrSgU+GwugfeWX1cUCgCg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
|
||||
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.0.6",
|
||||
"@next/env": "16.0.7",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -10248,14 +10248,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.0.6",
|
||||
"@next/swc-darwin-x64": "16.0.6",
|
||||
"@next/swc-linux-arm64-gnu": "16.0.6",
|
||||
"@next/swc-linux-arm64-musl": "16.0.6",
|
||||
"@next/swc-linux-x64-gnu": "16.0.6",
|
||||
"@next/swc-linux-x64-musl": "16.0.6",
|
||||
"@next/swc-win32-arm64-msvc": "16.0.6",
|
||||
"@next/swc-win32-x64-msvc": "16.0.6",
|
||||
"@next/swc-darwin-arm64": "16.0.7",
|
||||
"@next/swc-darwin-x64": "16.0.7",
|
||||
"@next/swc-linux-arm64-gnu": "16.0.7",
|
||||
"@next/swc-linux-arm64-musl": "16.0.7",
|
||||
"@next/swc-linux-x64-gnu": "16.0.7",
|
||||
"@next/swc-linux-x64-musl": "16.0.7",
|
||||
"@next/swc-win32-arm64-msvc": "16.0.7",
|
||||
"@next/swc-win32-x64-msvc": "16.0.7",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -10835,7 +10835,6 @@
|
||||
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -10931,6 +10930,7 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -10946,6 +10946,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -10954,17 +10955,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.0.1.tgz",
|
||||
"integrity": "sha512-zp93MdFMSU1IHPEXbUHVUuD8wauh2BUm14OVxhxGrWJQQpXpda0rW4VSST2bci4raoldX64/wQxHKkl/wqDskQ==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.1.0.tgz",
|
||||
"integrity": "sha512-dy/3urE4JjhdiW5b09pGjVhGI7kPESK2VlCDrCqeYK5m5SslAtG5FCGnZWP7E8Sdg+Ow1wV2mhJH5RTFL5gEsw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "7.0.1",
|
||||
"@prisma/dev": "0.13.0",
|
||||
"@prisma/engines": "7.0.1",
|
||||
"@prisma/config": "7.1.0",
|
||||
"@prisma/dev": "0.15.0",
|
||||
"@prisma/engines": "7.1.0",
|
||||
"@prisma/studio-core": "0.8.2",
|
||||
"mysql2": "3.15.3",
|
||||
"postgres": "3.4.7"
|
||||
@@ -11145,26 +11145,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.0"
|
||||
"react": "^19.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
@@ -11172,7 +11170,8 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
@@ -12395,8 +12394,7 @@
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
@@ -12498,7 +12496,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13275,7 +13272,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -13546,12 +13542,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/valibot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz",
|
||||
"integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
|
||||
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5"
|
||||
},
|
||||
@@ -13595,7 +13590,6 @@
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -13689,7 +13683,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13703,7 +13696,6 @@
|
||||
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.15",
|
||||
"@vitest/mocker": "4.0.15",
|
||||
@@ -14060,7 +14052,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -25,8 +25,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-better-sqlite3": "^7.0.1",
|
||||
"@prisma/client": "^7.0.1",
|
||||
"@prisma/adapter-better-sqlite3": "^7.1.0",
|
||||
"@prisma/client": "^7.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
@@ -49,11 +49,11 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next": "^16.0.6",
|
||||
"next": "^16.0.7",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"refractor": "^5.0.0",
|
||||
@@ -87,7 +87,7 @@
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.7.3",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"prisma": "^7.0.1",
|
||||
"prisma": "^7.1.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
507
scripts/core/alpine-tools.func
Normal file
507
scripts/core/alpine-tools.func
Normal file
@@ -0,0 +1,507 @@
|
||||
#!/bin/ash
|
||||
# shellcheck shell=ash
|
||||
|
||||
# Expects existing msg_* functions and optional $STD from the framework.
|
||||
|
||||
# ------------------------------
|
||||
# helpers
|
||||
# ------------------------------
|
||||
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||
has() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
need_tool() {
|
||||
# usage: need_tool curl jq unzip ...
|
||||
# setup missing tools via apk
|
||||
local missing=0 t
|
||||
for t in "$@"; do
|
||||
if ! has "$t"; then missing=1; fi
|
||||
done
|
||||
if [ "$missing" -eq 1 ]; then
|
||||
msg_info "Installing tools: $*"
|
||||
apk add --no-cache "$@" >/dev/null 2>&1 || {
|
||||
msg_error "apk add failed for: $*"
|
||||
return 1
|
||||
}
|
||||
msg_ok "Tools ready: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
net_resolves() {
|
||||
# better handling for missing getent on Alpine
|
||||
# usage: net_resolves api.github.com
|
||||
local host="$1"
|
||||
ping -c1 -W1 "$host" >/dev/null 2>&1 || nslookup "$host" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_usr_local_bin_persist() {
|
||||
local PROFILE_FILE="/etc/profile.d/10-localbin.sh"
|
||||
if [ ! -f "$PROFILE_FILE" ]; then
|
||||
echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE"
|
||||
chmod +x "$PROFILE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
download_with_progress() {
|
||||
# $1 url, $2 dest
|
||||
local url="$1" out="$2" cl
|
||||
need_tool curl pv || return 1
|
||||
cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r')
|
||||
if [ -n "$cl" ]; then
|
||||
curl -fsSL "$url" | pv -s "$cl" >"$out" || {
|
||||
msg_error "Download failed: $url"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
curl -fL# -o "$out" "$url" || {
|
||||
msg_error "Download failed: $url"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# GitHub: check Release
|
||||
# ------------------------------
|
||||
check_for_gh_release() {
|
||||
# app, repo, [pinned]
|
||||
local app="$1" source="$2" pinned="${3:-}"
|
||||
local app_lc
|
||||
app_lc="$(lower "$app" | tr -d ' ')"
|
||||
local current_file="$HOME/.${app_lc}"
|
||||
local current="" release tag
|
||||
|
||||
msg_info "Check for update: $app"
|
||||
|
||||
net_resolves api.github.com || {
|
||||
msg_error "DNS/network error: api.github.com"
|
||||
return 1
|
||||
}
|
||||
need_tool curl jq || return 1
|
||||
|
||||
tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty')
|
||||
[ -z "$tag" ] && {
|
||||
msg_error "Unable to fetch latest tag for $app"
|
||||
return 1
|
||||
}
|
||||
release="${tag#v}"
|
||||
|
||||
[ -f "$current_file" ] && current="$(cat "$current_file")"
|
||||
|
||||
if [ -n "$pinned" ]; then
|
||||
if [ "$pinned" = "$release" ]; then
|
||||
msg_ok "$app pinned to v$pinned (no update)"
|
||||
return 1
|
||||
fi
|
||||
if [ "$current" = "$pinned" ]; then
|
||||
msg_ok "$app pinned v$pinned installed (upstream v$release)"
|
||||
return 1
|
||||
fi
|
||||
msg_info "$app pinned v$pinned (upstream v$release) → update/downgrade"
|
||||
CHECK_UPDATE_RELEASE="$pinned"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$release" != "$current" ] || [ ! -f "$current_file" ]; then
|
||||
CHECK_UPDATE_RELEASE="$release"
|
||||
msg_info "New release available: v$release (current: v${current:-none})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "$app is up to date (v$release)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# GitHub: get Release & deploy (Alpine)
|
||||
# modes: tarball | prebuild | singlefile
|
||||
# ------------------------------
|
||||
fetch_and_deploy_gh() {
|
||||
# $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern
|
||||
local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}"
|
||||
local app_lc
|
||||
app_lc="$(lower "$app" | tr -d ' ')"
|
||||
local vfile="$HOME/.${app_lc}"
|
||||
local json url filename tmpd unpack
|
||||
|
||||
net_resolves api.github.com || {
|
||||
msg_error "DNS/network error"
|
||||
return 1
|
||||
}
|
||||
need_tool curl jq tar || return 1
|
||||
[ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true
|
||||
|
||||
tmpd="$(mktemp -d)" || return 1
|
||||
mkdir -p "$target"
|
||||
|
||||
# Release JSON
|
||||
if [ "$version" = "latest" ]; then
|
||||
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || {
|
||||
msg_error "GitHub API failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || {
|
||||
msg_error "GitHub API failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# correct Version
|
||||
version="$(printf '%s' "$json" | jq -r '.tag_name // empty')"
|
||||
version="${version#v}"
|
||||
|
||||
[ -z "$version" ] && {
|
||||
msg_error "No tag in release json"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
tarball | source)
|
||||
url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')"
|
||||
[ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
|
||||
filename="${app_lc}-${version}.tar.gz"
|
||||
download_with_progress "$url" "$tmpd/$filename" || {
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
tar -xzf "$tmpd/$filename" -C "$tmpd" || {
|
||||
msg_error "tar extract failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
||||
# copy content of unpack to target
|
||||
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||
msg_error "copy failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
prebuild)
|
||||
[ -n "$pattern" ] || {
|
||||
msg_error "prebuild requires asset pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
|
||||
BEGIN{IGNORECASE=1}
|
||||
$0 ~ p {print; exit}
|
||||
')"
|
||||
[ -z "$url" ] && {
|
||||
msg_error "asset not found for pattern: $pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
filename="${url##*/}"
|
||||
download_with_progress "$url" "$tmpd/$filename" || {
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
# unpack archive (Zip or tarball)
|
||||
case "$filename" in
|
||||
*.zip)
|
||||
need_tool unzip || {
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
mkdir -p "$tmpd/unp"
|
||||
unzip -q "$tmpd/$filename" -d "$tmpd/unp"
|
||||
;;
|
||||
*.tar.gz | *.tgz | *.tar.xz | *.tar.zst | *.tar.bz2)
|
||||
mkdir -p "$tmpd/unp"
|
||||
tar -xf "$tmpd/$filename" -C "$tmpd/unp"
|
||||
;;
|
||||
*)
|
||||
msg_error "unsupported archive: $filename"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
# top-level folder strippen
|
||||
if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then
|
||||
unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)"
|
||||
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||
msg_error "copy failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
(cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||
msg_error "copy failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
;;
|
||||
singlefile)
|
||||
[ -n "$pattern" ] || {
|
||||
msg_error "singlefile requires asset pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
|
||||
BEGIN{IGNORECASE=1}
|
||||
$0 ~ p {print; exit}
|
||||
')"
|
||||
[ -z "$url" ] && {
|
||||
msg_error "asset not found for pattern: $pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
filename="${url##*/}"
|
||||
download_with_progress "$url" "$target/$app" || {
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
chmod +x "$target/$app"
|
||||
;;
|
||||
*)
|
||||
msg_error "Unknown mode: $mode"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$version" >"$vfile"
|
||||
ensure_usr_local_bin_persist
|
||||
rm -rf "$tmpd"
|
||||
msg_ok "Deployed $app ($version) → $target"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# yq (mikefarah) – Alpine
|
||||
# ------------------------------
|
||||
setup_yq() {
|
||||
# prefer apk, unless FORCE_GH=1
|
||||
if [ "${FORCE_GH:-0}" != "1" ] && apk info -e yq >/dev/null 2>&1; then
|
||||
msg_info "Updating yq via apk"
|
||||
apk add --no-cache --upgrade yq >/dev/null 2>&1 || true
|
||||
msg_ok "yq ready ($(yq --version 2>/dev/null))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
need_tool curl || return 1
|
||||
local arch bin url tmp
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="amd64" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
*)
|
||||
msg_error "Unsupported arch for yq: $(uname -m)"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}"
|
||||
tmp="$(mktemp)"
|
||||
download_with_progress "$url" "$tmp" || return 1
|
||||
install -m 0755 "$tmp" /usr/local/bin/yq
|
||||
rm -f "$tmp"
|
||||
msg_ok "Setup yq ($(yq --version 2>/dev/null))"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Adminer – Alpine
|
||||
# ------------------------------
|
||||
setup_adminer() {
|
||||
need_tool curl || return 1
|
||||
msg_info "Setup Adminer (Alpine)"
|
||||
mkdir -p /var/www/localhost/htdocs/adminer
|
||||
curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
|
||||
-o /var/www/localhost/htdocs/adminer/index.php || {
|
||||
msg_error "Adminer download failed"
|
||||
return 1
|
||||
}
|
||||
msg_ok "Adminer at /adminer (served by your webserver)"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# uv – Alpine (musl tarball)
|
||||
# optional: PYTHON_VERSION="3.12"
|
||||
# ------------------------------
|
||||
setup_uv() {
|
||||
need_tool curl tar || return 1
|
||||
local UV_BIN="/usr/local/bin/uv"
|
||||
local arch tarball url tmpd ver installed
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="x86_64-unknown-linux-musl" ;;
|
||||
aarch64) arch="aarch64-unknown-linux-musl" ;;
|
||||
*)
|
||||
msg_error "Unsupported arch for uv: $(uname -m)"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
ver="$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' 2>/dev/null)"
|
||||
ver="${ver#v}"
|
||||
[ -z "$ver" ] && {
|
||||
msg_error "uv: cannot determine latest version"
|
||||
return 1
|
||||
}
|
||||
|
||||
if has "$UV_BIN"; then
|
||||
installed="$($UV_BIN -V 2>/dev/null | awk '{print $2}')"
|
||||
[ "$installed" = "$ver" ] && {
|
||||
msg_ok "uv $ver already installed"
|
||||
return 0
|
||||
}
|
||||
msg_info "Updating uv $installed → $ver"
|
||||
else
|
||||
msg_info "Setup uv $ver"
|
||||
fi
|
||||
|
||||
tmpd="$(mktemp -d)" || return 1
|
||||
tarball="uv-${arch}.tar.gz"
|
||||
url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}"
|
||||
|
||||
download_with_progress "$url" "$tmpd/uv.tar.gz" || {
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || {
|
||||
msg_error "uv: extract failed"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
|
||||
# tar contains ./uv
|
||||
if [ -x "$tmpd/uv" ]; then
|
||||
install -m 0755 "$tmpd/uv" "$UV_BIN"
|
||||
else
|
||||
# fallback: in subfolder
|
||||
install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
|
||||
msg_error "uv binary not found in tar"
|
||||
rm -rf "$tmpd"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
rm -rf "$tmpd"
|
||||
ensure_usr_local_bin_persist
|
||||
msg_ok "Setup uv $ver"
|
||||
|
||||
if [ -n "${PYTHON_VERSION:-}" ]; then
|
||||
local match
|
||||
match="$(uv python list --only-downloads 2>/dev/null | awk -v maj="$PYTHON_VERSION" '
|
||||
$0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)"
|
||||
[ -z "$match" ] && {
|
||||
msg_error "No matching Python for $PYTHON_VERSION"
|
||||
return 1
|
||||
}
|
||||
if ! uv python list | grep -q "cpython-${match}-linux"; then
|
||||
msg_info "Installing Python $match via uv"
|
||||
uv python install "$match" || {
|
||||
msg_error "uv python install failed"
|
||||
return 1
|
||||
}
|
||||
msg_ok "Python $match installed (uv)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Java – Alpine (OpenJDK)
|
||||
# JAVA_VERSION: 17|21 (Default 21)
|
||||
# ------------------------------
|
||||
setup_java() {
|
||||
local JAVA_VERSION="${JAVA_VERSION:-21}" pkg
|
||||
case "$JAVA_VERSION" in
|
||||
17) pkg="openjdk17-jdk" ;;
|
||||
21 | *) pkg="openjdk21-jdk" ;;
|
||||
esac
|
||||
msg_info "Setup Java (OpenJDK $JAVA_VERSION)"
|
||||
apk add --no-cache "$pkg" >/dev/null 2>&1 || {
|
||||
msg_error "apk add $pkg failed"
|
||||
return 1
|
||||
}
|
||||
# set JAVA_HOME
|
||||
local prof="/etc/profile.d/20-java.sh"
|
||||
if [ ! -f "$prof" ]; then
|
||||
echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(command -v java))))' >"$prof"
|
||||
echo 'case ":$PATH:" in *:$JAVA_HOME/bin:*) ;; *) export PATH="$JAVA_HOME/bin:$PATH";; esac' >>"$prof"
|
||||
chmod +x "$prof"
|
||||
fi
|
||||
msg_ok "Java ready: $(java -version 2>&1 | head -n1)"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Go – Alpine (apk prefers, else tarball)
|
||||
# ------------------------------
|
||||
setup_go() {
|
||||
if [ -z "${GO_VERSION:-}" ]; then
|
||||
msg_info "Setup Go (apk)"
|
||||
apk add --no-cache go >/dev/null 2>&1 || {
|
||||
msg_error "apk add go failed"
|
||||
return 1
|
||||
}
|
||||
msg_ok "Go ready: $(go version 2>/dev/null)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
need_tool curl tar || return 1
|
||||
local ARCH TARBALL URL TMP
|
||||
case "$(uname -m)" in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
*)
|
||||
msg_error "Unsupported arch for Go: $(uname -m)"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
|
||||
URL="https://go.dev/dl/${TARBALL}"
|
||||
msg_info "Setup Go $GO_VERSION (tarball)"
|
||||
TMP="$(mktemp)"
|
||||
download_with_progress "$URL" "$TMP" || return 1
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf "$TMP" || {
|
||||
msg_error "extract go failed"
|
||||
rm -f "$TMP"
|
||||
return 1
|
||||
}
|
||||
rm -f "$TMP"
|
||||
ln -sf /usr/local/go/bin/go /usr/local/bin/go
|
||||
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
||||
ensure_usr_local_bin_persist
|
||||
msg_ok "Go ready: $(go version 2>/dev/null)"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Composer – Alpine
|
||||
# uses php83-cli + openssl + phar
|
||||
# ------------------------------
|
||||
setup_composer() {
|
||||
local COMPOSER_BIN="/usr/local/bin/composer"
|
||||
if ! has php; then
|
||||
# prefers php83
|
||||
msg_info "Installing PHP CLI for Composer"
|
||||
apk add --no-cache php83-cli php83-openssl php83-phar php83-iconv >/dev/null 2>&1 || {
|
||||
# Fallback to generic php if 83 not available
|
||||
apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || {
|
||||
msg_error "Failed to install php-cli for composer"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
msg_ok "PHP CLI ready: $(php -v | head -n1)"
|
||||
fi
|
||||
|
||||
if [ -x "$COMPOSER_BIN" ]; then
|
||||
msg_info "Updating Composer"
|
||||
else
|
||||
msg_info "Setup Composer"
|
||||
fi
|
||||
|
||||
need_tool curl || return 1
|
||||
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
|
||||
msg_error "composer installer download failed"
|
||||
return 1
|
||||
}
|
||||
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || {
|
||||
msg_error "composer install failed"
|
||||
return 1
|
||||
}
|
||||
rm -f /tmp/composer-setup.php
|
||||
ensure_usr_local_bin_persist
|
||||
msg_ok "Composer ready: $(composer --version 2>/dev/null)"
|
||||
}
|
||||
@@ -2441,10 +2441,15 @@ build_container() {
|
||||
if echo "$pci_vga_info" | grep -q "\[10de:"; then
|
||||
msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
|
||||
|
||||
# Simple passthrough - just bind /dev/nvidia* devices if they exist
|
||||
for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do
|
||||
[[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||||
for d in /dev/nvidia*; do
|
||||
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||||
done
|
||||
# Also check for devices inside /dev/nvidia-caps/ directory
|
||||
if [[ -d /dev/nvidia-caps ]]; then
|
||||
for d in /dev/nvidia-caps/*; do
|
||||
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
|
||||
msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough"
|
||||
@@ -2954,15 +2959,14 @@ fix_gpu_gids() {
|
||||
|
||||
# For privileged containers: also fix permissions inside container
|
||||
if [[ "$CT_TYPE" == "0" ]]; then
|
||||
pct exec "$CTID" -- bash -c "
|
||||
pct exec "$CTID" -- sh -c "
|
||||
if [ -d /dev/dri ]; then
|
||||
for dev in /dev/dri/*; do
|
||||
if [ -e \"\$dev\" ]; then
|
||||
if [[ \"\$dev\" =~ renderD ]]; then
|
||||
chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
|
||||
else
|
||||
chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
|
||||
fi
|
||||
case \"\$dev\" in
|
||||
*renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;;
|
||||
*) chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;;
|
||||
esac
|
||||
chmod 660 \"\$dev\" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
@@ -3211,42 +3215,26 @@ create_lxc_container() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate content types
|
||||
msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
|
||||
STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm|linstor): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
|
||||
msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
|
||||
msg_info "Validating storage '$CONTAINER_STORAGE'"
|
||||
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1)
|
||||
|
||||
# Check storage type for special handling
|
||||
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1)
|
||||
if [[ "$STORAGE_TYPE" == "linstor" ]]; then
|
||||
msg_info "Detected LINSTOR storage - verifying cluster connectivity"
|
||||
if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then
|
||||
msg_error "LINSTOR storage '$CONTAINER_STORAGE' not accessible. Check LINSTOR cluster health."
|
||||
exit 217
|
||||
fi
|
||||
fi
|
||||
|
||||
grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
|
||||
msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
|
||||
exit 217
|
||||
}
|
||||
$STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
|
||||
|
||||
msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
|
||||
TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
|
||||
msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
|
||||
|
||||
# Check if template storage is LINSTOR (may need special handling)
|
||||
TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1)
|
||||
if [[ "$TEMPLATE_TYPE" == "linstor" ]]; then
|
||||
msg_info "Template storage uses LINSTOR - ensuring resource availability"
|
||||
fi
|
||||
|
||||
if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
|
||||
msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
|
||||
else
|
||||
$STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
|
||||
case "$STORAGE_TYPE" in
|
||||
iscsidirect) exit 212 ;;
|
||||
iscsi | zfs) exit 213 ;;
|
||||
cephfs) exit 219 ;;
|
||||
pbs) exit 224 ;;
|
||||
linstor | rbd | nfs | cifs)
|
||||
pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null || exit 217
|
||||
;;
|
||||
esac
|
||||
|
||||
pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE" || exit 213Add a comment on line R3227Add diff commentMarkdown input: edit mode selected.WritePreviewAdd a suggestionHeadingBoldItalicQuoteCodeLinkUnordered listNumbered listTask listMentionReferenceSaved repliesAdd FilesPaste, drop, or click to add filesCancelCommentStart a reviewReturn to code
|
||||
msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated"
|
||||
|
||||
if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then
|
||||
msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'"
|
||||
fi
|
||||
msg_ok "Template storage '$TEMPLATE_STORAGE' validated"
|
||||
|
||||
# Free space check
|
||||
STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
|
||||
@@ -3261,7 +3249,7 @@ create_lxc_container() {
|
||||
msg_info "Checking cluster quorum"
|
||||
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
|
||||
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
|
||||
exit 201
|
||||
exit 210
|
||||
fi
|
||||
msg_ok "Cluster is quorate"
|
||||
fi
|
||||
@@ -3417,6 +3405,15 @@ create_lxc_container() {
|
||||
ONLINE_TEMPLATE=""
|
||||
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||||
|
||||
if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
|
||||
count=0
|
||||
for idx in "${!ONLINE_TEMPLATES[@]}"; do
|
||||
((count++))
|
||||
[[ $count -ge 3 ]] && break
|
||||
done
|
||||
ONLINE_TEMPLATE="${ONLINE_TEMPLATES[$idx]}"
|
||||
fi
|
||||
|
||||
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
|
||||
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
|
||||
TEMPLATE_SOURCE="local"
|
||||
@@ -3554,7 +3551,7 @@ create_lxc_container() {
|
||||
}
|
||||
flock -w 60 9 || {
|
||||
msg_error "Timeout while waiting for template lock."
|
||||
exit 202
|
||||
exit 211
|
||||
}
|
||||
|
||||
LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
|
||||
@@ -3604,11 +3601,11 @@ create_lxc_container() {
|
||||
2)
|
||||
echo "Upgrade was declined. Please update and re-run:
|
||||
apt update && apt install --only-upgrade pve-container lxc-pve"
|
||||
exit 213
|
||||
exit 231
|
||||
;;
|
||||
3)
|
||||
echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
|
||||
exit 213
|
||||
exit 231
|
||||
;;
|
||||
esac
|
||||
else
|
||||
|
||||
505
scripts/core/cloud-init.func
Normal file
505
scripts/core/cloud-init.func
Normal file
@@ -0,0 +1,505 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Author: community-scripts ORG
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
|
||||
# Revision: 1
|
||||
|
||||
# ==============================================================================
|
||||
# CLOUD-INIT.FUNC - VM CLOUD-INIT CONFIGURATION LIBRARY
|
||||
# ==============================================================================
|
||||
#
|
||||
# Universal helper library for Cloud-Init configuration in Proxmox VMs.
|
||||
# Provides functions for:
|
||||
#
|
||||
# - Native Proxmox Cloud-Init setup (user, password, network, SSH keys)
|
||||
# - Interactive configuration dialogs (whiptail)
|
||||
# - IP address retrieval via qemu-guest-agent
|
||||
# - Cloud-Init status monitoring and waiting
|
||||
#
|
||||
# Usage:
|
||||
# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/cloud-init.func)
|
||||
# setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
|
||||
#
|
||||
# Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions
|
||||
# ==============================================================================
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 1: CONFIGURATION DEFAULTS
|
||||
# ==============================================================================
|
||||
# These can be overridden before sourcing this library
|
||||
|
||||
CLOUDINIT_DEFAULT_USER="${CLOUDINIT_DEFAULT_USER:-root}"
|
||||
CLOUDINIT_DNS_SERVERS="${CLOUDINIT_DNS_SERVERS:-1.1.1.1 8.8.8.8}"
|
||||
CLOUDINIT_SEARCH_DOMAIN="${CLOUDINIT_SEARCH_DOMAIN:-local}"
|
||||
CLOUDINIT_SSH_KEYS="${CLOUDINIT_SSH_KEYS:-/root/.ssh/authorized_keys}"
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: HELPER FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_msg - Internal message helper with fallback
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_msg_info() { msg_info "$1" 2>/dev/null || echo "[INFO] $1"; }
|
||||
function _ci_msg_ok() { msg_ok "$1" 2>/dev/null || echo "[OK] $1"; }
|
||||
function _ci_msg_warn() { msg_warn "$1" 2>/dev/null || echo "[WARN] $1"; }
|
||||
function _ci_msg_error() { msg_error "$1" 2>/dev/null || echo "[ERROR] $1"; }
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# validate_ip_cidr - Validate IP address in CIDR format
|
||||
# Usage: validate_ip_cidr "192.168.1.100/24" && echo "Valid"
|
||||
# Returns: 0 if valid, 1 if invalid
|
||||
# ------------------------------------------------------------------------------
|
||||
function validate_ip_cidr() {
|
||||
local ip_cidr="$1"
|
||||
# Match: 0-255.0-255.0-255.0-255/0-32
|
||||
if [[ "$ip_cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
|
||||
# Validate each octet is 0-255
|
||||
local ip="${ip_cidr%/*}"
|
||||
IFS='.' read -ra octets <<<"$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
((octet > 255)) && return 1
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# validate_ip - Validate plain IP address (no CIDR)
|
||||
# Usage: validate_ip "192.168.1.1" && echo "Valid"
|
||||
# ------------------------------------------------------------------------------
|
||||
function validate_ip() {
|
||||
local ip="$1"
|
||||
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||
IFS='.' read -ra octets <<<"$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
((octet > 255)) && return 1
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: MAIN CLOUD-INIT FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# setup_cloud_init - Configures Proxmox Native Cloud-Init
|
||||
# ------------------------------------------------------------------------------
|
||||
# Parameters:
|
||||
# $1 - VMID (required)
|
||||
# $2 - Storage name (required)
|
||||
# $3 - Hostname (optional, default: vm-<vmid>)
|
||||
# $4 - Enable Cloud-Init (yes/no, default: no)
|
||||
# $5 - User (optional, default: root)
|
||||
# $6 - Network mode (dhcp/static, default: dhcp)
|
||||
# $7 - Static IP (optional, format: 192.168.1.100/24)
|
||||
# $8 - Gateway (optional)
|
||||
# $9 - Nameservers (optional, default: 1.1.1.1 8.8.8.8)
|
||||
#
|
||||
# Returns: 0 on success, 1 on failure
|
||||
# Exports: CLOUDINIT_USER, CLOUDINIT_PASSWORD, CLOUDINIT_CRED_FILE
|
||||
# ==============================================================================
|
||||
function setup_cloud_init() {
|
||||
local vmid="$1"
|
||||
local storage="$2"
|
||||
local hostname="${3:-vm-${vmid}}"
|
||||
local enable="${4:-no}"
|
||||
local ciuser="${5:-$CLOUDINIT_DEFAULT_USER}"
|
||||
local network_mode="${6:-dhcp}"
|
||||
local static_ip="${7:-}"
|
||||
local gateway="${8:-}"
|
||||
local nameservers="${9:-$CLOUDINIT_DNS_SERVERS}"
|
||||
|
||||
# Skip if not enabled
|
||||
if [ "$enable" != "yes" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate static IP if provided
|
||||
if [ "$network_mode" = "static" ]; then
|
||||
if [ -n "$static_ip" ] && ! validate_ip_cidr "$static_ip"; then
|
||||
_ci_msg_error "Invalid static IP format: $static_ip (expected: x.x.x.x/xx)"
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$gateway" ] && ! validate_ip "$gateway"; then
|
||||
_ci_msg_error "Invalid gateway IP format: $gateway"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_ci_msg_info "Configuring Cloud-Init"
|
||||
|
||||
# Create Cloud-Init drive (try ide2 first, then scsi1 as fallback)
|
||||
if ! qm set "$vmid" --ide2 "${storage}:cloudinit" >/dev/null 2>&1; then
|
||||
qm set "$vmid" --scsi1 "${storage}:cloudinit" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Set user
|
||||
qm set "$vmid" --ciuser "$ciuser" >/dev/null
|
||||
|
||||
# Generate and set secure random password
|
||||
local cipassword=$(openssl rand -base64 16)
|
||||
qm set "$vmid" --cipassword "$cipassword" >/dev/null
|
||||
|
||||
# Add SSH keys if available
|
||||
if [ -f "$CLOUDINIT_SSH_KEYS" ]; then
|
||||
qm set "$vmid" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Configure network
|
||||
if [ "$network_mode" = "static" ] && [ -n "$static_ip" ] && [ -n "$gateway" ]; then
|
||||
qm set "$vmid" --ipconfig0 "ip=${static_ip},gw=${gateway}" >/dev/null
|
||||
else
|
||||
qm set "$vmid" --ipconfig0 "ip=dhcp" >/dev/null
|
||||
fi
|
||||
|
||||
# Set DNS servers
|
||||
qm set "$vmid" --nameserver "$nameservers" >/dev/null
|
||||
|
||||
# Set search domain
|
||||
qm set "$vmid" --searchdomain "$CLOUDINIT_SEARCH_DOMAIN" >/dev/null
|
||||
|
||||
# Enable package upgrades on first boot (if supported by Proxmox version)
|
||||
qm set "$vmid" --ciupgrade 1 >/dev/null 2>&1 || true
|
||||
|
||||
# Save credentials to file (with restrictive permissions)
|
||||
local cred_file="/tmp/${hostname}-${vmid}-cloud-init-credentials.txt"
|
||||
umask 077
|
||||
cat >"$cred_file" <<EOF
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ ⚠️ SECURITY WARNING: DELETE THIS FILE AFTER NOTING CREDENTIALS ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Cloud-Init Credentials
|
||||
────────────────────────────────────────
|
||||
VM ID: ${vmid}
|
||||
Hostname: ${hostname}
|
||||
Created: $(date)
|
||||
|
||||
Username: ${ciuser}
|
||||
Password: ${cipassword}
|
||||
|
||||
Network: ${network_mode}$([ "$network_mode" = "static" ] && echo " (IP: ${static_ip}, GW: ${gateway})" || echo " (DHCP)")
|
||||
DNS: ${nameservers}
|
||||
|
||||
────────────────────────────────────────
|
||||
SSH Access (if keys configured):
|
||||
ssh ${ciuser}@<vm-ip>
|
||||
|
||||
Proxmox UI Configuration:
|
||||
VM ${vmid} > Cloud-Init > Edit
|
||||
- User, Password, SSH Keys
|
||||
- Network (IP Config)
|
||||
- DNS, Search Domain
|
||||
|
||||
────────────────────────────────────────
|
||||
🗑️ To delete this file:
|
||||
rm -f ${cred_file}
|
||||
────────────────────────────────────────
|
||||
EOF
|
||||
chmod 600 "$cred_file"
|
||||
|
||||
_ci_msg_ok "Cloud-Init configured (User: ${ciuser})"
|
||||
|
||||
# Export for use in calling script (DO NOT display password here - will be shown in summary)
|
||||
export CLOUDINIT_USER="$ciuser"
|
||||
export CLOUDINIT_PASSWORD="$cipassword"
|
||||
export CLOUDINIT_CRED_FILE="$cred_file"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 4: INTERACTIVE CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# configure_cloud_init_interactive - Whiptail dialog for Cloud-Init setup
|
||||
# ------------------------------------------------------------------------------
|
||||
# Prompts user for Cloud-Init configuration choices
|
||||
# Returns configuration via exported variables:
|
||||
# - CLOUDINIT_ENABLE (yes/no)
|
||||
# - CLOUDINIT_USER
|
||||
# - CLOUDINIT_NETWORK_MODE (dhcp/static)
|
||||
# - CLOUDINIT_IP (if static)
|
||||
# - CLOUDINIT_GW (if static)
|
||||
# - CLOUDINIT_DNS
|
||||
# ------------------------------------------------------------------------------
|
||||
function configure_cloud_init_interactive() {
|
||||
local default_user="${1:-root}"
|
||||
|
||||
# Check if whiptail is available
|
||||
if ! command -v whiptail >/dev/null 2>&1; then
|
||||
echo "Warning: whiptail not available, skipping interactive configuration"
|
||||
export CLOUDINIT_ENABLE="no"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Ask if user wants to enable Cloud-Init
|
||||
if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
|
||||
--yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI." 16 68); then
|
||||
export CLOUDINIT_ENABLE="no"
|
||||
return 0
|
||||
fi
|
||||
|
||||
export CLOUDINIT_ENABLE="yes"
|
||||
|
||||
# Username
|
||||
if CLOUDINIT_USER=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"Cloud-Init Username" 8 58 "$default_user" --title "USERNAME" 3>&1 1>&2 2>&3); then
|
||||
export CLOUDINIT_USER="${CLOUDINIT_USER:-$default_user}"
|
||||
else
|
||||
export CLOUDINIT_USER="$default_user"
|
||||
fi
|
||||
|
||||
# Network configuration
|
||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "NETWORK MODE" \
|
||||
--yesno "Use DHCP for network configuration?\n\nSelect 'No' for static IP configuration." 10 58); then
|
||||
export CLOUDINIT_NETWORK_MODE="dhcp"
|
||||
else
|
||||
export CLOUDINIT_NETWORK_MODE="static"
|
||||
|
||||
# Static IP with validation
|
||||
while true; do
|
||||
if CLOUDINIT_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"Static IP Address (CIDR format)\nExample: 192.168.1.100/24" 9 58 "" --title "IP ADDRESS" 3>&1 1>&2 2>&3); then
|
||||
if validate_ip_cidr "$CLOUDINIT_IP"; then
|
||||
export CLOUDINIT_IP
|
||||
break
|
||||
else
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID IP" \
|
||||
--msgbox "Invalid IP format: $CLOUDINIT_IP\n\nPlease use CIDR format: x.x.x.x/xx\nExample: 192.168.1.100/24" 10 50
|
||||
fi
|
||||
else
|
||||
_ci_msg_warn "Static IP required, falling back to DHCP"
|
||||
export CLOUDINIT_NETWORK_MODE="dhcp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Gateway with validation
|
||||
if [ "$CLOUDINIT_NETWORK_MODE" = "static" ]; then
|
||||
while true; do
|
||||
if CLOUDINIT_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"Gateway IP Address\nExample: 192.168.1.1" 8 58 "" --title "GATEWAY" 3>&1 1>&2 2>&3); then
|
||||
if validate_ip "$CLOUDINIT_GW"; then
|
||||
export CLOUDINIT_GW
|
||||
break
|
||||
else
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID GATEWAY" \
|
||||
--msgbox "Invalid gateway format: $CLOUDINIT_GW\n\nPlease use format: x.x.x.x\nExample: 192.168.1.1" 10 50
|
||||
fi
|
||||
else
|
||||
_ci_msg_warn "Gateway required, falling back to DHCP"
|
||||
export CLOUDINIT_NETWORK_MODE="dhcp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# DNS Servers
|
||||
if CLOUDINIT_DNS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"DNS Servers (space-separated)" 8 58 "1.1.1.1 8.8.8.8" --title "DNS SERVERS" 3>&1 1>&2 2>&3); then
|
||||
export CLOUDINIT_DNS="${CLOUDINIT_DNS:-1.1.1.1 8.8.8.8}"
|
||||
else
|
||||
export CLOUDINIT_DNS="1.1.1.1 8.8.8.8"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 5: UTILITY FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# display_cloud_init_info - Show Cloud-Init summary after setup
|
||||
# ------------------------------------------------------------------------------
|
||||
function display_cloud_init_info() {
|
||||
local vmid="$1"
|
||||
local hostname="${2:-}"
|
||||
|
||||
if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
|
||||
if [ -n "${INFO:-}" ]; then
|
||||
echo -e "\n${INFO}${BOLD:-}${GN:-} Cloud-Init Configuration:${CL:-}"
|
||||
echo -e "${TAB:- }${DGN:-}User: ${BGN:-}${CLOUDINIT_USER:-root}${CL:-}"
|
||||
echo -e "${TAB:- }${DGN:-}Password: ${BGN:-}${CLOUDINIT_PASSWORD}${CL:-}"
|
||||
echo -e "${TAB:- }${DGN:-}Credentials: ${BL:-}${CLOUDINIT_CRED_FILE}${CL:-}"
|
||||
echo -e "${TAB:- }${RD:-}⚠️ Delete credentials file after noting password!${CL:-}"
|
||||
echo -e "${TAB:- }${YW:-}💡 Configure in Proxmox UI: VM ${vmid} > Cloud-Init${CL:-}"
|
||||
else
|
||||
echo ""
|
||||
echo "[INFO] Cloud-Init Configuration:"
|
||||
echo " User: ${CLOUDINIT_USER:-root}"
|
||||
echo " Password: ${CLOUDINIT_PASSWORD}"
|
||||
echo " Credentials: ${CLOUDINIT_CRED_FILE}"
|
||||
echo " ⚠️ Delete credentials file after noting password!"
|
||||
echo " Configure in Proxmox UI: VM ${vmid} > Cloud-Init"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# cleanup_cloud_init_credentials - Remove credentials file
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usage: cleanup_cloud_init_credentials
|
||||
# Call this after user has noted/saved the credentials
|
||||
# ------------------------------------------------------------------------------
|
||||
function cleanup_cloud_init_credentials() {
|
||||
if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
|
||||
rm -f "$CLOUDINIT_CRED_FILE"
|
||||
_ci_msg_ok "Credentials file removed: $CLOUDINIT_CRED_FILE"
|
||||
unset CLOUDINIT_CRED_FILE
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# has_cloud_init - Check if VM has Cloud-Init configured
|
||||
# ------------------------------------------------------------------------------
|
||||
function has_cloud_init() {
|
||||
local vmid="$1"
|
||||
qm config "$vmid" 2>/dev/null | grep -qE "(ide2|scsi1):.*cloudinit"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# regenerate_cloud_init - Regenerate Cloud-Init configuration
|
||||
# ------------------------------------------------------------------------------
|
||||
function regenerate_cloud_init() {
|
||||
local vmid="$1"
|
||||
|
||||
if has_cloud_init "$vmid"; then
|
||||
_ci_msg_info "Regenerating Cloud-Init configuration"
|
||||
qm cloudinit update "$vmid" >/dev/null 2>&1 || true
|
||||
_ci_msg_ok "Cloud-Init configuration regenerated"
|
||||
return 0
|
||||
else
|
||||
_ci_msg_warn "VM $vmid does not have Cloud-Init configured"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# get_vm_ip - Get VM IP address via qemu-guest-agent
|
||||
# ------------------------------------------------------------------------------
|
||||
function get_vm_ip() {
|
||||
local vmid="$1"
|
||||
local timeout="${2:-30}"
|
||||
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
local vm_ip=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null |
|
||||
jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$vm_ip" ]; then
|
||||
echo "$vm_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# wait_for_cloud_init - Wait for Cloud-Init to complete (requires SSH access)
|
||||
# ------------------------------------------------------------------------------
|
||||
function wait_for_cloud_init() {
|
||||
local vmid="$1"
|
||||
local timeout="${2:-300}"
|
||||
local vm_ip="${3:-}"
|
||||
|
||||
# Get IP if not provided
|
||||
if [ -z "$vm_ip" ]; then
|
||||
vm_ip=$(get_vm_ip "$vmid" 60)
|
||||
fi
|
||||
|
||||
if [ -z "$vm_ip" ]; then
|
||||
_ci_msg_warn "Unable to determine VM IP address"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_ci_msg_info "Waiting for Cloud-Init to complete on ${vm_ip}"
|
||||
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if timeout 10 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"${CLOUDINIT_USER:-root}@${vm_ip}" "cloud-init status --wait" 2>/dev/null; then
|
||||
_ci_msg_ok "Cloud-Init completed successfully"
|
||||
return 0
|
||||
fi
|
||||
sleep 10
|
||||
elapsed=$((elapsed + 10))
|
||||
done
|
||||
|
||||
_ci_msg_warn "Cloud-Init did not complete within ${timeout}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 6: EXPORTS
|
||||
# ==============================================================================
|
||||
# Export all functions for use in other scripts
|
||||
|
||||
export -f setup_cloud_init 2>/dev/null || true
|
||||
export -f configure_cloud_init_interactive 2>/dev/null || true
|
||||
export -f display_cloud_init_info 2>/dev/null || true
|
||||
export -f cleanup_cloud_init_credentials 2>/dev/null || true
|
||||
export -f has_cloud_init 2>/dev/null || true
|
||||
export -f regenerate_cloud_init 2>/dev/null || true
|
||||
export -f get_vm_ip 2>/dev/null || true
|
||||
export -f wait_for_cloud_init 2>/dev/null || true
|
||||
export -f validate_ip_cidr 2>/dev/null || true
|
||||
export -f validate_ip 2>/dev/null || true
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 7: EXAMPLES & DOCUMENTATION
|
||||
# ==============================================================================
|
||||
: <<'EXAMPLES'
|
||||
|
||||
# Example 1: Simple DHCP setup (most common)
|
||||
setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
|
||||
|
||||
# Example 2: Static IP setup
|
||||
setup_cloud_init "$VMID" "$STORAGE" "myserver" "yes" "root" "static" "192.168.1.100/24" "192.168.1.1"
|
||||
|
||||
# Example 3: Interactive configuration in advanced_settings()
|
||||
configure_cloud_init_interactive "admin"
|
||||
if [ "$CLOUDINIT_ENABLE" = "yes" ]; then
|
||||
setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" "$CLOUDINIT_USER" \
|
||||
"$CLOUDINIT_NETWORK_MODE" "$CLOUDINIT_IP" "$CLOUDINIT_GW" "$CLOUDINIT_DNS"
|
||||
fi
|
||||
|
||||
# Example 4: Display info after VM creation
|
||||
display_cloud_init_info "$VMID" "$HN"
|
||||
|
||||
# Example 5: Check if VM has Cloud-Init
|
||||
if has_cloud_init "$VMID"; then
|
||||
echo "Cloud-Init is configured"
|
||||
fi
|
||||
|
||||
# Example 6: Wait for Cloud-Init to complete after VM start
|
||||
if [ "$START_VM" = "yes" ]; then
|
||||
qm start "$VMID"
|
||||
sleep 30
|
||||
wait_for_cloud_init "$VMID" 300
|
||||
fi
|
||||
|
||||
# Example 7: Cleanup credentials file after user has noted password
|
||||
display_cloud_init_info "$VMID" "$HN"
|
||||
read -p "Have you saved the credentials? (y/N): " -r
|
||||
[[ $REPLY =~ ^[Yy]$ ]] && cleanup_cloud_init_credentials
|
||||
|
||||
# Example 8: Validate IP before using
|
||||
if validate_ip_cidr "192.168.1.100/24"; then
|
||||
echo "Valid IP/CIDR"
|
||||
fi
|
||||
|
||||
EXAMPLES
|
||||
322
scripts/core/error-handler.func
Normal file
322
scripts/core/error-handler.func
Normal file
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# Provides comprehensive error handling and signal management for all scripts.
|
||||
# Includes:
|
||||
# - Exit code explanations (shell, package managers, databases, custom codes)
|
||||
# - Error handler with detailed logging
|
||||
# - Signal handlers (EXIT, INT, TERM)
|
||||
# - Initialization function for trap setup
|
||||
#
|
||||
# Usage:
|
||||
# source <(curl -fsSL .../error_handler.func)
|
||||
# catch_errors
|
||||
#
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 1: EXIT CODE EXPLANATIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# explain_exit_code()
|
||||
#
|
||||
# - Maps numeric exit codes to human-readable error descriptions
|
||||
# - Supports:
|
||||
# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
|
||||
# * Package manager errors (APT, DPKG: 100, 101, 255)
|
||||
# * Node.js/npm errors (243-249, 254)
|
||||
# * Python/pip/uv errors (210-212)
|
||||
# * PostgreSQL errors (231-234)
|
||||
# * MySQL/MariaDB errors (260-263)
|
||||
# * MongoDB errors (251-253)
|
||||
# * Proxmox custom codes (200-209, 213-223, 225)
|
||||
# - Returns description string for given exit code
|
||||
# ------------------------------------------------------------------------------
|
||||
explain_exit_code() {
|
||||
local code="$1"
|
||||
case "$code" in
|
||||
# --- Generic / Shell ---
|
||||
1) echo "General error / Operation not permitted" ;;
|
||||
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
|
||||
126) echo "Command invoked cannot execute (permission problem?)" ;;
|
||||
127) echo "Command not found" ;;
|
||||
128) echo "Invalid argument to exit" ;;
|
||||
130) echo "Terminated by Ctrl+C (SIGINT)" ;;
|
||||
137) echo "Killed (SIGKILL / Out of memory?)" ;;
|
||||
139) echo "Segmentation fault (core dumped)" ;;
|
||||
143) echo "Terminated (SIGTERM)" ;;
|
||||
|
||||
# --- Package manager / APT / DPKG ---
|
||||
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
|
||||
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
|
||||
255) echo "DPKG: Fatal internal error" ;;
|
||||
|
||||
# --- Node.js / npm / pnpm / yarn ---
|
||||
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
|
||||
245) echo "Node.js: Invalid command-line option" ;;
|
||||
246) echo "Node.js: Internal JavaScript Parse Error" ;;
|
||||
247) echo "Node.js: Fatal internal error" ;;
|
||||
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||
249) echo "Node.js: Inspector error" ;;
|
||||
254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||
|
||||
# --- Python / pip / uv ---
|
||||
210) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
||||
211) echo "Python: Dependency resolution failed" ;;
|
||||
212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
||||
|
||||
# --- PostgreSQL ---
|
||||
231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
|
||||
232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
|
||||
233) echo "PostgreSQL: Database does not exist" ;;
|
||||
234) echo "PostgreSQL: Fatal error in query / syntax" ;;
|
||||
|
||||
# --- MySQL / MariaDB ---
|
||||
241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
|
||||
242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
|
||||
243) echo "MySQL/MariaDB: Database does not exist" ;;
|
||||
244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
|
||||
|
||||
# --- MongoDB ---
|
||||
251) echo "MongoDB: Connection failed (server not running)" ;;
|
||||
252) echo "MongoDB: Authentication failed (bad user/password)" ;;
|
||||
253) echo "MongoDB: Database not found" ;;
|
||||
254) echo "MongoDB: Fatal query error" ;;
|
||||
|
||||
# --- Proxmox Custom Codes ---
|
||||
200) echo "Proxmox: Failed to create lock file" ;;
|
||||
203) echo "Proxmox: Missing CTID variable" ;;
|
||||
204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
|
||||
205) echo "Proxmox: Invalid CTID (<100)" ;;
|
||||
206) echo "Proxmox: CTID already in use" ;;
|
||||
207) echo "Proxmox: Password contains unescaped special characters" ;;
|
||||
208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;;
|
||||
209) echo "Proxmox: Container creation failed" ;;
|
||||
210) echo "Proxmox: Cluster not quorate" ;;
|
||||
211) echo "Proxmox: Timeout waiting for template lock" ;;
|
||||
212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;;
|
||||
213) echo "Proxmox: Storage type does not support 'rootdir' content" ;;
|
||||
214) echo "Proxmox: Not enough storage space" ;;
|
||||
215) echo "Proxmox: Container created but not listed (ghost state)" ;;
|
||||
216) echo "Proxmox: RootFS entry missing in config" ;;
|
||||
217) echo "Proxmox: Storage not accessible" ;;
|
||||
219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
|
||||
224) echo "Proxmox: PBS storage is for backups only" ;;
|
||||
218) echo "Proxmox: Template file corrupted or incomplete" ;;
|
||||
220) echo "Proxmox: Unable to resolve template path" ;;
|
||||
221) echo "Proxmox: Template file not readable" ;;
|
||||
222) echo "Proxmox: Template download failed" ;;
|
||||
223) echo "Proxmox: Template not available after download" ;;
|
||||
225) echo "Proxmox: No template available for OS/Version" ;;
|
||||
231) echo "Proxmox: LXC stack upgrade failed" ;;
|
||||
|
||||
# --- Default ---
|
||||
*) echo "Unknown error" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: ERROR HANDLERS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# error_handler()
|
||||
#
|
||||
# - Main error handler triggered by ERR trap
|
||||
# - Arguments: exit_code, command, line_number
|
||||
# - Behavior:
|
||||
# * Returns silently if exit_code is 0 (success)
|
||||
# * Sources explain_exit_code() for detailed error description
|
||||
# * Displays error message with:
|
||||
# - Line number where error occurred
|
||||
# - Exit code with explanation
|
||||
# - Command that failed
|
||||
# * Shows last 20 lines of SILENT_LOGFILE if available
|
||||
# * Copies log to container /root for later inspection
|
||||
# * Exits with original exit code
|
||||
# ------------------------------------------------------------------------------
|
||||
error_handler() {
|
||||
local exit_code=${1:-$?}
|
||||
local command=${2:-${BASH_COMMAND:-unknown}}
|
||||
local line_number=${BASH_LINENO[0]:-unknown}
|
||||
|
||||
command="${command//\$STD/}"
|
||||
|
||||
if [[ "$exit_code" -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local explanation
|
||||
explanation="$(explain_exit_code "$exit_code")"
|
||||
|
||||
printf "\e[?25h"
|
||||
|
||||
# Use msg_error if available, fallback to echo
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "in line ${line_number}: exit code ${exit_code} (${explanation}): while executing command ${command}"
|
||||
else
|
||||
echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL} (${explanation}): while executing command ${YWB}${command}${CL}\n"
|
||||
fi
|
||||
|
||||
if [[ -n "${DEBUG_LOGFILE:-}" ]]; then
|
||||
{
|
||||
echo "------ ERROR ------"
|
||||
echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "Exit Code : $exit_code ($explanation)"
|
||||
echo "Line : $line_number"
|
||||
echo "Command : $command"
|
||||
echo "-------------------"
|
||||
} >>"$DEBUG_LOGFILE"
|
||||
fi
|
||||
|
||||
# Get active log file (BUILD_LOG or INSTALL_LOG)
|
||||
local active_log=""
|
||||
if declare -f get_active_logfile >/dev/null 2>&1; then
|
||||
active_log="$(get_active_logfile)"
|
||||
elif [[ -n "${SILENT_LOGFILE:-}" ]]; then
|
||||
active_log="$SILENT_LOGFILE"
|
||||
fi
|
||||
|
||||
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
echo "--- Last 20 lines of silent log ---"
|
||||
tail -n 20 "$active_log"
|
||||
echo "-----------------------------------"
|
||||
|
||||
# Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG)
|
||||
if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then
|
||||
# CONTAINER CONTEXT: Copy log and create flag file for host
|
||||
local container_log="/root/.install-${SESSION_ID:-error}.log"
|
||||
cp "$active_log" "$container_log" 2>/dev/null || true
|
||||
|
||||
# Create error flag file with exit code for host detection
|
||||
echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true
|
||||
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
msg_custom "📋" "${YW}" "Log saved to: ${container_log}"
|
||||
else
|
||||
echo -e "${YW}Log saved to:${CL} ${BL}${container_log}${CL}"
|
||||
fi
|
||||
else
|
||||
# HOST CONTEXT: Show local log path and offer container cleanup
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
msg_custom "📋" "${YW}" "Full log: ${active_log}"
|
||||
else
|
||||
echo -e "${YW}Full log:${CL} ${BL}${active_log}${CL}"
|
||||
fi
|
||||
|
||||
# Offer to remove container if it exists (build errors after container creation)
|
||||
if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
|
||||
echo ""
|
||||
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||||
|
||||
if read -t 60 -r response; then
|
||||
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
|
||||
echo -e "\n${YW}Removing container ${CTID}${CL}"
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
elif [[ "$response" =~ ^[Nn]$ ]]; then
|
||||
echo -e "\n${YW}Container ${CTID} kept for debugging${CL}"
|
||||
fi
|
||||
else
|
||||
# Timeout - auto-remove
|
||||
echo -e "\n${YW}No response - auto-removing container${CL}"
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: SIGNAL HANDLERS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_exit()
|
||||
#
|
||||
# - EXIT trap handler
|
||||
# - Cleans up lock files if lockfile variable is set
|
||||
# - Exits with captured exit code
|
||||
# - Always runs on script termination (success or failure)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_interrupt()
|
||||
#
|
||||
# - SIGINT (Ctrl+C) trap handler
|
||||
# - Displays "Interrupted by user" message
|
||||
# - Exits with code 130 (128 + SIGINT=2)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_interrupt() {
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Interrupted by user (SIGINT)"
|
||||
else
|
||||
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
|
||||
fi
|
||||
exit 130
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_terminate()
|
||||
#
|
||||
# - SIGTERM trap handler
|
||||
# - Displays "Terminated by signal" message
|
||||
# - Exits with code 143 (128 + SIGTERM=15)
|
||||
# - Triggered by external process termination
|
||||
# ------------------------------------------------------------------------------
|
||||
on_terminate() {
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Terminated by signal (SIGTERM)"
|
||||
else
|
||||
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
|
||||
fi
|
||||
exit 143
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 4: INITIALIZATION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# catch_errors()
|
||||
#
|
||||
# - Initializes error handling and signal traps
|
||||
# - Enables strict error handling:
|
||||
# * set -Ee: Exit on error, inherit ERR trap in functions
|
||||
# * set -o pipefail: Pipeline fails if any command fails
|
||||
# * set -u: (optional) Exit on undefined variable (if STRICT_UNSET=1)
|
||||
# - Sets up traps:
|
||||
# * ERR → error_handler
|
||||
# * EXIT → on_exit
|
||||
# * INT → on_interrupt
|
||||
# * TERM → on_terminate
|
||||
# - Call this function early in every script
|
||||
# ------------------------------------------------------------------------------
|
||||
catch_errors() {
|
||||
set -Ee -o pipefail
|
||||
if [ "${STRICT_UNSET:-0}" = "1" ]; then
|
||||
set -u
|
||||
fi
|
||||
|
||||
trap 'error_handler' ERR
|
||||
trap on_exit EXIT
|
||||
trap on_interrupt INT
|
||||
trap on_terminate TERM
|
||||
}
|
||||
@@ -192,6 +192,8 @@ install_packages_with_retry() {
|
||||
if [[ $retry -le $max_retries ]]; then
|
||||
msg_warn "Package installation failed, retrying ($retry/$max_retries)..."
|
||||
sleep 2
|
||||
# Fix any interrupted dpkg operations before retry
|
||||
$STD dpkg --configure -a 2>/dev/null || true
|
||||
$STD apt update 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
@@ -217,6 +219,8 @@ upgrade_packages_with_retry() {
|
||||
if [[ $retry -le $max_retries ]]; then
|
||||
msg_warn "Package upgrade failed, retrying ($retry/$max_retries)..."
|
||||
sleep 2
|
||||
# Fix any interrupted dpkg operations before retry
|
||||
$STD dpkg --configure -a 2>/dev/null || true
|
||||
$STD apt update 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
@@ -1182,6 +1186,11 @@ cleanup_orphaned_sources() {
|
||||
# This should be called at the start of any setup function
|
||||
# ------------------------------------------------------------------------------
|
||||
ensure_apt_working() {
|
||||
# Fix interrupted dpkg operations first
|
||||
# This can happen if a previous installation was interrupted (e.g., by script error)
|
||||
if [[ -f /var/lib/dpkg/lock-frontend ]] || dpkg --audit 2>&1 | grep -q "interrupted"; then
|
||||
$STD dpkg --configure -a 2>/dev/null || true
|
||||
fi
|
||||
# Clean up orphaned sources first
|
||||
cleanup_orphaned_sources
|
||||
|
||||
@@ -2949,7 +2958,7 @@ setup_mariadb() {
|
||||
if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then
|
||||
msg_ok "MariaDB repository configured via mariadb_repo_setup"
|
||||
# Extract version from configured repo
|
||||
MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2")
|
||||
MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2")Expand commentComment on line R2948ResolvedCode has comments. Press enter to view.
|
||||
else
|
||||
msg_warn "mariadb_repo_setup failed - using hardcoded fallback version"
|
||||
MARIADB_VERSION="12.2"
|
||||
|
||||
44
scripts/ct/debian.sh
Normal file
44
scripts/ct/debian.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
source "$SCRIPT_DIR/../core/build.func"
|
||||
# Copyright (c) 2021-2025 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.debian.org/
|
||||
|
||||
APP="Debian"
|
||||
var_tags="${var_tags:-os}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /var ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
msg_info "Updating $APP LXC"
|
||||
$STD apt update
|
||||
$STD apt -y upgrade
|
||||
msg_ok "Updated $APP LXC"
|
||||
msg_ok "Updated successfully!"
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
18
scripts/install/debian-install.sh
Normal file
18
scripts/install/debian-install.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2025 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.debian.org/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
43
server.js
43
server.js
@@ -82,6 +82,7 @@ const handle = app.getRequestHandler();
|
||||
* @property {number} [cloneCount]
|
||||
* @property {string[]} [hostnames]
|
||||
* @property {'lxc'|'vm'} [containerType]
|
||||
* @property {Record<string, string|number|boolean>} [envVars]
|
||||
*/
|
||||
|
||||
class ScriptExecutionHandler {
|
||||
@@ -299,7 +300,7 @@ class ScriptExecutionHandler {
|
||||
* @param {WebSocketMessage} message
|
||||
*/
|
||||
async handleMessage(ws, message) {
|
||||
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, isBackup, isClone, containerId, storage, backupStorage, cloneCount, hostnames, containerType } = message;
|
||||
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, isBackup, isClone, containerId, storage, backupStorage, cloneCount, hostnames, containerType, envVars } = message;
|
||||
|
||||
switch (action) {
|
||||
case 'start':
|
||||
@@ -313,7 +314,7 @@ class ScriptExecutionHandler {
|
||||
} else if (isShell && containerId) {
|
||||
await this.startShellExecution(ws, containerId, executionId, mode, server);
|
||||
} else {
|
||||
await this.startScriptExecution(ws, scriptPath, executionId, mode, server);
|
||||
await this.startScriptExecution(ws, scriptPath, executionId, mode, server, envVars);
|
||||
}
|
||||
} else {
|
||||
this.sendMessage(ws, {
|
||||
@@ -351,8 +352,9 @@ class ScriptExecutionHandler {
|
||||
* @param {string} executionId
|
||||
* @param {string} mode
|
||||
* @param {ServerInfo|null} server
|
||||
* @param {Object} [envVars] - Optional environment variables to pass to the script
|
||||
*/
|
||||
async startScriptExecution(ws, scriptPath, executionId, mode = 'local', server = null) {
|
||||
async startScriptExecution(ws, scriptPath, executionId, mode = 'local', server = null, envVars = {}) {
|
||||
/** @type {number|null} */
|
||||
let installationId = null;
|
||||
|
||||
@@ -381,7 +383,7 @@ class ScriptExecutionHandler {
|
||||
|
||||
// Handle SSH execution
|
||||
if (mode === 'ssh' && server) {
|
||||
await this.startSSHScriptExecution(ws, scriptPath, executionId, server, installationId);
|
||||
await this.startSSHScriptExecution(ws, scriptPath, executionId, server, installationId, envVars);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -407,19 +409,32 @@ class ScriptExecutionHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format environment variables for local execution
|
||||
// Convert envVars object to environment variables
|
||||
const envWithVars = {
|
||||
...process.env,
|
||||
TERM: 'xterm-256color', // Enable proper terminal support
|
||||
FORCE_ANSI: 'true', // Allow ANSI codes for proper display
|
||||
COLUMNS: '80', // Set terminal width
|
||||
LINES: '24' // Set terminal height
|
||||
};
|
||||
|
||||
// Add envVars to environment
|
||||
if (envVars && typeof envVars === 'object') {
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
/** @type {Record<string, string>} */
|
||||
const envRecord = envWithVars;
|
||||
envRecord[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Start script execution with pty for proper TTY support
|
||||
const childProcess = ptySpawn('bash', [resolvedPath], {
|
||||
cwd: scriptsDir,
|
||||
name: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: 'xterm-256color', // Enable proper terminal support
|
||||
FORCE_ANSI: 'true', // Allow ANSI codes for proper display
|
||||
COLUMNS: '80', // Set terminal width
|
||||
LINES: '24' // Set terminal height
|
||||
}
|
||||
env: envWithVars
|
||||
});
|
||||
|
||||
// pty handles encoding automatically
|
||||
@@ -522,8 +537,9 @@ class ScriptExecutionHandler {
|
||||
* @param {string} executionId
|
||||
* @param {ServerInfo} server
|
||||
* @param {number|null} installationId
|
||||
* @param {Object} [envVars] - Optional environment variables to pass to the script
|
||||
*/
|
||||
async startSSHScriptExecution(ws, scriptPath, executionId, server, installationId = null) {
|
||||
async startSSHScriptExecution(ws, scriptPath, executionId, server, installationId = null, envVars = {}) {
|
||||
const sshService = getSSHExecutionService();
|
||||
|
||||
// Send start message
|
||||
@@ -612,7 +628,8 @@ class ScriptExecutionHandler {
|
||||
|
||||
// Clean up
|
||||
this.activeExecutions.delete(executionId);
|
||||
}
|
||||
},
|
||||
envVars
|
||||
));
|
||||
|
||||
// Store the execution with installation ID
|
||||
|
||||
899
src/app/_components/ConfigurationModal.tsx
Normal file
899
src/app/_components/ConfigurationModal.tsx
Normal file
@@ -0,0 +1,899 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
import type { Script } from '~/types/script';
|
||||
import type { Server } from '~/types/server';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
export type EnvVars = Record<string, string | number | boolean>;
|
||||
|
||||
interface ConfigurationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (envVars: EnvVars) => void;
|
||||
script: Script | null;
|
||||
server: Server | null;
|
||||
mode: 'default' | 'advanced';
|
||||
}
|
||||
|
||||
export function ConfigurationModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
script,
|
||||
server,
|
||||
mode,
|
||||
}: ConfigurationModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'configuration-modal', allowEscape: true, onClose });
|
||||
|
||||
// Fetch script data if we only have slug
|
||||
const { data: scriptData } = api.scripts.getScriptBySlug.useQuery(
|
||||
{ slug: script?.slug ?? '' },
|
||||
{ enabled: !!script?.slug && isOpen }
|
||||
);
|
||||
|
||||
const actualScript = script ?? (scriptData?.script ?? null);
|
||||
|
||||
// Fetch storages
|
||||
const { data: rootfsStoragesData } = api.scripts.getRootfsStorages.useQuery(
|
||||
{ serverId: server?.id ?? 0, forceRefresh: false },
|
||||
{ enabled: !!server?.id && isOpen }
|
||||
);
|
||||
|
||||
const { data: templateStoragesData } = api.scripts.getTemplateStorages.useQuery(
|
||||
{ serverId: server?.id ?? 0, forceRefresh: false },
|
||||
{ enabled: !!server?.id && isOpen && mode === 'advanced' }
|
||||
);
|
||||
|
||||
// Get resources from JSON
|
||||
const resources = actualScript?.install_methods?.[0]?.resources;
|
||||
const slug = actualScript?.slug ?? '';
|
||||
|
||||
// Default mode state
|
||||
const [containerStorage, setContainerStorage] = useState<string>('');
|
||||
|
||||
// Advanced mode state
|
||||
const [advancedVars, setAdvancedVars] = useState<EnvVars>({});
|
||||
|
||||
// Validation errors
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Initialize defaults when script/server data is available
|
||||
useEffect(() => {
|
||||
if (!actualScript || !server) return;
|
||||
|
||||
if (mode === 'default') {
|
||||
// Default mode: minimal vars
|
||||
setContainerStorage('');
|
||||
} else {
|
||||
// Advanced mode: all vars with defaults
|
||||
const defaults: EnvVars = {
|
||||
// Resources from JSON
|
||||
var_cpu: resources?.cpu ?? 1,
|
||||
var_ram: resources?.ram ?? 1024,
|
||||
var_disk: resources?.hdd ?? 4,
|
||||
var_unprivileged: script?.privileged === false ? 1 : (script?.privileged === true ? 0 : 1),
|
||||
|
||||
// Network defaults
|
||||
var_net: 'dhcp',
|
||||
var_brg: 'vmbr0',
|
||||
var_gateway: '',
|
||||
var_ipv6_method: 'none',
|
||||
var_ipv6_static: '',
|
||||
var_vlan: '',
|
||||
var_mtu: 1500,
|
||||
var_mac: '',
|
||||
var_ns: '',
|
||||
|
||||
// Identity
|
||||
var_hostname: slug,
|
||||
var_pw: '',
|
||||
var_tags: 'community-script',
|
||||
|
||||
// SSH
|
||||
var_ssh: 'no',
|
||||
var_ssh_authorized_key: '',
|
||||
|
||||
// Features
|
||||
var_nesting: 1,
|
||||
var_fuse: 0,
|
||||
var_keyctl: 0,
|
||||
var_mknod: 0,
|
||||
var_mount_fs: '',
|
||||
var_protection: 'no',
|
||||
|
||||
// System
|
||||
var_timezone: '',
|
||||
var_verbose: 'no',
|
||||
var_apt_cacher: 'no',
|
||||
var_apt_cacher_ip: '',
|
||||
|
||||
// Storage
|
||||
var_container_storage: '',
|
||||
var_template_storage: '',
|
||||
};
|
||||
setAdvancedVars(defaults);
|
||||
}
|
||||
}, [actualScript, server, mode, resources, slug]);
|
||||
|
||||
// Validation functions
|
||||
const validateIPv4 = (ip: string): boolean => {
|
||||
if (!ip) return true; // Empty is allowed (auto)
|
||||
const pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (!pattern.test(ip)) return false;
|
||||
const parts = ip.split('.').map(Number);
|
||||
return parts.every(p => p >= 0 && p <= 255);
|
||||
};
|
||||
|
||||
const validateCIDR = (cidr: string): boolean => {
|
||||
if (!cidr) return true; // Empty is allowed
|
||||
const pattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
||||
if (!pattern.test(cidr)) return false;
|
||||
const parts = cidr.split('/');
|
||||
if (parts.length !== 2) return false;
|
||||
const [ip, prefix] = parts;
|
||||
if (!ip || !prefix) return false;
|
||||
const ipParts = ip.split('.').map(Number);
|
||||
if (!ipParts.every(p => p >= 0 && p <= 255)) return false;
|
||||
const prefixNum = parseInt(prefix, 10);
|
||||
return prefixNum >= 0 && prefixNum <= 32;
|
||||
};
|
||||
|
||||
const validateIPv6 = (ipv6: string): boolean => {
|
||||
if (!ipv6) return true; // Empty is allowed
|
||||
// Basic IPv6 validation (simplified - allows compressed format)
|
||||
const pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}(\/\d{1,3})?$/;
|
||||
return pattern.test(ipv6);
|
||||
};
|
||||
|
||||
const validateMAC = (mac: string): boolean => {
|
||||
if (!mac) return true; // Empty is allowed (auto)
|
||||
const pattern = /^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$/;
|
||||
return pattern.test(mac);
|
||||
};
|
||||
|
||||
const validatePositiveInt = (value: string | number | undefined): boolean => {
|
||||
if (value === '' || value === undefined) return true;
|
||||
const num = typeof value === 'string' ? parseInt(value, 10) : value;
|
||||
return !isNaN(num) && num > 0;
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (mode === 'default') {
|
||||
// Default mode: only storage is optional
|
||||
// No validation needed
|
||||
} else {
|
||||
// Advanced mode: validate all fields
|
||||
if (advancedVars.var_gateway && !validateIPv4(advancedVars.var_gateway as string)) {
|
||||
newErrors.var_gateway = 'Invalid IPv4 address';
|
||||
}
|
||||
if (advancedVars.var_mac && !validateMAC(advancedVars.var_mac as string)) {
|
||||
newErrors.var_mac = 'Invalid MAC address format (XX:XX:XX:XX:XX:XX)';
|
||||
}
|
||||
if (advancedVars.var_ns && !validateIPv4(advancedVars.var_ns as string)) {
|
||||
newErrors.var_ns = 'Invalid IPv4 address';
|
||||
}
|
||||
if (advancedVars.var_apt_cacher_ip && !validateIPv4(advancedVars.var_apt_cacher_ip as string)) {
|
||||
newErrors.var_apt_cacher_ip = 'Invalid IPv4 address';
|
||||
}
|
||||
// Validate IPv4 CIDR if network mode is static
|
||||
const netValue = advancedVars.var_net;
|
||||
const isStaticMode = netValue === 'static' || (typeof netValue === 'string' && netValue.includes('/'));
|
||||
if (isStaticMode) {
|
||||
const cidrValue = (typeof netValue === 'string' && netValue.includes('/')) ? netValue : (advancedVars.var_ip as string ?? '');
|
||||
if (cidrValue && !validateCIDR(cidrValue)) {
|
||||
newErrors.var_ip = 'Invalid CIDR format (e.g., 10.10.10.1/24)';
|
||||
}
|
||||
}
|
||||
// Validate IPv6 static if IPv6 method is static
|
||||
if (advancedVars.var_ipv6_method === 'static' && advancedVars.var_ipv6_static) {
|
||||
if (!validateIPv6(advancedVars.var_ipv6_static as string)) {
|
||||
newErrors.var_ipv6_static = 'Invalid IPv6 address';
|
||||
}
|
||||
}
|
||||
if (!validatePositiveInt(advancedVars.var_cpu as string | number | undefined)) {
|
||||
newErrors.var_cpu = 'Must be a positive integer';
|
||||
}
|
||||
if (!validatePositiveInt(advancedVars.var_ram as string | number | undefined)) {
|
||||
newErrors.var_ram = 'Must be a positive integer';
|
||||
}
|
||||
if (!validatePositiveInt(advancedVars.var_disk as string | number | undefined)) {
|
||||
newErrors.var_disk = 'Must be a positive integer';
|
||||
}
|
||||
if (advancedVars.var_mtu && !validatePositiveInt(advancedVars.var_mtu as string | number | undefined)) {
|
||||
newErrors.var_mtu = 'Must be a positive integer';
|
||||
}
|
||||
if (advancedVars.var_vlan && !validatePositiveInt(advancedVars.var_vlan as string | number | undefined)) {
|
||||
newErrors.var_vlan = 'Must be a positive integer';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let envVars: EnvVars = {};
|
||||
|
||||
if (mode === 'default') {
|
||||
// Default mode: minimal vars
|
||||
envVars = {
|
||||
var_hostname: slug,
|
||||
var_brg: 'vmbr0',
|
||||
var_net: 'dhcp',
|
||||
var_ipv6_method: 'auto',
|
||||
var_ssh: 'no',
|
||||
var_nesting: 1,
|
||||
var_verbose: 'no',
|
||||
var_cpu: resources?.cpu ?? 1,
|
||||
var_ram: resources?.ram ?? 1024,
|
||||
var_disk: resources?.hdd ?? 4,
|
||||
var_unprivileged: script?.privileged === false ? 1 : (script?.privileged === true ? 0 : 1),
|
||||
};
|
||||
|
||||
if (containerStorage) {
|
||||
envVars.var_container_storage = containerStorage;
|
||||
}
|
||||
} else {
|
||||
// Advanced mode: all vars
|
||||
envVars = { ...advancedVars };
|
||||
|
||||
// If network mode is static and var_ip is set, replace var_net with the CIDR
|
||||
if (envVars.var_net === 'static' && envVars.var_ip) {
|
||||
envVars.var_net = envVars.var_ip as string;
|
||||
delete envVars.var_ip; // Remove the temporary var_ip
|
||||
}
|
||||
|
||||
// Format password correctly: if var_pw is set, format it as "-password <password>"
|
||||
// build.func expects PW to be in "-password <password>" format when added to PCT_OPTIONS
|
||||
const rawPassword = envVars.var_pw;
|
||||
const hasPassword = rawPassword && typeof rawPassword === 'string' && rawPassword.trim() !== '';
|
||||
const hasSSHKey = envVars.var_ssh_authorized_key && typeof envVars.var_ssh_authorized_key === 'string' && envVars.var_ssh_authorized_key.trim() !== '';
|
||||
|
||||
if (hasPassword) {
|
||||
// Remove any existing "-password" prefix to avoid double-formatting
|
||||
const cleanPassword = rawPassword.startsWith('-password ')
|
||||
? rawPassword.substring(11)
|
||||
: rawPassword;
|
||||
// Format as "-password <password>" for build.func
|
||||
envVars.var_pw = `-password ${cleanPassword}`;
|
||||
} else {
|
||||
// Empty password means auto-login, clear var_pw
|
||||
envVars.var_pw = '';
|
||||
}
|
||||
|
||||
|
||||
if ((hasPassword || hasSSHKey) && envVars.var_ssh !== 'no') {
|
||||
envVars.var_ssh = 'yes';
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty string values (but keep 0, false, etc.)
|
||||
const cleaned: EnvVars = {};
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
if (value !== '' && value !== undefined) {
|
||||
cleaned[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Always set mode to "default" (build.func line 1783 expects this)
|
||||
cleaned.mode = 'default';
|
||||
|
||||
onConfirm(cleaned);
|
||||
};
|
||||
|
||||
const updateAdvancedVar = (key: string, value: string | number | boolean) => {
|
||||
setAdvancedVars(prev => ({ ...prev, [key]: value }));
|
||||
// Clear error for this field
|
||||
if (errors[key]) {
|
||||
setErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[key];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const rootfsStorages = rootfsStoragesData?.storages ?? [];
|
||||
const templateStorages = templateStoragesData?.storages ?? [];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full border border-border max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h2 className="text-xl font-bold text-foreground">
|
||||
{mode === 'default' ? 'Default Configuration' : 'Advanced Configuration'}
|
||||
</h2>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{mode === 'default' ? (
|
||||
/* Default Mode */
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Container Storage
|
||||
</label>
|
||||
<select
|
||||
value={containerStorage}
|
||||
onChange={(e) => setContainerStorage(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="">Auto (let script choose)</option>
|
||||
{rootfsStorages.map((storage) => (
|
||||
<option key={storage.name} value={storage.name}>
|
||||
{storage.name} ({storage.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{rootfsStorages.length === 0 && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Could not fetch storages. Script will use default selection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 border border-border">
|
||||
<h3 className="text-sm font-medium text-foreground mb-2">Default Values</h3>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>Hostname: {slug}</p>
|
||||
<p>Bridge: vmbr0</p>
|
||||
<p>Network: DHCP</p>
|
||||
<p>IPv6: Auto</p>
|
||||
<p>SSH: Disabled</p>
|
||||
<p>Nesting: Enabled</p>
|
||||
<p>CPU: {resources?.cpu ?? 1}</p>
|
||||
<p>RAM: {resources?.ram ?? 1024} MB</p>
|
||||
<p>Disk: {resources?.hdd ?? 4} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Advanced Mode */
|
||||
<div className="space-y-6">
|
||||
{/* Resources */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Resources</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
CPU Cores *
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={typeof advancedVars.var_cpu === 'boolean' ? '' : (advancedVars.var_cpu ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_cpu', parseInt(e.target.value) || 1)}
|
||||
className={errors.var_cpu ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_cpu && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_cpu}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
RAM (MB) *
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={typeof advancedVars.var_ram === 'boolean' ? '' : (advancedVars.var_ram ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_ram', parseInt(e.target.value) || 1024)}
|
||||
className={errors.var_ram ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_ram && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_ram}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Disk Size (GB) *
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={typeof advancedVars.var_disk === 'boolean' ? '' : (advancedVars.var_disk ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_disk', parseInt(e.target.value) || 4)}
|
||||
className={errors.var_disk ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_disk && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_disk}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Unprivileged
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_unprivileged === 'boolean' ? (advancedVars.var_unprivileged ? 0 : 1) : (advancedVars.var_unprivileged ?? 1)}
|
||||
onChange={(e) => updateAdvancedVar('var_unprivileged', parseInt(e.target.value))}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value={1}>Yes (Unprivileged)</option>
|
||||
<option value={0}>No (Privileged)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Network</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Network Mode
|
||||
</label>
|
||||
<select
|
||||
value={(typeof advancedVars.var_net === 'string' && advancedVars.var_net.includes('/')) ? 'static' : (typeof advancedVars.var_net === 'boolean' ? 'dhcp' : (advancedVars.var_net ?? 'dhcp'))}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === 'static') {
|
||||
updateAdvancedVar('var_net', 'static');
|
||||
} else {
|
||||
updateAdvancedVar('var_net', e.target.value);
|
||||
// Clear IPv4 IP when switching away from static
|
||||
if (advancedVars.var_ip) {
|
||||
updateAdvancedVar('var_ip', '');
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="dhcp">DHCP</option>
|
||||
<option value="static">Static</option>
|
||||
</select>
|
||||
</div>
|
||||
{(advancedVars.var_net === 'static' || (typeof advancedVars.var_net === 'string' && advancedVars.var_net.includes('/'))) && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
IPv4 Address (CIDR) *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={(typeof advancedVars.var_net === 'string' && advancedVars.var_net.includes('/')) ? advancedVars.var_net : (advancedVars.var_ip as string | undefined ?? '')}
|
||||
onChange={(e) => {
|
||||
// Store in var_ip temporarily, will be moved to var_net on confirm
|
||||
updateAdvancedVar('var_ip', e.target.value);
|
||||
}}
|
||||
placeholder="10.10.10.1/24"
|
||||
className={errors.var_ip ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_ip && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_ip}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Bridge
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_brg === 'boolean' ? '' : String(advancedVars.var_brg ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_brg', e.target.value)}
|
||||
placeholder="vmbr0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Gateway (IP)
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_gateway === 'boolean' ? '' : String(advancedVars.var_gateway ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_gateway', e.target.value)}
|
||||
placeholder="Auto"
|
||||
className={errors.var_gateway ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_gateway && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_gateway}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
IPv6 Method
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_ipv6_method === 'boolean' ? 'none' : String(advancedVars.var_ipv6_method ?? 'none')}
|
||||
onChange={(e) => {
|
||||
updateAdvancedVar('var_ipv6_method', e.target.value);
|
||||
// Clear IPv6 static when switching away from static
|
||||
if (e.target.value !== 'static' && advancedVars.var_ipv6_static) {
|
||||
updateAdvancedVar('var_ipv6_static', '');
|
||||
}
|
||||
}}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="dhcp">DHCP</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="disable">Disable</option>
|
||||
</select>
|
||||
</div>
|
||||
{advancedVars.var_ipv6_method === 'static' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
IPv6 Static Address *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_ipv6_static === 'boolean' ? '' : String(advancedVars.var_ipv6_static ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_ipv6_static', e.target.value)}
|
||||
placeholder="2001:db8::1/64"
|
||||
className={errors.var_ipv6_static ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_ipv6_static && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_ipv6_static}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
VLAN Tag
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={typeof advancedVars.var_vlan === 'boolean' ? '' : String(advancedVars.var_vlan ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_vlan', e.target.value ? parseInt(e.target.value) : '')}
|
||||
placeholder="None"
|
||||
className={errors.var_vlan ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_vlan && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_vlan}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
MTU
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={typeof advancedVars.var_mtu === 'boolean' ? '' : String(advancedVars.var_mtu ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_mtu', e.target.value ? parseInt(e.target.value) : 1500)}
|
||||
placeholder="1500"
|
||||
className={errors.var_mtu ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_mtu && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_mtu}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
MAC Address
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_mac === 'boolean' ? '' : String(advancedVars.var_mac ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_mac', e.target.value)}
|
||||
placeholder="Auto"
|
||||
className={errors.var_mac ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_mac && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_mac}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
DNS Nameserver (IP)
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_ns === 'boolean' ? '' : String(advancedVars.var_ns ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_ns', e.target.value)}
|
||||
placeholder="Auto"
|
||||
className={errors.var_ns ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_ns && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_ns}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Identity & Metadata */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Identity & Metadata</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Hostname *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_hostname === 'boolean' ? '' : String(advancedVars.var_hostname ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_hostname', e.target.value)}
|
||||
placeholder={slug}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Root Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={typeof advancedVars.var_pw === 'boolean' ? '' : String(advancedVars.var_pw ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_pw', e.target.value)}
|
||||
placeholder="Random (empty = auto-login)"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Tags (comma-separated)
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_tags === 'boolean' ? '' : String(advancedVars.var_tags ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_tags', e.target.value)}
|
||||
placeholder="community-script"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SSH Access */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">SSH Access</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Enable SSH
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_ssh === 'boolean' ? (advancedVars.var_ssh ? 'yes' : 'no') : String(advancedVars.var_ssh ?? 'no')}
|
||||
onChange={(e) => updateAdvancedVar('var_ssh', e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
SSH Authorized Key
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_ssh_authorized_key === 'boolean' ? '' : String(advancedVars.var_ssh_authorized_key ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_ssh_authorized_key', e.target.value)}
|
||||
placeholder="ssh-rsa AAAA..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container Features */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Container Features</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Nesting (Docker)
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_nesting === 'boolean' ? 1 : (advancedVars.var_nesting ?? 1)}
|
||||
onChange={(e) => updateAdvancedVar('var_nesting', parseInt(e.target.value))}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value={1}>Enabled</option>
|
||||
<option value={0}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
FUSE
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_fuse === 'boolean' ? 0 : (advancedVars.var_fuse ?? 0)}
|
||||
onChange={(e) => updateAdvancedVar('var_fuse', parseInt(e.target.value))}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value={0}>Disabled</option>
|
||||
<option value={1}>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Keyctl
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_keyctl === 'boolean' ? 0 : (advancedVars.var_keyctl ?? 0)}
|
||||
onChange={(e) => updateAdvancedVar('var_keyctl', parseInt(e.target.value))}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value={0}>Disabled</option>
|
||||
<option value={1}>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Mknod
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_mknod === 'boolean' ? 0 : (advancedVars.var_mknod ?? 0)}
|
||||
onChange={(e) => updateAdvancedVar('var_mknod', parseInt(e.target.value))}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value={0}>Disabled</option>
|
||||
<option value={1}>Enabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Mount Filesystems
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_mount_fs === 'boolean' ? '' : String(advancedVars.var_mount_fs ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_mount_fs', e.target.value)}
|
||||
placeholder="nfs,cifs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Protection
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_protection === 'boolean' ? (advancedVars.var_protection ? 'yes' : 'no') : String(advancedVars.var_protection ?? 'no')}
|
||||
onChange={(e) => updateAdvancedVar('var_protection', e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Configuration */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">System Configuration</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Timezone
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_timezone === 'boolean' ? '' : String(advancedVars.var_timezone ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_timezone', e.target.value)}
|
||||
placeholder="System"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Verbose
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_verbose === 'boolean' ? (advancedVars.var_verbose ? 'yes' : 'no') : String(advancedVars.var_verbose ?? 'no')}
|
||||
onChange={(e) => updateAdvancedVar('var_verbose', e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
APT Cacher
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_apt_cacher === 'boolean' ? (advancedVars.var_apt_cacher ? 'yes' : 'no') : String(advancedVars.var_apt_cacher ?? 'no')}
|
||||
onChange={(e) => updateAdvancedVar('var_apt_cacher', e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="no">No</option>
|
||||
<option value="yes">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
APT Cacher IP
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={typeof advancedVars.var_apt_cacher_ip === 'boolean' ? '' : String(advancedVars.var_apt_cacher_ip ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_apt_cacher_ip', e.target.value)}
|
||||
placeholder="192.168.1.10"
|
||||
className={errors.var_apt_cacher_ip ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.var_apt_cacher_ip && (
|
||||
<p className="mt-1 text-xs text-destructive">{errors.var_apt_cacher_ip}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Storage Selection */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-4">Storage Selection</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Container Storage
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_container_storage === 'boolean' ? '' : String(advancedVars.var_container_storage ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_container_storage', e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="">Auto</option>
|
||||
{rootfsStorages.map((storage) => (
|
||||
<option key={storage.name} value={storage.name}>
|
||||
{storage.name} ({storage.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{rootfsStorages.length === 0 && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Could not fetch storages. Leave empty for auto selection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Template Storage
|
||||
</label>
|
||||
<select
|
||||
value={typeof advancedVars.var_template_storage === 'boolean' ? '' : String(advancedVars.var_template_storage ?? '')}
|
||||
onChange={(e) => updateAdvancedVar('var_template_storage', e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
>
|
||||
<option value="">Auto</option>
|
||||
{templateStorages.map((storage) => (
|
||||
<option key={storage.name} value={storage.name}>
|
||||
{storage.name} ({storage.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{templateStorages.length === 0 && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Could not fetch storages. Leave empty for auto selection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-3 mt-6 pt-6 border-t border-border">
|
||||
<Button onClick={onClose} variant="outline" size="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} variant="default" size="default">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,26 +2,31 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Server } from '../../types/server';
|
||||
import type { Script } from '../../types/script';
|
||||
import { Button } from './ui/button';
|
||||
import { ColorCodedDropdown } from './ColorCodedDropdown';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { ConfigurationModal, type EnvVars } from './ConfigurationModal';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
|
||||
interface ExecutionModeModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onExecute: (mode: 'local' | 'ssh', server?: Server) => void;
|
||||
onExecute: (mode: 'local' | 'ssh', server?: Server, envVars?: EnvVars) => void;
|
||||
scriptName: string;
|
||||
script?: Script | null;
|
||||
}
|
||||
|
||||
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: ExecutionModeModalProps) {
|
||||
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName, script }: ExecutionModeModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [configMode, setConfigMode] = useState<'default' | 'advanced'>('default');
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -64,19 +69,25 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = () => {
|
||||
const handleConfigModeSelect = (mode: 'default' | 'advanced') => {
|
||||
if (!selectedServer) {
|
||||
setError('Please select a server for SSH execution');
|
||||
setError('Please select a server first');
|
||||
return;
|
||||
}
|
||||
setConfigMode(mode);
|
||||
setConfigModalOpen(true);
|
||||
};
|
||||
|
||||
onExecute('ssh', selectedServer);
|
||||
const handleConfigConfirm = (envVars: EnvVars) => {
|
||||
if (!selectedServer) return;
|
||||
setConfigModalOpen(false);
|
||||
onExecute('ssh', selectedServer, envVars);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
const handleServerSelect = (server: Server | null) => {
|
||||
setSelectedServer(server);
|
||||
setError(null); // Clear error when server is selected
|
||||
};
|
||||
|
||||
|
||||
@@ -164,6 +175,31 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Mode Selection */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Choose configuration mode:
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => handleConfigModeSelect('default')}
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1"
|
||||
>
|
||||
Default
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleConfigModeSelect('advanced')}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1"
|
||||
>
|
||||
Advanced (Beta)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button
|
||||
@@ -173,13 +209,6 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
variant="default"
|
||||
size="default"
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -204,6 +233,33 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Configuration Mode Selection - only show when server is selected */}
|
||||
{selectedServer && (
|
||||
<div className="space-y-3 pt-4 border-t border-border">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Choose configuration mode:
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => handleConfigModeSelect('default')}
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1"
|
||||
>
|
||||
Default
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleConfigModeSelect('advanced')}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1"
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button
|
||||
@@ -213,15 +269,6 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
disabled={!selectedServer}
|
||||
variant="default"
|
||||
size="default"
|
||||
className={!selectedServer ? 'bg-muted-foreground cursor-not-allowed' : ''}
|
||||
>
|
||||
Run on Server
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -234,6 +281,16 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
isOpen={settingsModalOpen}
|
||||
onClose={handleSettingsModalClose}
|
||||
/>
|
||||
|
||||
{/* Configuration Modal */}
|
||||
<ConfigurationModal
|
||||
isOpen={configModalOpen}
|
||||
onClose={() => setConfigModalOpen(false)}
|
||||
onConfirm={handleConfigConfirm}
|
||||
script={script ?? null}
|
||||
server={selectedServer}
|
||||
mode={configMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ScriptDetailModalProps {
|
||||
scriptName: string,
|
||||
mode?: "local" | "ssh",
|
||||
server?: Server,
|
||||
envVars?: Record<string, string | number | boolean>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@ export function ScriptDetailModal({
|
||||
setExecutionModeOpen(true);
|
||||
};
|
||||
|
||||
const handleExecuteScript = (mode: "local" | "ssh", server?: Server) => {
|
||||
const handleExecuteScript = (mode: "local" | "ssh", server?: Server, envVars?: Record<string, string | number | boolean>) => {
|
||||
if (!script || !onInstallScript) return;
|
||||
|
||||
// Find the script path based on selected version type
|
||||
@@ -197,8 +198,8 @@ export function ScriptDetailModal({
|
||||
const scriptPath = `scripts/${scriptMethod.script}`;
|
||||
const scriptName = script.name;
|
||||
|
||||
// Pass execution mode and server info to the parent
|
||||
onInstallScript(scriptPath, scriptName, mode, server);
|
||||
// Pass execution mode, server info, and envVars to the parent
|
||||
onInstallScript(scriptPath, scriptName, mode, server, envVars);
|
||||
|
||||
onClose(); // Close the modal when starting installation
|
||||
}
|
||||
@@ -935,6 +936,7 @@ export function ScriptDetailModal({
|
||||
{script && (
|
||||
<ExecutionModeModal
|
||||
scriptName={script.name}
|
||||
script={script}
|
||||
isOpen={executionModeOpen}
|
||||
onClose={() => setExecutionModeOpen(false)}
|
||||
onExecute={handleExecuteScript}
|
||||
|
||||
@@ -21,6 +21,7 @@ interface TerminalProps {
|
||||
cloneCount?: number;
|
||||
hostnames?: string[];
|
||||
containerType?: 'lxc' | 'vm';
|
||||
envVars?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
interface TerminalMessage {
|
||||
@@ -29,7 +30,7 @@ interface TerminalMessage {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, isBackup = false, isClone = false, containerId, storage, backupStorage, executionId: propExecutionId, cloneCount, hostnames, containerType }: TerminalProps) {
|
||||
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, isBackup = false, isClone = false, containerId, storage, backupStorage, executionId: propExecutionId, cloneCount, hostnames, containerType, envVars }: TerminalProps) {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
@@ -360,7 +361,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
backupStorage,
|
||||
cloneCount,
|
||||
hostnames,
|
||||
containerType
|
||||
containerType,
|
||||
envVars
|
||||
};
|
||||
ws.send(JSON.stringify(message));
|
||||
}
|
||||
@@ -400,7 +402,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]);
|
||||
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile, envVars]);
|
||||
|
||||
const startScript = () => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {
|
||||
@@ -417,6 +419,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
executionId: newExecutionId,
|
||||
mode,
|
||||
server,
|
||||
envVars,
|
||||
isUpdate,
|
||||
isShell,
|
||||
isBackup,
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function Home() {
|
||||
name: string;
|
||||
mode?: "local" | "ssh";
|
||||
server?: Server;
|
||||
envVars?: Record<string, string | number | boolean>;
|
||||
} | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"scripts" | "downloaded" | "installed" | "backups"
|
||||
@@ -209,8 +210,9 @@ export default function Home() {
|
||||
scriptName: string,
|
||||
mode?: "local" | "ssh",
|
||||
server?: Server,
|
||||
envVars?: Record<string, string | number | boolean>,
|
||||
) => {
|
||||
setRunningScript({ path: scriptPath, name: scriptName, mode, server });
|
||||
setRunningScript({ path: scriptPath, name: scriptName, mode, server, envVars });
|
||||
// Scroll to terminal after a short delay to ensure it's rendered
|
||||
setTimeout(scrollToTerminal, 100);
|
||||
};
|
||||
@@ -360,6 +362,7 @@ export default function Home() {
|
||||
onClose={handleCloseTerminal}
|
||||
mode={runningScript.mode}
|
||||
server={runningScript.server}
|
||||
envVars={runningScript.envVars}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,10 @@ import { localScriptsService } from "~/server/services/localScripts";
|
||||
import { scriptDownloaderService } from "~/server/services/scriptDownloader.js";
|
||||
import { AutoSyncService } from "~/server/services/autoSyncService";
|
||||
import { repositoryService } from "~/server/services/repositoryService";
|
||||
import { getStorageService } from "~/server/services/storageService";
|
||||
import { getDatabase } from "~/server/database-prisma";
|
||||
import type { ScriptCard } from "~/types/script";
|
||||
import type { Server } from "~/types/server";
|
||||
|
||||
export const scriptsRouter = createTRPCRouter({
|
||||
// Get all available scripts
|
||||
@@ -637,5 +640,194 @@ export const scriptsRouter = createTRPCRouter({
|
||||
status: null
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
// Get rootfs storages for a server (for container creation)
|
||||
getRootfsStorages: publicProcedure
|
||||
.input(z.object({
|
||||
serverId: z.number(),
|
||||
forceRefresh: z.boolean().optional().default(false)
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const server = await db.getServerById(input.serverId);
|
||||
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Server not found',
|
||||
storages: []
|
||||
};
|
||||
}
|
||||
|
||||
// Get server hostname to filter storages by node assignment
|
||||
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||
const sshExecutionService = getSSHExecutionService();
|
||||
let serverHostname = '';
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
server as Server,
|
||||
'hostname',
|
||||
(data: string) => {
|
||||
serverHostname += data;
|
||||
},
|
||||
(error: string) => {
|
||||
reject(new Error(`Failed to get hostname: ${error}`));
|
||||
},
|
||||
(exitCode: number) => {
|
||||
if (exitCode === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`hostname command failed with exit code ${exitCode}`));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting server hostname:', error);
|
||||
// Continue without filtering if hostname can't be retrieved
|
||||
}
|
||||
|
||||
const normalizedHostname = serverHostname.trim().toLowerCase();
|
||||
|
||||
const storageService = getStorageService();
|
||||
const allStorages = await storageService.getStorages(server as Server, input.forceRefresh);
|
||||
|
||||
// Filter storages by node hostname matching and content type (rootdir for containers)
|
||||
const rootfsStorages = allStorages.filter(storage => {
|
||||
// Check content type - must have rootdir for containers
|
||||
const hasRootdir = storage.content.includes('rootdir');
|
||||
if (!hasRootdir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If storage has no nodes specified, it's available on all nodes
|
||||
if (!storage.nodes || storage.nodes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we couldn't get hostname, include all storages (fallback)
|
||||
if (!normalizedHostname) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if server hostname is in the nodes array (case-insensitive, trimmed)
|
||||
const normalizedNodes = storage.nodes.map(node => node.trim().toLowerCase());
|
||||
return normalizedNodes.includes(normalizedHostname);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
storages: rootfsStorages.map(s => ({
|
||||
name: s.name,
|
||||
type: s.type,
|
||||
content: s.content
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching rootfs storages:', error);
|
||||
// Return empty array on error (as per plan requirement)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch storages',
|
||||
storages: []
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
// Get template storages for a server (for template storage selection)
|
||||
getTemplateStorages: publicProcedure
|
||||
.input(z.object({
|
||||
serverId: z.number(),
|
||||
forceRefresh: z.boolean().optional().default(false)
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const server = await db.getServerById(input.serverId);
|
||||
|
||||
if (!server) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Server not found',
|
||||
storages: []
|
||||
};
|
||||
}
|
||||
|
||||
// Get server hostname to filter storages by node assignment
|
||||
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||
const sshExecutionService = getSSHExecutionService();
|
||||
let serverHostname = '';
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
void sshExecutionService.executeCommand(
|
||||
server as Server,
|
||||
'hostname',
|
||||
(data: string) => {
|
||||
serverHostname += data;
|
||||
},
|
||||
(error: string) => {
|
||||
reject(new Error(`Failed to get hostname: ${error}`));
|
||||
},
|
||||
(exitCode: number) => {
|
||||
if (exitCode === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`hostname command failed with exit code ${exitCode}`));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting server hostname:', error);
|
||||
// Continue without filtering if hostname can't be retrieved
|
||||
}
|
||||
|
||||
const normalizedHostname = serverHostname.trim().toLowerCase();
|
||||
|
||||
const storageService = getStorageService();
|
||||
const allStorages = await storageService.getStorages(server as Server, input.forceRefresh);
|
||||
|
||||
// Filter storages by node hostname matching and content type (vztmpl for templates)
|
||||
const templateStorages = allStorages.filter(storage => {
|
||||
// Check content type - must have vztmpl for templates
|
||||
const hasVztmpl = storage.content.includes('vztmpl');
|
||||
if (!hasVztmpl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If storage has no nodes specified, it's available on all nodes
|
||||
if (!storage.nodes || storage.nodes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we couldn't get hostname, include all storages (fallback)
|
||||
if (!normalizedHostname) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if server hostname is in the nodes array (case-insensitive, trimmed)
|
||||
const normalizedNodes = storage.nodes.map(node => node.trim().toLowerCase());
|
||||
return normalizedNodes.includes(normalizedHostname);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
storages: templateStorages.map(s => ({
|
||||
name: s.name,
|
||||
type: s.type,
|
||||
content: s.content
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching template storages:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch storages',
|
||||
storages: []
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -85,9 +85,10 @@ class SSHExecutionService {
|
||||
* @param {Function} onData - Callback for data output
|
||||
* @param {Function} onError - Callback for errors
|
||||
* @param {Function} onExit - Callback for process exit
|
||||
* @param {Object} [envVars] - Optional environment variables to pass to the script
|
||||
* @returns {Promise<Object>} Process information
|
||||
*/
|
||||
async executeScript(server, scriptPath, onData, onError, onExit) {
|
||||
async executeScript(server, scriptPath, onData, onError, onExit, envVars = {}) {
|
||||
try {
|
||||
await this.transferScriptsFolder(server, onData, onError);
|
||||
|
||||
@@ -98,8 +99,43 @@ class SSHExecutionService {
|
||||
// Build SSH command based on authentication type
|
||||
const { command, args } = this.buildSSHCommand(server);
|
||||
|
||||
// Format environment variables as var_name=value pairs
|
||||
const envVarsString = Object.entries(envVars)
|
||||
.map(([key, value]) => {
|
||||
// Escape special characters in values
|
||||
const escapedValue = String(value).replace(/'/g, "'\\''");
|
||||
return `${key}='${escapedValue}'`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
// Build the command with environment variables
|
||||
let scriptCommand = `cd /tmp/scripts && chmod +x ${relativeScriptPath} && export TERM=xterm-256color && export COLUMNS=120 && export LINES=30 && export COLORTERM=truecolor && export FORCE_COLOR=1 && export NO_COLOR=0 && export CLICOLOR=1 && export CLICOLOR_FORCE=1`;
|
||||
|
||||
if (envVarsString) {
|
||||
scriptCommand += ` && ${envVarsString} bash ${relativeScriptPath}`;
|
||||
} else {
|
||||
scriptCommand += ` && bash ${relativeScriptPath}`;
|
||||
}
|
||||
|
||||
// Log the full command that will be executed
|
||||
console.log('='.repeat(80));
|
||||
console.log(`[SSH Execution] Executing on host: ${server.ip} (${server.name || 'Unnamed'})`);
|
||||
console.log(`[SSH Execution] Script path: ${scriptPath}`);
|
||||
console.log(`[SSH Execution] Relative script path: ${relativeScriptPath}`);
|
||||
if (Object.keys(envVars).length > 0) {
|
||||
console.log(`[SSH Execution] Environment variables (${Object.keys(envVars).length} vars):`);
|
||||
Object.entries(envVars).forEach(([key, value]) => {
|
||||
console.log(` ${key}=${String(value)}`);
|
||||
});
|
||||
} else {
|
||||
console.log(`[SSH Execution] No environment variables provided`);
|
||||
}
|
||||
console.log(`[SSH Execution] Full command:`);
|
||||
console.log(scriptCommand);
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// Add the script execution command to the args
|
||||
args.push(`cd /tmp/scripts && chmod +x ${relativeScriptPath} && export TERM=xterm-256color && export COLUMNS=120 && export LINES=30 && export COLORTERM=truecolor && export FORCE_COLOR=1 && export NO_COLOR=0 && export CLICOLOR=1 && export CLICOLOR_FORCE=1 && bash ${relativeScriptPath}`);
|
||||
args.push(scriptCommand);
|
||||
|
||||
// Use ptySpawn for proper terminal emulation and color support
|
||||
const sshCommand = ptySpawn(command, args, {
|
||||
|
||||
Reference in New Issue
Block a user