Compare commits

...

171 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
6b33afca1e refactor: remove frontend, move JSONs to json/ top-level
- Archive frontend to community-scripts/ProxmoxVE-Frontend-Archive
- Move frontend/public/json/ -> json/
- Update workflow paths in 5 actions:
  - update-versions-github.yml
  - update-json-date.yml
  - push-json-to-pocketbase.yml
  - delete-pocketbase-entry-on-removal.yml
  - autolabeler.yml
- Remove frontend-cicd.yml (no longer needed)
- Clean up .gitignore (remove frontend-specific entries)
2026-03-12 13:52:25 +01:00
community-scripts-pr-app[bot]
12bdbcce5c chore: update github-versions.json (#12811)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 12:11:11 +00:00
community-scripts-pr-app[bot]
51418f0d99 Update CHANGELOG.md (#12809)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 11:21:56 +00:00
CanbiZ (MickLesk)
3601388abe core: add mode=generated for unattended frontend installs (#12807) 2026-03-12 12:21:28 +01:00
community-scripts-pr-app[bot]
4189137f55 Update CHANGELOG.md (#12803)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:17:48 +00:00
community-scripts-pr-app[bot]
043401876b Update CHANGELOG.md (#12802)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:17:40 +00:00
community-scripts-pr-app[bot]
543b93ced0 Update CHANGELOG.md (#12801)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:17:22 +00:00
CanbiZ (MickLesk)
dd3b381813 core: validate storage availability when loading defaults (#12794) 2026-03-12 09:17:18 +01:00
CanbiZ (MickLesk)
667efeab5e SparkyFitness: install pnpm dependencies from workspace root (#12792) 2026-03-12 09:16:59 +01:00
community-scripts-pr-app[bot]
4103efd10b Update CHANGELOG.md (#12800)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:11:40 +00:00
CanbiZ (MickLesk)
00be37a151 n8n: add build-essential to update dependencies (#12795) 2026-03-12 09:11:14 +01:00
community-scripts-pr-app[bot]
c9f7453222 Update CHANGELOG.md (#12799)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:09:41 +00:00
semtex1987
7f95652e80 Frigate openvino labelmap patch (#12751)
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-03-12 09:09:12 +01:00
community-scripts-pr-app[bot]
15f6591d4c Update CHANGELOG.md (#12798)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:08:20 +00:00
community-scripts-pr-app[bot]
a530da5760 Update CHANGELOG.md (#12797)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 08:07:56 +00:00
CanbiZ (MickLesk)
cc95ef2987 tools.func: support older NVIDIA driver versions with 2 segments (xxx.xxx) (#12796) 2026-03-12 09:07:49 +01:00
CanbiZ (MickLesk)
38c9421493 tools.func: correct PATH escaping in ROCm profile script (#12793) 2026-03-12 09:07:33 +01:00
community-scripts-pr-app[bot]
5abd9170ba chore: update github-versions.json (#12791)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 06:19:42 +00:00
community-scripts-pr-app[bot]
375e526108 Update CHANGELOG.md (#12788)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 00:19:05 +00:00
community-scripts-pr-app[bot]
eb9013f1ce chore: update github-versions.json (#12787)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 00:18:39 +00:00
community-scripts-pr-app[bot]
de9168f6a7 Update CHANGELOG.md (#12785)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 22:19:00 +00:00
CanbiZ (MickLesk)
24a15d2c12 fix: Init telemetry in addon scripts (#12777)
* Init telemetry in addon scripts

Add a guarded call to init_tool_telemetry at the top of multiple tools/addon scripts (if the function is defined) and remove the previous placeholder init_tool_telemetry "" "addon" calls later in the files. This initializes telemetry earlier with the proper tool name and avoids invoking the initializer with an empty name. Affected files include: adguardhome-sync, arcane, coolify, copyparty, cronmaster, dockge, dokploy, immich-public-proxy, jellystat, komodo, nextcloud-exporter, pihole-exporter, prometheus-paperless-ngx-exporter, qbittorrent-exporter, and runtipi.

* Update umbrel-os-vm.sh
2026-03-11 23:18:35 +01:00
community-scripts-pr-app[bot]
a9ebaad37e chore: update github-versions.json (#12781)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 18:18:55 +00:00
Michel Roegl-Brunner
968e96e2c7 Delete removed scripts from pocketbase 2026-03-11 13:37:35 +01:00
community-scripts-pr-app[bot]
02b5c7f7a8 chore: update github-versions.json (#12772)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 12:12:49 +00:00
community-scripts-pr-app[bot]
f1f77e5283 Update CHANGELOG.md (#12769)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 11:03:53 +00:00
Michel Roegl-Brunner
1e726852df Tracearr: Increase default disk variable from 5 to 10 (#12762)
* Increase default disk variable from 5 to 10

* Increase HDD resource allocation from 5 to 10
2026-03-11 12:03:26 +01:00
community-scripts-pr-app[bot]
ba85fad318 Update CHANGELOG.md (#12768)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 10:43:43 +00:00
Markus Zellner
6ae5eefdf5 Add -y flag to wgd.sh update command (#12767)
Otherwise script waits, which seems like hanging if called without verbose mode
2026-03-11 11:43:18 +01:00
Michel Roegl-Brunner
b6805bb845 HotFix variable assignment syntax in coder-code-server.sh 2026-03-11 09:29:22 +01:00
community-scripts-pr-app[bot]
3117145e6c Update CHANGELOG.md (#12759)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 07:44:05 +00:00
Michel Roegl-Brunner
c144915a74 Coder-Code-Server: Check if config file exists (#12758)
* Check if config file exists

* Apply suggestion from @tremor021

---------

Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>
2026-03-11 08:43:42 +01:00
Michel Roegl-Brunner
ad1e207ee1 Add GitHub Actions workflow for Pages redirect 2026-03-11 08:37:46 +01:00
community-scripts-pr-app[bot]
f9cb07ee4c chore: update github-versions.json (#12756)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 06:17:43 +00:00
community-scripts-pr-app[bot]
f5ec5b0e47 Update CHANGELOG.md (#12754)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 00:19:03 +00:00
community-scripts-pr-app[bot]
ba8b85972e chore: update github-versions.json (#12753)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 00:18:34 +00:00
community-scripts-pr-app[bot]
766f678321 chore: update github-versions.json (#12749)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 18:17:20 +00:00
CanbiZ (MickLesk)
c69c4afd25 update rybbit link 2026-03-10 16:56:47 +01:00
Michel Roegl-Brunner
516d8d7a0f toggle is_dev to false when a new script gets merged 2026-03-10 16:07:51 +01:00
Michel Roegl-Brunner
a0c93900e9 Remove unnecessary blank line in 2fauth.sh 2026-03-10 15:45:15 +01:00
Michel Roegl-Brunner
a11f282a43 Update workflow 2026-03-10 15:44:28 +01:00
Michel Roegl-Brunner
cac6b4ec59 Check workflow to update date in Pocketbase 2026-03-10 15:39:07 +01:00
Michel Roegl-Brunner
eba96a55d2 New workflow to update last updated field in Database when a script gets changed, also adds last_update_link to link to the latest changes 2026-03-10 15:33:15 +01:00
Michel Roegl-Brunner
37f4585110 New workflow to update last updated field in Database when a script gets changed, also adds last_update_link to link to the latest changes 2026-03-10 15:33:15 +01:00
community-scripts-pr-app[bot]
e6931434b0 Update CHANGELOG.md (#12745)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 14:23:26 +00:00
Chris
eec763bed0 [Fix] Immich: Pin libvips to 8.17.3 (#12744) 2026-03-10 15:22:48 +01:00
community-scripts-pr-app[bot]
fa62363628 chore: update github-versions.json (#12743)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 12:12:48 +00:00
community-scripts-pr-app[bot]
0bbb5a1c74 chore: update github-versions.json (#12741)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 06:15:50 +00:00
community-scripts-pr-app[bot]
064e440d00 Update CHANGELOG.md (#12737)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 00:19:06 +00:00
community-scripts-pr-app[bot]
129b85a8cf chore: update github-versions.json (#12736)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 00:18:44 +00:00
community-scripts-pr-app[bot]
586154d4e1 Update CHANGELOG.md (#12734)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 22:27:24 +00:00
Bram
b819231a01 feat: add CopycatWarningToast component for user warnings (#12733)
Introduced a new CopycatWarningToast component that displays a warning about copycat sites. The toast appears at the top-center of the screen and can be dismissed, with the dismissal state stored in local storage to prevent reappearing. Integrated the component into the RootLayout for global visibility.
2026-03-09 23:27:01 +01:00
community-scripts-pr-app[bot]
3ec9eba736 chore: update github-versions.json (#12732)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 18:17:57 +00:00
community-scripts-pr-app[bot]
75d4bc2b61 Update CHANGELOG.md (#12731)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 17:59:58 +00:00
Chris
a5ac56be7a [Hotfix] qBittorrent: Disable UPnP port forwarding by default (#12728) 2026-03-09 18:59:35 +01:00
community-scripts-pr-app[bot]
fe46d8c22d Update CHANGELOG.md (#12730)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 17:39:24 +00:00
Chris
93cbd51d5b [Quickfix] Opencloud: ensure correct case for binary (#12729) 2026-03-09 18:38:58 +01:00
CanbiZ (MickLesk)
0b99873194 Add dependency check for zstd before backup
Ensure zstd dependency is installed before backup.
2026-03-09 18:36:28 +01:00
community-scripts-pr-app[bot]
8113c7da22 Update CHANGELOG.md (#12726)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 16:06:44 +00:00
CanbiZ (MickLesk)
85023dab51 Omada: Bump libssl (#12724) 2026-03-09 17:06:07 +01:00
community-scripts-pr-app[bot]
f8ab3dc4b9 Update CHANGELOG.md (#12722)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 14:23:41 +00:00
Chris
fedabe4889 Pin Opencloud to 5.2.0 (#12721) 2026-03-09 15:23:07 +01:00
community-scripts-pr-app[bot]
eba19d8e42 Update CHANGELOG.md (#12718)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 12:52:21 +00:00
CanbiZ (MickLesk)
e180a3bc44 openwebui: Ensure required dependencies (#12717)
* openwebui: Ensure required dependencies

Added zstd and build-essential as dependencies for the script.

* Update dependencies in openwebui-install.sh

Added build-essential and libmariadb-dev to dependencies.
2026-03-09 13:51:59 +01:00
community-scripts-pr-app[bot]
a1a465708f Update CHANGELOG.md (#12716)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 12:24:37 +00:00
CanbiZ (MickLesk)
346d6c6a0a feat: improve zigbee2mqtt backup handler (#12714)
- Name backups by installed version (e.g. Zigbee2MQTT_backup_2.5.1.tar.zst)
- Use zstd compression instead of gzip
- Keep last 5 backups instead of deleting all previous ones
2026-03-09 13:24:09 +01:00
community-scripts-pr-app[bot]
c76813cbcb chore: update github-versions.json (#12715)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 12:12:25 +00:00
CanbiZ (MickLesk)
e7f551dab6 fix(hwaccel): install ROCm runtime only, reduce disk resize to +4GB
The full 'rocm' meta-package includes 15GB+ of dev tools (compilers,
debuggers, dev headers) which are unnecessary in LXC containers.
Install only runtime packages: rocm-opencl-runtime, rocm-hip-runtime,
rocm-smi-lib. Reduce disk resize from +8GB to +4GB accordingly.
2026-03-09 11:14:26 +01:00
CanbiZ (MickLesk)
d8b2a37228 fix(build): auto-resize disk +8GB when AMD GPU detected for ROCm 2026-03-09 10:18:23 +01:00
community-scripts-pr-app[bot]
af3950fafc Update CHANGELOG.md (#12710)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 09:03:30 +00:00
CanbiZ (MickLesk)
b20bf9c658 tools: add Alpine (apk) support to ensure_dependencies and is_package_installed (#12703) 2026-03-09 10:03:04 +01:00
CanbiZ (MickLesk)
8c5e340ad0 fix(hwaccel): use amdgpu/latest/ubuntu instead of versioned URL 2026-03-09 09:57:28 +01:00
community-scripts-pr-app[bot]
f3cd063816 Update CHANGELOG.md (#12709)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 08:48:16 +00:00
CanbiZ (MickLesk)
5adfa3cb45 Frigate: try an OpenVino model build fallback (#12704)
Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-03-09 09:47:50 +01:00
community-scripts-pr-app[bot]
3398fe9361 Update CHANGELOG.md (#12708)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 08:37:47 +00:00
CanbiZ (MickLesk)
2afc25d51f tools.func: extend hwaccel with ROCm (#12707) 2026-03-09 09:37:26 +01:00
CanbiZ (MickLesk)
8c5d5c6679 Downgrade Node.js version from 24 to 22 2026-03-09 09:13:45 +01:00
community-scripts-pr-app[bot]
fca66c5b56 Update CHANGELOG.md (#12706)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 08:05:40 +00:00
CanbiZ (MickLesk)
d38ca1a7fc Reactive Resume: rewrite for v5 using original repo amruthpilla/reactive-resume (#12705)
* fix(reactive-resume): rewrite for v5 using original repo amruthpillai/reactive-resume

Replaces lazy-media fork with original upstream repo (amruthpillai/reactive-resume).
Rewrites install script for v5 architecture:
- TanStack Start / Drizzle ORM single-package build
- Headless Chromium for PDF generation (replaces Browserless)
- Local filesystem storage (removes MinIO dependency)
- Node 24 + pnpm
- Runtime: node .output/server/index.mjs

Fixes #12672, Fixes #11651

* add clean_install
2026-03-09 09:05:14 +01:00
community-scripts-pr-app[bot]
b3bedd720f Update CHANGELOG.md (#12702)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 07:06:16 +00:00
Nícolas Pastorello
b0858920d5 Change cronjob setup to use www-data user (#12695) 2026-03-09 08:05:50 +01:00
community-scripts-pr-app[bot]
e4e365b701 Update CHANGELOG.md (#12701)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 06:35:35 +00:00
Slaviša Arežina
c4315713b5 Fix check_for_gh_release function call (#12694) 2026-03-09 07:35:11 +01:00
community-scripts-pr-app[bot]
047ea2c66d chore: update github-versions.json (#12700)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 06:22:52 +00:00
community-scripts-pr-app[bot]
5e6eb400b5 Update CHANGELOG.md (#12697)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 00:21:57 +00:00
community-scripts-pr-app[bot]
db7880cab5 chore: update github-versions.json (#12696)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 00:21:34 +00:00
community-scripts-pr-app[bot]
3909095a2c Update CHANGELOG.md (#12693)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 23:05:51 +00:00
Chris
6685b88695 [Fix] Immich: chown install dir before machine-learning update (#12684) 2026-03-09 00:05:29 +01:00
community-scripts-pr-app[bot]
6076a7ecc7 Update CHANGELOG.md (#12692)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 23:00:50 +00:00
Chris
cc351a4817 [Fix] Scanopy: Build generate-fixtures (#12686) 2026-03-09 00:00:30 +01:00
community-scripts-pr-app[bot]
9217a0fb79 chore: update github-versions.json (#12688)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 18:07:32 +00:00
community-scripts-pr-app[bot]
5abaa2e7e3 Update CHANGELOG.md (#12685)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 15:13:19 +00:00
Slaviša Arežina
c3b8285584 Change slug from 'lxc-execute' to 'execute' (#12681) 2026-03-08 16:12:53 +01:00
community-scripts-pr-app[bot]
bf2667827b Update CHANGELOG.md (#12683)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 14:09:52 +00:00
Tobias
3b7283a13f fix: rustdeskserver: use correct repo string (#12682) 2026-03-08 15:09:28 +01:00
community-scripts-pr-app[bot]
5aaca69e91 Update CHANGELOG.md (#12680)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 12:25:47 +00:00
Slaviša Arežina
8be52ab1ad Fixes (#12675) 2026-03-08 13:25:24 +01:00
community-scripts-pr-app[bot]
447fe2c2e3 chore: update github-versions.json (#12678)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 12:08:17 +00:00
community-scripts-pr-app[bot]
0ecbbdf669 Update .app files (#12661)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-08 08:47:53 +01:00
community-scripts-pr-app[bot]
efce5888d7 chore: update github-versions.json (#12668)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 06:14:07 +00:00
community-scripts-pr-app[bot]
ab93083a9d chore: update github-versions.json (#12665)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 00:21:31 +00:00
community-scripts-pr-app[bot]
f757401d65 Update CHANGELOG.md (#12664)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 00:05:04 +00:00
community-scripts-pr-app[bot]
1e5d52098c Archive old changelog entries (#12663)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 00:04:39 +00:00
community-scripts-pr-app[bot]
152b703389 Update CHANGELOG.md (#12662)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 22:47:16 +00:00
community-scripts-pr-app[bot]
b0cf05fa5f Update CHANGELOG.md (#12660)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 22:47:00 +00:00
community-scripts-pr-app[bot]
e07f2ff160 Update date in json (#12659)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-07 22:46:53 +00:00
push-app-to-main[bot]
2bb60e2cec ImmichFrame (#12653)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
Co-authored-by: MickLesk <mickey.leskowitz@gmail.com>
Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>
Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-03-07 23:46:36 +01:00
community-scripts-pr-app[bot]
f0d070b857 Update CHANGELOG.md (#12658)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 21:15:02 +00:00
community-scripts-pr-app[bot]
732de1a5bc Update CHANGELOG.md (#12657)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 21:14:52 +00:00
community-scripts-pr-app[bot]
5d7701275b Update CHANGELOG.md (#12656)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 21:14:39 +00:00
CanbiZ (MickLesk)
8f557e460d fix(grocy): update PHP version from 8.3 to 8.5 (#12651)
Grocy now requires PHP 8.5. Update both the install script and the
update function to use PHP 8.5 instead of 8.3.

Closes #12647
2026-03-07 22:14:29 +01:00
CanbiZ (MickLesk)
148f0121df fix: add interactive GitHub PAT prompt on rate limit / auth failure (#12652)
When a GitHub API call fails with HTTP 401 (invalid token) or HTTP 403
(rate limit exceeded), the user is now prompted interactively to enter a
GitHub Personal Access Token (PAT). The token is validated (no empty
input, no whitespace) before being set and the API call is retried.

This applies to both github_api_call() and fetch_and_deploy_gh_release().

Closes #12615
2026-03-07 22:14:18 +01:00
community-scripts-pr-app[bot]
8eb2e58891 Update CHANGELOG.md (#12655)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 21:13:59 +00:00
community-scripts-pr-app[bot]
3ac7216e63 Update CHANGELOG.md (#12654)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 21:13:39 +00:00
CanbiZ (MickLesk)
80f58e1e07 fix(papra): update repository URL to papra-hq/papra (#12650)
The Papra repository moved from CorentinTh/papra to papra-hq/papra.
Update documentation and website URLs in the JSON config to avoid 404s.

Closes #12609
2026-03-07 22:13:34 +01:00
Markus Zellner
5446706286 Check for influxdb3 installation in update_script (#12648)
Fix current check only working for influxdb 1.x and 2.x
2026-03-07 22:13:18 +01:00
community-scripts-pr-app[bot]
79ed7e4b73 Update CHANGELOG.md (#12646)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 18:21:27 +00:00
Sam Heinz
94d95ac5d2 Update Rdtclient to dotnet 10.0 (#12638)
* rdtclient: Upgrade aspnetcore-runtime from 9.0 to 10.0

* rdtclient: change update function to grab dotnet 10.0
2026-03-07 19:21:04 +01:00
community-scripts-pr-app[bot]
a39b457888 chore: update github-versions.json (#12645)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 18:06:50 +00:00
community-scripts-pr-app[bot]
94ff34d0df Update CHANGELOG.md (#12644)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 14:44:57 +00:00
Copilot
ff4648b7f3 fix(immich): fix update script failing to add Debian testing repo when preferences file already exists (#12631)
* Initial plan

* fix(immich): fix testing repo not being added during update when preferences file already exists

Co-authored-by: michelroegl-brunner <73236783+michelroegl-brunner@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: michelroegl-brunner <73236783+michelroegl-brunner@users.noreply.github.com>
2026-03-07 15:44:33 +01:00
community-scripts-pr-app[bot]
acedb5fb55 chore: update github-versions.json (#12643)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 12:08:09 +00:00
community-scripts-pr-app[bot]
4bd39e7bae chore: update github-versions.json (#12640)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 06:10:26 +00:00
community-scripts-pr-app[bot]
5c2cf61455 Update CHANGELOG.md (#12636)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 00:20:28 +00:00
community-scripts-pr-app[bot]
10b47eae33 chore: update github-versions.json (#12635)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 00:20:03 +00:00
community-scripts-pr-app[bot]
c61b0e766a Update CHANGELOG.md (#12629)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 20:54:29 +00:00
Slaviša Arežina
9ff1d088c5 Semaphore: Move from BoltDB to SQLite (#12624)
* Fix

* start

* Apply suggestion from @tremor021
2026-03-06 21:53:47 +01:00
community-scripts-pr-app[bot]
be803ced6f Update CHANGELOG.md (#12626)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 20:07:11 +00:00
Slaviša Arežina
5949e9e32e Fix (#12625) 2026-03-06 21:06:45 +01:00
community-scripts-pr-app[bot]
bb3276bbbd Update CHANGELOG.md (#12623)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 19:44:23 +00:00
Aurélien
b7a09989cb fix(node-red): restart service after update (#12621) 2026-03-06 20:43:58 +01:00
community-scripts-pr-app[bot]
039d046649 chore: update github-versions.json (#12622)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 18:13:22 +00:00
community-scripts-pr-app[bot]
58301651e4 Update CHANGELOG.md (#12618)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 15:17:25 +00:00
Tobias
b848c2d1bf wealthfolio: update cors (#12617)
* Update wealthfolio-install.sh

* Update wealthfolio.sh
2026-03-06 16:17:00 +01:00
CanbiZ (MickLesk)
9e2bb23d35 fix(wealthfolio): pin to v3.0.3 due to breaking changes 2026-03-06 14:16:54 +01:00
CanbiZ (MickLesk)
eb848fd70f databasus: make fetch_and_deploy_from_url accept empty dir
Pass an explicit empty second argument when downloading MongoDB tools and update fetch_and_deploy_from_url to default the directory parameter to an empty string. This avoids unset/ambiguous directory handling at the call site and ensures the function has a defined directory variable even when none is provided.
2026-03-06 14:11:57 +01:00
community-scripts-pr-app[bot]
176fffff0b Update CHANGELOG.md (#12616)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 13:04:52 +00:00
Slaviša Arežina
14e13edeef Add better update handling (#12611) 2026-03-06 14:04:23 +01:00
community-scripts-pr-app[bot]
4f70196444 chore: update github-versions.json (#12613)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 12:10:47 +00:00
community-scripts-pr-app[bot]
acc715de82 Update CHANGELOG.md (#12610)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 09:21:14 +00:00
Slaviša Arežina
5bfb5f486f RustDesk Server: Switch to updated repository (#12083)
* switch repo

* Add info about Alpine admin password and script usage

---------

Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-03-06 10:20:46 +01:00
Michel Roegl-Brunner
6fbd785a87 Change runner to self-hosted for PR checks 2026-03-06 09:09:56 +01:00
community-scripts-pr-app[bot]
75b1c63884 chore: update github-versions.json (#12608)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 06:16:34 +00:00
community-scripts-pr-app[bot]
ad0808008a Update CHANGELOG.md (#12607)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 00:25:50 +00:00
community-scripts-pr-app[bot]
fcf75b06bf chore: update github-versions.json (#12606)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 00:25:26 +00:00
community-scripts-pr-app[bot]
b8d47cdce3 Update CHANGELOG.md (#12604)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 21:30:45 +00:00
Copilot
8301f04b58 fluid-calendar: add build-essential to install and update dependencies (#12602)
* Initial plan

* fix: add build-essential dependency to fluid-calendar install script

Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>

* fix: add ensure_dependencies build-essential in update_script

Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>
2026-03-05 22:30:20 +01:00
community-scripts-pr-app[bot]
34942ac799 chore: update github-versions.json (#12600)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 18:38:18 +00:00
community-scripts-pr-app[bot]
599f11683d Update CHANGELOG.md (#12599)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 18:10:05 +00:00
community-scripts-pr-app[bot]
55c699f841 Update CHANGELOG.md (#12598)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 18:00:27 +00:00
CanbiZ (MickLesk)
3a7f922d90 remove(suwayomiserver): remove due to inactivity (#12596)
Removed due to very low usage (3 installs in 120 days).
An alternative will be provided. Thanks for your understanding.
2026-03-05 18:57:51 +01:00
Chris
352716319f Refactor: BentoPDF (#12597)
* Refactor BentoPDF

* preserve .env.production if it exists

* Increase CPU cores to 2
2026-03-05 18:53:15 +01:00
community-scripts-pr-app[bot]
5162f13372 Update .app files (#12591)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-05 16:06:55 +01:00
community-scripts-pr-app[bot]
cbe8cc82c6 Update CHANGELOG.md (#12595)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:59:13 +00:00
community-scripts-pr-app[bot]
c02010ea58 Update date in json (#12594)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:59:10 +00:00
community-scripts-pr-app[bot]
45e39b21ec Update CHANGELOG.md (#12593)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:58:59 +00:00
push-app-to-main[bot]
7cbaf9c055 ddclient (#12587)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-03-05 15:58:47 +01:00
community-scripts-pr-app[bot]
555f18d8df Update CHANGELOG.md (#12592)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:58:36 +00:00
push-app-to-main[bot]
f915c68a4b Netbird (#12585)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-03-05 15:58:10 +01:00
community-scripts-pr-app[bot]
da9ddcb382 Update CHANGELOG.md (#12590)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:56:31 +00:00
Slaviša Arežina
55e6283075 Tianji: Fix the bug introduced by the refactor (#12564) 2026-03-05 15:56:02 +01:00
community-scripts-pr-app[bot]
e56b9dce35 Update CHANGELOG.md (#12589)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:28:32 +00:00
community-scripts-pr-app[bot]
363be950f0 Update CHANGELOG.md (#12588)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:28:09 +00:00
CanbiZ (MickLesk)
0f1a06ca32 core: add var_os / var_version to whitelist for app.vars (#12576)
Include var_os and var_version in VAR_WHITELIST across load_vars_file, default_var_settings, and the global whitelist declaration. Also emit sanitized var_os and var_version in _build_current_app_vars_tmp so they are included in generated app variable output and can be recognized/persisted by the build tooling.
2026-03-05 15:28:07 +01:00
CanbiZ (MickLesk)
24957ea881 fix(powerdns): use 'launch=' instead of 'launch+=' for gsqlite3 backend (#12579)
PowerDNS 5.x requires a parent 'launch=' directive before any
incremental 'launch+=' can be used. Since the install script
comments out the default 'launch=' in pdns.conf, the 'launch+='
in gsqlite3.conf fails with:
  Fatal error: Incremental setting 'launch' without a parent

Use 'launch=gsqlite3' (non-incremental) since gsqlite3 is the
only backend needed.
2026-03-05 15:27:40 +01:00
community-scripts-pr-app[bot]
0be065bbef Update .app files (#12583)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-05 14:13:16 +01:00
community-scripts-pr-app[bot]
10118f4350 Update CHANGELOG.md (#12584)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 12:51:11 +00:00
community-scripts-pr-app[bot]
ac435455a0 Update CHANGELOG.md (#12582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 12:50:46 +00:00
community-scripts-pr-app[bot]
929cd08718 Update date in json (#12581)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-05 12:50:42 +00:00
push-app-to-main[bot]
299e7bfabe Papra (#12577)
* Add papra (ct)

* Update papra.sh

* Enhance update_script with configuration backup

Added backup and restore of configuration during update process.

* Update install/papra-install.sh

Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>

* Format

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>
Co-authored-by: tremor021 <arezina.slavisa@gmail.com>
2026-03-05 13:50:20 +01:00
community-scripts-pr-app[bot]
3465f053f5 chore: update github-versions.json (#12580)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 12:12:38 +00:00
689 changed files with 2433 additions and 12341 deletions

183
.github/changelogs/2026/03.md generated vendored Normal file
View File

@@ -0,0 +1,183 @@
## 2026-03-07
### 🆕 New Scripts
- ImmichFrame ([#12653](https://github.com/community-scripts/ProxmoxVE/pull/12653))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Grocy: bump PHP version from 8.3 to 8.5 [@MickLesk](https://github.com/MickLesk) ([#12651](https://github.com/community-scripts/ProxmoxVE/pull/12651))
- Check for influxdb3 installation in update_script [@odin568](https://github.com/odin568) ([#12648](https://github.com/community-scripts/ProxmoxVE/pull/12648))
- Update Rdtclient to dotnet 10.0 [@asylumexp](https://github.com/asylumexp) ([#12638](https://github.com/community-scripts/ProxmoxVE/pull/12638))
- fix(immich): fix update script failing to add Debian testing repo when preferences file already exists [@Copilot](https://github.com/Copilot) ([#12631](https://github.com/community-scripts/ProxmoxVE/pull/12631))
### 💾 Core
- #### ✨ New Features
- tools: add interactive GitHub PAT prompt on rate limit / auth failure [@MickLesk](https://github.com/MickLesk) ([#12652](https://github.com/community-scripts/ProxmoxVE/pull/12652))
### 🌐 Website
- #### 📝 Script Information
- Papra: update repository URL to papra-hq/papra [@MickLesk](https://github.com/MickLesk) ([#12650](https://github.com/community-scripts/ProxmoxVE/pull/12650))
## 2026-03-06
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- RustDesk Server: Fix update script [@tremor021](https://github.com/tremor021) ([#12625](https://github.com/community-scripts/ProxmoxVE/pull/12625))
- [Node-RED] Restart service after update [@Aurelien30000](https://github.com/Aurelien30000) ([#12621](https://github.com/community-scripts/ProxmoxVE/pull/12621))
- wealthfolio: update cors [@CrazyWolf13](https://github.com/CrazyWolf13) ([#12617](https://github.com/community-scripts/ProxmoxVE/pull/12617))
- CryptPad: Better update handling [@tremor021](https://github.com/tremor021) ([#12611](https://github.com/community-scripts/ProxmoxVE/pull/12611))
- #### ✨ New Features
- RustDesk Server: Switch to updated repository [@tremor021](https://github.com/tremor021) ([#12083](https://github.com/community-scripts/ProxmoxVE/pull/12083))
- #### 💥 Breaking Changes
- Semaphore: Move from BoltDB to SQLite [@tremor021](https://github.com/tremor021) ([#12624](https://github.com/community-scripts/ProxmoxVE/pull/12624))
## 2026-03-05
### 🆕 New Scripts
- ddclient ([#12587](https://github.com/community-scripts/ProxmoxVE/pull/12587))
- Netbird ([#12585](https://github.com/community-scripts/ProxmoxVE/pull/12585))
- Papra ([#12577](https://github.com/community-scripts/ProxmoxVE/pull/12577))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- fluid-calendar: add build-essential to install and update dependencies [@Copilot](https://github.com/Copilot) ([#12602](https://github.com/community-scripts/ProxmoxVE/pull/12602))
- Refactor: BentoPDF [@vhsdream](https://github.com/vhsdream) ([#12597](https://github.com/community-scripts/ProxmoxVE/pull/12597))
- Tianji: Fix the bug introduced by the refactor [@tremor021](https://github.com/tremor021) ([#12564](https://github.com/community-scripts/ProxmoxVE/pull/12564))
- PowerDNS: use 'launch=' instead of 'launch+=' for gsqlite3 backend [@MickLesk](https://github.com/MickLesk) ([#12579](https://github.com/community-scripts/ProxmoxVE/pull/12579))
### 🗑️ Deleted Scripts
- Suwayomi-Server: remove due to inactivity and very low usage [@MickLesk](https://github.com/MickLesk) ([#12596](https://github.com/community-scripts/ProxmoxVE/pull/12596))
### 💾 Core
- #### 🔧 Refactor
- core: add var_os / var_version to whitelist for app.vars [@MickLesk](https://github.com/MickLesk) ([#12576](https://github.com/community-scripts/ProxmoxVE/pull/12576))
## 2026-03-04
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- fix: gitea-mirror [@CrazyWolf13](https://github.com/CrazyWolf13) ([#12549](https://github.com/community-scripts/ProxmoxVE/pull/12549))
- fix(immich): correct LibRaw clone URL to official upstream [@DenislavDenev](https://github.com/DenislavDenev) ([#12526](https://github.com/community-scripts/ProxmoxVE/pull/12526))
- update: stirling-pdf: java 25 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#12552](https://github.com/community-scripts/ProxmoxVE/pull/12552))
- Docmost: register NoopAuditService globally when EE submodule is missing [@MickLesk](https://github.com/MickLesk) ([#12551](https://github.com/community-scripts/ProxmoxVE/pull/12551))
- jellyseer/overseer migration corrupting /usr/bin/update [@MickLesk](https://github.com/MickLesk) ([#12539](https://github.com/community-scripts/ProxmoxVE/pull/12539))
- PowerDNS: use gsqlite3 backend instead of BIND [@MickLesk](https://github.com/MickLesk) ([#12538](https://github.com/community-scripts/ProxmoxVE/pull/12538))
- addon migrations: /usr/bin/update replacement to prevent syntax error [@MickLesk](https://github.com/MickLesk) ([#12540](https://github.com/community-scripts/ProxmoxVE/pull/12540))
- #### 🔧 Refactor
- Fluid-Calendar: NodeJS bump [@tremor021](https://github.com/tremor021) ([#12558](https://github.com/community-scripts/ProxmoxVE/pull/12558))
- Refactor: LiteLLM [@tremor021](https://github.com/tremor021) ([#12550](https://github.com/community-scripts/ProxmoxVE/pull/12550))
### 💾 Core
- #### 🐞 Bug Fixes
- tools: fall back to distro packages for psql [@MickLesk](https://github.com/MickLesk) ([#12542](https://github.com/community-scripts/ProxmoxVE/pull/12542))
- fix: whitelist var_searchdomain and fix the handling of var_ns and va… [@tommoyer](https://github.com/tommoyer) ([#12521](https://github.com/community-scripts/ProxmoxVE/pull/12521))
## 2026-03-03
### 🆕 New Scripts
- Tinyauth: v5 Support & add Debian Version [@MickLesk](https://github.com/MickLesk) ([#12501](https://github.com/community-scripts/ProxmoxVE/pull/12501))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- cross-seed: install build-essential to resolve missing `make` error [@Copilot](https://github.com/Copilot) ([#12522](https://github.com/community-scripts/ProxmoxVE/pull/12522))
- meshcentral: increased disk space to 4GB [@MickLesk](https://github.com/MickLesk) ([#12509](https://github.com/community-scripts/ProxmoxVE/pull/12509))
- #### 🔧 Refactor
- opnsense-vm: harden temp dir, bridge detection and network selection [@MickLesk](https://github.com/MickLesk) ([#12513](https://github.com/community-scripts/ProxmoxVE/pull/12513))
### 🗑️ Deleted Scripts
- Remove Unifi Network Server scripts (dead APT repo) [@Copilot](https://github.com/Copilot) ([#12500](https://github.com/community-scripts/ProxmoxVE/pull/12500))
### 💾 Core
- #### ✨ New Features
- core: recovery - add ENOSPC disk-full detection with auto-retry using * 2 hdd [@MickLesk](https://github.com/MickLesk) ([#12511](https://github.com/community-scripts/ProxmoxVE/pull/12511))
### 📚 Documentation
- Fix config_path casing in reactive-resume.json [@ScubyG](https://github.com/ScubyG) ([#12525](https://github.com/community-scripts/ProxmoxVE/pull/12525))
### 🌐 Website
- #### 🐞 Bug Fixes
- Revert #11534 PR that messed up search [@BramSuurdje](https://github.com/BramSuurdje) ([#12492](https://github.com/community-scripts/ProxmoxVE/pull/12492))
## 2026-03-02
### 🆕 New Scripts
- PowerDNS ([#12481](https://github.com/community-scripts/ProxmoxVE/pull/12481))
- Profilarr ([#12441](https://github.com/community-scripts/ProxmoxVE/pull/12441))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Tracearr: prepare for imminent v1.4.19 release [@durzo](https://github.com/durzo) ([#12413](https://github.com/community-scripts/ProxmoxVE/pull/12413))
- #### ✨ New Features
- Frigate: Bump to v0.17 [@MickLesk](https://github.com/MickLesk) ([#12474](https://github.com/community-scripts/ProxmoxVE/pull/12474))
- #### 💥 Breaking Changes
- Migrate: DokPloy, Komodo, Coolify, Dockge, Runtipi to Addons [@MickLesk](https://github.com/MickLesk) ([#12275](https://github.com/community-scripts/ProxmoxVE/pull/12275))
- #### 🔧 Refactor
- ref: replace generic exit 1 with specific exit codes in ct & install [@MickLesk](https://github.com/MickLesk) ([#12475](https://github.com/community-scripts/ProxmoxVE/pull/12475))
### 💾 Core
- #### ✨ New Features
- tools.func: Improve stability with retry logic, caching, and debug mode [@MickLesk](https://github.com/MickLesk) ([#10351](https://github.com/community-scripts/ProxmoxVE/pull/10351))
- #### 🔧 Refactor
- core: standardize exit codes and add mappings [@MickLesk](https://github.com/MickLesk) ([#12467](https://github.com/community-scripts/ProxmoxVE/pull/12467))
### 🌐 Website
- frontend: improve detail view badges, addon texts, and HTML title [@MickLesk](https://github.com/MickLesk) ([#12461](https://github.com/community-scripts/ProxmoxVE/pull/12461))
## 2026-03-01
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Sparkyfitness: use pnpm [@tomfrenzel](https://github.com/tomfrenzel) ([#12445](https://github.com/community-scripts/ProxmoxVE/pull/12445))
- OpenArchiver: Fix installation [@tremor021](https://github.com/tremor021) ([#12447](https://github.com/community-scripts/ProxmoxVE/pull/12447))

2
.github/workflows/autolabeler.yml generated vendored
View File

@@ -93,7 +93,7 @@ jobs:
const websiteRegex = new RegExp(`- \\[(x|X)\\]\\s*${escapedWebsite}`, "i");
if (websiteRegex.test(prBody)) {
const hasJson = prFiles.some((f) => f.filename.startsWith("frontend/public/json/"));
const hasJson = prFiles.some((f) => f.filename.startsWith("json/"));
const hasUpdateScript = labelsToAdd.has("update script");
const hasContentLabel = ["bugfix", "feature", "refactor"].some((l) => labelsToAdd.has(l));

View File

@@ -8,7 +8,7 @@ on:
jobs:
check-new-script:
if: github.repository == 'community-scripts/ProxmoxVE'
runs-on: coolify-runner
runs-on: self-hosted
permissions:
pull-requests: write
contents: read

View File

@@ -6,14 +6,14 @@ on:
jobs:
close_issue:
if: github.event.pull_request.merged == true && github.repository == 'community-scripts/ProxmoxVE'
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Checkout target repo (main)
- name: Checkout target repo (merge commit)
uses: actions/checkout@v4
with:
repository: community-scripts/ProxmoxVE
ref: main
ref: ${{ github.event.pull_request.merge_commit_sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract and Process PR Title
@@ -23,6 +23,39 @@ jobs:
echo "Processed Title: $title"
echo "title=$title" >> $GITHUB_ENV
- name: Get slugs from merged PR
id: get_slugs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pr_files=$(gh pr view ${{ github.event.pull_request.number }} --repo community-scripts/ProxmoxVE --json files -q '.files[].path' 2>/dev/null || true)
slugs=""
for path in $pr_files; do
[[ -f "$path" ]] || continue
if [[ "$path" == frontend/public/json/*.json ]]; then
s=$(jq -r '.slug // empty' "$path" 2>/dev/null)
[[ -n "$s" ]] && slugs="$slugs $s"
elif [[ "$path" == ct/*.sh ]] || [[ "$path" == install/*.sh ]] || [[ "$path" == tools/*.sh ]] || [[ "$path" == turnkey/*.sh ]] || [[ "$path" == vm/*.sh ]]; then
base=$(basename "$path" .sh)
if [[ "$path" == install/* && "$base" == *-install ]]; then
s="${base%-install}"
else
s="$base"
fi
[[ -n "$s" ]] && slugs="$slugs $s"
fi
done
slugs=$(echo $slugs | xargs -n1 | sort -u | tr '\n' ' ')
if [[ -z "$slugs" && -n "$title" ]]; then
slugs="$title"
fi
if [[ -z "$slugs" ]]; then
echo "count=0" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "$slugs" > pocketbase_slugs.txt
echo "count=$(echo $slugs | wc -w)" >> "$GITHUB_OUTPUT"
- name: Search for Issues with Similar Titles
id: find_issue
env:
@@ -63,3 +96,104 @@ jobs:
run: |
gh issue comment $issue_number --repo community-scripts/ProxmoxVED --body "Merged with #${{ github.event.pull_request.number }} in ProxmoxVE"
gh issue close $issue_number --repo community-scripts/ProxmoxVED
- name: Set is_dev to false in PocketBase
if: steps.get_slugs.outputs.count != '0'
env:
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
PR_URL: ${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.pull_request.number }}
run: |
node << 'ENDSCRIPT'
(async function() {
const fs = require('fs');
const https = require('https');
const http = require('http');
const url = require('url');
function request(fullUrl, opts) {
return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
const body = opts.body;
const options = {
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.path,
method: opts.method || 'GET',
headers: opts.headers || {}
};
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function(res) {
let data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
const slugsText = fs.readFileSync('pocketbase_slugs.txt', 'utf8').trim();
const slugs = slugsText ? slugsText.split(/\s+/).filter(Boolean) : [];
if (slugs.length === 0) {
console.log('No slugs to update.');
return;
}
const authUrl = apiBase + '/collections/users/auth-with-password';
const authBody = JSON.stringify({
identity: process.env.POCKETBASE_ADMIN_EMAIL,
password: process.env.POCKETBASE_ADMIN_PASSWORD
});
const authRes = await request(authUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: authBody
});
if (!authRes.ok) {
throw new Error('Auth failed: ' + authRes.body);
}
const token = JSON.parse(authRes.body).token;
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
const prUrl = process.env.PR_URL || '';
for (const slug of slugs) {
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
const record = list.items && list.items[0];
if (!record) {
console.log('Slug not in DB, skipping: ' + slug);
continue;
}
const patchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: record.name || record.slug,
last_update_commit: prUrl,
is_dev: false
})
});
if (!patchRes.ok) {
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
continue;
}
console.log('Set is_dev=false for slug: ' + slug);
}
console.log('Done.');
})().catch(e => { console.error(e); process.exit(1); });
ENDSCRIPT
shell: bash

View File

@@ -0,0 +1,150 @@
name: Delete PocketBase entry on script/JSON removal
on:
push:
branches:
- main
paths:
- "json/**"
- "vm/**"
- "tools/**"
- "turnkey/**"
- "ct/**"
- "install/**"
jobs:
delete-pocketbase-entry:
runs-on: self-hosted
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get slugs from deleted JSON and script files
id: slugs
run: |
BEFORE="${{ github.event.before }}"
AFTER="${{ github.event.after }}"
slugs=""
# Deleted JSON files: get slug from previous commit
deleted_json=$(git diff --name-only --diff-filter=D "$BEFORE" "$AFTER" -- json/ | grep '\.json$' || true)
for f in $deleted_json; do
[[ -z "$f" ]] && continue
s=$(git show "$BEFORE:$f" 2>/dev/null | jq -r '.slug // empty' 2>/dev/null || true)
[[ -n "$s" ]] && slugs="$slugs $s"
done
# Deleted script files: derive slug from path
deleted_sh=$(git diff --name-only --diff-filter=D "$BEFORE" "$AFTER" -- ct/ install/ tools/ turnkey/ vm/ | grep '\.sh$' || true)
for f in $deleted_sh; do
[[ -z "$f" ]] && continue
base="${f##*/}"
base="${base%.sh}"
if [[ "$f" == install/* && "$base" == *-install ]]; then
s="${base%-install}"
else
s="$base"
fi
[[ -n "$s" ]] && slugs="$slugs $s"
done
slugs=$(echo $slugs | xargs -n1 | sort -u | tr '\n' ' ')
if [[ -z "$slugs" ]]; then
echo "No deleted JSON or script files to remove from PocketBase."
echo "count=0" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "$slugs" > slugs_to_delete.txt
echo "count=$(echo $slugs | wc -w)" >> "$GITHUB_OUTPUT"
echo "Slugs to delete: $slugs"
- name: Delete from PocketBase
if: steps.slugs.outputs.count != '0'
env:
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
run: |
node << 'ENDSCRIPT'
(async function() {
const fs = require('fs');
const https = require('https');
const http = require('http');
const url = require('url');
function request(fullUrl, opts) {
return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
const body = opts.body;
const options = {
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.path,
method: opts.method || 'GET',
headers: opts.headers || {}
};
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function(res) {
let data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
const slugs = fs.readFileSync('slugs_to_delete.txt', 'utf8').trim().split(/\s+/).filter(Boolean);
const authUrl = apiBase + '/collections/users/auth-with-password';
const authBody = JSON.stringify({
identity: process.env.POCKETBASE_ADMIN_EMAIL,
password: process.env.POCKETBASE_ADMIN_PASSWORD
});
const authRes = await request(authUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: authBody
});
if (!authRes.ok) {
throw new Error('Auth failed. Response: ' + authRes.body);
}
const token = JSON.parse(authRes.body).token;
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
for (const slug of slugs) {
const filter = "(slug='" + slug + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
const existingId = list.items && list.items[0] && list.items[0].id;
if (!existingId) {
console.log('No PocketBase record for slug "' + slug + '", skipping.');
continue;
}
const delRes = await request(recordsUrl + '/' + existingId, {
method: 'DELETE',
headers: { 'Authorization': token }
});
if (delRes.ok) {
console.log('Deleted PocketBase record for slug "' + slug + '" (id=' + existingId + ').');
} else {
console.warn('DELETE failed for slug "' + slug + '": ' + delRes.statusCode + ' ' + delRes.body);
}
}
console.log('Done.');
})().catch(e => { console.error(e); process.exit(1); });
ENDSCRIPT
shell: bash

147
.github/workflows/frontend-cicd.yml generated vendored
View File

@@ -1,147 +0,0 @@
# Based on https://github.com/actions/starter-workflows/blob/main/pages/nextjs.yml
name: Frontend CI/CD
on:
push:
branches: ["main"]
paths:
- frontend/**
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened, edited]
paths:
- frontend/**
workflow_dispatch:
permissions:
contents: read
concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: false
jobs:
test-json-files:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Test JSON files
run: |
python3 << 'EOF'
import json
import glob
import os
import sys
def test_json_files():
# Change to the correct directory
json_dir = "public/json"
if not os.path.exists(json_dir):
print(f"❌ Directory not found: {json_dir}")
return False
# Find all JSON files
pattern = os.path.join(json_dir, "*.json")
json_files = glob.glob(pattern)
if not json_files:
print(f"⚠️ No JSON files found in {json_dir}")
return True
print(f"Testing {len(json_files)} JSON files for valid syntax...")
invalid_files = []
for file_path in json_files:
try:
with open(file_path, 'r', encoding='utf-8') as f:
json.load(f)
print(f"✅ Valid JSON: {file_path}")
except json.JSONDecodeError as e:
print(f"❌ Invalid JSON syntax in: {file_path}")
print(f" Error: {e}")
invalid_files.append(file_path)
except Exception as e:
print(f"⚠️ Error reading: {file_path}")
print(f" Error: {e}")
invalid_files.append(file_path)
print("\n=== JSON Validation Summary ===")
print(f"Total files tested: {len(json_files)}")
print(f"Valid files: {len(json_files) - len(invalid_files)}")
print(f"Invalid files: {len(invalid_files)}")
if invalid_files:
print("\n❌ Found invalid JSON file(s):")
for file_path in invalid_files:
print(f" - {file_path}")
return False
else:
print("\n✅ All JSON files have valid syntax!")
return True
if __name__ == "__main__":
success = test_json_files()
sys.exit(0 if success else 1)
EOF
build:
if: github.repository == 'community-scripts/ProxmoxVE'
needs: test-json-files
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Configure Next.js for pages
uses: actions/configure-pages@v5
with:
static_site_generator: next
- name: Build with Next.js
run: bun run build
- name: Upload artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: frontend/out
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.repository == 'community-scripts/ProxmoxVE'
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- "frontend/public/json/**"
- "json/**"
jobs:
push-json:
@@ -19,7 +19,7 @@ jobs:
- name: Get changed JSON files with slug
id: changed
run: |
changed=$(git diff --name-only "${{ github.event.before }}" "${{ github.event.after }}" -- frontend/public/json/ | grep '\.json$' || true)
changed=$(git diff --name-only "${{ github.event.before }}" "${{ github.event.after }}" -- json/ | grep '\.json$' || true)
with_slug=""
for f in $changed; do
[[ -f "$f" ]] || continue
@@ -96,7 +96,7 @@ jobs:
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
let categoryIdToName = {};
try {
const metadata = JSON.parse(fs.readFileSync('frontend/public/json/metadata.json', 'utf8'));
const metadata = JSON.parse(fs.readFileSync('json/metadata.json', 'utf8'));
(metadata.categories || []).forEach(function(cat) { categoryIdToName[cat.id] = cat.name; });
} catch (e) { console.warn('Could not load metadata.json:', e.message); }
let typeValueToId = {};

41
.github/workflows/trigger_github_pages_redirect.yml generated vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Pages Redirect
on:
workflow_dispatch:
permissions:
pages: write
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create redirect page
run: |
mkdir site
cat <<EOF > site/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=https://community-scripts.org/">
<link rel="canonical" href="https://community-scripts.org/">
<title>Redirecting...</title>
</head>
<body>
Redirecting...
</body>
</html>
EOF
- uses: actions/upload-pages-artifact@v3
with:
path: site
- name: Deploy
uses: actions/deploy-pages@v4

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- "frontend/public/json/**.json"
- "json/**.json"
workflow_dispatch:
jobs:
@@ -57,7 +57,7 @@ jobs:
- name: Get Newly Added JSON Files
id: new_json_files
run: |
git diff --name-only --diff-filter=A ${{ env.prev_commit }} HEAD | grep '^frontend/public/json/.*\.json$' > new_files.txt || true
git diff --name-only --diff-filter=A ${{ env.prev_commit }} HEAD | grep '^json/.*\.json$' > new_files.txt || true
echo "New files detected:"
cat new_files.txt || echo "No new files."

View File

@@ -0,0 +1,167 @@
name: Update script timestamp on .sh changes
on:
push:
branches:
- main
paths:
- "ct/**/*.sh"
- "install/**/*.sh"
- "tools/**/*.sh"
- "turnkey/**/*.sh"
- "vm/**/*.sh"
jobs:
update-script-timestamp:
runs-on: self-hosted
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed .sh files and derive slugs
id: slugs
run: |
changed=$(git diff --name-only "${{ github.event.before }}" "${{ github.event.after }}" -- ct/ install/ tools/ turnkey/ vm/ | grep '\.sh$' || true)
if [[ -z "$changed" ]]; then
echo "No .sh files changed in ct/, install/, tools/, turnkey/, or vm/."
echo "count=0" >> "$GITHUB_OUTPUT"
exit 0
fi
declare -A seen
slugs=""
for f in $changed; do
[[ -f "$f" ]] || continue
base="${f##*/}"
base="${base%.sh}"
if [[ "$f" == install/* && "$base" == *-install ]]; then
slug="${base%-install}"
else
slug="$base"
fi
if [[ -z "${seen[$slug]:-}" ]]; then
seen[$slug]=1
slugs="$slugs $slug"
fi
done
slugs=$(echo $slugs | xargs -n1 | sort -u)
if [[ -z "$slugs" ]]; then
echo "No slugs to update."
echo "count=0" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "$slugs" > changed_slugs.txt
echo "count=$(echo "$slugs" | wc -w)" >> "$GITHUB_OUTPUT"
- name: Parse PR number from merge commit
id: pr
run: |
re='#([0-9]+)'
if [[ "$COMMIT_MSG" =~ $re ]]; then
echo "number=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT"
else
echo "number=" >> "$GITHUB_OUTPUT"
fi
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
- name: Update script timestamps in PocketBase
if: steps.slugs.outputs.count != '0'
env:
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
PR_URL: ${{ steps.pr.outputs.number != '' && format('{0}/{1}/pull/{2}', github.server_url, github.repository, steps.pr.outputs.number) || '' }}
run: |
node << 'ENDSCRIPT'
(async function() {
const fs = require('fs');
const https = require('https');
const http = require('http');
const url = require('url');
function request(fullUrl, opts) {
return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
const body = opts.body;
const options = {
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.path,
method: opts.method || 'GET',
headers: opts.headers || {}
};
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function(res) {
let data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
const slugsText = fs.readFileSync('changed_slugs.txt', 'utf8').trim();
const slugs = slugsText ? slugsText.split(/\s+/).filter(Boolean) : [];
if (slugs.length === 0) {
console.log('No slugs to update.');
return;
}
const authUrl = apiBase + '/collections/users/auth-with-password';
const authBody = JSON.stringify({
identity: process.env.POCKETBASE_ADMIN_EMAIL,
password: process.env.POCKETBASE_ADMIN_PASSWORD
});
const authRes = await request(authUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: authBody
});
if (!authRes.ok) {
throw new Error('Auth failed: ' + authRes.body);
}
const token = JSON.parse(authRes.body).token;
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
for (const slug of slugs) {
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
const record = list.items && list.items[0];
if (!record) {
console.log('Slug not in DB, skipping: ' + slug);
continue;
}
const patchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: record.name || record.slug,
last_update_commit: process.env.PR_URL || process.env.COMMIT_URL || ''
})
});
if (!patchRes.ok) {
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
continue;
}
console.log('Updated timestamp for slug: ' + slug);
}
console.log('Done.');
})().catch(e => { console.error(e); process.exit(1); });
ENDSCRIPT
shell: bash

View File

@@ -11,7 +11,7 @@ permissions:
pull-requests: write
env:
VERSIONS_FILE: frontend/public/json/github-versions.json
VERSIONS_FILE: json/github-versions.json
BRANCH_NAME: automated/update-github-versions
AUTOMATED_PR_LABEL: "automated pr"
@@ -74,7 +74,7 @@ jobs:
echo ""
echo "=== Scanning JSON files for slugs ==="
for json_file in frontend/public/json/*.json; do
for json_file in json/*.json; do
[[ ! -f "$json_file" ]] && continue
# Skip non-app JSON files

7
.gitignore vendored
View File

@@ -24,13 +24,6 @@ venv/
env/
*.env
# Node.js dependencies (frontend folder was excluded, but keeping this rule for reference)
frontend/node_modules/
frontend/.svelte-kit/
frontend/.turbo/
frontend/.vite/
frontend/build/
# API and Backend specific exclusions
api/.env
api/__pycache__/

View File

@@ -24,6 +24,9 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
<details>
<summary><h2>📜 History</h2></summary>
@@ -32,6 +35,13 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
<summary><h3>2026</h3></summary>
<details>
<summary><h4>March (7 entries)</h4></summary>
[View March 2026 Changelog](.github/changelogs/2026/03.md)
</details>
<details>
<summary><h4>February (28 entries)</h4></summary>
@@ -410,8 +420,182 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-03-12
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- SparkyFitness: install pnpm dependencies from workspace root [@MickLesk](https://github.com/MickLesk) ([#12792](https://github.com/community-scripts/ProxmoxVE/pull/12792))
- n8n: add build-essential to update dependencies [@MickLesk](https://github.com/MickLesk) ([#12795](https://github.com/community-scripts/ProxmoxVE/pull/12795))
- Frigate openvino labelmap patch [@semtex1987](https://github.com/semtex1987) ([#12751](https://github.com/community-scripts/ProxmoxVE/pull/12751))
### 💾 Core
- #### 🐞 Bug Fixes
- tools.func: correct PATH escaping in ROCm profile script [@MickLesk](https://github.com/MickLesk) ([#12793](https://github.com/community-scripts/ProxmoxVE/pull/12793))
- #### ✨ New Features
- core: add mode=generated for unattended frontend installs [@MickLesk](https://github.com/MickLesk) ([#12807](https://github.com/community-scripts/ProxmoxVE/pull/12807))
- core: validate storage availability when loading defaults [@MickLesk](https://github.com/MickLesk) ([#12794](https://github.com/community-scripts/ProxmoxVE/pull/12794))
- #### 🔧 Refactor
- tools.func: support older NVIDIA driver versions with 2 segments (xxx.xxx) [@MickLesk](https://github.com/MickLesk) ([#12796](https://github.com/community-scripts/ProxmoxVE/pull/12796))
## 2026-03-11
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- fix: Init telemetry in addon scripts [@MickLesk](https://github.com/MickLesk) ([#12777](https://github.com/community-scripts/ProxmoxVE/pull/12777))
- Tracearr: Increase default disk variable from 5 to 10 [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12762](https://github.com/community-scripts/ProxmoxVE/pull/12762))
- Fix Wireguard Dashboard update [@odin568](https://github.com/odin568) ([#12767](https://github.com/community-scripts/ProxmoxVE/pull/12767))
### 🧰 Tools
- #### ✨ New Features
- Coder-Code-Server: Check if config file exists [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12758](https://github.com/community-scripts/ProxmoxVE/pull/12758))
## 2026-03-10
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- [Fix] Immich: Pin libvips to 8.17.3 [@vhsdream](https://github.com/vhsdream) ([#12744](https://github.com/community-scripts/ProxmoxVE/pull/12744))
## 2026-03-09
### 🚀 Updated Scripts
- Pin Opencloud to 5.2.0 [@vhsdream](https://github.com/vhsdream) ([#12721](https://github.com/community-scripts/ProxmoxVE/pull/12721))
- #### 🐞 Bug Fixes
- [Hotfix] qBittorrent: Disable UPnP port forwarding by default [@vhsdream](https://github.com/vhsdream) ([#12728](https://github.com/community-scripts/ProxmoxVE/pull/12728))
- [Quickfix] Opencloud: ensure correct case for binary [@vhsdream](https://github.com/vhsdream) ([#12729](https://github.com/community-scripts/ProxmoxVE/pull/12729))
- Omada: Bump libssl [@MickLesk](https://github.com/MickLesk) ([#12724](https://github.com/community-scripts/ProxmoxVE/pull/12724))
- openwebui: Ensure required dependencies [@MickLesk](https://github.com/MickLesk) ([#12717](https://github.com/community-scripts/ProxmoxVE/pull/12717))
- Frigate: try an OpenVino model build fallback [@MickLesk](https://github.com/MickLesk) ([#12704](https://github.com/community-scripts/ProxmoxVE/pull/12704))
- Change cronjob setup to use www-data user [@opastorello](https://github.com/opastorello) ([#12695](https://github.com/community-scripts/ProxmoxVE/pull/12695))
- RustDesk Server: Fix check_for_gh_release function call [@tremor021](https://github.com/tremor021) ([#12694](https://github.com/community-scripts/ProxmoxVE/pull/12694))
- #### ✨ New Features
- feat: improve zigbee2mqtt backup handler [@MickLesk](https://github.com/MickLesk) ([#12714](https://github.com/community-scripts/ProxmoxVE/pull/12714))
- #### 💥 Breaking Changes
- Reactive Resume: rewrite for v5 using original repo amruthpilla/reactive-resume [@MickLesk](https://github.com/MickLesk) ([#12705](https://github.com/community-scripts/ProxmoxVE/pull/12705))
### 💾 Core
- #### ✨ New Features
- tools: add Alpine (apk) support to ensure_dependencies and is_package_installed [@MickLesk](https://github.com/MickLesk) ([#12703](https://github.com/community-scripts/ProxmoxVE/pull/12703))
- tools.func: extend hwaccel with ROCm [@MickLesk](https://github.com/MickLesk) ([#12707](https://github.com/community-scripts/ProxmoxVE/pull/12707))
### 🌐 Website
- #### ✨ New Features
- feat: add CopycatWarningToast component for user warnings [@BramSuurdje](https://github.com/BramSuurdje) ([#12733](https://github.com/community-scripts/ProxmoxVE/pull/12733))
## 2026-03-08
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- [Fix] Immich: chown install dir before machine-learning update [@vhsdream](https://github.com/vhsdream) ([#12684](https://github.com/community-scripts/ProxmoxVE/pull/12684))
- [Fix] Scanopy: Build generate-fixtures [@vhsdream](https://github.com/vhsdream) ([#12686](https://github.com/community-scripts/ProxmoxVE/pull/12686))
- fix: rustdeskserver: use correct repo string [@CrazyWolf13](https://github.com/CrazyWolf13) ([#12682](https://github.com/community-scripts/ProxmoxVE/pull/12682))
- NZBGet: Fixes for RAR5 handling [@tremor021](https://github.com/tremor021) ([#12675](https://github.com/community-scripts/ProxmoxVE/pull/12675))
### 🌐 Website
- #### 🐞 Bug Fixes
- LXC-Execute: Fix slug [@tremor021](https://github.com/tremor021) ([#12681](https://github.com/community-scripts/ProxmoxVE/pull/12681))
## 2026-03-07
### 🆕 New Scripts
- ImmichFrame ([#12653](https://github.com/community-scripts/ProxmoxVE/pull/12653))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Grocy: bump PHP version from 8.3 to 8.5 [@MickLesk](https://github.com/MickLesk) ([#12651](https://github.com/community-scripts/ProxmoxVE/pull/12651))
- Check for influxdb3 installation in update_script [@odin568](https://github.com/odin568) ([#12648](https://github.com/community-scripts/ProxmoxVE/pull/12648))
- Update Rdtclient to dotnet 10.0 [@asylumexp](https://github.com/asylumexp) ([#12638](https://github.com/community-scripts/ProxmoxVE/pull/12638))
- fix(immich): fix update script failing to add Debian testing repo when preferences file already exists [@Copilot](https://github.com/Copilot) ([#12631](https://github.com/community-scripts/ProxmoxVE/pull/12631))
### 💾 Core
- #### ✨ New Features
- tools: add interactive GitHub PAT prompt on rate limit / auth failure [@MickLesk](https://github.com/MickLesk) ([#12652](https://github.com/community-scripts/ProxmoxVE/pull/12652))
### 🌐 Website
- #### 📝 Script Information
- Papra: update repository URL to papra-hq/papra [@MickLesk](https://github.com/MickLesk) ([#12650](https://github.com/community-scripts/ProxmoxVE/pull/12650))
## 2026-03-06
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- RustDesk Server: Fix update script [@tremor021](https://github.com/tremor021) ([#12625](https://github.com/community-scripts/ProxmoxVE/pull/12625))
- [Node-RED] Restart service after update [@Aurelien30000](https://github.com/Aurelien30000) ([#12621](https://github.com/community-scripts/ProxmoxVE/pull/12621))
- wealthfolio: update cors [@CrazyWolf13](https://github.com/CrazyWolf13) ([#12617](https://github.com/community-scripts/ProxmoxVE/pull/12617))
- CryptPad: Better update handling [@tremor021](https://github.com/tremor021) ([#12611](https://github.com/community-scripts/ProxmoxVE/pull/12611))
- #### ✨ New Features
- RustDesk Server: Switch to updated repository [@tremor021](https://github.com/tremor021) ([#12083](https://github.com/community-scripts/ProxmoxVE/pull/12083))
- #### 💥 Breaking Changes
- Semaphore: Move from BoltDB to SQLite [@tremor021](https://github.com/tremor021) ([#12624](https://github.com/community-scripts/ProxmoxVE/pull/12624))
## 2026-03-05
### 🆕 New Scripts
- ddclient ([#12587](https://github.com/community-scripts/ProxmoxVE/pull/12587))
- Netbird ([#12585](https://github.com/community-scripts/ProxmoxVE/pull/12585))
- Papra ([#12577](https://github.com/community-scripts/ProxmoxVE/pull/12577))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- fluid-calendar: add build-essential to install and update dependencies [@Copilot](https://github.com/Copilot) ([#12602](https://github.com/community-scripts/ProxmoxVE/pull/12602))
- Refactor: BentoPDF [@vhsdream](https://github.com/vhsdream) ([#12597](https://github.com/community-scripts/ProxmoxVE/pull/12597))
- Tianji: Fix the bug introduced by the refactor [@tremor021](https://github.com/tremor021) ([#12564](https://github.com/community-scripts/ProxmoxVE/pull/12564))
- PowerDNS: use 'launch=' instead of 'launch+=' for gsqlite3 backend [@MickLesk](https://github.com/MickLesk) ([#12579](https://github.com/community-scripts/ProxmoxVE/pull/12579))
### 🗑️ Deleted Scripts
- Suwayomi-Server: remove due to inactivity and very low usage [@MickLesk](https://github.com/MickLesk) ([#12596](https://github.com/community-scripts/ProxmoxVE/pull/12596))
### 💾 Core
- #### 🔧 Refactor
- core: add var_os / var_version to whitelist for app.vars [@MickLesk](https://github.com/MickLesk) ([#12576](https://github.com/community-scripts/ProxmoxVE/pull/12576))
## 2026-03-04
### 🚀 Updated Scripts
@@ -1247,217 +1431,4 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
### ❔ Uncategorized
- Opencloud: fix JSON [@vhsdream](https://github.com/vhsdream) ([#11617](https://github.com/community-scripts/ProxmoxVE/pull/11617))
## 2026-02-05
### 🆕 New Scripts
- OpenCloud ([#11538](https://github.com/community-scripts/ProxmoxVE/pull/11538))
- Nginx-UI ([#11573](https://github.com/community-scripts/ProxmoxVE/pull/11573))
- New: SQL-Server 2025 | Refactor SQL-Server 2022 [@MickLesk](https://github.com/MickLesk) ([#11546](https://github.com/community-scripts/ProxmoxVE/pull/11546))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- OpenCloud: pin version to 5.0.2; Collabora CSP fix [@vhsdream](https://github.com/vhsdream) ([#11585](https://github.com/community-scripts/ProxmoxVE/pull/11585))
- Wanderer: Fix repo [@tremor021](https://github.com/tremor021) ([#11567](https://github.com/community-scripts/ProxmoxVE/pull/11567))
- #### ✨ New Features
- Refactor: Docker-VM (Multi-OS / Cloud-Init / Stabilization) [@MickLesk](https://github.com/MickLesk) ([#9047](https://github.com/community-scripts/ProxmoxVE/pull/9047))
### 💾 Core
- #### ✨ New Features
- cloud-init: add interactive SSH key discovery and selection [@MickLesk](https://github.com/MickLesk) ([#11547](https://github.com/community-scripts/ProxmoxVE/pull/11547))
### 📚 Documentation
- github: extend docs / contribution / templates [@MickLesk](https://github.com/MickLesk) ([#10921](https://github.com/community-scripts/ProxmoxVE/pull/10921))
### 🌐 Website
- #### 🐞 Bug Fixes
- fix(frontend): theme respective syntax highlighting [@ls-root](https://github.com/ls-root) ([#11565](https://github.com/community-scripts/ProxmoxVE/pull/11565))
## 2026-02-04
### 🆕 New Scripts
- Wishlist ([#11527](https://github.com/community-scripts/ProxmoxVE/pull/11527))
- WriteFreely ([#11524](https://github.com/community-scripts/ProxmoxVE/pull/11524))
### 🚀 Updated Scripts
- Add log directory and permissions for koillection [@shineangelic](https://github.com/shineangelic) ([#11553](https://github.com/community-scripts/ProxmoxVE/pull/11553))
- #### 🐞 Bug Fixes
- [FIX] Scanopy: ensure Scanopy Daemon update [@vhsdream](https://github.com/vhsdream) ([#11541](https://github.com/community-scripts/ProxmoxVE/pull/11541))
- Immich: pin version to 2.5.3 [@vhsdream](https://github.com/vhsdream) ([#11515](https://github.com/community-scripts/ProxmoxVE/pull/11515))
### 💾 Core
- #### ✨ New Features
- core: create vm-core.func from dev [@MickLesk](https://github.com/MickLesk) ([#11528](https://github.com/community-scripts/ProxmoxVE/pull/11528))
### 🧰 Tools
- #### ✨ New Features
- [ADDON] Immich Public Proxy addon [@vhsdream](https://github.com/vhsdream) ([#11518](https://github.com/community-scripts/ProxmoxVE/pull/11518))
### 🌐 Website
- #### 🐞 Bug Fixes
- fix(frontend): implement weighted search scoring for command menu [@ls-root](https://github.com/ls-root) ([#11534](https://github.com/community-scripts/ProxmoxVE/pull/11534))
### ❔ Uncategorized
- [FIX] Immich Public Proxy docs link [@vhsdream](https://github.com/vhsdream) ([#11543](https://github.com/community-scripts/ProxmoxVE/pull/11543))
## 2026-02-03
### 🆕 New Scripts
- Wealthfolio ([#11511](https://github.com/community-scripts/ProxmoxVE/pull/11511))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- [FIX] Shelfmark: unpin Chromium version [@vhsdream](https://github.com/vhsdream) ([#11505](https://github.com/community-scripts/ProxmoxVE/pull/11505))
- #### ✨ New Features
- [FEAT] Scanopy: automatically update integrated daemon [@vhsdream](https://github.com/vhsdream) ([#11506](https://github.com/community-scripts/ProxmoxVE/pull/11506))
### 💾 Core
- #### 🐞 Bug Fixes
- [FIX] tools.func: trim spaces in app_lc when checking for gh release [@vhsdream](https://github.com/vhsdream) ([#11512](https://github.com/community-scripts/ProxmoxVE/pull/11512))
### 🌐 Website
- #### 🐞 Bug Fixes
- fix(frontend): decouple table pagination from summary fetching [@ls-root](https://github.com/ls-root) ([#11495](https://github.com/community-scripts/ProxmoxVE/pull/11495))
## 2026-02-02
### 🆕 New Scripts
- rustypaste | Alpine-rustypaste ([#11457](https://github.com/community-scripts/ProxmoxVE/pull/11457))
- KitchenOwl ([#11453](https://github.com/community-scripts/ProxmoxVE/pull/11453))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Grist: Update dependencies [@tremor021](https://github.com/tremor021) ([#11489](https://github.com/community-scripts/ProxmoxVE/pull/11489))
- Allow "downgrade" of libigdgmm12 [@vhsdream](https://github.com/vhsdream) ([#11478](https://github.com/community-scripts/ProxmoxVE/pull/11478))
- Disable NPM install and update due to OpenResty SHA-1 signature issues [@MickLesk](https://github.com/MickLesk) ([#11471](https://github.com/community-scripts/ProxmoxVE/pull/11471))
- #### ✨ New Features
- Refactor: Forgejo & readeck - migrate to codeberg functions [@MickLesk](https://github.com/MickLesk) ([#11460](https://github.com/community-scripts/ProxmoxVE/pull/11460))
- #### 💥 Breaking Changes
- [FIX] Scanopy: remove daemon build [@vhsdream](https://github.com/vhsdream) ([#11444](https://github.com/community-scripts/ProxmoxVE/pull/11444))
- #### 🔧 Refactor
- Refactor: Vaultwarden [@MickLesk](https://github.com/MickLesk) ([#11445](https://github.com/community-scripts/ProxmoxVE/pull/11445))
- various scripts: use ensure_dependencies instead of apt [@MickLesk](https://github.com/MickLesk) ([#11463](https://github.com/community-scripts/ProxmoxVE/pull/11463))
### 🌐 Website
- cleanup(frontend): remove unused /category-view route [@ls-root](https://github.com/ls-root) ([#11461](https://github.com/community-scripts/ProxmoxVE/pull/11461))
- #### ✨ New Features
- feat(frontend): preview tab [@ls-root](https://github.com/ls-root) ([#11475](https://github.com/community-scripts/ProxmoxVE/pull/11475))
## 2026-02-01
### 🚀 Updated Scripts
- fix headers [@CrazyWolf13](https://github.com/CrazyWolf13) ([#11422](https://github.com/community-scripts/ProxmoxVE/pull/11422))
- #### 🐞 Bug Fixes
- 2fauth: export PHP_VERSION for nginx config [@MickLesk](https://github.com/MickLesk) ([#11441](https://github.com/community-scripts/ProxmoxVE/pull/11441))
- Prometheus Paperless NGX Exporter: Set correct binary path in systemd unit file [@andygrunwald](https://github.com/andygrunwald) ([#11438](https://github.com/community-scripts/ProxmoxVE/pull/11438))
- tracearr: install/update new prestart script from upstream [@durzo](https://github.com/durzo) ([#11433](https://github.com/community-scripts/ProxmoxVE/pull/11433))
- n8n: Fix dependencies [@tremor021](https://github.com/tremor021) ([#11429](https://github.com/community-scripts/ProxmoxVE/pull/11429))
- [Hotfix] Bunkerweb update [@vhsdream](https://github.com/vhsdream) ([#11402](https://github.com/community-scripts/ProxmoxVE/pull/11402))
- [Hotfix] Immich: revert healthcheck feature [@vhsdream](https://github.com/vhsdream) ([#11427](https://github.com/community-scripts/ProxmoxVE/pull/11427))
- #### ✨ New Features
- tools.func: add codeberg functions & autocaliweb: migrate from GitHub to Codeberg [@MickLesk](https://github.com/MickLesk) ([#11440](https://github.com/community-scripts/ProxmoxVE/pull/11440))
- Immich Refactor #2 [@vhsdream](https://github.com/vhsdream) ([#11375](https://github.com/community-scripts/ProxmoxVE/pull/11375))
- #### 🔧 Refactor
- WordPress: Refactor [@tremor021](https://github.com/tremor021) ([#11408](https://github.com/community-scripts/ProxmoxVE/pull/11408))
- Refactor: Whisparr [@tremor021](https://github.com/tremor021) ([#11411](https://github.com/community-scripts/ProxmoxVE/pull/11411))
### 💾 Core
- #### ✨ New Features
- [tools]: Update `fetch_and_deply_from_url()` [@tremor021](https://github.com/tremor021) ([#11410](https://github.com/community-scripts/ProxmoxVE/pull/11410))
### 🌐 Website
- feat(frontend): implement UX refinements and syntax highlighting [@ls-root](https://github.com/ls-root) ([#11423](https://github.com/community-scripts/ProxmoxVE/pull/11423))
- #### ✨ New Features
- feat(frontend): add contribution CTA to empty search state [@ls-root](https://github.com/ls-root) ([#11412](https://github.com/community-scripts/ProxmoxVE/pull/11412))
## 2026-01-31
### 🆕 New Scripts
- shelfmark ([#11371](https://github.com/community-scripts/ProxmoxVE/pull/11371))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- fix: yubal: add git [@CrazyWolf13](https://github.com/CrazyWolf13) ([#11394](https://github.com/community-scripts/ProxmoxVE/pull/11394))
## 2026-01-30
### 🆕 New Scripts
- languagetool ([#11370](https://github.com/community-scripts/ProxmoxVE/pull/11370))
- Ampache ([#11369](https://github.com/community-scripts/ProxmoxVE/pull/11369))
### 🚀 Updated Scripts
- #### 🔧 Refactor
- Refactor: remove redundant PHP_MODULE entries in several scripts [@MickLesk](https://github.com/MickLesk) ([#11362](https://github.com/community-scripts/ProxmoxVE/pull/11362))
- Refactor: Koillection [@MickLesk](https://github.com/MickLesk) ([#11361](https://github.com/community-scripts/ProxmoxVE/pull/11361))
### 💾 Core
- #### 🐞 Bug Fixes
- core: meilisearch - add data migration for version upgrades [@MickLesk](https://github.com/MickLesk) ([#11356](https://github.com/community-scripts/ProxmoxVE/pull/11356))
- #### ✨ New Features
- [tools] Add `fetch_and_deploy_from_url()` [@tremor021](https://github.com/tremor021) ([#11376](https://github.com/community-scripts/ProxmoxVE/pull/11376))
- core: php - improve module handling and prevent installation failures [@MickLesk](https://github.com/MickLesk) ([#11358](https://github.com/community-scripts/ProxmoxVE/pull/11358))
- Opencloud: fix JSON [@vhsdream](https://github.com/vhsdream) ([#11617](https://github.com/community-scripts/ProxmoxVE/pull/11617))

View File

@@ -31,6 +31,10 @@ function update_script() {
msg_info "Updating Node-RED"
$STD npm install -g --unsafe-perm node-red
msg_ok "Updated Node-RED"
msg_info "Restarting Node-RED"
$STD rc-service nodered restart
msg_ok "Restarted Node-RED"
msg_ok "Updated successfully!"
exit 0
}

View File

@@ -27,21 +27,21 @@ function update_script() {
fi
APIRELEASE=$(curl -s https://api.github.com/repos/lejianwen/rustdesk-api/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
RELEASE=$(curl -s https://api.github.com/repos/rustdesk/rustdesk-server/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
RELEASE=$(curl -s https://api.github.com/repos/lejianwen/rustdesk-server/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
if [ "${RELEASE}" != "$(cat ~/.rustdesk-server 2>/dev/null)" ] || [ ! -f ~/.rustdesk-server ]; then
msg_info "Updating RustDesk Server to v${RELEASE}"
$STD apk -U upgrade
$STD service rustdesk-server-hbbs stop
$STD service rustdesk-server-hbbr stop
temp_file1=$(mktemp)
curl -fsSL "https://github.com/rustdesk/rustdesk-server/releases/download/${RELEASE}/rustdesk-server-linux-amd64.zip" -o "$temp_file1"
curl -fsSL "https://github.com/lejianwen/rustdesk-server/releases/download/${RELEASE}/rustdesk-server-linux-amd64.zip" -o "$temp_file1"
$STD unzip "$temp_file1"
cp -r amd64/* /opt/rustdesk-server/
echo "${RELEASE}" >~/.rustdesk-server
$STD service rustdesk-server-hbbs start
$STD service rustdesk-server-hbbr start
rm -rf amd64
rm -f $temp_file1
rm -f "$temp_file1"
msg_ok "Updated RustDesk Server"
else
msg_ok "No update required. ${APP} is already at v${RELEASE}"
@@ -56,7 +56,7 @@ function update_script() {
echo "${APIRELEASE}" >~/.rustdesk-api
$STD service rustdesk-api start
rm -rf release
rm -f $temp_file2
rm -f "$temp_file2"
msg_ok "Updated RustDesk API"
else
msg_ok "No update required. RustDesk API is already at v${APIRELEASE}"

View File

@@ -7,7 +7,7 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
APP="BentoPDF"
var_tags="${var_tags:-pdf-editor}"
var_cpu="${var_cpu:-1}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
@@ -35,16 +35,32 @@ function update_script() {
systemctl stop bentopdf
msg_ok "Stopped Service"
[[ -f /opt/bentopdf/.env.production ]] && cp /opt/bentopdf/.env.production /opt/production.env
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bentopdf" "alam00000/bentopdf" "tarball" "latest" "/opt/bentopdf"
msg_info "Updating BentoPDF"
cd /opt/bentopdf
$STD npm ci --no-audit --no-fund
$STD npm install http-server -g
if [[ -f /opt/production.env ]]; then
mv /opt/production.env ./.env.production
else
cp ./.env.example ./.env.production
fi
export NODE_OPTIONS="--max-old-space-size=3072"
export SIMPLE_MODE=true
$STD npm run build -- --mode production
export VITE_USE_CDN=true
$STD npm run build:all
msg_ok "Updated BentoPDF"
msg_info "Starting Service"
if grep -q '8080' /etc/systemd/system/bentopdf.service; then
sed -i -e 's|/bentopdf|/bentopdf/dist|' \
-e 's|npx.*|npx http-server -g -b -d false -r --no-dotfiles|' \
/etc/systemd/system/bentopdf.service
systemctl daemon-reload
fi
systemctl start bentopdf
msg_ok "Started Service"
msg_ok "Updated successfully!"

View File

@@ -33,17 +33,23 @@ function update_script() {
systemctl stop cryptpad
msg_info "Stopped Service"
msg_info "Backing up configuration"
msg_info "Creating backup"
[ -f /opt/cryptpad/config/config.js ] && mv /opt/cryptpad/config/config.js /opt/
msg_ok "Backed up configuration"
for dir in blob block customize data datastore www/common/onlyoffice/dist onlyoffice-conf; do
[ -d "/opt/cryptpad/${dir}" ] && mv "/opt/cryptpad/${dir}" "/tmp/cryptpad_${dir//\//_}"
done
msg_ok "Created backup"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cryptpad" "cryptpad/cryptpad" "tarball"
msg_info "Restoring configuration"
msg_info "Restoring backup"
mv /opt/config.js /opt/cryptpad/config/
msg_ok "Configuration restored"
for dir in blob block customize data datastore www/common/onlyoffice/dist onlyoffice-conf; do
[ -d "/tmp/cryptpad_${dir//\//_}" ] && mv "/tmp/cryptpad_${dir//\//_}" "/opt/cryptpad/${dir}"
done
msg_ok "Restored backup"
msg_info "Updating CryptaPad"
msg_info "Updating CryptPad"
cd /opt/cryptpad
$STD npm ci
$STD npm run install:components

46
ct/ddclient.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: mitchscobell
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://ddclient.net/
APP="ddclient"
var_tags="${var_tags:-network}"
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 [[ ! -f /etc/ddclient.conf ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Updating ddclient"
$STD apt update
$STD apt install --only-upgrade -y ddclient
$STD systemctl restart ddclient
msg_ok "Updated ddclient"
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

@@ -29,6 +29,7 @@ function update_script() {
exit
fi
ensure_dependencies build-essential
NODE_VERSION="24" setup_nodejs
if check_for_gh_release "fluid-calendar" "dotnetfactory/fluid-calendar"; then

View File

@@ -28,8 +28,8 @@ function update_script() {
exit
fi
php_ver=$(php -v | head -n 1 | awk '{print $2}')
if [[ ! $php_ver == "8.3"* ]]; then
PHP_VERSION="8.3" PHP_APACHE="YES" setup_php
if [[ ! $php_ver == "8.5"* ]]; then
PHP_VERSION="8.5" PHP_APACHE="YES" setup_php
fi
if check_for_gh_release "grocy" "grocy/grocy"; then
msg_info "Updating grocy"

6
ct/headers/ddclient Normal file
View File

@@ -0,0 +1,6 @@
__ __ ___ __
____/ /___/ /____/ (_)__ ____ / /_
/ __ / __ / ___/ / / _ \/ __ \/ __/
/ /_/ / /_/ / /__/ / / __/ / / / /_
\__,_/\__,_/\___/_/_/\___/_/ /_/\__/

6
ct/headers/immichframe Normal file
View File

@@ -0,0 +1,6 @@
____ _ __ ______
/ _/___ ___ ____ ___ (_)____/ /_ / ____/________ _____ ___ ___
/ // __ `__ \/ __ `__ \/ / ___/ __ \/ /_ / ___/ __ `/ __ `__ \/ _ \
_/ // / / / / / / / / / / / /__/ / / / __/ / / / /_/ / / / / / / __/
/___/_/ /_/ /_/_/ /_/ /_/_/\___/_/ /_/_/ /_/ \__,_/_/ /_/ /_/\___/

6
ct/headers/netbird Normal file
View File

@@ -0,0 +1,6 @@
_ __ __ ____ _ __
/ | / /__ / /_/ __ )(_)________/ /
/ |/ / _ \/ __/ __ / / ___/ __ /
/ /| / __/ /_/ /_/ / / / / /_/ /
/_/ |_/\___/\__/_____/_/_/ \__,_/

6
ct/headers/papra Normal file
View File

@@ -0,0 +1,6 @@
____
/ __ \____ _____ _________ _
/ /_/ / __ `/ __ \/ ___/ __ `/
/ ____/ /_/ / /_/ / / / /_/ /
/_/ \__,_/ .___/_/ \__,_/
/_/

View File

@@ -1,6 +0,0 @@
_____ _ _____
/ ___/__ ___ ______ ___ ______ ____ ___ (_) ___/___ ______ _____ _____
\__ \/ / / / | /| / / __ `/ / / / __ \/ __ `__ \/ /\__ \/ _ \/ ___/ | / / _ \/ ___/
___/ / /_/ /| |/ |/ / /_/ / /_/ / /_/ / / / / / / /___/ / __/ / | |/ / __/ /
/____/\__,_/ |__/|__/\__,_/\__, /\____/_/ /_/ /_/_//____/\___/_/ |___/\___/_/
/____/

View File

@@ -36,9 +36,13 @@ function update_script() {
exit
fi
if [[ ! -f /etc/apt/preferences.d/preferences ]]; then
if ! grep -qE '(^|[[:space:]])testing([[:space:]]|$)' /etc/apt/sources.list.d/debian.sources 2>/dev/null; then
msg_info "Adding Debian Testing repo"
sed -i 's/ trixie-updates/ trixie-updates testing/g' /etc/apt/sources.list.d/debian.sources
if grep -q "trixie-updates" /etc/apt/sources.list.d/debian.sources 2>/dev/null; then
sed -i 's/ trixie-updates/ trixie-updates testing/g' /etc/apt/sources.list.d/debian.sources
else
sed -i '/^[[:space:]]*Suites:.*trixie/ s/$/ testing/' /etc/apt/sources.list.d/debian.sources
fi
cat <<EOF >/etc/apt/preferences.d/preferences
Package: *
Pin: release a=unstable
@@ -209,7 +213,8 @@ EOF
msg_ok "Updated Immich server, web, cli and plugins"
cd "$SRC_DIR"/machine-learning
mkdir -p "$ML_DIR" && chown -R immich:immich "$ML_DIR"
mkdir -p "$ML_DIR"
chown -R immich:immich "$INSTALL_DIR"
chown immich:immich ./uv.lock
export VIRTUAL_ENV="${ML_DIR}"/ml-venv
if [[ -f ~/.openvino ]]; then
@@ -375,7 +380,7 @@ function compile_imagemagick() {
function compile_libvips() {
SOURCE=$SOURCE_DIR/libvips
: "${LIBVIPS_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libvips.json)}"
LIBVIPS_REVISION="0c9151a4f416d2f8ae20a755db218f6637050eec"
if [[ "$LIBVIPS_REVISION" != "$(grep 'libvips' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
msg_info "Recompiling libvips"
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"

81
ct/immichframe.sh Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Thiago Canozzo Lahr (tclahr)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/immichFrame/ImmichFrame
APP="ImmichFrame"
var_tags="${var_tags:-photos;slideshow}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-8}"
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 /opt/immichframe ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "immichframe" "immichFrame/ImmichFrame"; then
msg_info "Stopping Service"
systemctl stop immichframe
msg_ok "Stopped Service"
msg_info "Backing up Configuration"
cp -r /opt/immichframe/Config /tmp/immichframe_config.bak
msg_ok "Backed up Configuration"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "immichframe" "immichFrame/ImmichFrame" "tarball" "latest" "/tmp/immichframe"
msg_info "Setting up ImmichFrame"
cd /tmp/immichframe
$STD dotnet publish ImmichFrame.WebApi/ImmichFrame.WebApi.csproj \
--configuration Release \
--runtime linux-x64 \
--self-contained false \
--output /opt/immichframe
cd /tmp/immichframe/immichFrame.Web
$STD npm ci --silent
$STD npm run build
rm -rf /opt/immichframe/wwwroot/*
cp -r build/* /opt/immichframe/wwwroot
rm -rf /tmp/immichframe
msg_ok "Setup ImmichFrame"
msg_info "Restoring Configuration"
cp -r /tmp/immichframe_config.bak/* /opt/immichframe/Config/
rm -rf /tmp/immichframe_config.bak
chown -R immichframe:immichframe /opt/immichframe
msg_ok "Restored Configuration"
msg_info "Starting Service"
systemctl start immichframe
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8080${CL}"

View File

@@ -23,7 +23,7 @@ function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /usr/bin/influxd ]]; then
if [[ ! -f /usr/bin/influxd && ! -f /usr/bin/influxdb3 ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi

View File

@@ -28,7 +28,7 @@ function update_script() {
exit
fi
ensure_dependencies graphicsmagick
ensure_dependencies build-essential python3-setuptools graphicsmagick
NODE_VERSION="24" setup_nodejs
msg_info "Updating n8n"

47
ct/netbird.sh Normal file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: TechHutTV
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://netbird.io/
APP="NetBird"
var_tags="${var_tags:-network;vpn}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
var_tun="${var_tun:-yes}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /etc/netbird/config.json ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Updating Netbird"
$STD apt update
$STD apt upgrade -y
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}"
echo -e "${INFO}${YW} Access NetBird by entering the container and running:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}netbird up${CL}"

View File

@@ -27,6 +27,16 @@ function update_script() {
msg_error "No ${APP} Installation Found!"
exit
fi
if ! command -v unrar &>/dev/null; then
setup_nonfree
$STD apt install -y unrar
if grep -q "UnrarCmd=unrar-free" /var/lib/nzbget/nzbget.conf; then
sed -i "s|UnrarCmd=unrar-free|UnrarCmd=unrar|g" /var/lib/nzbget/nzbget.conf
fi
fi
msg_info "Updating NZBGet"
$STD apt update
$STD apt upgrade -y

View File

@@ -29,7 +29,7 @@ function update_script() {
exit
fi
RELEASE="v5.1.0"
RELEASE="v5.2.0"
if check_for_gh_release "OpenCloud" "opencloud-eu/opencloud" "${RELEASE}"; then
msg_info "Stopping services"
systemctl stop opencloud opencloud-wopi
@@ -41,7 +41,9 @@ function update_script() {
ensure_dependencies "inotify-tools"
msg_ok "Updated packages"
rm -f /usr/bin/{OpenCloud,opencloud}
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "OpenCloud" "opencloud-eu/opencloud" "singlefile" "${RELEASE}" "/usr/bin" "opencloud-*-linux-amd64"
mv /usr/bin/OpenCloud /usr/bin/opencloud
if ! grep -q 'POSIX_WATCH' /etc/opencloud/opencloud.env; then
sed -i '/^## External/i ## Uncomment below to enable PosixFS Collaborative Mode\

View File

@@ -25,6 +25,8 @@ function update_script() {
check_container_storage
check_container_resources
ensure_dependencies zstd build-essential libmariadb-dev
if [[ -d /opt/open-webui ]]; then
msg_warn "Legacy installation detected — migrating to uv based install..."
msg_info "Stopping Service"
@@ -92,7 +94,6 @@ EOF
OLLAMA_VERSION=$(ollama -v | awk '{print $NF}')
RELEASE=$(curl -s https://api.github.com/repos/ollama/ollama/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4)}')
if [ "$OLLAMA_VERSION" != "$RELEASE" ]; then
ensure_dependencies zstd
msg_info "Ollama update available: v$OLLAMA_VERSION -> v$RELEASE"
msg_info "Downloading Ollama v$RELEASE \n"
curl -fS#LO https://github.com/ollama/ollama/releases/download/v${RELEASE}/ollama-linux-amd64.tar.zst

68
ct/papra.sh Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/papra-hq/papra
APP="Papra"
var_tags="${var_tags:-document-management}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}"
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 /opt/papra ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "papra" "papra-hq/papra"; then
msg_info "Stopping Service"
systemctl stop papra
msg_ok "Stopped Service"
msg_info "Backing up Configuration"
cp /opt/papra/apps/papra-server/.env /opt/papra_env.bak
msg_ok "Backed up Configuration"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "papra" "papra-hq/papra" "tarball"
msg_info "Building Application"
cd /opt/papra
cp /opt/papra_env.bak /opt/papra/apps/papra-server/.env
$STD pnpm install --frozen-lockfile
$STD pnpm --filter "@papra/app-client..." run build
$STD pnpm --filter "@papra/app-server..." run build
ln -sf /opt/papra/apps/papra-client/dist /opt/papra/apps/papra-server/public
rm -f /opt/papra_env.bak
msg_ok "Built Application"
msg_info "Starting Service"
systemctl start papra
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:1221${CL}"

View File

@@ -39,9 +39,9 @@ function update_script() {
fetch_and_deploy_gh_release "rdt-client" "rogerfar/rdt-client" "prebuild" "latest" "/opt/rdtc" "RealDebridClient.zip"
cp -R /opt/rdtc-backup/appsettings.json /opt/rdtc/
if dpkg-query -W dotnet-sdk-8.0 >/dev/null 2>&1; then
$STD apt remove --purge -y dotnet-sdk-8.0
ensure_dependencies aspnetcore-runtime-9.0
if dpkg-query -W aspnetcore-runtime-9.0 >/dev/null 2>&1; then
$STD apt remove --purge -y aspnetcore-runtime-9.0
ensure_dependencies aspnetcore-runtime-10.0
fi
rm -rf /opt/rdtc-backup

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: vhsdream
# Author: vhsdream | MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://rxresume.org | Github: https://github.com/lazy-media/Reactive-Resume
# Source: https://rxresume.org | Github: https://github.com/amruthpillai/reactive-resume
APP="Reactive-Resume"
var_tags="${var_tags:-documents}"
@@ -24,62 +24,29 @@ function update_script() {
check_container_storage
check_container_resources
if [[ ! -f /etc/systemd/system/Reactive-Resume.service ]]; then
if [[ ! -f /etc/systemd/system/reactive-resume.service ]]; then
msg_error "No $APP Installation Found!"
exit
fi
if check_for_gh_release "Reactive-Resume" "lazy-media/Reactive-Resume"; then
if check_for_gh_release "reactive-resume" "amruthpillai/reactive-resume"; then
msg_info "Stopping services"
systemctl stop Reactive-Resume
systemctl stop reactive-resume
msg_ok "Stopped services"
cp /opt/Reactive-Resume/.env /opt/rxresume.env
fetch_and_deploy_gh_release "Reactive-Resume" "lazy-media/Reactive-Resume" "tarball" "latest" "/opt/Reactive-Resume"
cp /opt/reactive-resume/.env /opt/reactive-resume.env.bak
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "reactive-resume" "amruthpillai/reactive-resume" "tarball" "latest" "/opt/reactive-resume"
msg_info "Updating Reactive-Resume"
cd /opt/Reactive-Resume
export PUPPETEER_SKIP_DOWNLOAD="true"
export NEXT_TELEMETRY_DISABLED=1
msg_info "Updating Reactive Resume (Patience)"
cd /opt/reactive-resume
export CI="true"
export NODE_ENV="production"
$STD pnpm install --frozen-lockfile
$STD pnpm run build
$STD pnpm run prisma:generate
mv /opt/rxresume.env /opt/Reactive-Resume/.env
msg_ok "Updated Reactive-Resume"
msg_info "Updating Minio"
systemctl stop minio
cd /tmp
curl -fsSL https://dl.min.io/server/minio/release/linux-amd64/minio.deb -o minio.deb
$STD dpkg -i minio.deb
rm -f /tmp/minio.deb
msg_ok "Updated Minio"
msg_info "Updating Browserless (Patience)"
systemctl stop browserless
cp /opt/browserless/.env /opt/browserless.env
rm -rf /opt/browserless
brwsr_tmp=$(mktemp)
TAG=$(curl -fsSL https://api.github.com/repos/browserless/browserless/tags?per_page=1 | grep "name" | awk '{print substr($2, 3, length($2)-4) }')
curl -fsSL https://github.com/browserless/browserless/archive/refs/tags/v"$TAG".zip -o "$brwsr_tmp"
$STD unzip "$brwsr_tmp"
mv browserless-"$TAG"/ /opt/browserless
cd /opt/browserless
$STD npm install typescript
$STD npm install esbuild
$STD npm install
rm -rf src/routes/{chrome,edge,firefox,webkit}
$STD node_modules/playwright-core/cli.js install --with-deps chromium
$STD npm run build
$STD npm run build:function
$STD npm prune production
mv /opt/browserless.env /opt/browserless/.env
rm -f "$brwsr_tmp"
msg_ok "Updated Browserless"
mv /opt/reactive-resume.env.bak /opt/reactive-resume/.env
msg_ok "Updated Reactive Resume"
msg_info "Restarting services"
systemctl start minio Reactive-Resume browserless
systemctl start chromium-printer reactive-resume
msg_ok "Restarted services"
msg_ok "Updated successfully!"
fi

View File

@@ -29,9 +29,7 @@ function update_script() {
exit
fi
RELEASE=$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk-server/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
APIRELEASE=$(curl -fsSL https://api.github.com/repos/lejianwen/rustdesk-api/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
if [[ "${RELEASE}" != "$(cat ~/.rustdesk-hbbr)" ]] || [[ "${APIRELEASE}" != "$(cat ~/.rustdesk-api)" ]] || [[ ! -f ~/.rustdesk-hbbr ]] || [[ ! -f ~/.rustdesk-api ]]; then
if check_for_gh_release "rustdesk-hbbs" "lejianwen/rustdesk-server"; then
msg_info "Stopping Service"
systemctl stop rustdesk-hbbr
systemctl stop rustdesk-hbbs
@@ -40,13 +38,13 @@ function update_script() {
fi
msg_info "Stopped Service"
fetch_and_deploy_gh_release "rustdesk-hbbr" "rustdesk/rustdesk-server" "binary" "latest" "/opt/rustdesk" "rustdesk-server-hbbr*amd64.deb"
fetch_and_deploy_gh_release "rustdesk-hbbs" "rustdesk/rustdesk-server" "binary" "latest" "/opt/rustdesk" "rustdesk-server-hbbs*amd64.deb"
fetch_and_deploy_gh_release "rustdesk-utils" "rustdesk/rustdesk-server" "binary" "latest" "/opt/rustdesk" "rustdesk-server-utils*amd64.deb"
fetch_and_deploy_gh_release "rustdesk-hbbr" "lejianwen/rustdesk-server" "binary" "latest" "/opt/rustdesk" "rustdesk-server-hbbr*amd64.deb"
fetch_and_deploy_gh_release "rustdesk-hbbs" "lejianwen/rustdesk-server" "binary" "latest" "/opt/rustdesk" "rustdesk-server-hbbs*amd64.deb"
fetch_and_deploy_gh_release "rustdesk-utils" "lejianwen/rustdesk-server" "binary" "latest" "/opt/rustdesk" "rustdesk-server-utils*amd64.deb"
fetch_and_deploy_gh_release "rustdesk-api" "lejianwen/rustdesk-api" "binary" "latest" "/opt/rustdesk" "rustdesk-api-server*amd64.deb"
msg_info "Starting services"
systemctl start -q rustdesk-* --all
systemctl start -q rustdesk-*
msg_ok "Services started"
msg_ok "Updated successfully!"

View File

@@ -53,6 +53,13 @@ function update_script() {
fi
sed -i 's|_TARGET=.*$|_URL=http://127.0.0.1:60072|' /opt/scanopy/.env
msg_info "Building Scanopy Server (patience)"
cd /opt/scanopy/backend
$STD cargo build --release --bin server --bin generate-fixtures
$STD ./target/release/generate-fixtures --output-dir /opt/scanopy/ui/src/lib/data
mv ./target/release/server /usr/bin/scanopy-server
msg_ok "Built Scanopy Server"
msg_info "Creating frontend UI"
export PUBLIC_SERVER_HOSTNAME=default
export PUBLIC_SERVER_PORT=""
@@ -61,12 +68,6 @@ function update_script() {
$STD npm run build
msg_ok "Created frontend UI"
msg_info "Building Scanopy Server (patience)"
cd /opt/scanopy/backend
$STD cargo build --release --bin server
mv ./target/release/server /usr/bin/scanopy-server
msg_ok "Built Scanopy Server"
if [[ -f /etc/systemd/system/scanopy-daemon.service ]]; then
fetch_and_deploy_gh_release "Scanopy Daemon" "scanopy/scanopy" "singlefile" "latest" "/usr/local/bin" "scanopy-daemon-linux-amd64"
mv "/usr/local/bin/Scanopy Daemon" /usr/local/bin/scanopy-daemon

View File

@@ -28,6 +28,34 @@ function update_script() {
msg_error "No ${APP} Installation Found!"
exit
fi
if [[ -f /opt/semaphore/semaphore_db.bolt ]]; then
msg_warn "WARNING: Due to bugs with BoltDB database, update script will move your application"
msg_warn "to use SQLite database instead. Unfortunately, this will reset your application and make it a fresh"
msg_warn "installation. All your data will be lost!"
echo ""
read -r -p "${TAB3}Do you want to continue? (y/N): " CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
exit 0
else
msg_info "Moving from BoltDB to SQLite"
systemctl stop semaphore
rm -rf /opt/semaphore/semaphore_db.bolt
sed -i \
-e 's|"bolt": {|"sqlite": {|' \
-e 's|/semaphore_db.bolt"|/database.sqlite"|' \
-e '/semaphore_db.bolt/d' \
-e '/"dialect"/d' \
-e '/^ },$/a\ "dialect": "sqlite",' \
/opt/semaphore/config.json
SEM_PW=$(cat ~/semaphore.creds)
systemctl start semaphore
$STD semaphore user add --admin --login admin --email admin@helper-scripts.com --name Administrator --password "${SEM_PW}" --config /opt/semaphore/config.json
msg_ok "Moved from BoltDB to SQLite"
fi
fi
if check_for_gh_release "semaphore" "semaphoreui/semaphore"; then
msg_info "Stopping Service"
systemctl stop semaphore

View File

@@ -55,8 +55,9 @@ function update_script() {
msg_ok "Updated Sparky Fitness Backend"
msg_info "Updating Sparky Fitness Frontend (Patience)"
cd /opt/sparkyfitness/SparkyFitnessFrontend
cd /opt/sparkyfitness
$STD pnpm install
cd /opt/sparkyfitness/SparkyFitnessFrontend
$STD pnpm run build
cp -a /opt/sparkyfitness/SparkyFitnessFrontend/dist/. /var/www/sparkyfitness/
msg_ok "Updated Sparky Fitness Frontend"

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Slaviša Arežina (tremor021)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/Suwayomi/Suwayomi-Server
APP="SuwayomiServer"
var_tags="${var_tags:-media;manga}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
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 [[ ! -f /usr/bin/suwayomi-server ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "suwayomi-server" "Suwayomi/Suwayomi-Server"; then
JAVA_VERSION=21 setup_java
msg_info "Stopping Service"
systemctl stop suwayomi-server
msg_info "Stopped Service"
fetch_and_deploy_gh_release "suwayomi-server" "Suwayomi/Suwayomi-Server" "binary"
msg_info "Starting Service"
systemctl start suwayomi-server
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:4567${CL}"

View File

@@ -9,7 +9,7 @@ APP="Tracearr"
var_tags="${var_tags:-media}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-5}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"

View File

@@ -29,6 +29,10 @@ function update_script() {
exit
fi
if grep -q '^WF_CORS_ALLOW_ORIGINS=\*$' /opt/wealthfolio/.env; then
sed -i "s|^WF_CORS_ALLOW_ORIGINS=\*$|WF_CORS_ALLOW_ORIGINS=http://${LOCAL_IP}:8080|" /opt/wealthfolio/.env
fi
if check_for_gh_release "wealthfolio" "afadil/wealthfolio"; then
msg_info "Stopping Service"
systemctl stop wealthfolio

View File

@@ -37,7 +37,7 @@ function update_script() {
if [[ -d /etc/wgdashboard ]]; then
sleep 2
cd /etc/wgdashboard/src
$STD ./wgd.sh update
$STD ./wgd.sh update -y
$STD ./wgd.sh start
fi
msg_ok "Updated LXC"

View File

@@ -35,13 +35,16 @@ function update_script() {
msg_ok "Stopped Service"
msg_info "Creating Backup"
rm -rf /opt/${APP}_backup*.tar.gz
mkdir -p /opt/z2m_backup
$STD tar -czf /opt/z2m_backup/${APP}_backup_$(date +%Y%m%d%H%M%S).tar.gz -C /opt zigbee2mqtt
mv /opt/zigbee2mqtt/data /opt/z2m_backup
msg_ok "Backup Created"
ensure_dependencies zstd
mkdir -p /opt/{backups,z2m_backup}
BACKUP_VERSION="$(<"$HOME/.zigbee2mqtt")"
BACKUP_FILE="/opt/backups/${APP}_backup_${BACKUP_VERSION}.tar.zst"
$STD tar -cf - -C /opt zigbee2mqtt | zstd -q -o "$BACKUP_FILE"
ls -t /opt/backups/${APP}_backup_*.tar.zst 2>/dev/null | tail -n +6 | xargs -r rm -f
mv /opt/zigbee2mqtt/data /opt/z2m_backup/data
msg_ok "Backup Created (${BACKUP_VERSION})"
fetch_and_deploy_gh_release "Zigbee2MQTT" "Koenkk/zigbee2mqtt" "tarball" "latest" "/opt/zigbee2mqtt"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Zigbee2MQTT" "Koenkk/zigbee2mqtt" "tarball" "latest" "/opt/zigbee2mqtt"
msg_info "Updating Zigbee2MQTT"
rm -rf /opt/zigbee2mqtt/data

39
frontend/.gitignore vendored
View File

@@ -1,39 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# wrangler
.worker-next
.wrangler
# testing
/coverage
# next.js
/.next/
out
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# # local env files
# .env*.local
# .env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

51
frontend/.vscode/settings.json generated vendored
View File

@@ -1,51 +0,0 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024-Present Bram Suurd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,281 +0,0 @@
# Proxmox VE Helper-Scripts Frontend
> 🚀 **Modern frontend for the Community-Scripts Proxmox VE Helper-Scripts repository**
A comprehensive, user-friendly interface built with Next.js that provides access to 300+ automation scripts for Proxmox Virtual Environment management. This frontend serves as the official website for the Community-Scripts organization's Proxmox VE Helper-Scripts repository.
![Next.js](https://img.shields.io/badge/Next.js-15.2.4-black?style=flat-square&logo=next.js)
![React](https://img.shields.io/badge/React-19.0.0-blue?style=flat-square&logo=react)
![TypeScript](https://img.shields.io/badge/TypeScript-5.8.2-blue?style=flat-square&logo=typescript)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-3.4.17-06B6D4?style=flat-square&logo=tailwindcss)
![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
## 🌟 Features
### Core Functionality
- **📜 Script Management**: Browse, search, and filter 300+ Proxmox VE scripts
- **📱 Responsive Design**: Mobile-first approach with modern UI/UX
- **🔍 Advanced Search**: Fuzzy search with category filtering
- **📊 Analytics Integration**: Built-in analytics for usage tracking
- **🌙 Dark/Light Mode**: Theme switching with system preference detection
- **⚡ Performance Optimized**: Static site generation for lightning-fast loading
### Technical Features
- **🎨 Modern UI Components**: Built with Radix UI and shadcn/ui
- **📈 Data Visualization**: Charts and metrics using Chart.js
- **🔄 State Management**: React Query for efficient data fetching
- **📝 Type Safety**: Full TypeScript implementation
- **🚀 Static Export**: Optimized for GitHub Pages deployment
## 🛠️ Tech Stack
### Frontend Framework
- **[Next.js 15.2.4](https://nextjs.org/)** - React framework with App Router
- **[React 19.0.0](https://react.dev/)** - Latest React with concurrent features
- **[TypeScript 5.8.2](https://www.typescriptlang.org/)** - Type-safe JavaScript
### Styling & UI
- **[Tailwind CSS 3.4.17](https://tailwindcss.com/)** - Utility-first CSS framework
- **[Radix UI](https://www.radix-ui.com/)** - Unstyled, accessible UI components
- **[shadcn/ui](https://ui.shadcn.com/)** - Re-usable components built on Radix UI
- **[Framer Motion](https://www.framer.com/motion/)** - Animation library
- **[Lucide React](https://lucide.dev/)** - Icon library
### Data & State Management
- **[TanStack Query 5.71.1](https://tanstack.com/query)** - Powerful data synchronization
- **[Zod 3.24.2](https://zod.dev/)** - TypeScript-first schema validation
- **[nuqs 2.4.1](https://nuqs.47ng.com/)** - Type-safe search params state manager
### Development Tools
- **[Vitest 3.1.1](https://vitest.dev/)** - Fast unit testing framework
- **[React Testing Library](https://testing-library.com/react)** - Simple testing utilities
- **[ESLint](https://eslint.org/)** - Code linting and formatting
- **[Prettier](https://prettier.io/)** - Code formatting
### Additional Libraries
- **[Chart.js](https://www.chartjs.org/)** - Data visualization
- **[Fuse.js](https://fusejs.io/)** - Fuzzy search
- **[date-fns](https://date-fns.org/)** - Date utility library
- **[Next Themes](https://github.com/pacocoursey/next-themes)** - Theme management
## 🚀 Getting Started
### Prerequisites
- **Node.js 18+** (recommend using the latest LTS version)
- **npm**, **yarn**, **pnpm**, or **bun** package manager
- **Git** for version control
### Installation
1. **Clone the repository**
```bash
git clone https://github.com/community-scripts/ProxmoxVE.git
cd ProxmoxVE/frontend
```
2. **Install dependencies**
```bash
# Using npm
npm install
# Using yarn
yarn install
# Using pnpm
pnpm install
# Using bun
bun install
```
3. **Start the development server**
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
4. **Open your browser**
Navigate to [http://localhost:3000](http://localhost:3000) to see the application running.
### Environment Configuration
The application uses the following environment variables:
- `BASE_PATH`: Set to "ProxmoxVE" for GitHub Pages deployment
- Analytics configuration is handled in `src/config/siteConfig.tsx`
## 🧪 Development
### Available Scripts
```bash
# Development
npm run dev # Start development server with Turbopack
npm run build # Build for production
npm run start # Start production server (after build)
# Code Quality
npm run lint # Run ESLint
npm run typecheck # Run TypeScript type checking
npm run format:write # Format code with Prettier
npm run format:check # Check code formatting
# Deployment
npm run deploy # Build and deploy to GitHub Pages
```
### Development Workflow
1. **Feature Development**
- Create a new branch for your feature
- Follow the established TypeScript and React patterns
- Use the existing component library (shadcn/ui)
- Ensure responsive design principles
2. **Code Standards**
- Follow TypeScript strict mode
- Use functional components with hooks
- Implement proper error boundaries
- Write descriptive variable and function names
- Use early returns for better readability
3. **Styling Guidelines**
- Use Tailwind CSS utility classes
- Follow mobile-first responsive design
- Implement dark/light mode considerations
- Use CSS variables from the design system
4. **Testing**
- Write unit tests for utility functions
- Test React components with React Testing Library
- Ensure accessibility standards are met
- Run tests before committing
### Component Development
The project uses a component-driven development approach:
```typescript
// Example component structure
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
interface ComponentProps {
title: string;
className?: string;
}
export const Component = ({ title, className }: ComponentProps) => {
return (
<div className={cn("default-classes", className)}>
<Button>{title}</Button>
</div>
);
};
```
### Configuration for Static Export
The application is configured for static export in `next.config.mjs`:
```javascript
const nextConfig = {
output: "export",
basePath: `/ProxmoxVE`,
images: {
unoptimized: true // Required for static export
}
};
```
## 🤝 Contributing
We welcome contributions from the community! Here's how you can help:
### Getting Started
1. **Fork the repository** on GitHub
2. **Clone your fork** locally
3. **Create a new branch** for your feature or bugfix
4. **Make your changes** following our coding standards
5. **Submit a pull request** with a clear description
### Contribution Guidelines
#### Code Style
- Follow the existing TypeScript and React patterns
- Use descriptive variable and function names
- Implement proper error handling
- Write self-documenting code with appropriate comments
#### Component Guidelines
- Use functional components with hooks
- Implement proper TypeScript types
- Follow accessibility best practices
- Ensure responsive design
- Use the existing design system components
#### Pull Request Process
1. Update documentation if needed
2. Update the README if you've added new features
3. Request review from maintainers
### Areas for Contribution
- **🐛 Bug fixes**: Report and fix issues
- **✨ New features**: Enhance functionality
- **📚 Documentation**: Improve guides and examples
- **🎨 UI/UX**: Improve design and user experience
- **♿ Accessibility**: Enhance accessibility features
- **🚀 Performance**: Optimize loading and runtime performance
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- **[tteck](https://github.com/tteck)** - Original creator of the Proxmox VE Helper-Scripts
- **[Community-Scripts Organization](https://github.com/community-scripts)** - Maintaining and expanding the project
- **[Proxmox Community](https://forum.proxmox.com/)** - For continuous feedback and support
- **All Contributors** - Thank you for your valuable contributions!
## 📚 Additional Resources
- **[Proxmox VE Documentation](https://pve.proxmox.com/pve-docs/)**
- **[Community Scripts Repository](https://github.com/community-scripts/ProxmoxVE)**
- **[Discord Community](https://discord.gg/3AnUqsXnmK)**
- **[GitHub Discussions](https://github.com/community-scripts/ProxmoxVE/discussions)**
## 🔗 Links
- **🌐 Live Website**: [https://community-scripts.github.io/ProxmoxVE/](https://community-scripts.github.io/ProxmoxVE/)
- **💬 Discord Server**: [https://discord.gg/3AnUqsXnmK](https://discord.gg/3AnUqsXnmK)
- **📝 Change Log**: [https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md](https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md)
---
**Made with ❤️ by the Community-Scripts team and contributors**

2031
frontend/bun.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "@/styles/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
},
"registries": {
"@animate-ui": "https://animate-ui.com/r/{name}.json"
}
}

View File

@@ -1,41 +0,0 @@
import antfu from "@antfu/eslint-config";
export default antfu(
{
type: "app",
typescript: true,
formatters: true,
next: true,
stylistic: {
indent: 2,
semi: true,
quotes: "double",
},
ignores: ["src/components/ui/**", "README.md", "public/json/**"],
},
{
rules: {
"ts/no-redeclare": "off",
"ts/consistent-type-definitions": ["error", "type"],
"no-console": ["warn"],
"antfu/no-top-level-await": ["off"],
"node/prefer-global/process": ["off"],
"node/no-process-env": ["error"],
"perfectionist/sort-imports": [
"error",
{
type: "line-length",
order: "desc",
},
],
"unicorn/filename-case": [
"error",
{
case: "kebabCase",
ignore: ["README.md"],
},
],
},
},
);

View File

@@ -1,29 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.resolve.alias.canvas = false;
return config;
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
},
env: {
BASE_PATH: "ProxmoxVE",
},
eslint: {
ignoreDuringBuilds: true,
},
output: "export",
basePath: `/ProxmoxVE`,
};
export default nextConfig;

87
frontend/package.json generated
View File

@@ -1,87 +0,0 @@
{
"name": "proxmox-helper-scripts-website",
"type": "module",
"version": "1.0.0",
"private": true,
"author": {
"name": "Bram Suurd",
"url": "https://github.com/community-scripts"
},
"license": "MIT",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint . --fix",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.12",
"@types/react-syntax-highlighter": "^15.5.13",
"chart.js": "^4.5.1",
"chartjs-plugin-datalabels": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.26",
"fuse.js": "^7.1.0",
"lucide-react": "^0.561.0",
"mini-svg-data-uri": "^1.4.4",
"motion": "^12.23.26",
"next": "15.5.8",
"next-themes": "^0.4.6",
"nuqs": "^2.8.5",
"react": "19.2.3",
"react-chartjs-2": "^5.3.1",
"react-code-blocks": "^0.1.6",
"react-datepicker": "^9.0.0",
"react-day-picker": "^9.12.0",
"react-dom": "19.2.3",
"react-icons": "^5.5.0",
"react-syntax-highlighter": "^16.1.0",
"react-use-measure": "^2.1.7",
"recharts": "3.6.0",
"sharp": "^0.34.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"zod": "^4.2.1"
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.1",
"@eslint-react/eslint-plugin": "^2.3.13",
"@next/eslint-plugin-next": "^15.5.8",
"@tanstack/eslint-plugin-query": "^5.91.2",
"@types/node": "^25.0.2",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-config-next": "15.5.8",
"eslint-plugin-format": "^1.1.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.25",
"jsdom": "^27.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-animated": "^1.1.2",
"typescript": "^5.9.3"
}
}

View File

@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -1,40 +0,0 @@
{
"name": "Suwayomi-Server",
"slug": "suwayomi-server",
"categories": [
13
],
"date_created": "2025-08-01",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 4567,
"documentation": "https://github.com/Suwayomi/Suwayomi-Server/wiki",
"website": "https://github.com/Suwayomi/Suwayomi-Server",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/suwayomi.webp",
"config_path": "",
"description": "A free and open source manga reader server that runs extensions built for Mihon (Tachiyomi).",
"install_methods": [
{
"type": "default",
"script": "ct/suwayomiserver.sh",
"resources": {
"cpu": 1,
"ram": 1024,
"hdd": 4,
"os": "debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "This application can be conflicting with Kaspersky products. You maybe need to disable Kaspersky in order to use this application.",
"type": "info"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,63 +0,0 @@
import { NextResponse } from "next/server";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { Metadata, Script } from "@/lib/types";
export const dynamic = "force-static";
const jsonDir = "public/json";
const metadataFileName = "metadata.json";
const versionFileName = "version.json";
const encoding = "utf-8";
async function getMetadata() {
const filePath = path.resolve(jsonDir, metadataFileName);
const fileContent = await fs.readFile(filePath, encoding);
const metadata: Metadata = JSON.parse(fileContent);
return metadata;
}
async function getScripts() {
const filePaths = (await fs.readdir(jsonDir))
.filter(fileName =>
fileName.endsWith(".json")
&& fileName !== metadataFileName
&& fileName !== versionFileName,
)
.map(fileName => path.resolve(jsonDir, fileName));
const scripts = await Promise.all(
filePaths.map(async (filePath) => {
const fileContent = await fs.readFile(filePath, encoding);
const script: Script = JSON.parse(fileContent);
return script;
}),
);
return scripts;
}
export async function GET() {
try {
const metadata = await getMetadata();
const scripts = await getScripts();
const categories = metadata.categories
.map((category) => {
category.scripts = scripts.filter(script =>
script.categories?.includes(category.id),
);
return category;
})
.sort((a, b) => a.sort_order - b.sort_order);
return NextResponse.json(categories);
}
catch (error) {
console.error(error as Error);
return NextResponse.json(
{ error: "Failed to fetch categories" },
{ status: 500 },
);
}
}

View File

@@ -1,36 +0,0 @@
import { NextResponse } from "next/server";
import { promises as fs } from "node:fs";
import path from "node:path";
import type { GitHubVersionsResponse } from "@/lib/types";
export const dynamic = "force-static";
const jsonDir = "public/json";
const versionsFileName = "github-versions.json";
const encoding = "utf-8";
async function getVersions(): Promise<GitHubVersionsResponse> {
const filePath = path.resolve(jsonDir, versionsFileName);
const fileContent = await fs.readFile(filePath, encoding);
const data: GitHubVersionsResponse = JSON.parse(fileContent);
return data;
}
export async function GET() {
try {
const versions = await getVersions();
return NextResponse.json(versions);
}
catch (error) {
console.error(error);
const err = error as globalThis.Error;
return NextResponse.json({
generated: "",
versions: [],
error: err.message || "An unexpected error occurred",
}, {
status: 500,
});
}
}

View File

@@ -1,48 +0,0 @@
// import Error from "next/error";
import { NextResponse } from "next/server";
import { promises as fs } from "node:fs";
import path from "node:path";
export const dynamic = "force-static";
const jsonDir = "public/json";
const versionsFileName = "versions.json";
const encoding = "utf-8";
interface LegacyVersion {
name: string;
version: string;
date: string;
}
async function getVersions() {
const filePath = path.resolve(jsonDir, versionsFileName);
const fileContent = await fs.readFile(filePath, encoding);
const versions: LegacyVersion[] = JSON.parse(fileContent);
const modifiedVersions = versions.map((version) => {
let newName = version.name;
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, "");
return { ...version, name: newName, date: new Date(version.date) };
});
return modifiedVersions;
}
export async function GET() {
try {
const versions = await getVersions();
return NextResponse.json(versions);
}
catch (error) {
console.error(error);
const err = error as globalThis.Error;
return NextResponse.json({
name: err.name,
message: err.message || "An unexpected error occurred",
version: "No version found - Error",
}, {
status: 500,
});
}
}

View File

@@ -1,509 +0,0 @@
"use client";
import {
ArrowUpDown,
Box,
CheckCircle2,
ChevronLeft,
ChevronRight,
List,
Loader2,
Trophy,
XCircle,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Bar, BarChart, CartesianGrid, Cell, LabelList, XAxis } from "recharts";
import type { ChartConfig } from "@/components/ui/chart";
import { formattedBadge } from "@/components/command-menu";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
type DataModel = {
id: number;
ct_type: number;
disk_size: number;
core_count: number;
ram_size: number;
os_type: string;
os_version: string;
disableip6: string;
nsapp: string;
created_at: string;
method: string;
pve_version: string;
status: string;
error: string;
type: string;
[key: string]: any;
};
type SummaryData = {
total_entries: number;
status_count: Record<string, number>;
nsapp_count: Record<string, number>;
};
// Chart colors optimized for both light and dark modes
// Medium-toned colors that are visible and not too flashy in both themes
const CHART_COLORS = [
"#5B8DEF", // blue - medium tone
"#4ECDC4", // teal - medium tone
"#FF8C42", // orange - medium tone
"#A78BFA", // purple - medium tone
"#F472B6", // pink - medium tone
"#38BDF8", // cyan - medium tone
"#4ADE80", // green - medium tone
"#FBBF24", // yellow - medium tone
"#818CF8", // indigo - medium tone
"#FB7185", // rose - medium tone
"#2DD4BF", // turquoise - medium tone
"#C084FC", // violet - medium tone
"#60A5FA", // sky blue - medium tone
"#84CC16", // lime - medium tone
"#F59E0B", // amber - medium tone
"#A855F7", // purple - medium tone
"#10B981", // emerald - medium tone
"#EAB308", // gold - medium tone
"#3B82F6", // royal blue - medium tone
"#EF4444", // red - medium tone
];
const chartConfigApps = {
count: {
label: "Installations",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export default function DataPage() {
const [data, setData] = useState<DataModel[]>([]);
const [summary, setSummary] = useState<SummaryData | null>(null);
const [summaryLoading, setSummaryLoading] = useState<boolean>(true);
const [dataLoading, setDataLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [sortConfig, setSortConfig] = useState<{
key: string;
direction: "ascending" | "descending";
} | null>(null);
const nf = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
// Fetch summary only once on mount
useEffect(() => {
const fetchSummary = async () => {
try {
const summaryRes = await fetch("https://api.htl-braunau.at/data/summary");
if (!summaryRes.ok) {
throw new Error(`Failed to fetch summary: ${summaryRes.statusText}`);
}
const summaryData: SummaryData = await summaryRes.json();
setSummary(summaryData);
} catch (err) {
setError((err as Error).message);
} finally {
setSummaryLoading(false);
}
};
fetchSummary();
}, []);
useEffect(() => {
const fetchData = async () => {
setDataLoading(true);
try {
const dataRes = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage}`);
if (!dataRes.ok) {
throw new Error(`Failed to fetch data: ${dataRes.statusText}`);
}
const pageData: DataModel[] = await dataRes.json();
setData(pageData);
} catch (err) {
setError((err as Error).message);
} finally {
setDataLoading(false);
}
};
fetchData();
}, [currentPage, itemsPerPage]);
const sortedData = useMemo(() => {
if (!sortConfig) return data;
return [...data].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
}, [data, sortConfig]);
const requestSort = (key: string) => {
let direction: "ascending" | "descending" = "ascending";
if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
direction = "descending";
}
setSortConfig({ key, direction });
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
};
const getTypeBadge = (type: string) => {
if (type === "lxc") return formattedBadge("ct");
if (type === "vm") return formattedBadge("vm");
return null;
};
// Stats calculations
const successCount = summary?.status_count.done ?? 0;
const failureCount = summary?.status_count.failed ?? 0;
const totalCount = summary?.total_entries ?? 0;
const successRate = totalCount > 0 ? (successCount / totalCount) * 100 : 0;
const allApps = useMemo(() => {
if (!summary?.nsapp_count) return [];
return Object.entries(summary.nsapp_count).sort(([, a], [, b]) => b - a);
}, [summary]);
const topApps = useMemo(() => {
return allApps.slice(0, 15);
}, [allApps]);
const mostPopularApp = topApps[0];
// Chart Data
const appsChartData = topApps.map(([name, count], index) => ({
app: name,
count,
fill: CHART_COLORS[index % CHART_COLORS.length],
}));
if (error) {
return (
<div className="p-6 text-center text-red-500">
<p>
Error loading data:
{error}
</p>
</div>
);
}
return (
<div className="mb-3">
<div className="mt-20 flex sm:px-4 xl:px-0">
<div className="mx-4 w-full sm:mx-0 space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold tracking-tight">Analytics</h1>
<p className="text-muted-foreground">Overview of container installations and system statistics.</p>
</div>
{/* Widgets */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Created</CardTitle>
<Box className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{nf.format(totalCount)}</div>
<p className="text-xs text-muted-foreground">Total LXC/VM entries found</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Success Rate</CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{successRate.toFixed(1)}%</div>
<p className="text-xs text-muted-foreground">{nf.format(successCount)} successful installations</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Failures</CardTitle>
<XCircle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{nf.format(failureCount)}</div>
<p className="text-xs text-muted-foreground">Installations encountered errors</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Most Popular</CardTitle>
<Trophy className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="truncate text-2xl font-bold">{mostPopularApp ? mostPopularApp[0] : "N/A"}</div>
<p className="text-xs text-muted-foreground">
{mostPopularApp ? nf.format(mostPopularApp[1]) : 0} installations
</p>
</CardContent>
</Card>
</div>
{/* Graphs */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div className="space-y-1.5">
<CardTitle>Top Applications</CardTitle>
<CardDescription>The most frequently installed applications.</CardDescription>
</div>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="ml-auto">
<List className="mr-2 h-4 w-4" />
View All
</Button>
</DialogTrigger>
<DialogContent className="max-h-[80vh] sm:max-w-md">
<DialogHeader>
<DialogTitle>Application Statistics</DialogTitle>
<DialogDescription>Installation counts for all {allApps.length} applications.</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[60vh] w-full rounded-md border p-4">
<div className="space-y-4">
{allApps.map(([name, count], index) => (
<div key={name} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="w-8 font-mono text-muted-foreground">{index + 1}.</span>
<span className="font-medium">{name}</span>
</div>
<span className="font-mono">{nf.format(count)}</span>
</div>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent className="pl-2">
<div className="h-[300px] w-full">
{summaryLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<ChartContainer config={chartConfigApps} className="h-full w-full">
<BarChart
accessibilityLayer
data={appsChartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="app"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => (value.length > 8 ? `${value.slice(0, 8)}...` : value)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent nameKey="app" />} />
<Bar dataKey="count" radius={8}>
{appsChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
</Bar>
</BarChart>
</ChartContainer>
)}
</div>
</CardContent>
</Card>
{/* Data Table */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Installation Log</CardTitle>
<CardDescription>Detailed records of all container creation attempts.</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select value={String(itemsPerPage)} onValueChange={(val) => setItemsPerPage(Number(val))}>
<SelectTrigger className="w-[80px]">
<SelectValue placeholder="Limit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] cursor-pointer" onClick={() => requestSort("status")}>
Status
{sortConfig?.key === "status" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead className="cursor-pointer" onClick={() => requestSort("type")}>
Type
{sortConfig?.key === "type" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead className="cursor-pointer" onClick={() => requestSort("nsapp")}>
Application
{sortConfig?.key === "nsapp" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead className="hidden cursor-pointer md:table-cell" onClick={() => requestSort("os_type")}>
OS
{sortConfig?.key === "os_type" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead
className="hidden cursor-pointer md:table-cell"
onClick={() => requestSort("disk_size")}
>
Disk Size
{sortConfig?.key === "disk_size" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead
className="hidden cursor-pointer lg:table-cell"
onClick={() => requestSort("core_count")}
>
Core Count
{sortConfig?.key === "core_count" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead
className="hidden cursor-pointer lg:table-cell"
onClick={() => requestSort("ram_size")}
>
RAM Size
{sortConfig?.key === "ram_size" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
<TableHead className="cursor-pointer text-right" onClick={() => requestSort("created_at")}>
Created At
{sortConfig?.key === "created_at" && <ArrowUpDown className="ml-2 inline h-4 w-4" />}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{dataLoading ? (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> Loading data...
</div>
</TableCell>
</TableRow>
) : sortedData.length > 0 ? (
sortedData.map((item, idx) => (
<TableRow key={`${item.id}-${idx}`}>
<TableCell>
{item.status === "done" ? (
<Badge className="text-green-500/75 border-green-500/75">Success</Badge>
) : item.status === "failed" ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge className="text-red-500/75 border-red-500/75 cursor-help">Failed</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-semibold">Error:</p>
<p className="text-sm">{item.error || "Unknown error"}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : item.status === "installing" ? (
<Badge className="text-blue-500/75 border-blue-500/75">Installing</Badge>
) : (
<Badge variant="outline">{item.status}</Badge>
)}
</TableCell>
<TableCell>
{getTypeBadge(item.type) || <Badge variant="outline">{item.type}</Badge>}
</TableCell>
<TableCell className="font-medium">{item.nsapp}</TableCell>
<TableCell className="hidden md:table-cell">
{item.os_type} {item.os_version}
</TableCell>
<TableCell className="hidden md:table-cell">
{item.disk_size}
GB
</TableCell>
<TableCell className="hidden lg:table-cell">{item.core_count}</TableCell>
<TableCell className="hidden lg:table-cell">
{item.ram_size}
MB
</TableCell>
<TableCell className="text-right">{formatDate(item.created_at)}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1 || dataLoading}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<div className="text-sm text-muted-foreground">Page {currentPage}</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => prev + 1)}
disabled={dataLoading || sortedData.length < itemsPerPage}
>
Next
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,125 +0,0 @@
import type { z } from "zod";
import { memo } from "react";
import type { Category } from "@/lib/types";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import type { Script } from "../_schemas/schemas";
type CategoryProps = {
script: Script;
setScript: (script: Script) => void;
setIsValid: (isValid: boolean) => void;
setZodErrors: (zodErrors: z.ZodError | null) => void;
categories: Category[];
};
const CategoryTag = memo(({
category,
onRemove,
}: {
category: Category;
onRemove: () => void;
}) => (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{category.name}
<button
type="button"
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
onClick={onRemove}
>
<span className="sr-only">Remove</span>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</span>
));
CategoryTag.displayName = "CategoryTag";
function Categories({
script,
setScript,
categories,
}: Omit<CategoryProps, "setIsValid" | "setZodErrors">) {
const addCategory = (categoryId: number) => {
setScript({
...script,
categories: [...new Set([...script.categories, categoryId])],
});
};
const removeCategory = (categoryId: number) => {
setScript({
...script,
categories: script.categories.filter((id: number) => id !== categoryId),
});
};
const categoryMap = new Map(categories.map(c => [c.id, c]));
return (
<div>
<Label>
Category
{" "}
<span className="text-red-500">*</span>
</Label>
<Select onValueChange={value => addCategory(Number(value))}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categories.map(category => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div
className={cn(
"flex flex-wrap gap-2",
script.categories.length !== 0 && "mt-2",
)}
>
{script.categories.map((categoryId) => {
const category = categoryMap.get(categoryId);
return category
? (
<CategoryTag
key={categoryId}
category={category}
onRemove={() => removeCategory(categoryId)}
/>
)
: null;
})}
</div>
</div>
);
}
export default memo(Categories);

View File

@@ -1,233 +0,0 @@
import type { z } from "zod";
import { PlusCircle, Trash2 } from "lucide-react";
import { memo, useCallback, useRef } from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { OperatingSystems } from "@/config/site-config";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { Script } from "../_schemas/schemas";
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
type InstallMethodProps = {
script: Script;
setScript: (value: Script | ((prevState: Script) => Script)) => void;
setIsValid: (isValid: boolean) => void;
setZodErrors: (zodErrors: z.ZodError | null) => void;
};
function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallMethodProps) {
const cpuRefs = useRef<(HTMLInputElement | null)[]>([]);
const ramRefs = useRef<(HTMLInputElement | null)[]>([]);
const hddRefs = useRef<(HTMLInputElement | null)[]>([]);
const addInstallMethod = useCallback(() => {
setScript((prev) => {
const { type, slug } = prev;
const newMethodType = "default";
let scriptPath = "";
if (type === "pve") {
scriptPath = `tools/pve/${slug}.sh`;
}
else if (type === "addon") {
scriptPath = `tools/addon/${slug}.sh`;
}
else {
scriptPath = `${type}/${slug}.sh`;
}
const method = InstallMethodSchema.parse({
type: newMethodType,
script: scriptPath,
resources: {
cpu: null,
ram: null,
hdd: null,
os: null,
version: null,
},
});
return {
...prev,
install_methods: [...prev.install_methods, method],
};
});
}, [setScript]);
const updateInstallMethod = useCallback(
(
index: number,
key: keyof Script["install_methods"][number],
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
) => {
setScript((prev) => {
const updatedMethods = prev.install_methods.map((method, i) => {
if (i === index) {
const updatedMethod = { ...method, [key]: value };
if (key === "type") {
updatedMethod.script
= value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
// Set OS to Alpine and reset version if type is alpine
if (value === "alpine") {
updatedMethod.resources.os = "Alpine";
updatedMethod.resources.version = null;
}
}
return updatedMethod;
}
return method;
});
const updated = {
...prev,
install_methods: updatedMethods,
};
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
if (!result.success) {
setZodErrors(result.error);
}
else {
setZodErrors(null);
}
return updated;
});
},
[setScript, setIsValid, setZodErrors],
);
const removeInstallMethod = useCallback(
(index: number) => {
setScript(prev => ({
...prev,
install_methods: prev.install_methods.filter((_, i) => i !== index),
}));
},
[setScript],
);
return (
<>
<h3 className="text-xl font-semibold">Install Methods</h3>
{script.install_methods.map((method, index) => (
<div key={index} className="space-y-2 border p-4 rounded">
<Select value={method.type} onValueChange={value => updateInstallMethod(index, "type", value)}>
<SelectTrigger>
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="alpine">Alpine</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-2">
<Input
ref={(el) => {
cpuRefs.current[index] = el;
}}
placeholder="CPU in Cores"
type="number"
value={method.resources.cpu || ""}
onChange={e =>
updateInstallMethod(index, "resources", {
...method.resources,
cpu: e.target.value ? Number(e.target.value) : null,
})}
/>
<Input
ref={(el) => {
ramRefs.current[index] = el;
}}
placeholder="RAM in MB"
type="number"
value={method.resources.ram || ""}
onChange={e =>
updateInstallMethod(index, "resources", {
...method.resources,
ram: e.target.value ? Number(e.target.value) : null,
})}
/>
<Input
ref={(el) => {
hddRefs.current[index] = el;
}}
placeholder="HDD in GB"
type="number"
value={method.resources.hdd || ""}
onChange={e =>
updateInstallMethod(index, "resources", {
...method.resources,
hdd: e.target.value ? Number(e.target.value) : null,
})}
/>
</div>
<div className="flex gap-2">
<Select
value={method.resources.os || undefined}
onValueChange={value =>
updateInstallMethod(index, "resources", {
...method.resources,
os: value || null,
version: null, // Reset version when OS changes
})}
disabled={method.type === "alpine"}
>
<SelectTrigger>
<SelectValue placeholder="OS" />
</SelectTrigger>
<SelectContent>
{OperatingSystems.map(os => (
<SelectItem key={os.name} value={os.name}>
{os.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={method.resources.version || undefined}
onValueChange={value =>
updateInstallMethod(index, "resources", {
...method.resources,
version: value || null,
})}
disabled={method.type === "alpine"}
>
<SelectTrigger>
<SelectValue placeholder="Version" />
</SelectTrigger>
<SelectContent>
{OperatingSystems.find(os => os.name === method.resources.os)?.versions.map(version => (
<SelectItem key={version.slug} value={version.name}>
{version.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
<Trash2 className="mr-2 h-4 w-4" />
{" "}
Remove Install Method
</Button>
</div>
))}
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
<PlusCircle className="mr-2 h-4 w-4" />
{" "}
Add Install Method
</Button>
</>
);
}
export default memo(InstallMethod);

View File

@@ -1,159 +0,0 @@
import type { z } from "zod";
import { PlusCircle, Trash2 } from "lucide-react";
import { memo, useCallback, useRef } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { AlertColors } from "@/config/site-config";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import type { Script } from "../_schemas/schemas";
import { ScriptSchema } from "../_schemas/schemas";
const NoteItem = memo(
({
note,
index,
updateNote,
removeNote,
}: {
note: Script["notes"][number];
index: number;
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
removeNote: (index: number) => void;
}) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
updateNote(index, "text", e.target.value);
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}, [index, updateNote]);
return (
<div className="space-y-2 border p-4 rounded">
<Input
placeholder="Note Text"
value={note.text}
onChange={handleTextChange}
ref={inputRef}
/>
<Select
value={note.type}
onValueChange={value => updateNote(index, "type", value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{Object.keys(AlertColors).map(type => (
<SelectItem key={type} value={type}>
<span className="flex items-center gap-2">
{type.charAt(0).toUpperCase() + type.slice(1)}
{" "}
<div
className={cn(
"size-4 rounded-full border",
AlertColors[type as keyof typeof AlertColors],
)}
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
variant="destructive"
type="button"
onClick={() => removeNote(index)}
>
<Trash2 className="mr-2 h-4 w-4" />
{" "}
Remove Note
</Button>
</div>
);
},
);
type NoteProps = {
script: Script;
setScript: (script: Script) => void;
setIsValid: (isValid: boolean) => void;
setZodErrors: (zodErrors: z.ZodError | null) => void;
};
function Note({
script,
setScript,
setIsValid,
setZodErrors,
}: NoteProps) {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const addNote = useCallback(() => {
setScript({
...script,
notes: [...script.notes, { text: "", type: "info" }],
});
}, [script, setScript]);
const updateNote = useCallback((
index: number,
key: keyof Script["notes"][number],
value: string,
) => {
const updated: Script = {
...script,
notes: script.notes.map((note, i) =>
i === index ? { ...note, [key]: value } : note,
),
};
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
setZodErrors(result.success ? null : result.error);
setScript(updated);
// Restore focus after state update
if (key === "text") {
setTimeout(() => {
inputRefs.current[index]?.focus();
}, 0);
}
}, [script, setScript, setIsValid, setZodErrors]);
const removeNote = useCallback((index: number) => {
setScript({
...script,
notes: script.notes.filter((_, i) => i !== index),
});
}, [script, setScript]);
return (
<>
<h3 className="text-xl font-semibold">Notes</h3>
{script.notes.map((note, index) => (
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
))}
<Button type="button" size="sm" onClick={addNote}>
<PlusCircle className="mr-2 h-4 w-4" />
{" "}
Add Note
</Button>
</>
);
}
NoteItem.displayName = "NoteItem";
export default memo(Note);

View File

@@ -1,59 +0,0 @@
import { z } from "zod";
import { AlertColors } from "@/config/site-config";
export const InstallMethodSchema = z.object({
type: z.enum(["default", "alpine"], {
message: "Type must be either 'default' or 'alpine'",
}),
script: z.string().min(1, "Script content cannot be empty"),
resources: z.object({
cpu: z.number().nullable(),
ram: z.number().nullable(),
hdd: z.number().nullable(),
os: z.string().nullable(),
version: z.string().nullable(),
}),
});
const NoteSchema = z.object({
text: z.string().min(1, "Note text cannot be empty"),
type: z.enum(Object.keys(AlertColors) as [keyof typeof AlertColors, ...(keyof typeof AlertColors)[]], {
message: `Type must be one of: ${Object.keys(AlertColors).join(", ")}`,
}),
});
export const ScriptSchema = z.object({
name: z.string().min(1, "Name is required"),
slug: z.string().min(1, "Slug is required"),
categories: z.array(z.number()),
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'",
}),
updateable: z.boolean(),
privileged: z.boolean(),
interface_port: z.number().nullable(),
documentation: z.string().nullable(),
website: z.url().nullable(),
logo: z.url().nullable(),
config_path: z.string(),
description: z.string().min(1, "Description is required"),
disable: z.boolean().optional(),
disable_description: z.string().optional(),
install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"),
default_credentials: z.object({
username: z.string().nullable(),
password: z.string().nullable(),
}),
notes: z.array(NoteSchema).optional().default([]),
}).refine((data) => {
if (data.disable === true && !data.disable_description) {
return false;
}
return true;
}, {
message: "disable_description is required when disable is true",
path: ["disable_description"],
});
export type Script = z.infer<typeof ScriptSchema>;

View File

@@ -1,590 +0,0 @@
"use client";
import type { z } from "zod";
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import SyntaxHighlighter from "react-syntax-highlighter";
import { useTheme } from "next-themes";
import { format } from "date-fns";
import { toast } from "sonner";
import Image from "next/image";
import type { Category } from "@/lib/types";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Calendar } from "@/components/ui/calendar";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { basePath } from "@/config/site-config";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { fetchCategories } from "@/lib/data";
import { cn } from "@/lib/utils";
import type { Script } from "./_schemas/schemas";
import { ScriptItem } from "../scripts/_components/script-item";
import InstallMethod from "./_components/install-method";
import { ScriptSchema } from "./_schemas/schemas";
import Categories from "./_components/categories";
import Note from "./_components/note";
function search(scripts: Script[], query: string): Script[] {
const queryLower = query.toLowerCase().trim();
const searchWords = queryLower.split(/\s+/).filter(Boolean);
return scripts
.map((script) => {
const nameLower = script.name.toLowerCase();
const descriptionLower = (script.description || "").toLowerCase();
let score = 0;
for (const word of searchWords) {
if (nameLower.includes(word)) {
score += 10;
}
if (descriptionLower.includes(word)) {
score += 5;
}
}
return { script, score };
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 20)
.map(({ script }) => script);
}
const initialScript: Script = {
name: "",
slug: "",
categories: [],
date_created: format(new Date(), "yyyy-MM-dd"),
type: "ct",
updateable: false,
privileged: false,
interface_port: null,
documentation: null,
config_path: "",
website: null,
logo: null,
description: "",
disable: undefined,
disable_description: undefined,
install_methods: [],
default_credentials: {
username: null,
password: null,
},
notes: [],
};
export default function JSONGenerator() {
const { theme } = useTheme();
const [script, setScript] = useState<Script>(initialScript);
const [isCopied, setIsCopied] = useState(false);
const [isValid, setIsValid] = useState(false);
const [categories, setCategories] = useState<Category[]>([]);
const [currentTab, setCurrentTab] = useState<"json" | "preview">("json");
const [selectedCategory, setSelectedCategory] = useState<string>("");
const [searchQuery, setSearchQuery] = useState<string>("");
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
const selectedCategoryObj = useMemo(
() => categories.find(cat => cat.id.toString() === selectedCategory),
[categories, selectedCategory],
);
const allScripts = useMemo(
() => categories.flatMap(cat => cat.scripts || []),
[categories],
);
const scripts = useMemo(() => {
const query = searchQuery.trim();
if (query) {
return search(allScripts, query);
}
if (selectedCategoryObj) {
return selectedCategoryObj.scripts || [];
}
return [];
}, [allScripts, selectedCategoryObj, searchQuery]);
useEffect(() => {
fetchCategories()
.then(setCategories)
.catch(error => console.error("Error fetching categories:", error));
}, []);
useEffect(() => {
if (!isValid && currentTab === "preview") {
setCurrentTab("json");
toast.error("Switched to JSON tab due to invalid configuration.");
}
}, [isValid, currentTab]);
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
setScript((prev) => {
const updated = { ...prev, [key]: value };
if (updated.slug && updated.type) {
updated.install_methods = updated.install_methods.map((method) => {
let scriptPath = "";
if (updated.type === "pve") {
scriptPath = `tools/pve/${updated.slug}.sh`;
}
else if (updated.type === "addon") {
scriptPath = `tools/addon/${updated.slug}.sh`;
}
else if (method.type === "alpine") {
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
}
else {
scriptPath = `${updated.type}/${updated.slug}.sh`;
}
return {
...method,
script: scriptPath,
};
});
}
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
setZodErrors(result.success ? null : result.error);
return updated;
});
}, []);
const handleCopy = useCallback(() => {
if (!isValid)
toast.warning("JSON schema is invalid. Copying anyway.");
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
if (isValid)
toast.success("Copied metadata to clipboard");
}, [script]);
const importScript = (script: Script) => {
try {
const result = ScriptSchema.safeParse(script);
if (!result.success) {
setIsValid(false);
setZodErrors(result.error);
toast.error("Imported JSON is invalid according to the schema.");
return;
}
setScript(result.data);
setIsValid(true);
setZodErrors(null);
toast.success("Imported JSON successfully");
}
catch (error) {
toast.error("Failed to read or parse the JSON file.");
}
};
const handleFileImport = useCallback(() => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.onchange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file)
return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const content = event.target?.result as string;
const parsed = JSON.parse(content);
importScript(parsed);
toast.success("Imported JSON successfully");
}
catch (error) {
toast.error("Failed to read the JSON file.");
}
};
reader.readAsText(file);
};
input.click();
}, [setScript]);
const handleDownload = useCallback(() => {
if (isValid === false) {
toast.error("Cannot download invalid JSON");
return;
}
const jsonString = JSON.stringify(script, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${script.slug || "script"}.json`;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
}, [script]);
const handleDateSelect = useCallback(
(date: Date | undefined) => {
updateScript("date_created", format(date || new Date(), "yyyy-MM-dd"));
},
[updateScript],
);
const formattedDate = useMemo(
() => (script.date_created ? format(script.date_created, "PPP") : undefined),
[script.date_created],
);
const validationAlert = useMemo(
() => (
<Alert className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}>
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
<AlertDescription>
{isValid
? "The current JSON is valid according to the schema."
: "The current JSON does not match the required schema."}
</AlertDescription>
{zodErrors && (
<div className="mt-2 space-y-1">
{zodErrors.issues.map((error, index) => (
<AlertDescription key={index} className="p-1 text-red-500">
{error.path.join(".")}
{" "}
-
{error.message}
</AlertDescription>
))}
</div>
)}
</Alert>
),
[isValid, zodErrors],
);
return (
<div className="flex h-screen mt-20">
<div className="w-1/2 p-4 overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">JSON Generator</h2>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>Import</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start">
<DropdownMenuGroup>
<DropdownMenuItem onSelect={handleFileImport}>Import local JSON file</DropdownMenuItem>
<Dialog
open={isImportDialogOpen}
onOpenChange={setIsImportDialogOpen}
>
<DialogTrigger asChild>
<DropdownMenuItem onSelect={e => e.preventDefault()}>
Import existing script
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-md w-full">
<DialogHeader>
<DialogTitle>Import existing script</DialogTitle>
<DialogDescription>
Select one of the puplished scripts to import its metadata.
</DialogDescription>
</DialogHeader>
<div className="flex items-center gap-2">
<div className="grid flex-1 gap-2">
<Select
value={selectedCategory}
onValueChange={setSelectedCategory}
>
<SelectTrigger>
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
{categories.map(category => (
<SelectItem key={category.id} value={category.id.toString()}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
placeholder="Search for a script..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
{!selectedCategory && !searchQuery
? (
<p className="text-muted-foreground text-sm text-center">
Select a category or search for a script
</p>
)
: scripts.length === 0
? (
<p className="text-muted-foreground text-sm text-center">
No scripts found
</p>
)
: (
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
{scripts.map(script => (
<div
key={script.slug}
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
onClick={() => {
importScript(script);
setIsImportDialogOpen(false);
}}
>
<Image
src={script.logo || `/${basePath}/logo.png`}
alt={script.name}
className="w-full h-12 object-contain mb-2"
width={16}
height={16}
unoptimized
/>
<p className="text-sm text-center">{script.name}</p>
</div>
))}
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<form className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>
Name
{" "}
<span className="text-red-500">*</span>
</Label>
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
</div>
<div>
<Label>
Slug
{" "}
<span className="text-red-500">*</span>
</Label>
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
</div>
</div>
<div>
<Label>
Logo
</Label>
<Input
placeholder="Full logo URL"
value={script.logo || ""}
onChange={e => updateScript("logo", e.target.value || null)}
/>
</div>
<div>
<Label>Config Path</Label>
<Input
placeholder="Path to config file"
value={script.config_path || ""}
onChange={e => updateScript("config_path", e.target.value || "")}
/>
</div>
<div>
<Label>
Description
{" "}
<span className="text-red-500">*</span>
</Label>
<Textarea
placeholder="Example"
value={script.description}
onChange={e => updateScript("description", e.target.value)}
/>
</div>
<Categories script={script} setScript={setScript} categories={categories} />
<div className="flex gap-2">
<div className="flex flex-col gap-2 w-full">
<Label>
Date Created
{" "}
<span className="text-red-500">*</span>
</Label>
<Popover>
<PopoverTrigger asChild className="flex-1">
<Button
variant="outline"
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
>
{formattedDate || <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={new Date(script.date_created)}
onSelect={handleDateSelect}
autoFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col gap-2 w-full">
<Label>Type</Label>
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ct">LXC Container</SelectItem>
<SelectItem value="vm">Virtual Machine</SelectItem>
<SelectItem value="pve">PVE-Tool</SelectItem>
<SelectItem value="addon">Add-On</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="w-full flex gap-5">
<div className="flex items-center space-x-2">
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
<label>Updateable</label>
</div>
<div className="flex items-center space-x-2">
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
<label>Privileged</label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={script.disable || false}
onCheckedChange={checked => updateScript("disable", checked)}
/>
<label>Disabled</label>
</div>
</div>
{script.disable && (
<div>
<Label>
Disable Description
{" "}
<span className="text-red-500">*</span>
</Label>
<Textarea
placeholder="Explain why this script is disabled..."
value={script.disable_description || ""}
onChange={e => updateScript("disable_description", e.target.value)}
/>
</div>
)}
<Input
placeholder="Interface Port"
type="number"
value={script.interface_port || ""}
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
/>
<div className="flex gap-2">
<Input
placeholder="Website URL"
value={script.website || ""}
onChange={e => updateScript("website", e.target.value || null)}
/>
<Input
placeholder="Documentation URL"
value={script.documentation || ""}
onChange={e => updateScript("documentation", e.target.value || null)}
/>
</div>
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
<h3 className="text-xl font-semibold">Default Credentials</h3>
<Input
placeholder="Username"
value={script.default_credentials.username || ""}
onChange={e =>
updateScript("default_credentials", {
...script.default_credentials,
username: e.target.value || null,
})}
/>
<Input
placeholder="Password"
value={script.default_credentials.password || ""}
onChange={e =>
updateScript("default_credentials", {
...script.default_credentials,
password: e.target.value || null,
})}
/>
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
</form>
</div>
<div className="w-1/2 p-4 bg-background overflow-y-auto">
<Tabs
defaultValue="json"
className="w-full"
onValueChange={value => setCurrentTab(value as "json" | "preview")}
value={currentTab}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="json">JSON</TabsTrigger>
<TabsTrigger disabled={!isValid} value="preview">Preview</TabsTrigger>
</TabsList>
<TabsContent value="json" className="h-full w-full">
{validationAlert}
<div className="relative">
<div className="absolute right-2 top-2 flex gap-1">
<Button size="icon" variant="outline" onClick={handleCopy}>
{isCopied ? <Check className="h-4 w-4" /> : <Clipboard className="h-4 w-4" />}
</Button>
<Button size="icon" variant="outline" onClick={handleDownload}>
<Download className="h-4 w-4" />
</Button>
</div>
<SyntaxHighlighter
language="json"
style={theme === "light" ? githubGist : nord}
className="mt-4 p-4 bg-secondary rounded shadow overflow-x-scroll"
>
{JSON.stringify(script, null, 2)}
</SyntaxHighlighter>
</div>
</TabsContent>
<TabsContent value="preview" className="h-full w-full">
<ScriptItem item={script} />
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@@ -1,130 +0,0 @@
import type { Metadata } from "next";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { Inter } from "next/font/google";
import Script from "next/script";
import React from "react";
import { ThemeProvider } from "@/components/theme-provider";
import { analytics, basePath } from "@/config/site-config";
import QueryProvider from "@/components/query-provider";
import { Toaster } from "@/components/ui/sonner";
import Footer from "@/components/footer";
import Navbar from "@/components/navbar";
import "@/styles/globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Proxmox VE Helper-Scripts",
description:
"The official website for the Proxmox VE Helper-Scripts (Community) repository. Featuring over 400+ scripts to help you manage your Proxmox Virtual Environment.",
applicationName: "Proxmox VE Helper-Scripts",
generator: "Next.js",
referrer: "origin-when-cross-origin",
keywords: [
"Proxmox VE",
"Helper-Scripts",
"tteck",
"helper",
"scripts",
"proxmox",
"VE",
"virtualization",
"containers",
"LXC",
"VM",
],
authors: [
{ name: "Bram Suurd", url: "https://github.com/BramSuurdje" },
{ name: "Community Scripts", url: "https://github.com/Community-Scripts" },
],
creator: "Bram Suurd",
publisher: "Community Scripts",
metadataBase: new URL(`https://community-scripts.github.io/${basePath}/`),
alternates: {
canonical: `https://community-scripts.github.io/${basePath}/`,
},
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 5,
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
title: "Proxmox VE Helper-Scripts",
description:
"The official website for the Proxmox VE Helper-Scripts (Community) repository. Featuring over 400+ scripts to help you manage your Proxmox Virtual Environment.",
url: `https://community-scripts.github.io/${basePath}/`,
siteName: "Proxmox VE Helper-Scripts",
images: [
{
url: `https://community-scripts.github.io/${basePath}/defaultimg.png`,
width: 1200,
height: 630,
alt: "Proxmox VE Helper-Scripts",
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Proxmox VE Helper-Scripts",
creator: "@BramSuurdje",
description:
"The official website for the Proxmox VE Helper-Scripts (Community) repository. Featuring over 400+ scripts to help you manage your Proxmox Virtual Environment.",
images: [`https://community-scripts.github.io/${basePath}/defaultimg.png`],
},
manifest: "/manifest.webmanifest",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "Proxmox VE Helper-Scripts",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="canonical" href={metadata.metadataBase?.href} />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="preconnect" href="https://api.github.com" />
</head>
<body className={inter.className}>
<Script
src={`https://${analytics.url}/api/script.js`}
data-site-id={analytics.token}
strategy="afterInteractive"
/>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
<div className="flex w-full flex-col justify-center">
<NuqsAdapter>
<QueryProvider>
<Navbar />
<div className="flex min-h-screen flex-col justify-center">
<div className="flex w-full justify-center">
<div className="w-full max-w-[1440px] ">
{children}
<Toaster richColors />
</div>
</div>
<Footer />
</div>
</QueryProvider>
</NuqsAdapter>
</div>
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -1,29 +0,0 @@
import type { MetadataRoute } from "next";
import { basePath } from "@/config/site-config";
export function generateStaticParams() {
return [];
}
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Proxmox VE Helper-Scripts",
short_name: "Proxmox VE Helper-Scripts",
description:
"A redesigned front-end for the Proxmox VE Helper-Scripts repository. Featuring over 300+ scripts to help you manage your Proxmox Virtual Environment.",
theme_color: "#030712",
background_color: "#030712",
display: "standalone",
orientation: "portrait",
scope: `${basePath}`,
start_url: `${basePath}`,
icons: [
{
src: "logo.png",
sizes: "512x512",
type: "image/png",
},
],
};
}

View File

@@ -1,31 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { basePath } from "@/config/site-config";
export default function NotFoundPage() {
return (
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
<div className="space-y-2 text-center">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
404
</h1>
<p className="text-muted-foreground md:text-xl">
Oops, the page you are looking for could not be found.
</p>
</div>
<Button
onClick={() => {
if (window.history.length > 1) {
window.history.back();
}
else {
window.location.href = `/${basePath}`;
}
}}
variant="secondary"
>
Go Back
</Button>
</div>
);
}

View File

@@ -1,153 +0,0 @@
"use client";
import { ArrowRightIcon, ExternalLink } from "lucide-react";
import { useEffect, useState } from "react";
import { FaGithub } from "react-icons/fa";
import { useTheme } from "next-themes";
import Link from "next/link";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
import { Separator } from "@/components/ui/separator";
import { CardFooter } from "@/components/ui/card";
import Particles from "@/components/ui/particles";
import { Button } from "@/components/ui/button";
import { basePath } from "@/config/site-config";
import FAQ from "@/components/faq";
import { cn } from "@/lib/utils";
function CustomArrowRightIcon() {
return <ArrowRightIcon className="h-4 w-4" width={1} />;
}
export default function Page() {
const { theme } = useTheme();
const [color, setColor] = useState("#000000");
useEffect(() => {
setColor(theme === "dark" ? "#ffffff" : "#000000");
}, [theme]);
return (
<>
<div className="w-full mt-16">
<Particles className="absolute inset-0 -z-40" quantity={100} ease={80} color={color} refresh />
<div className="container mx-auto">
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
<Dialog>
<DialogTrigger>
<div>
<AnimatedGradientText>
<div
className={cn(
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
`p-px ![mask-composite:subtract]`,
)}
/>
{" "}
<Separator className="mx-2 h-4" orientation="vertical" />
<span
className={cn(
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
`inline`,
)}
>
Scripts by tteck
</span>
</AnimatedGradientText>
</div>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Thank You!</DialogTitle>
<DialogDescription>
A big thank you to tteck and the many contributors who have made this project possible. Your hard
work is truly appreciated by the entire Proxmox community!
</DialogDescription>
</DialogHeader>
<CardFooter className="flex flex-col gap-2">
<Button className="w-full" variant="outline" asChild>
<a
href="https://github.com/tteck"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center"
>
<FaGithub className="mr-2 h-4 w-4" />
{" "}
Tteck&apos;s GitHub
</a>
</Button>
<Button className="w-full" asChild>
<a
href={`https://github.com/community-scripts/${basePath}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center"
>
<ExternalLink className="mr-2 h-4 w-4" />
{" "}
Proxmox Helper Scripts
</a>
</Button>
</CardFooter>
</DialogContent>
</Dialog>
<div className="flex flex-col gap-4">
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl">
Make managing your Homelab a breeze
</h1>
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
<p>
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
</p>
<p>
With 400+ scripts to help you manage your
{" "}
<b>Proxmox VE</b>
, whether you&#39;re a seasoned user or a
newcomer, we&#39;ve got you covered.
</p>
</div>
</div>
<div className="flex flex-row gap-3">
<Link href="/scripts">
<Button
size="lg"
variant="expandIcon"
Icon={CustomArrowRightIcon}
iconPlacement="right"
className="hover:"
>
View Scripts
</Button>
</Link>
</div>
</div>
{/* FAQ Section */}
<div className="py-20" id="faq">
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold tracking-tighter md:text-5xl mb-4">Frequently Asked Questions</h2>
<p className="text-muted-foreground text-lg">
Find answers to common questions about our Proxmox VE scripts
</p>
</div>
<FAQ />
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,15 +0,0 @@
import type { MetadataRoute } from "next";
import { basePath } from "@/config/site-config";
export const dynamic = "force-static";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `https://community-scripts.github.io/${basePath}/sitemap.xml`,
};
}

View File

@@ -1,43 +0,0 @@
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
type ResourceDisplayProps = {
title: string;
cpu: number | null;
ram: number | null;
hdd: number | null;
};
type IconTextProps = {
icon: React.ReactNode;
label: string;
};
function IconText({ icon, label }: IconTextProps) {
return (
<span className="inline-flex items-center gap-1.5 rounded-md bg-accent/20 px-2 py-1 text-sm">
{icon}
<span className="text-foreground/90">{label}</span>
</span>
);
}
export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps) {
const hasCPU = typeof cpu === "number" && cpu > 0;
const hasRAM = typeof ram === "number" && ram > 0;
const hasHDD = typeof hdd === "number" && hdd > 0;
if (!hasCPU && !hasRAM && !hasHDD)
return null;
return (
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">{title}</span>
<div className="flex flex-wrap gap-2">
{hasCPU && <IconText icon={<CPUIcon />} label={`${cpu} vCPU`} />}
{hasRAM && <IconText icon={<RAMIcon />} label={getDisplayValueFromRAM(ram!)} />}
{hasHDD && <IconText icon={<HDDIcon />} label={`${hdd} GB`} />}
</div>
</div>
);
}

View File

@@ -1,159 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import * as Icons from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import type { Category } from "@/lib/types";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { formattedBadge } from "@/components/command-menu";
import { basePath } from "@/config/site-config";
import { cn } from "@/lib/utils";
function getCategoryIcon(iconName: string) {
// Convert kebab-case to PascalCase for Lucide icon names
const pascalCaseName = iconName
.split("-")
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
const IconComponent = (Icons as any)[pascalCaseName];
return IconComponent ? <IconComponent className="size-4 text-[#0083c3] mr-2" /> : null;
}
export default function ScriptAccordion({
items,
selectedScript,
setSelectedScript,
selectedCategory,
setSelectedCategory,
onItemSelect,
}: {
items: Category[];
selectedScript: string | null;
setSelectedScript: (script: string | null) => void;
selectedCategory: string | null;
setSelectedCategory: (category: string | null) => void;
onItemSelect?: () => void;
}) {
const [expandedItem, setExpandedItem] = useState<string | undefined>(undefined);
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
const handleAccordionChange = (value: string | undefined) => {
setExpandedItem(value);
};
const handleSelected = useCallback(
(slug: string) => {
setSelectedScript(slug);
},
[setSelectedScript],
);
useEffect(() => {
if (selectedScript) {
let category;
// If we have a selected category, try to find the script in that specific category
if (selectedCategory) {
category = items.find(
cat => cat.name === selectedCategory && cat.scripts.some(script => script.slug === selectedScript),
);
}
// Fallback: if no category is selected or script not found in selected category,
// use the first category containing the script (backward compatibility)
if (!category) {
category = items.find(category => category.scripts.some(script => script.slug === selectedScript));
}
if (category) {
setExpandedItem(category.name);
handleSelected(selectedScript);
}
}
}, [selectedScript, selectedCategory, items, handleSelected]);
return (
<Accordion
type="single"
value={expandedItem}
onValueChange={handleAccordionChange}
collapsible
className="overflow-y-scroll sm:max-h-[calc(100vh-209px)] overflow-x-hidden p-1"
>
{items.map(category => (
<AccordionItem
key={`${category.id}:category`}
value={category.name}
className={cn("sm:text-sm flex flex-col border-none", {
"rounded-lg bg-accent/30": expandedItem === category.name,
})}
>
<AccordionTrigger
className={cn(
"duration-250 rounded-lg transition ease-in-out hover:-translate-y-1 hover:scale-105 hover:bg-accent",
)}
>
<div className="mr-2 flex w-full items-center justify-between">
<div className="flex items-center pl-2 text-left">
{getCategoryIcon(category.icon)}
<span>
{category.name}
{" "}
</span>
</div>
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
{category.scripts.length}
</span>
</div>
{" "}
</AccordionTrigger>
<AccordionContent data-state={expandedItem === category.name ? "open" : "closed"} className="pt-0">
{category.scripts
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((script, index) => (
<div key={index}>
<Link
href={{
pathname: "/scripts",
query: { id: script.slug, category: category.name },
}}
prefetch={false}
className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${selectedScript === script.slug
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
: ""
} ${script.disable ? "opacity-60" : ""}`}
onClick={() => {
handleSelected(script.slug);
setSelectedCategory(category.name);
onItemSelect?.();
}}
ref={(el) => {
linkRefs.current[script.slug] = el;
}}
>
<div className="flex items-center">
<Image
src={script.logo || `/${basePath}/logo.png`}
height={16}
width={16}
unoptimized
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
alt={script.name}
className="mr-1 w-4 h-4 rounded-full"
/>
<span className="flex items-center gap-2">
{script.name}
</span>
</div>
{formattedBadge(script.type)}
</Link>
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}

View File

@@ -1,211 +0,0 @@
import { CalendarPlus } from "lucide-react";
import { useEffect, useMemo } from "react";
import Image from "next/image";
import Link from "next/link";
import type { Category, Script } from "@/lib/types";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { basePath, mostPopularScripts } from "@/config/site-config";
import { Button } from "@/components/ui/button";
import { extractDate } from "@/lib/time";
const ITEMS_PER_PAGE = 3;
export function getDisplayValueFromType(type: string) {
switch (type) {
case "ct":
return "LXC";
case "vm":
return "VM";
case "pve":
return "PVE";
case "addon":
return "ADDON";
default:
return "";
}
}
export function LatestScripts({
items,
page,
onPageChange,
}: {
items: Category[];
page: number;
onPageChange: (page: number) => void;
}) {
const latestScripts = useMemo(() => {
if (!items) return [];
const scripts = items.flatMap((category) => category.scripts || []);
// Filter out duplicates by slug
const uniqueScriptsMap = new Map<string, Script>();
scripts.forEach((script) => {
if (!uniqueScriptsMap.has(script.slug)) {
uniqueScriptsMap.set(script.slug, script);
}
});
return Array.from(uniqueScriptsMap.values()).sort(
(a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
);
}, [items]);
const totalPages = Math.max(1, Math.ceil(latestScripts.length / ITEMS_PER_PAGE));
useEffect(() => {
if (page > totalPages) {
onPageChange(totalPages);
}
}, [page, totalPages, onPageChange]);
const goToNextPage = () => {
onPageChange(Math.min(totalPages, page + 1));
};
const goToPreviousPage = () => {
onPageChange(Math.max(1, page - 1));
};
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const endIndex = page * ITEMS_PER_PAGE;
if (!items) {
return null;
}
return (
<div className="">
{latestScripts.length > 0 && (
<div className="flex w-full items-center justify-between">
<h2 className="text-lg font-semibold">Newest Scripts</h2>
<div className="flex items-center justify-end gap-1">
{page > 1 && (
<div className="cursor-pointer select-none p-2 text-sm font-semibold" onClick={goToPreviousPage}>
Previous
</div>
)}
{endIndex < latestScripts.length && (
<div onClick={goToNextPage} className="cursor-pointer select-none p-2 text-sm font-semibold">
{page === 1 ? "More.." : "Next"}
</div>
)}
</div>
</div>
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{latestScripts.slice(startIndex, endIndex).map((script) => (
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex h-16 w-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
<Image
src={script.logo || `/${basePath}/logo.png`}
unoptimized
height={64}
width={64}
alt=""
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
className="h-11 w-11 object-contain"
/>
</div>
<div className="flex flex-col">
<p className="text-lg line-clamp-1">
{script.name} {getDisplayValueFromType(script.type)}
</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<CalendarPlus className="h-4 w-4" />
{extractDate(script.date_created)}
</p>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-3 text-card-foreground">{script.description}</CardDescription>
</CardContent>
<CardFooter className="">
<Button asChild variant="outline">
<Link
href={{
pathname: "/scripts",
query: { id: script.slug },
}}
>
View Script
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
}
export function MostViewedScripts({ items }: { items: Category[] }) {
const mostViewedScripts = items.reduce((acc: Script[], category) => {
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug));
return acc.concat(foundScripts);
}, []);
return (
<div className="">
{mostViewedScripts.length > 0 && (
<>
<h2 className="text-lg font-semibold mb-1">Most Viewed Scripts</h2>
</>
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{mostViewedScripts.map((script) => (
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex size-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
<Image
unoptimized
src={script.logo || `/${basePath}/logo.png`}
height={64}
width={64}
alt=""
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
className="h-11 w-11 object-contain"
/>
</div>
<div className="flex flex-col">
<p className="line-clamp-1 text-lg">
{script.name} {getDisplayValueFromType(script.type)}
</p>
<p className="flex items-center gap-1 text-sm text-muted-foreground">
<CalendarPlus className="h-4 w-4" />
{extractDate(script.date_created)}
</p>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-3 text-card-foreground break-words">
{script.description}
</CardDescription>
</CardContent>
<CardFooter className="">
<Button asChild variant="outline">
<Link
href={{
pathname: "/scripts",
query: { id: script.slug },
}}
prefetch={false}
>
View Script
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
}

View File

@@ -1,191 +0,0 @@
"use client";
import { X, HelpCircle } from "lucide-react";
import { Suspense } from "react";
import Image from "next/image";
import type { AppVersion } from "@/lib/types";
import type { Script } from "@/app/json-editor/_schemas/schemas";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useVersions } from "@/hooks/use-versions";
import { basePath } from "@/config/site-config";
import { extractDate } from "@/lib/time";
import DisableDescription from "./script-items/disable-description";
import { formattedBadge } from "@/components/command-menu";
import { getDisplayValueFromType } from "./script-info-blocks";
import DefaultPassword from "./script-items/default-password";
import InstallCommand from "./script-items/install-command";
import { ResourceDisplay } from "./resource-display";
import Description from "./script-items/description";
import ConfigFile from "./script-items/config-file";
import InterFaces from "./script-items/interfaces";
import Tooltips from "./script-items/tool-tips";
import Buttons from "./script-items/buttons";
import Alerts from "./script-items/alerts";
type ScriptItemProps = {
item: Script;
};
function ScriptHeader({ item }: { item: Script }) {
const defaultInstallMethod = item.install_methods?.[0];
const os = defaultInstallMethod?.resources?.os || (item.type === "addon" ? "Existing LXC or Proxmox Node" : "Proxmox Node");
const version = defaultInstallMethod?.resources?.version || "";
return (
<div className="flex flex-col lg:flex-row gap-6 w-full">
<div className="flex flex-col md:flex-row gap-6 flex-grow">
<div className="flex-shrink-0">
<Image
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
src={item.logo || `/${basePath}/logo.png`}
width={400}
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
height={400}
alt={item.name}
unoptimized
/>
</div>
<div className="flex flex-col justify-between flex-grow space-y-4">
<div className="space-y-2">
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
{item.name}
<VersionInfo item={item} />
{formattedBadge(item.type)}
</h1>
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
<span>
Added
{" "}
{extractDate(item.date_created)}
</span>
<span></span>
<span className=" capitalize">
{os}
{" "}
{version}
</span>
</div>
</div>
{/* <VersionInfo item={item} /> */}
</div>
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
{defaultInstallMethod?.resources && (
<ResourceDisplay
title="Default"
cpu={defaultInstallMethod.resources.cpu}
ram={defaultInstallMethod.resources.ram}
hdd={defaultInstallMethod.resources.hdd}
/>
)}
{item.install_methods.find(method => method.type === "alpine")?.resources && (
<ResourceDisplay
title="Alpine"
{...item.install_methods.find(method => method.type === "alpine")!.resources!}
/>
)}
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4 justify-between">
<InterFaces item={item} />
<div className="flex justify-end">
<Buttons item={item} />
</div>
</div>
</div>
);
}
function VersionInfo({ item }: { item: Script }) {
const { data: versions = [], isLoading } = useVersions();
if (isLoading || versions.length === 0) {
return null;
}
const matchedVersion = versions.find((v: AppVersion) => v.slug === item.slug);
if (!matchedVersion)
return null;
return (
<span className="font-medium text-sm flex items-center gap-1">
{matchedVersion.version}
{matchedVersion.pinned && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>This version is pinned. We test each update for breaking changes before releasing new versions.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</span>
);
}
export function ScriptItem({ item }: ScriptItemProps) {
return (
<div className="w-full mx-auto">
<div className="flex w-full flex-col">
<div className="rounded-xl border border-border bg-accent/30 backdrop-blur-sm shadow-sm">
<div className="p-6 space-y-6">
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
<ScriptHeader item={item} />
</Suspense>
{item.disable && item.disable_description && (
<DisableDescription item={item} />
)}
{!item.disable && (
<>
<Description item={item} />
<Alerts item={item} />
<div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">
How to
{" "}
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
</h2>
<Tooltips item={item} />
</div>
<Separator />
<div className="">
<InstallCommand item={item} />
</div>
{item.config_path && (
<>
<Separator />
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">Location of config file</h2>
</div>
<Separator />
<div className="">
<ConfigFile configPath={item.config_path} />
</div>
</>
)}
</div>
<DefaultPassword item={item} />
</>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,39 +0,0 @@
import { AlertCircle, NotepadText } from "lucide-react";
import type { Script } from "@/lib/types";
import TextCopyBlock from "@/components/text-copy-block";
import { AlertColors } from "@/config/site-config";
import { cn } from "@/lib/utils";
type NoteProps = {
text: string;
type: keyof typeof AlertColors;
};
export default function Alerts({ item }: { item: Script }) {
return (
<>
{item?.notes?.length > 0
&& item.notes.map((note: NoteProps, index: number) => (
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
<p
className={cn(
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-sm",
AlertColors[note.type],
)}
>
{note.type === "info"
? (
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
)
: (
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
)}
<span>{TextCopyBlock(note.text)}</span>
</p>
</div>
))}
</>
);
}

View File

@@ -1,104 +0,0 @@
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
import type { Script } from "@/lib/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { basePath } from "@/config/site-config";
function generateInstallSourceUrl(slug: string) {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return `${baseUrl}/install/${slug}-install.sh`;
}
function generateSourceUrl(slug: string, type: string) {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
switch (type) {
case "vm":
return `${baseUrl}/vm/${slug}.sh`;
case "pve":
return `${baseUrl}/tools/pve/${slug}.sh`;
case "addon":
return `${baseUrl}/tools/addon/${slug}.sh`;
case "turnkey":
return `${baseUrl}/turnkey/${slug}.sh`;
default:
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
}
}
function generateUpdateUrl(slug: string) {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return `${baseUrl}/ct/${slug}.sh`;
}
type LinkItem = {
href: string;
icon: React.ReactNode;
text: string;
};
export default function Buttons({ item }: { item: Script }) {
const isCtOrDefault = ["ct"].includes(item.type);
const installSourceUrl = isCtOrDefault ? generateInstallSourceUrl(item.slug) : null;
const updateSourceUrl = isCtOrDefault ? generateUpdateUrl(item.slug) : null;
const sourceUrl = !isCtOrDefault ? generateSourceUrl(item.slug, item.type) : null;
const links = [
item.website && {
href: item.website,
icon: <Globe className="h-4 w-4" />,
text: "Website",
},
item.documentation && {
href: item.documentation,
icon: <BookOpenText className="h-4 w-4" />,
text: "Documentation",
},
installSourceUrl && {
href: installSourceUrl,
icon: <Code className="h-4 w-4" />,
text: "Install Source",
},
updateSourceUrl && item.updateable && {
href: updateSourceUrl,
icon: <RefreshCcw className="h-4 w-4" />,
text: "Update Source",
},
sourceUrl && {
href: sourceUrl,
icon: <Code className="h-4 w-4" />,
text: "Source Code",
},
].filter(Boolean) as LinkItem[];
if (links.length === 0)
return null;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<LinkIcon className="size-4" />
Links
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{links.map((link, index) => (
<DropdownMenuItem key={index} asChild>
<a href={link.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<span className="text-muted-foreground size-4">{link.icon}</span>
<span>{link.text}</span>
</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,9 +0,0 @@
import ConfigCopyButton from "@/components/ui/config-copy-button";
export default function ConfigFile({ configPath }: { configPath: string }) {
return (
<div className="px-4 pb-4">
<ConfigCopyButton>{configPath}</ConfigCopyButton>
</div>
);
}

View File

@@ -1,51 +0,0 @@
import type { Script } from "@/lib/types";
import { Separator } from "@/components/ui/separator";
import handleCopy from "@/components/handle-copy";
import { Button } from "@/components/ui/button";
export default function DefaultPassword({ item }: { item: Script }) {
const { username, password } = item.default_credentials;
const hasDefaultLogin = username || password;
if (!hasDefaultLogin)
return null;
const copyCredential = (type: "username" | "password") => {
handleCopy(type, item.default_credentials[type] ?? "");
};
return (
<div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">Default Login Credentials</h2>
</div>
<Separator className="w-full" />
<div className="flex flex-col gap-2 p-4">
<p className="mb-2 text-sm">
You can use the following credentials to login to the
{" "}
{item.name}
{" "}
{item.type}
.
</p>
{["username", "password"].map((type) => {
const value = item.default_credentials[type as "username" | "password"];
return value && value.trim() !== ""
? (
<div key={type} className="text-sm">
{type.charAt(0).toUpperCase() + type.slice(1)}
:
{" "}
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
{value}
</Button>
</div>
)
: null;
})}
</div>
</div>
);
}

View File

@@ -1,40 +0,0 @@
import type { Script } from "@/lib/types";
export default function DefaultSettings({ item }: { item: Script }) {
const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`);
const ResourceDisplay = ({ settings, title }: { settings: (typeof item.install_methods)[0]; title: string }) => {
const { cpu, ram, hdd } = settings.resources;
return (
<div>
<h2 className="text-md font-semibold">{title}</h2>
<p className="text-sm text-muted-foreground">
CPU:
{cpu}
vCPU
</p>
<p className="text-sm text-muted-foreground">
RAM:
{getDisplayValueFromRAM(ram ?? 0)}
</p>
<p className="text-sm text-muted-foreground">
HDD:
{hdd}
GB
</p>
</div>
);
};
const defaultSettings = item.install_methods.find(method => method.type === "default");
const defaultAlpineSettings = item.install_methods.find(method => method.type === "alpine");
const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);
return (
<div className="space-y-4 flex-col flex">
{hasDefaultSettings && <ResourceDisplay settings={defaultSettings} title="Default settings" />}
{defaultAlpineSettings && <ResourceDisplay settings={defaultAlpineSettings} title="Default Alpine settings" />}
</div>
);
}

View File

@@ -1,14 +0,0 @@
import type { Script } from "@/lib/types";
import TextCopyBlock from "@/components/text-copy-block";
export default function Description({ item }: { item: Script }) {
return (
<div className="p-2">
<h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2>
<p className="text-sm text-muted-foreground">
{TextCopyBlock(item.description)}
</p>
</div>
);
}

View File

@@ -1,26 +0,0 @@
import { AlertCircle } from "lucide-react";
import type { Script } from "@/lib/types";
import TextParseLinks from "@/components/text-parse-links";
import { AlertColors } from "@/config/site-config";
import { cn } from "@/lib/utils";
export default function DisableDescription({ item }: { item: Script }) {
return (
<div className="mt-4 flex flex-col shadow-sm gap-2">
<div
className={cn(
"flex items-start gap-3 rounded-lg border p-4 text-sm",
AlertColors.warning,
)}
>
<AlertCircle className="h-5 min-h-5 w-5 min-w-5 mt-0.5" />
<div className="flex flex-col gap-2">
<h3 className="font-semibold text-base">Script Disabled</h3>
<p>{TextParseLinks(item.disable_description!)}</p>
</div>
</div>
</div>
);
}

View File

@@ -1,149 +0,0 @@
import { Info } from "lucide-react";
import type { Script } from "@/lib/types";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription } from "@/components/ui/alert";
import CodeCopyButton from "@/components/ui/code-copy-button";
import { basePath } from "@/config/site-config";
import { getDisplayValueFromType } from "../script-info-blocks";
function getInstallCommand(scriptPath = "", isAlpine = false, useGitea = false) {
const githubUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
const giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
const url = useGitea ? giteaUrl : githubUrl;
return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
}
export default function InstallCommand({ item }: { item: Script }) {
const alpineScript = item.install_methods.find(method => method.type === "alpine");
const defaultScript = item.install_methods.find(method => method.type === "default");
const renderInstructions = (isAlpine = false) => (
<>
<p className="text-sm mt-2">
{isAlpine
? (
<>
As an alternative option, you can use Alpine Linux and the
{" "}
{item.name}
{" "}
package to create a
{" "}
{item.name}
{" "}
{getDisplayValueFromType(item.type)}
{" "}
container with faster creation time and minimal system resource usage.
You are also obliged to adhere to updates provided by the package maintainer.
</>
)
: item.type === "pve"
? (
<>
To use the
{" "}
{item.name}
{" "}
script, run the command below **only** in the Proxmox VE Shell. This script is
intended for managing or enhancing the host system directly.
</>
)
: item.type === "addon"
? (
<>
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
Proxmox VE host to extend functionality with
{" "}
{item.name}
.
</>
)
: (
<>
To create a new Proxmox VE
{" "}
{item.name}
{" "}
{getDisplayValueFromType(item.type)}
, run the command below in the
Proxmox VE Shell.
</>
)}
</p>
{isAlpine && (
<p className="mt-2 text-sm">
To create a new Proxmox VE Alpine-
{item.name}
{" "}
{getDisplayValueFromType(item.type)}
, run the command below in
the Proxmox VE Shell.
</p>
)}
</>
);
const renderGiteaInfo = () => (
<Alert className="mt-3 mb-3">
<Info className="h-4 w-4" />
<AlertDescription className="text-sm">
<strong>When to use Gitea:</strong>
{" "}
GitHub may have issues including slow connections, delayed updates after bug
fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
experiencing these issues.
</AlertDescription>
</Alert>
);
const renderScriptTabs = (useGitea = false) => {
if (alpineScript) {
return (
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
<TabsList>
<TabsTrigger value="default">Default</TabsTrigger>
<TabsTrigger value="alpine">Alpine Linux</TabsTrigger>
</TabsList>
<TabsContent value="default">
{renderInstructions()}
<CodeCopyButton>{getInstallCommand(defaultScript?.script, false, useGitea)}</CodeCopyButton>
</TabsContent>
<TabsContent value="alpine">
{renderInstructions(true)}
<CodeCopyButton>{getInstallCommand(alpineScript.script, true, useGitea)}</CodeCopyButton>
</TabsContent>
</Tabs>
);
}
else if (defaultScript?.script) {
return (
<>
{renderInstructions()}
<CodeCopyButton>{getInstallCommand(defaultScript.script, false, useGitea)}</CodeCopyButton>
</>
);
}
return null;
};
return (
<div className="p-4">
<Tabs defaultValue="github" className="w-full max-w-4xl">
<TabsList>
<TabsTrigger value="github">GitHub</TabsTrigger>
<TabsTrigger value="gitea">Gitea</TabsTrigger>
</TabsList>
<TabsContent value="github">
{renderScriptTabs(false)}
</TabsContent>
<TabsContent value="gitea">
{renderGiteaInfo()}
{renderScriptTabs(true)}
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import { ClipboardIcon } from "lucide-react";
import type { Script } from "@/lib/types";
import { buttonVariants } from "@/components/ui/button";
import handleCopy from "@/components/handle-copy";
import { cn } from "@/lib/utils";
export default function InterFaces({ item }: { item: Script }) {
return (
<div className="flex flex-col gap-2 w-full">
{item.interface_port !== null
? (
<div className="flex items-center justify-end">
<h2 className="mr-2 text-end text-lg font-semibold">Default Interface:</h2>
<span className={cn(buttonVariants({ size: "sm", variant: "outline" }), "flex items-center gap-2")}>
{item.interface_port}
<ClipboardIcon onClick={() => handleCopy("default interface", String(item.interface_port))} className="size-4 cursor-pointer" />
</span>
</div>
)
: null}
</div>
);
}

View File

@@ -1,59 +0,0 @@
import { CircleHelp } from "lucide-react";
import React from "react";
import type { BadgeProps } from "@/components/ui/badge";
import type { Script } from "@/lib/types";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
type TooltipProps = {
variant: BadgeProps["variant"];
label: string;
content?: string;
};
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
<Badge variant={variant} className="flex items-center gap-1">
{label}
{" "}
{content && <CircleHelp className="size-3" />}
</Badge>
</TooltipTrigger>
{content && (
<TooltipContent side="bottom" className="text-sm max-w-64">
{content}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
);
export default function Tooltips({ item }: { item: Script }) {
return (
<div className="flex items-center gap-2">
{item.privileged && item.type !== "addon" && (
<TooltipBadge variant="warning" label="Privileged" content="This script will be run in a privileged LXC" />
)}
{item.updateable && item.type !== "pve" && item.type !== "addon" && (
<TooltipBadge
variant="success"
label="Updateable"
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
/>
)}
{item.updateable && item.type === "addon" && (
<TooltipBadge
variant="success"
label="Updateable"
content={`Run update_${item.slug} to update or use the bash command inside the LXC.`}
/>
)}
{!item.updateable && item.type !== "pve" && item.type !== "addon" && <TooltipBadge variant="failure" label="Not Updateable" />}
</div>
);
}

View File

@@ -1,61 +0,0 @@
"use client";
import type { Category, Script } from "@/lib/types";
import { cn } from "@/lib/utils";
import ScriptAccordion from "./script-accordion";
type SidebarProps = {
items: Category[];
selectedScript: string | null;
setSelectedScript: (script: string | null) => void;
selectedCategory: string | null;
setSelectedCategory: (category: string | null) => void;
onItemSelect?: () => void;
className?: string;
};
function Sidebar({
items,
selectedScript,
setSelectedScript,
selectedCategory,
setSelectedCategory,
onItemSelect,
className,
}: SidebarProps) {
const uniqueScripts = items.reduce((acc, category) => {
for (const script of category.scripts) {
if (!acc.some(s => s.name === script.name)) {
acc.push(script);
}
}
return acc;
}, [] as Script[]);
return (
<div className={cn("flex w-full flex-col sm:min-w-[350px] sm:max-w-[350px]", className)}>
<div className="flex items-end justify-between pb-4">
<h1 className="text-xl font-bold">Categories</h1>
<p className="text-xs italic text-muted-foreground">
{uniqueScripts.length}
{" "}
Total scripts
</p>
</div>
<div className="rounded-lg">
<ScriptAccordion
items={items}
selectedScript={selectedScript}
setSelectedScript={setSelectedScript}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
onItemSelect={onItemSelect}
/>
</div>
</div>
);
}
export default Sidebar;

View File

@@ -1,13 +0,0 @@
import type { AppVersion } from "@/lib/types";
type VersionBadgeProps = {
version: AppVersion;
};
export function VersionBadge({ version }: VersionBadgeProps) {
return (
<div className="flex items-center">
<span className="font-medium text-sm">{version.version}</span>
</div>
);
}

View File

@@ -1,105 +0,0 @@
"use client";
import { Suspense, useEffect, useState } from "react";
import { Loader2, X } from "lucide-react";
import { useQueryState } from "nuqs";
import type { Category, Script } from "@/lib/types";
import { ScriptItem } from "@/app/scripts/_components/script-item";
import { fetchCategories } from "@/lib/data";
import { LatestScripts, MostViewedScripts } from "./_components/script-info-blocks";
import Sidebar from "./_components/sidebar";
export const dynamic = "force-static";
function ScriptContent() {
const [selectedScript, setSelectedScript] = useQueryState("id");
const [selectedCategory, setSelectedCategory] = useQueryState("category");
const [links, setLinks] = useState<Category[]>([]);
const [item, setItem] = useState<Script>();
const [latestPage, setLatestPage] = useState(1);
const closeScript = () => {
window.history.pushState({}, document.title, window.location.pathname);
setSelectedScript(null);
};
useEffect(() => {
if (selectedScript && links.length > 0) {
const script = links
.map(category => category.scripts)
.flat()
.find(script => script.slug === selectedScript);
setItem(script);
if (script) {
document.title = `${script.name} | Proxmox VE Helper-Scripts`;
}
} else {
document.title = "Proxmox VE Helper-Scripts";
}
}, [selectedScript, links]);
useEffect(() => {
fetchCategories()
.then((categories) => {
setLinks(categories);
})
.catch(error => console.error(error));
}, []);
return (
<div className="mb-3">
<div className="mt-20 flex gap-4 sm:px-4 xl:px-0">
<div className="hidden sm:flex">
<Sidebar
items={links}
selectedScript={selectedScript}
setSelectedScript={setSelectedScript}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/>
</div>
<div className="px-4 w-full sm:max-w-[calc(100%-350px-16px)]">
{selectedScript && item
? (
<div className="flex w-full flex-col">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">Selected Script</h2>
<button
onClick={closeScript}
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
<ScriptItem item={item} />
</div>
)
: (
<div className="flex w-full flex-col gap-5">
<LatestScripts items={links} page={latestPage} onPageChange={setLatestPage} />
<MostViewedScripts items={links} />
</div>
)}
</div>
</div>
</div>
);
}
export default function Page() {
return (
<Suspense
fallback={(
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
<div className="space-y-2 text-center">
<Loader2 className="h-10 w-10 animate-spin" />
</div>
</div>
)}
>
<ScriptContent />
</Suspense>
);
}

View File

@@ -1,24 +0,0 @@
import type { MetadataRoute } from "next";
import { basePath } from "@/config/site-config";
export const dynamic = "force-static";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const domain = "community-scripts.github.io";
const protocol = "https";
return [
{
url: `${protocol}://${domain}/${basePath}`,
lastModified: new Date(),
},
{
url: `${protocol}://${domain}/${basePath}/scripts`,
lastModified: new Date(),
},
{
url: `${protocol}://${domain}/${basePath}/json-editor`,
lastModified: new Date(),
},
];
}

View File

@@ -1,61 +0,0 @@
"use client";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import * as React from "react";
import type { ButtonProps as ButtonPrimitiveProps } from "@/components/animate-ui/primitives/buttons/button";
import {
Button as ButtonPrimitive,
} from "@/components/animate-ui/primitives/buttons/button";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[box-shadow,_color,_background-color,_border-color,_outline-color,_text-decoration-color,_fill,_stroke] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
accent: "bg-accent text-accent-foreground shadow-xs hover:bg-accent/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
"icon": "size-9",
"icon-sm": "size-8 rounded-md",
"icon-lg": "size-10 rounded-md",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type ButtonProps = ButtonPrimitiveProps & VariantProps<typeof buttonVariants>;
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<ButtonPrimitive
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, type ButtonProps, buttonVariants };

View File

@@ -1,109 +0,0 @@
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { StarIcon } from "lucide-react";
import Link from "next/link";
import type { ButtonProps as ButtonPrimitiveProps } from "@/components/animate-ui/primitives/buttons/button";
import type { GithubStarsProps } from "@/components/animate-ui/primitives/animate/github-stars";
import {
GithubStars,
GithubStarsIcon,
GithubStarsLogo,
GithubStarsNumber,
GithubStarsParticles,
} from "@/components/animate-ui/primitives/animate/github-stars";
import { Button as ButtonPrimitive } from "@/components/animate-ui/primitives/buttons/button";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[box-shadow,_color,_background-color,_border-color,_outline-color,_text-decoration-color,_fill,_stroke] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
accent: "bg-accent text-accent-foreground shadow-xs hover:bg-accent/90",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const buttonStarVariants = cva("", {
variants: {
variant: {
default: "fill-neutral-700 stroke-neutral-700 dark:fill-neutral-300 dark:stroke-neutral-300",
accent: "fill-neutral-300 stroke-neutral-300 dark:fill-neutral-700 dark:stroke-neutral-700",
outline: "fill-neutral-300 stroke-neutral-300 dark:fill-neutral-700 dark:stroke-neutral-700",
ghost: "fill-neutral-300 stroke-neutral-300 dark:fill-neutral-700 dark:stroke-neutral-700",
},
},
defaultVariants: {
variant: "default",
},
});
type GitHubStarsButtonProps = Omit<ButtonPrimitiveProps & GithubStarsProps, "asChild" | "children">
& VariantProps<typeof buttonVariants>;
function GitHubStarsButton({
className,
username,
repo,
value,
delay,
inView,
inViewMargin,
inViewOnce,
variant,
size,
...props
}: GitHubStarsButtonProps) {
return (
<Link
target="_blank"
rel="noopener noreferrer"
data-umami-event="github-stars"
href={`https://github.com/${username}/${repo}`}
>
<GithubStars
asChild
username={username}
repo={repo}
value={value}
delay={delay}
inView={inView}
inViewMargin={inViewMargin}
inViewOnce={inViewOnce}
>
<ButtonPrimitive className={cn(buttonVariants({ variant, size, className }))} {...props}>
<GithubStarsLogo />
<GithubStarsNumber />
<GithubStarsParticles className="text-yellow-500">
<GithubStarsIcon
icon={StarIcon}
data-variant={variant}
className={cn(buttonStarVariants({ variant }))}
activeClassName="text-yellow-500"
size={18}
/>
</GithubStarsParticles>
</ButtonPrimitive>
</GithubStars>
</Link>
);
}
export { GitHubStarsButton, type GitHubStarsButtonProps };

View File

@@ -1,206 +0,0 @@
"use client";
import type { HTMLMotionProps } from "motion/react";
import { motion } from "motion/react";
import * as React from "react";
import type { SlidingNumberProps } from "@/components/animate-ui/primitives/texts/sliding-number";
import type { ParticlesEffectProps } from "@/components/animate-ui/primitives/effects/particles";
import type { WithAsChild } from "@/components/animate-ui/primitives/animate/slot";
import type { UseIsInViewOptions } from "@/hooks/use-is-in-view";
import { Particles, ParticlesEffect } from "@/components/animate-ui/primitives/effects/particles";
import { SlidingNumber } from "@/components/animate-ui/primitives/texts/sliding-number";
import { Slot } from "@/components/animate-ui/primitives/animate/slot";
import { getStrictContext } from "@/lib/get-strict-context";
import { useIsInView } from "@/hooks/use-is-in-view";
import { cn } from "@/lib/utils";
type GithubStarsContextType = {
stars: number;
setStars: (stars: number) => void;
currentStars: number;
setCurrentStars: (stars: number) => void;
isCompleted: boolean;
isLoading: boolean;
};
const [GithubStarsProvider, useGithubStars] = getStrictContext<GithubStarsContextType>("GithubStarsContext");
type GithubStarsProps = WithAsChild<
{
children: React.ReactNode;
username?: string;
repo?: string;
value?: number;
delay?: number;
} & UseIsInViewOptions
& HTMLMotionProps<"div">
>;
function GithubStars({
ref,
children,
username,
repo,
value,
delay = 0,
inView = false,
inViewMargin = "0px",
inViewOnce = true,
asChild = false,
...props
}: GithubStarsProps) {
const { ref: localRef, isInView } = useIsInView(ref as React.Ref<HTMLDivElement>, {
inView,
inViewOnce,
inViewMargin,
});
const [stars, setStars] = React.useState(value ?? 0);
const [currentStars, setCurrentStars] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(true);
const isCompleted = React.useMemo(() => currentStars === stars, [currentStars, stars]);
const Component = asChild ? Slot : motion.div;
React.useEffect(() => {
if (value !== undefined && username && repo)
return;
if (!isInView) {
setStars(0);
setIsLoading(true);
return;
}
const timeout = setTimeout(() => {
fetch(`https://api.github.com/repos/${username}/${repo}`)
.then(response => response.json())
.then((data) => {
if (data && typeof data.stargazers_count === "number") {
setStars(data.stargazers_count);
}
})
.catch(console.error)
.finally(() => setIsLoading(false));
}, delay);
return () => clearTimeout(timeout);
}, [username, repo, value, isInView, delay]);
return (
<GithubStarsProvider
value={{
stars,
currentStars,
isCompleted,
isLoading,
setStars,
setCurrentStars,
}}
>
{!isLoading && (
<Component ref={localRef} {...props}>
{children}
</Component>
)}
</GithubStarsProvider>
);
}
type GithubStarsNumberProps = Omit<SlidingNumberProps, "number" | "fromNumber">;
function GithubStarsNumber({ padStart = true, ...props }: GithubStarsNumberProps) {
const { stars, setCurrentStars } = useGithubStars();
return (
<SlidingNumber number={stars} fromNumber={0} onNumberChange={setCurrentStars} padStart={padStart} {...props} />
);
}
type GithubStarsIconProps<T extends React.ElementType> = {
icon: React.ReactElement<T>;
color?: string;
activeClassName?: string;
} & React.ComponentProps<T>;
function GithubStarsIcon<T extends React.ElementType>({
icon: Icon,
color = "currentColor",
activeClassName,
className,
...props
}: GithubStarsIconProps<T>) {
const { stars, currentStars, isCompleted } = useGithubStars();
const fillPercentage = (currentStars / stars) * 100;
return (
<div style={{ position: "relative" }}>
<Icon aria-hidden="true" className={cn(className)} {...props} />
<Icon
aria-hidden="true"
style={{
position: "absolute",
top: 0,
left: 0,
fill: color,
stroke: color,
clipPath: `inset(${100 - (isCompleted ? fillPercentage : fillPercentage - 10)}% 0 0 0)`,
}}
className={cn(className, activeClassName)}
{...props}
/>
</div>
);
}
type GithubStarsParticlesProps = ParticlesEffectProps & {
children: React.ReactElement;
size?: number;
};
function GithubStarsParticles({ children, size = 4, style, ...props }: GithubStarsParticlesProps) {
const { isCompleted } = useGithubStars();
return (
<Particles animate={isCompleted}>
{children}
<ParticlesEffect
style={{
backgroundColor: "currentcolor",
borderRadius: "50%",
width: size,
height: size,
...style,
}}
{...props}
/>
</Particles>
);
}
type GithubStarsLogoProps = React.SVGProps<SVGSVGElement>;
function GithubStarsLogo(props: GithubStarsLogoProps) {
return (
<svg role="img" viewBox="0 0 24 24" fill="currentColor" aria-label="GitHub" {...props}>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
);
}
export {
GithubStars,
type GithubStarsContextType,
GithubStarsIcon,
type GithubStarsIconProps,
GithubStarsLogo,
type GithubStarsLogoProps,
GithubStarsNumber,
type GithubStarsNumberProps,
GithubStarsParticles,
type GithubStarsParticlesProps,
type GithubStarsProps,
useGithubStars,
};

View File

@@ -1,101 +0,0 @@
"use client";
import type { HTMLMotionProps } from "motion/react";
import { isMotionComponent, motion } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
type AnyProps = Record<string, unknown>;
type DOMMotionProps<T extends HTMLElement = HTMLElement> = Omit<
HTMLMotionProps<keyof HTMLElementTagNameMap>,
"ref"
> & { ref?: React.Ref<T> };
type WithAsChild<Base extends object>
= | (Base & { asChild: true; children: React.ReactElement })
| (Base & { asChild?: false | undefined });
type SlotProps<T extends HTMLElement = HTMLElement> = {
children?: any;
} & DOMMotionProps<T>;
function mergeRefs<T>(
...refs: (React.Ref<T> | undefined)[]
): React.RefCallback<T> {
return (node) => {
refs.forEach((ref) => {
if (!ref)
return;
if (typeof ref === "function") {
ref(node);
}
else {
(ref as React.RefObject<T | null>).current = node;
}
});
};
}
function mergeProps<T extends HTMLElement>(
childProps: AnyProps,
slotProps: DOMMotionProps<T>,
): AnyProps {
const merged: AnyProps = { ...childProps, ...slotProps };
if (childProps.className || slotProps.className) {
merged.className = cn(
childProps.className as string,
slotProps.className as string,
);
}
if (childProps.style || slotProps.style) {
merged.style = {
...(childProps.style as React.CSSProperties),
...(slotProps.style as React.CSSProperties),
};
}
return merged;
}
function Slot<T extends HTMLElement = HTMLElement>({
children,
ref,
...props
}: SlotProps<T>) {
const isAlreadyMotion
= typeof children.type === "object"
&& children.type !== null
&& isMotionComponent(children.type);
const Base = React.useMemo(
() =>
isAlreadyMotion
? (children.type as React.ElementType)
: motion.create(children.type as React.ElementType),
[isAlreadyMotion, children.type],
);
if (!React.isValidElement(children))
return null;
const { ref: childRef, ...childProps } = children.props as AnyProps;
const mergedProps = mergeProps(childProps, props);
return (
<Base {...mergedProps} ref={mergeRefs(childRef as React.Ref<T>, ref)} />
);
}
export {
type AnyProps,
type DOMMotionProps,
Slot,
type SlotProps,
type WithAsChild,
};

View File

@@ -1,36 +0,0 @@
"use client";
import type { HTMLMotionProps } from "motion/react";
import { motion } from "motion/react";
import * as React from "react";
import type { WithAsChild } from "@/components/animate-ui/primitives/animate/slot";
import { Slot } from "@/components/animate-ui/primitives/animate/slot";
type ButtonProps = WithAsChild<
HTMLMotionProps<"button"> & {
hoverScale?: number;
tapScale?: number;
}
>;
function Button({
hoverScale = 1.05,
tapScale = 0.95,
asChild = false,
...props
}: ButtonProps) {
const Component = asChild ? Slot : motion.button;
return (
<Component
whileTap={{ scale: tapScale }}
whileHover={{ scale: hoverScale }}
{...props}
/>
);
}
export { Button, type ButtonProps };

View File

@@ -1,160 +0,0 @@
"use client";
import type { HTMLMotionProps } from "motion/react";
import { AnimatePresence, motion } from "motion/react";
import * as React from "react";
import type { WithAsChild } from "@/components/animate-ui/primitives/animate/slot";
import type { UseIsInViewOptions } from "@/hooks/use-is-in-view";
import { Slot } from "@/components/animate-ui/primitives/animate/slot";
import { getStrictContext } from "@/lib/get-strict-context";
import {
useIsInView,
} from "@/hooks/use-is-in-view";
type Side = "top" | "bottom" | "left" | "right";
type Align = "start" | "center" | "end";
type ParticlesContextType = {
animate: boolean;
isInView: boolean;
};
const [ParticlesProvider, useParticles]
= getStrictContext<ParticlesContextType>("ParticlesContext");
type ParticlesProps = WithAsChild<
Omit<HTMLMotionProps<"div">, "children"> & {
animate?: boolean;
children: React.ReactNode;
} & UseIsInViewOptions
>;
function Particles({
ref,
animate = true,
asChild = false,
inView = false,
inViewMargin = "0px",
inViewOnce = true,
children,
style,
...props
}: ParticlesProps) {
const { ref: localRef, isInView } = useIsInView(
ref as React.Ref<HTMLDivElement>,
{ inView, inViewOnce, inViewMargin },
);
const Component = asChild ? Slot : motion.div;
return (
<ParticlesProvider value={{ animate, isInView }}>
<Component
ref={localRef}
style={{ position: "relative", ...style }}
{...props}
>
{children}
</Component>
</ParticlesProvider>
);
}
type ParticlesEffectProps = Omit<HTMLMotionProps<"div">, "children"> & {
side?: Side;
align?: Align;
count?: number;
radius?: number;
spread?: number;
duration?: number;
holdDelay?: number;
sideOffset?: number;
alignOffset?: number;
delay?: number;
};
function ParticlesEffect({
side = "top",
align = "center",
count = 6,
radius = 30,
spread = 360,
duration = 0.8,
holdDelay = 0.05,
sideOffset = 0,
alignOffset = 0,
delay = 0,
transition,
style,
...props
}: ParticlesEffectProps) {
const { animate, isInView } = useParticles();
const isVertical = side === "top" || side === "bottom";
const alignPct = align === "start" ? "0%" : align === "end" ? "100%" : "50%";
const top = isVertical
? side === "top"
? `calc(0% - ${sideOffset}px)`
: `calc(100% + ${sideOffset}px)`
: `calc(${alignPct} + ${alignOffset}px)`;
const left = isVertical
? `calc(${alignPct} + ${alignOffset}px)`
: side === "left"
? `calc(0% - ${sideOffset}px)`
: `calc(100% + ${sideOffset}px)`;
const containerStyle: React.CSSProperties = {
position: "absolute",
top,
left,
transform: "translate(-50%, -50%)",
};
const angleStep = (spread * (Math.PI / 180)) / Math.max(1, count - 1);
return (
<AnimatePresence>
{animate
&& isInView
&& Array.from({ length: count }).map((_, i) => {
const angle = i * angleStep;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return (
<motion.div
key={i}
style={{ ...containerStyle, ...style }}
initial={{ scale: 0, opacity: 0 }}
animate={{
x: `${x}px`,
y: `${y}px`,
scale: [0, 1, 0],
opacity: [0, 1, 0],
}}
transition={{
duration,
delay: delay + i * holdDelay,
ease: "easeOut",
...transition,
}}
{...props}
/>
);
})}
</AnimatePresence>
);
}
export {
Particles,
ParticlesEffect,
type ParticlesEffectProps,
type ParticlesProps,
};

View File

@@ -1,338 +0,0 @@
"use client";
import type { MotionValue, SpringOptions } from "motion/react";
import {
motion,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import useMeasure from "react-use-measure";
import * as React from "react";
import type { UseIsInViewOptions } from "@/hooks/use-is-in-view";
import {
useIsInView,
} from "@/hooks/use-is-in-view";
type SlidingNumberRollerProps = {
prevValue: number;
value: number;
place: number;
transition: SpringOptions;
delay?: number;
};
function SlidingNumberRoller({
prevValue,
value,
place,
transition,
delay = 0,
}: SlidingNumberRollerProps) {
const startNumber = Math.floor(prevValue / place) % 10;
const targetNumber = Math.floor(value / place) % 10;
const animatedValue = useSpring(startNumber, transition);
React.useEffect(() => {
const timeoutId = setTimeout(() => {
animatedValue.set(targetNumber);
}, delay);
return () => clearTimeout(timeoutId);
}, [targetNumber, animatedValue, delay]);
const [measureRef, { height }] = useMeasure();
return (
<span
ref={measureRef}
data-slot="sliding-number-roller"
style={{
position: "relative",
display: "inline-block",
width: "1ch",
overflowX: "visible",
overflowY: "clip",
lineHeight: 1,
fontVariantNumeric: "tabular-nums",
}}
>
<span style={{ visibility: "hidden" }}>0</span>
{Array.from({ length: 10 }, (_, i) => (
<SlidingNumberDisplay
key={i}
motionValue={animatedValue}
number={i}
height={height}
transition={transition}
/>
))}
</span>
);
}
type SlidingNumberDisplayProps = {
motionValue: MotionValue<number>;
number: number;
height: number;
transition: SpringOptions;
};
function SlidingNumberDisplay({
motionValue,
number,
height,
transition,
}: SlidingNumberDisplayProps) {
const y = useTransform(motionValue, (latest) => {
if (!height)
return 0;
const currentNumber = latest % 10;
const offset = (10 + number - currentNumber) % 10;
let translateY = offset * height;
if (offset > 5)
translateY -= 10 * height;
return translateY;
});
if (!height) {
return (
<span style={{ visibility: "hidden", position: "absolute" }}>
{number}
</span>
);
}
return (
<motion.span
data-slot="sliding-number-display"
style={{
y,
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
transition={{ ...transition, type: "spring" }}
>
{number}
</motion.span>
);
}
type SlidingNumberProps = Omit<React.ComponentProps<"span">, "children"> & {
number: number;
fromNumber?: number;
onNumberChange?: (number: number) => void;
padStart?: boolean;
decimalSeparator?: string;
decimalPlaces?: number;
thousandSeparator?: string;
transition?: SpringOptions;
delay?: number;
} & UseIsInViewOptions;
function SlidingNumber({
ref,
number,
fromNumber,
onNumberChange,
inView = false,
inViewMargin = "0px",
inViewOnce = true,
padStart = false,
decimalSeparator = ".",
decimalPlaces = 0,
thousandSeparator,
transition = { stiffness: 200, damping: 20, mass: 0.4 },
delay = 0,
...props
}: SlidingNumberProps) {
const { ref: localRef, isInView } = useIsInView(
ref as React.Ref<HTMLElement>,
{
inView,
inViewOnce,
inViewMargin,
},
);
const prevNumberRef = React.useRef<number>(0);
const hasAnimated = fromNumber !== undefined;
const motionVal = useMotionValue(fromNumber ?? 0);
const springVal = useSpring(motionVal, { stiffness: 90, damping: 50 });
React.useEffect(() => {
if (!hasAnimated)
return;
const timeoutId = setTimeout(() => {
if (isInView)
motionVal.set(number);
}, delay);
return () => clearTimeout(timeoutId);
}, [hasAnimated, isInView, number, motionVal, delay]);
const [effectiveNumber, setEffectiveNumber] = React.useState(0);
React.useEffect(() => {
if (hasAnimated) {
const inferredDecimals
= typeof decimalPlaces === "number" && decimalPlaces >= 0
? decimalPlaces
: (() => {
const s = String(number);
const idx = s.indexOf(".");
return idx >= 0 ? s.length - idx - 1 : 0;
})();
const factor = 10 ** inferredDecimals;
const unsubscribe = springVal.on("change", (latest: number) => {
const newValue
= inferredDecimals > 0
? Math.round(latest * factor) / factor
: Math.round(latest);
if (effectiveNumber !== newValue) {
setEffectiveNumber(newValue);
onNumberChange?.(newValue);
}
});
return () => unsubscribe();
}
else {
setEffectiveNumber(!isInView ? 0 : Math.abs(Number(number)));
}
}, [
hasAnimated,
springVal,
isInView,
number,
decimalPlaces,
onNumberChange,
effectiveNumber,
]);
const formatNumber = React.useCallback(
(num: number) =>
decimalPlaces != null ? num.toFixed(decimalPlaces) : num.toString(),
[decimalPlaces],
);
const numberStr = formatNumber(effectiveNumber);
const [newIntStrRaw, newDecStrRaw = ""] = numberStr.split(".");
const finalIntLength = padStart
? Math.max(
Math.floor(Math.abs(number)).toString().length,
newIntStrRaw.length,
)
: newIntStrRaw.length;
const newIntStr = padStart
? newIntStrRaw.padStart(finalIntLength, "0")
: newIntStrRaw;
const prevFormatted = formatNumber(prevNumberRef.current);
const [prevIntStrRaw = "", prevDecStrRaw = ""] = prevFormatted.split(".");
const prevIntStr = padStart
? prevIntStrRaw.padStart(finalIntLength, "0")
: prevIntStrRaw;
const adjustedPrevInt = React.useMemo(() => {
return prevIntStr.length > finalIntLength
? prevIntStr.slice(-finalIntLength)
: prevIntStr.padStart(finalIntLength, "0");
}, [prevIntStr, finalIntLength]);
const adjustedPrevDec = React.useMemo(() => {
if (!newDecStrRaw)
return "";
return prevDecStrRaw.length > newDecStrRaw.length
? prevDecStrRaw.slice(0, newDecStrRaw.length)
: prevDecStrRaw.padEnd(newDecStrRaw.length, "0");
}, [prevDecStrRaw, newDecStrRaw]);
React.useEffect(() => {
if (isInView)
prevNumberRef.current = effectiveNumber;
}, [effectiveNumber, isInView]);
const intPlaces = React.useMemo(
() =>
Array.from({ length: finalIntLength }, (_, i) =>
10 ** (finalIntLength - i - 1)),
[finalIntLength],
);
const decPlaces = React.useMemo(
() =>
newDecStrRaw
? Array.from({ length: newDecStrRaw.length }, (_, i) =>
10 ** (newDecStrRaw.length - i - 1))
: [],
[newDecStrRaw],
);
const newDecValue = newDecStrRaw ? Number.parseInt(newDecStrRaw, 10) : 0;
const prevDecValue = adjustedPrevDec ? Number.parseInt(adjustedPrevDec, 10) : 0;
return (
<span
ref={localRef}
data-slot="sliding-number"
style={{
display: "inline-flex",
alignItems: "center",
}}
{...props}
>
{isInView && Number(number) < 0 && (
<span style={{ marginRight: "0.25rem" }}>-</span>
)}
{intPlaces.map((place, idx) => {
const digitsToRight = intPlaces.length - idx - 1;
const isSeparatorPosition
= typeof thousandSeparator !== "undefined"
&& digitsToRight > 0
&& digitsToRight % 3 === 0;
return (
<React.Fragment key={`int-${place}`}>
<SlidingNumberRoller
prevValue={Number.parseInt(adjustedPrevInt, 10)}
value={Number.parseInt(newIntStr ?? "0", 10)}
place={place}
transition={transition}
/>
{isSeparatorPosition && <span>{thousandSeparator}</span>}
</React.Fragment>
);
})}
{newDecStrRaw && (
<>
<span>{decimalSeparator}</span>
{decPlaces.map(place => (
<SlidingNumberRoller
key={`dec-${place}`}
prevValue={prevDecValue}
value={newDecValue}
place={place}
transition={transition}
delay={delay}
/>
))}
</>
)}
</span>
);
}
export { SlidingNumber, type SlidingNumberProps };

View File

@@ -1,282 +0,0 @@
import { ArrowRightIcon, Sparkles } from "lucide-react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import type { Category, Script } from "@/lib/types";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { basePath } from "@/config/site-config";
import { fetchCategories } from "@/lib/data";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
import { DialogTitle } from "./ui/dialog";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
export function formattedBadge(type: string) {
switch (type) {
case "vm":
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
case "ct":
return <Badge className="text-yellow-500/75 border-yellow-500/75">LXC</Badge>;
case "pve":
return <Badge className="text-orange-500/75 border-orange-500/75">PVE</Badge>;
case "addon":
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
}
return null;
}
function getRandomScript(categories: Category[], previouslySelected: Set<string> = new Set()): Script | null {
const allScripts = categories.flatMap(cat => cat.scripts || []);
if (allScripts.length === 0)
return null;
const availableScripts = allScripts.filter(script => !previouslySelected.has(script.slug));
if (availableScripts.length === 0) {
return allScripts[Math.floor(Math.random() * allScripts.length)];
}
const idx = Math.floor(Math.random() * availableScripts.length);
return availableScripts[idx];
}
function CommandMenu() {
const [open, setOpen] = React.useState(false);
const [links, setLinks] = React.useState<Category[]>([]);
const [isLoading, setIsLoading] = React.useState(false);
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
const router = useRouter();
const fetchSortedCategories = () => {
setIsLoading(true);
fetchCategories()
.then((categories) => {
setLinks(categories);
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
console.error(error);
});
};
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
fetchSortedCategories();
setOpen(open => !open);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
const handleOpenRandomScript = async () => {
if (links.length === 0) {
setIsLoading(true);
try {
const categories = await fetchCategories();
setLinks(categories);
const randomScript = getRandomScript(categories, selectedScripts);
if (randomScript) {
setSelectedScripts(prev => new Set([...prev, randomScript.slug]));
router.push(`/scripts?id=${randomScript.slug}`);
}
}
finally {
setIsLoading(false);
}
}
else {
const randomScript = getRandomScript(links, selectedScripts);
if (randomScript) {
setSelectedScripts(prev => new Set([...prev, randomScript.slug]));
router.push(`/scripts?id=${randomScript.slug}`);
}
}
};
const getUniqueScriptsMap = React.useCallback(() => {
const scriptMap = new Map<string, { script: Script; categoryName: string }>();
for (const category of links) {
for (const script of category.scripts) {
if (!scriptMap.has(script.slug)) {
scriptMap.set(script.slug, { script, categoryName: category.name });
}
}
}
return scriptMap;
}, [links]);
const getUniqueScriptsByCategory = React.useCallback(() => {
const scriptMap = getUniqueScriptsMap();
const categoryOrder = links.map(cat => cat.name);
const grouped: Record<string, Script[]> = {};
for (const name of categoryOrder) {
grouped[name] = [];
}
for (const { script, categoryName } of scriptMap.values()) {
if (grouped[categoryName]) {
grouped[categoryName].push(script);
}
else {
grouped[categoryName] = [script];
}
}
Object.keys(grouped).forEach((cat) => {
if (grouped[cat].length === 0)
delete grouped[cat];
});
return grouped;
}, [getUniqueScriptsMap, links]);
const uniqueScriptsByCategory = getUniqueScriptsByCategory();
return (
<>
<div className="flex gap-2">
<Button
variant="outline"
className={cn(
"relative h-9 w-full justify-start rounded-[0.5rem] bg-muted/50 text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64",
)}
onClick={() => {
fetchSortedCategories();
setOpen(true);
}}
>
<span className="inline-flex">Search scripts...</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>
K
</kbd>
</Button>
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={handleOpenRandomScript}
disabled={isLoading}
className="hidden lg:flex"
aria-label="Open Random Script"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleOpenRandomScript();
}
}}
>
<Sparkles className="size-4" />
<span className="sr-only">Open Random Script</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open Random Script</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<CommandDialog
open={open}
onOpenChange={setOpen}
filter={(value: string, search: string) => {
const searchLower = search.toLowerCase().trim();
if (!searchLower)
return 1;
const valueLower = value.toLowerCase();
const searchWords = searchLower.split(/\s+/).filter(Boolean);
// All search words must appear somewhere in the value (name + description)
const allWordsMatch = searchWords.every((word: string) => valueLower.includes(word));
return allWordsMatch ? 1 : 0;
}}
>
<DialogTitle className="sr-only">Search scripts</DialogTitle>
<CommandInput placeholder="Search for a script..." />
<CommandList>
<CommandEmpty>
{isLoading
? (
"Searching..."
)
: (
<div className="flex flex-col items-center justify-center py-6 text-center">
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
<div className="mt-4">
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
<Button variant="outline" size="sm" asChild>
<Link
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
target="_blank"
rel="noopener noreferrer"
>
Documentation
{" "}
<ArrowRightIcon className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
)}
</CommandEmpty>
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
{scripts.map(script => (
<CommandItem
key={`script:${script.slug}`}
value={`${script.name} ${script.type} ${script.description || ""}`}
onSelect={() => {
setOpen(false);
router.push(`/scripts?id=${script.slug}`);
}}
tabIndex={0}
aria-label={`Open script ${script.name}`}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setOpen(false);
router.push(`/scripts?id=${script.slug}`);
}
}}
>
<div className="flex gap-2" onClick={() => setOpen(false)}>
<Image
src={script.logo || `/${basePath}/logo.png`}
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
unoptimized
width={16}
height={16}
alt=""
className="h-5 w-5"
/>
<span>{script.name}</span>
<span>{formattedBadge(script.type)}</span>
</div>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
</>
);
}
export default CommandMenu;

View File

@@ -1,30 +0,0 @@
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { Plus } from "lucide-react";
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
import { FAQ_Items } from "../config/faq-config";
export default function FAQ() {
return (
<div className="space-y-4">
<Accordion type="single" collapsible className="w-full">
{FAQ_Items.map((item, index) => (
<AccordionItem value={index.toString()} key={index} className="py-2">
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger className="flex flex-1 items-center gap-3 py-2 text-left text-[15px] font-semibold leading-6 transition-all [&>svg>path:last-child]:origin-center [&>svg>path:last-child]:transition-all [&>svg>path:last-child]:duration-200 [&>svg]:-order-1 [&[data-state=open]>svg>path:last-child]:rotate-90 [&[data-state=open]>svg>path:last-child]:opacity-0 [&[data-state=open]>svg]:rotate-180">
{item.title}
<Plus
size={16}
strokeWidth={2}
className="shrink-0 opacity-60 transition-transform duration-200"
aria-hidden="true"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionContent className="pb-2 ps-7 text-muted-foreground">{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More