Compare commits

..

52 Commits

Author SHA1 Message Date
CanbiZ
f338036ed2 Update copyright years to 2026 in core scripts
Updated the copyright year from 2025 to 2026 in alpine-install.func, api.func, and cloud-init.func to reflect the new year. No functional changes were made.
2026-01-07 20:43:01 +01:00
CanbiZ
b38083e8c9 Update error-handler.func 2026-01-07 20:42:10 +01:00
CanbiZ
633612cd8f Enhance hardware acceleration and MariaDB setup
Refactors and expands the hardware acceleration setup to support multiple GPU types (Intel, AMD, NVIDIA), adds user selection for GPU configuration, and improves driver installation logic for Debian and Ubuntu. Adds runtime directory persistence for MariaDB using tmpfiles.d to ensure /run/mysqld exists after reboot. Includes minor robustness improvements and error handling throughout the script.
2026-01-07 20:41:58 +01:00
CanbiZ
e6d11196bd update install.func 2026-01-07 20:41:34 +01:00
CanbiZ
cf36b1b86a Add advanced container features and IP range scanning
Introduces support for scanning and assigning the first free IP from a user-specified range, and expands advanced LXC container settings to include GPU passthrough, TUN/TAP, nesting, keyctl, mknod, timezone, protection, and APT cacher options. Refactors advanced_settings wizard to support these new features, updates variable handling and defaults, and improves summary and output formatting. Also enhances SSH key configuration, storage/template validation, and GPU passthrough logic.
2026-01-07 20:40:33 +01:00
CanbiZ
be7fadeee2 update core.func 2026-01-07 20:36:54 +01:00
dependabot[bot]
94eb2820fd build(deps): Bump @trpc/react-query from 11.7.2 to 11.8.0 (#417)
Bumps [@trpc/react-query](https://github.com/trpc/trpc/tree/HEAD/packages/react) from 11.7.2 to 11.8.0.
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v11.8.0/packages/react)

---
updated-dependencies:
- dependency-name: "@trpc/react-query"
  dependency-version: 11.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 20:35:00 +01:00
dependabot[bot]
e49708770c build(deps-dev): Bump @types/node from 24.10.1 to 24.10.4 (#418)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.1 to 24.10.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.10.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 20:33:48 +01:00
dependabot[bot]
5eafa01843 build(deps-dev): Bump @tailwindcss/postcss from 4.1.17 to 4.1.18 (#416)
Bumps [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) from 4.1.17 to 4.1.18.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/@tailwindcss-postcss)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 20:33:35 +01:00
dependabot[bot]
0c1477e087 build(deps-dev): Bump eslint-config-next from 16.0.7 to 16.1.0 (#415)
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.0.7 to 16.1.0.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.1.0/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 20:33:28 +01:00
dependabot[bot]
ef73d98873 build(deps): Bump @trpc/server (#413)
Bumps the npm_and_yarn group with 1 update in the / directory: [@trpc/server](https://github.com/trpc/trpc/tree/HEAD/packages/server).


Updates `@trpc/server` from 11.7.2 to 11.8.0
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v11.8.0/packages/server)

---
updated-dependencies:
- dependency-name: "@trpc/server"
  dependency-version: 11.8.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 20:33:20 +01:00
Slaviša Arežina
ec92c0ea6d Merge pull request #421 from zeosamaster/patch-1 2025-12-30 13:51:57 +01:00
Fábio Matias
ee14b89868 Fix directory path in README after cloning 2025-12-30 11:46:33 +00:00
Michel Roegl-Brunner
be68160cd9 Merge pull request #411 from community-scripts/dependabot/npm_and_yarn/t3-oss/env-nextjs-0.13.10 2025-12-18 19:43:58 +01:00
Michel Roegl-Brunner
dbc15b1bc3 Merge pull request #410 from community-scripts/dependabot/npm_and_yarn/jsdom-27.3.0 2025-12-18 19:43:44 +01:00
Michel Roegl-Brunner
dc6ce16e5a Merge pull request #409 from community-scripts/dependabot/npm_and_yarn/tanstack/react-query-5.90.12 2025-12-18 19:43:33 +01:00
Michel Roegl-Brunner
0c9d4ad6e2 Merge pull request #408 from community-scripts/dependabot/npm_and_yarn/react-dom-19.2.3 2025-12-18 19:43:20 +01:00
dependabot[bot]
13d57b77d4 build(deps): Bump @t3-oss/env-nextjs from 0.13.8 to 0.13.10
Bumps [@t3-oss/env-nextjs](https://github.com/t3-oss/t3-env/tree/HEAD/packages/nextjs) from 0.13.8 to 0.13.10.
- [Release notes](https://github.com/t3-oss/t3-env/releases)
- [Changelog](https://github.com/t3-oss/t3-env/blob/main/packages/nextjs/CHANGELOG.md)
- [Commits](https://github.com/t3-oss/t3-env/commits/@t3-oss/env-nextjs@0.13.10/packages/nextjs)

---
updated-dependencies:
- dependency-name: "@t3-oss/env-nextjs"
  dependency-version: 0.13.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:25:26 +00:00
dependabot[bot]
f9e5bd5bf0 build(deps-dev): Bump jsdom from 27.2.0 to 27.3.0
Bumps [jsdom](https://github.com/jsdom/jsdom) from 27.2.0 to 27.3.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/compare/27.2.0...27.3.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 27.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:25:15 +00:00
dependabot[bot]
adf2b06efa build(deps): Bump @tanstack/react-query from 5.90.11 to 5.90.12
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.90.11 to 5.90.12.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.12/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:25:03 +00:00
dependabot[bot]
80e3966e4e build(deps): Bump react-dom from 19.2.1 to 19.2.3
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.1 to 19.2.3.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:24:52 +00:00
Michel Roegl-Brunner
3662a057dc Merge pull request #401 from community-scripts/dependabot/npm_and_yarn/tailwindcss-4.1.18 2025-12-14 11:10:27 +01:00
Michel Roegl-Brunner
bdf336f9bf Merge pull request #400 from community-scripts/dependabot/npm_and_yarn/react-19.2.3 2025-12-14 11:10:17 +01:00
Michel Roegl-Brunner
f6c310fa22 Merge pull request #399 from community-scripts/dependabot/npm_and_yarn/vitejs/plugin-react-5.1.2 2025-12-14 11:10:04 +01:00
Michel Roegl-Brunner
d658894b7f Merge pull request #397 from community-scripts/dependabot/npm_and_yarn/npm_and_yarn-3b9adafb77 2025-12-14 11:09:56 +01:00
dependabot[bot]
783744b497 build(deps-dev): Bump tailwindcss from 4.1.17 to 4.1.18
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.1.17 to 4.1.18.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:49:47 +00:00
dependabot[bot]
de9ac41f76 build(deps): Bump react from 19.2.1 to 19.2.3
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.1 to 19.2.3.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:49:38 +00:00
dependabot[bot]
060202e557 build(deps-dev): Bump @vitejs/plugin-react from 5.1.1 to 5.1.2
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.1.2/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 19:49:27 +00:00
dependabot[bot]
8d45ac14cc build(deps): Bump next in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 16.0.9 to 16.0.10
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.9...v16.0.10)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.10
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 17:26:19 +00:00
CanbiZ
47ee2247c8 fix for code scanning alert no. 4: Insecure randomness (#396)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-12 11:45:43 +01:00
dependabot[bot]
c16c8d54db build(deps): Bump lucide-react from 0.555.0 to 0.556.0 (#392)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.555.0 to 0.556.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.556.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 0.556.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 11:29:10 +01:00
dependabot[bot]
3e669a0739 build(deps): Bump jsonwebtoken from 9.0.2 to 9.0.3 (#390)
Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 9.0.2 to 9.0.3.
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v9.0.2...v9.0.3)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-version: 9.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 11:27:47 +01:00
dependabot[bot]
02e175c8a0 build(deps-dev): Bump eslint-config-next from 16.0.6 to 16.0.7 (#391)
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.0.6 to 16.0.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.0.7/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.0.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 11:27:41 +01:00
dependabot[bot]
b4e98e7624 build(deps-dev): Bump prettier from 3.7.3 to 3.7.4 (#393)
Bumps [prettier](https://github.com/prettier/prettier) from 3.7.3 to 3.7.4.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.3...3.7.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 11:27:23 +01:00
dependabot[bot]
2392529092 build(deps-dev): Bump baseline-browser-mapping from 2.8.32 to 2.9.3 (#394)
Bumps [baseline-browser-mapping](https://github.com/web-platform-dx/baseline-browser-mapping) from 2.8.32 to 2.9.3.
- [Release notes](https://github.com/web-platform-dx/baseline-browser-mapping/releases)
- [Commits](https://github.com/web-platform-dx/baseline-browser-mapping/compare/v2.8.32...v2.9.3)

---
updated-dependencies:
- dependency-name: baseline-browser-mapping
  dependency-version: 2.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 11:27:17 +01:00
dependabot[bot]
f9f5772d92 build(deps): Bump next in the npm_and_yarn group across 1 directory (#395)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 16.0.7 to 16.0.9
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.7...v16.0.9)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.9
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 11:27:11 +01:00
github-actions[bot]
4267d7340e chore: add VERSION v0.5.2 (#389)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-05 15:13:42 +00:00
Michel Roegl-Brunner
dcf923551b Merge pull request #388 from community-scripts/feat/use_new_core_features
feat: Add default and advanced install method selection
2025-12-05 16:05:33 +01:00
Michel Roegl-Brunner
69a5ac3a56 Nump core 2025-12-05 16:03:28 +01:00
Michel Roegl-Brunner
7b8c1ebdf1 feat: Add default and advanced install method selection
- Add ConfigurationModal component for selecting default or advanced installation mode
- Default mode: Uses predefined defaults with minimal user input (hostname from slug, vmbr0, dhcp, etc.)
- Advanced mode: Full configuration modal with all environment variables customizable
- Add support for IPv4 CIDR input when network mode is 'static'
- Add support for IPv6 static address input when IPv6 method is 'static'
- Implement password formatting as '-password <password>' for build.func compatibility
- Auto-enable SSH when password or SSH keys are provided
- Add storage selection dropdowns filtered by server node assignment
- Pass environment variables through entire execution stack (frontend -> WebSocket -> SSH/local execution)
- Add mode environment variable (always set to 'default' for script execution)
- Update ExecutionModeModal to show 'Advanced (Beta)' option
2025-12-05 15:53:50 +01:00
dependabot[bot]
580b623939 build(deps): Bump @prisma/adapter-better-sqlite3 from 7.0.1 to 7.1.0 (#382)
Bumps [@prisma/adapter-better-sqlite3](https://github.com/prisma/prisma/tree/HEAD/packages/adapter-better-sqlite3) from 7.0.1 to 7.1.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/7.1.0/packages/adapter-better-sqlite3)

---
updated-dependencies:
- dependency-name: "@prisma/adapter-better-sqlite3"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 22:01:38 +01:00
dependabot[bot]
ac21fbb181 build(deps): Bump jws in the npm_and_yarn group across 1 directory (#386)
Bumps the npm_and_yarn group with 1 update in the / directory: [jws](https://github.com/brianloveswords/node-jws).


Updates `jws` from 3.2.2 to 3.2.3
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 22:01:27 +01:00
Michel Roegl-Brunner
588ae65dfd Merge pull request #380 from community-scripts/dependabot/npm_and_yarn/next-16.0.7 2025-12-04 21:42:15 +01:00
Michel Roegl-Brunner
30acba39a5 Merge pull request #381 from community-scripts/dependabot/npm_and_yarn/react-dom-19.2.1 2025-12-04 21:42:03 +01:00
Michel Roegl-Brunner
3a5bb3dc45 Merge pull request #383 from community-scripts/dependabot/npm_and_yarn/prisma/client-7.1.0 2025-12-04 21:41:41 +01:00
Michel Roegl-Brunner
f42c0d956e Merge pull request #384 from community-scripts/dependabot/npm_and_yarn/npm_and_yarn-494b550551 2025-12-04 21:41:29 +01:00
Michel Roegl-Brunner
0ed13fcf0f Fix linter errors: use Record type, remove unused function, fix floating promises and unsafe types 2025-12-04 14:36:54 +01:00
dependabot[bot]
afc87910e6 build(deps): Bump react-dom from 19.2.0 to 19.2.1
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.0 to 19.2.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 09:11:09 +00:00
Michel Roegl-Brunner
b97eca9620 Merge pull request #385 from community-scripts/fix/source-calls-path-resolution
Bump all .func files in /core to be inline with the Main repo.
2025-12-04 10:09:56 +01:00
dependabot[bot]
8f0ae3a341 build(deps): Bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [next](https://github.com/vercel/next.js) and [hono](https://github.com/honojs/hono).


Updates `next` from 16.0.6 to 16.0.7
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.6...v16.0.7)

Updates `hono` from 4.7.10 to 4.10.6
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.7.10...v4.10.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.7
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: hono
  dependency-version: 4.10.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 20:49:31 +00:00
dependabot[bot]
b5450bd221 build(deps): Bump @prisma/client from 7.0.1 to 7.1.0
Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 7.0.1 to 7.1.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/7.1.0/packages/client)

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 19:28:10 +00:00
dependabot[bot]
88dbe4ea85 build(deps): Bump next from 16.0.6 to 16.0.7
Bumps [next](https://github.com/vercel/next.js) from 16.0.6 to 16.0.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.6...v16.0.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 19:27:21 +00:00
24 changed files with 4424 additions and 711 deletions

View File

@@ -100,7 +100,7 @@ apt install -y nodejs
```bash
# Clone the repository
git clone https://github.com/community-scripts/ProxmoxVE-Local.git /opt/PVESciptslocal
cd PVESciptslocal
cd /opt/PVESciptslocal
# Install dependencies and build
npm install

View File

@@ -1 +1 @@
0.5.1
0.5.2

676
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,16 +25,16 @@
"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",
"@t3-oss/env-nextjs": "^0.13.10",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.90.11",
"@trpc/client": "^11.7.2",
"@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.7.2",
"@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.8.0",
"@trpc/react-query": "^11.8.1",
"@trpc/server": "^11.8.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.18.1",
"@xterm/addon-fit": "^0.10.0",
@@ -47,13 +47,13 @@
"clsx": "^2.1.1",
"cron-validator": "^1.4.0",
"dotenv": "^17.2.3",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.555.0",
"next": "^16.0.6",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.561.0",
"next": "^16.0.10",
"node-cron": "^4.2.1",
"node-pty": "^1.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"refractor": "^5.0.0",
@@ -66,29 +66,29 @@
"zod": "^4.1.13"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/postcss": "^4.1.18",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.1",
"@types/node": "^24.10.4",
"@types/node-cron": "^3.0.11",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.14",
"baseline-browser-mapping": "^2.8.32",
"baseline-browser-mapping": "^2.9.3",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.6",
"jsdom": "^27.2.0",
"eslint-config-next": "^16.1.0",
"jsdom": "^27.3.0",
"postcss": "^8.5.6",
"prettier": "^3.7.3",
"prettier": "^3.7.4",
"prettier-plugin-tailwindcss": "^0.7.2",
"prisma": "^7.0.1",
"tailwindcss": "^4.1.17",
"prisma": "^7.1.0",
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2021-2025 community-scripts ORG
# Copyright (c) 2021-2026 community-scripts ORG
# Author: tteck (tteckster)
# Co-Author: MickLesk
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE

View 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)"
}

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2021-2025 community-scripts ORG
# Copyright (c) 2021-2026 community-scripts ORG
# Author: michelroegl-brunner
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,505 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 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

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2025 community-scripts ORG
# Copyright (c) 2021-2026 community-scripts ORG
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
# ==============================================================================
@@ -123,6 +123,7 @@ icons() {
CREATING="${TAB}🚀${TAB}${CL}"
ADVANCED="${TAB}🧩${TAB}${CL}"
FUSE="${TAB}🗂️${TAB}${CL}"
GPU="${TAB}🎮${TAB}${CL}"
HOURGLASS="${TAB}⏳${TAB}"
}
@@ -808,18 +809,15 @@ cleanup_lxc() {
find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true
find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true
# Truncate writable log files silently (permission errors ignored)
if command -v truncate >/dev/null 2>&1; then
find /var/log -type f -writable -print0 2>/dev/null |
xargs -0 -n1 truncate -s 0 2>/dev/null || true
# Node.js npm - directly remove cache directory
# npm cache clean/verify can fail with ENOTEMPTY errors, so we skip them
if command -v npm &>/dev/null; then
rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true
fi
# Node.js npm
if command -v npm &>/dev/null; then $STD npm cache clean --force || true; fi
# Node.js yarn
if command -v yarn &>/dev/null; then $STD yarn cache clean || true; fi
if command -v yarn &>/dev/null; then yarn cache clean &>/dev/null || true; fi
# Node.js pnpm
if command -v pnpm &>/dev/null; then $STD pnpm store prune || true; fi
if command -v pnpm &>/dev/null; then pnpm store prune &>/dev/null || true; fi
# Go
if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi
# Rust cargo
@@ -827,11 +825,8 @@ cleanup_lxc() {
# Ruby gem
if command -v gem &>/dev/null; then $STD gem cleanup || true; fi
# Composer (PHP)
if command -v composer &>/dev/null; then $STD composer clear-cache || true; fi
if command -v composer &>/dev/null; then COMPOSER_ALLOW_SUPERUSER=1 $STD composer clear-cache || true; fi
if command -v journalctl &>/dev/null; then
$STD journalctl --vacuum-time=10m || true
fi
msg_ok "Cleaned"
}
@@ -887,4 +882,4 @@ check_or_create_swap() {
# SIGNAL TRAPS
# ==============================================================================
trap 'stop_spinner' EXIT INT TERM
trap 'stop_spinner' EXIT INT TERM

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
# ------------------------------------------------------------------------------
# Copyright (c) 2021-2026 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 (241-244)
# * MongoDB errors (251-254)
# * Proxmox custom codes (200-231)
# - 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
}

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2021-2025 community-scripts ORG
# Copyright (c) 2021-2026 community-scripts ORG
# Author: tteck (tteckster)
# Co-Author: MickLesk
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
@@ -222,21 +222,12 @@ motd_ssh() {
# Set terminal to 256-color mode
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
# Get OS information (Debian / Ubuntu)
if [ -f "/etc/os-release" ]; then
OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
elif [ -f "/etc/debian_version" ]; then
OS_NAME="Debian"
OS_VERSION=$(cat /etc/debian_version)
fi
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
echo "echo -e \"\"" >"$PROFILE_FILE"
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
echo "echo \"\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE"

File diff suppressed because it is too large Load Diff

44
scripts/ct/debian.sh Normal file
View 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}"

View 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

View File

@@ -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

View 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>
);
}

View File

@@ -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;
}
onExecute('ssh', selectedServer);
setConfigMode(mode);
setConfigModalOpen(true);
};
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}
/>
</>
);
}

View File

@@ -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}

View File

@@ -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,

View File

@@ -416,11 +416,20 @@ export function VersionDisplay({ onOpenReleaseNotes }: VersionDisplayProps = {})
setShowUpdateConfirmation(true);
};
// Helper to generate secure random string
function getSecureRandomString(length: number): string {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
// Convert to base36 string (alphanumeric)
return Array.from(array, b => b.toString(36)).join('').substr(0, length);
}
const handleConfirmUpdate = () => {
// Close the confirmation modal
setShowUpdateConfirmation(false);
// Start the actual update process
const sessionId = `update_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const randomSuffix = getSecureRandomString(9);
const sessionId = `update_${Date.now()}_${randomSuffix}`;
const startTime = Date.now();
setIsUpdating(true);

View File

@@ -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>
)}

View File

@@ -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: []
};
}
})
});

View File

@@ -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, {